From d5fcdc5ab0937d114216cc62a821eac294d2334c Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Tue, 12 Dec 2023 12:54:46 +0100 Subject: [PATCH 1/5] chore: enhance types for base-command.ts (#6261) --- src/commands/base-command.ts | 221 ++++++++++------------------ src/commands/dev/dev.ts | 4 +- src/commands/link/link.ts | 5 - src/commands/serve/serve.ts | 4 +- src/commands/types.d.ts | 26 +++- src/utils/command-helpers.ts | 13 +- src/utils/detect-server-settings.ts | 52 +++---- src/utils/init/utils.ts | 54 ++++--- 8 files changed, 158 insertions(+), 221 deletions(-) diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 7a73b601ddc..91832c5a99a 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -38,6 +38,13 @@ import openBrowser from '../utils/open-browser.js' import StateConfig from '../utils/state-config.js' import { identify, reportError, track } from '../utils/telemetry/index.js' +import { type NetlifyOptions } from './types.js' + +type Analytics = { + startTime: bigint + payload?: Record +} + // load the autocomplete plugin inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt) /** Netlify CLI client id. Lives in bot@netlify.com */ @@ -61,21 +68,11 @@ const HELP_SEPARATOR_WIDTH = 5 */ const COMMANDS_WITHOUT_WORKSPACE_OPTIONS = new Set(['api', 'recipes', 'completion', 'status', 'switch', 'login', 'lm']) -/** - * Formats a help list correctly with the correct indent - * @param {string[]} textArray - * @returns - */ -// @ts-expect-error TS(7006) FIXME: Parameter 'textArray' implicitly has an 'any' type... Remove this comment to see the full error message -const formatHelpList = (textArray) => textArray.join('\n').replace(/^/gm, ' '.repeat(HELP_INDENT_WIDTH)) +/** Formats a help list correctly with the correct indent */ +const formatHelpList = (textArray: string[]) => textArray.join('\n').replace(/^/gm, ' '.repeat(HELP_INDENT_WIDTH)) -/** - * Get the duration between a start time and the current time - * @param {bigint} startTime - * @returns - */ -// @ts-expect-error TS(7006) FIXME: Parameter 'startTime' implicitly has an 'any' type... Remove this comment to see the full error message -const getDuration = function (startTime) { +/** Get the duration between a start time and the current time */ +const getDuration = (startTime: bigint) => { const durationNs = process.hrtime.bigint() - startTime return Math.round(Number(durationNs / BigInt(NANO_SECS_TO_MSECS))) } @@ -83,13 +80,8 @@ const getDuration = function (startTime) { /** * Retrieves a workspace package based of the filter flag that is provided. * If the filter flag does not match a workspace package or is not defined then it will prompt with an autocomplete to select a package - * @param {Project} project - * @param {string=} filter - * @returns {Promise} */ -// @ts-expect-error TS(7006) FIXME: Parameter 'project' implicitly has an 'any' type. -async function selectWorkspace(project, filter) { - // @ts-expect-error TS(7006) FIXME: Parameter 'pkg' implicitly has an 'any' type. +async function selectWorkspace(project: Project, filter?: string): Promise { const selected = project.workspace?.packages.find((pkg) => { if ( project.relativeBaseDirectory && @@ -113,9 +105,7 @@ async function selectWorkspace(project, filter) { // @ts-expect-error TS(7006) FIXME: Parameter '_' implicitly has an 'any' type. source: (/** @type {string} */ _, input = '') => (project.workspace?.packages || []) - // @ts-expect-error TS(7006) FIXME: Parameter 'pkg' implicitly has an 'any' type. .filter((pkg) => pkg.path.includes(input)) - // @ts-expect-error TS(7006) FIXME: Parameter 'pkg' implicitly has an 'any' type. .map((pkg) => ({ name: `${pkg.name ? `${chalk.bold(pkg.name)} ` : ''}${pkg.path} ${chalk.dim( `--filter ${pkg.name || pkg.path}`, @@ -138,19 +128,12 @@ async function getRepositoryRoot(cwd?: string): Promise { /** Base command class that provides tracking and config initialization */ export default class BaseCommand extends Command { - /** - * The netlify object inside each command with the state - * @type {import('./types.js').NetlifyOptions} - */ - // @ts-expect-error TS(7008) FIXME: Member 'netlify' implicitly has an 'any' type. - netlify - - /** @type {{ startTime: bigint, payload?: any}} */ - analytics = { startTime: process.hrtime.bigint() } - - /** @type {Project} */ - // @ts-expect-error TS(7008) FIXME: Member 'project' implicitly has an 'any' type. - project + /** The netlify object inside each command with the state */ + // @ts-expect-error This will be set for each command, TypeScript is just not able to infer it + netlify: NetlifyOptions + analytics: Analytics = { startTime: process.hrtime.bigint() } + // @ts-expect-error This will be set for each command, TypeScript is just not able to infer it + project: Project /** * The working directory that is used for reading the `netlify.toml` file and storing the state. @@ -164,25 +147,16 @@ export default class BaseCommand extends Command { /** * The workspace root if inside a mono repository. * Must not be the repository root! - * @type {string|undefined} */ - // @ts-expect-error TS(7008) FIXME: Member 'jsWorkspaceRoot' implicitly has an 'any' t... Remove this comment to see the full error message - jsWorkspaceRoot - /** - * The current workspace package we should execute the commands in - * @type {string|undefined} - */ - // @ts-expect-error TS(7008) FIXME: Member 'workspacePackage' implicitly has an 'any' ... Remove this comment to see the full error message - workspacePackage + jsWorkspaceRoot?: string + /** The current workspace package we should execute the commands in */ + workspacePackage?: string /** * IMPORTANT this function will be called for each command! * Don't do anything expensive in there. - * @param {string} name The command name - * @returns */ - // @ts-expect-error TS(7006) FIXME: Parameter 'name' implicitly has an 'any' type. - createCommand(name) { + createCommand(name: string): BaseCommand { const base = new BaseCommand(name) // If --silent or --json flag passed disable logger .addOption(new Option('--json', 'Output return values as JSON').hideHelp(true)) @@ -229,38 +203,28 @@ export default class BaseCommand extends Command { } debug(`${name}:preAction`)('start') this.analytics = { startTime: process.hrtime.bigint() } - await this.init(actionCommand) + await this.init(actionCommand as BaseCommand) debug(`${name}:preAction`)('end') }) } - /** @private */ - noBaseOptions = false - + #noBaseOptions = false /** don't show help options on command overview (mostly used on top commands like `addons` where options only apply on children) */ noHelpOptions() { - this.noBaseOptions = true + this.#noBaseOptions = true return this } - /** @type {string[]} The examples list for the command (used inside doc generation and help page) */ - examples = [] - - /** - * Set examples for the command - * @param {string[]} examples - */ - // @ts-expect-error TS(7006) FIXME: Parameter 'examples' implicitly has an 'any' type. - addExamples(examples) { + /** The examples list for the command (used inside doc generation and help page) */ + examples: string[] = [] + /** Set examples for the command */ + addExamples(examples: string[]) { this.examples = examples return this } - /** - * Overrides the help output of commander with custom styling - * @returns {import('commander').Help} - */ - createHelp() { + /** Overrides the help output of commander with custom styling */ + createHelp(): Help { const help = super.createHelp() help.commandUsage = (command) => { @@ -294,13 +258,8 @@ export default class BaseCommand extends Command { help.longestSubcommandTermLength = (command: BaseCommand): number => getCommands(command).reduce((max, cmd) => Math.max(max, cmd.name().length), 0) - /** - * override the longestOptionTermLength to react on hide options flag - * @param {BaseCommand} command - * @param {import('commander').Help} helper - * @returns {number} - */ - help.longestOptionTermLength = (command, helper) => + /** override the longestOptionTermLength to react on hide options flag */ + help.longestOptionTermLength = (command: BaseCommand, helper: Help): number => // @ts-expect-error TS(2551) FIXME: Property 'noBaseOptions' does not exist on type 'C... Remove this comment to see the full error message (command.noBaseOptions === false && helper.visibleOptions(command).reduce((max, option) => Math.max(max, helper.optionTerm(option).length), 0)) || @@ -310,15 +269,8 @@ export default class BaseCommand extends Command { const parentCommand = this.name() === 'netlify' ? command : command.parent const termWidth = helper.padWidth(command, helper) const helpWidth = helper.helpWidth || FALLBACK_HELP_CMD_WIDTH - /** - * formats a term correctly - * @param {string} term - * @param {string} [description] - * @param {boolean} [isCommand] - * @returns {string} - */ - // @ts-expect-error TS(7006) FIXME: Parameter 'term' implicitly has an 'any' type. - const formatItem = (term, description, isCommand = false) => { + // formats a term correctly + const formatItem = (term: string, description?: string, isCommand = false): string => { const bang = isCommand ? `${HELP_$} ` : '' if (description) { @@ -330,25 +282,20 @@ export default class BaseCommand extends Command { return `${bang}${term}` } - /** @type {string[]} */ - // @ts-expect-error TS(7034) FIXME: Variable 'output' implicitly has type 'any[]' in s... Remove this comment to see the full error message - let output = [] + let output: string[] = [] // Description const [topDescription, ...commandDescription] = (helper.commandDescription(command) || '').split('\n') if (topDescription.length !== 0) { - // @ts-expect-error TS(7005) FIXME: Variable 'output' implicitly has an 'any[]' type. output = [...output, topDescription, ''] } // on the parent help command the version should be displayed if (this.name() === 'netlify') { - // @ts-expect-error TS(7005) FIXME: Variable 'output' implicitly has an 'any[]' type. output = [...output, chalk.bold('VERSION'), formatHelpList([formatItem(USER_AGENT)]), ''] } // Usage - // @ts-expect-error TS(7005) FIXME: Variable 'output' implicitly has an 'any[]' type. output = [...output, chalk.bold('USAGE'), helper.commandUsage(command), ''] // Arguments @@ -359,7 +306,7 @@ export default class BaseCommand extends Command { output = [...output, chalk.bold('ARGUMENTS'), formatHelpList(argumentList), ''] } - if (command.noBaseOptions === false) { + if (command.#noBaseOptions === false) { // Options const optionList = helper .visibleOptions(command) @@ -405,14 +352,9 @@ export default class BaseCommand extends Command { return help } - /** - * Will be called on the end of an action to track the metrics - * @param {*} [error_] - */ - // @ts-expect-error TS(7006) FIXME: Parameter 'error_' implicitly has an 'any' type. - async onEnd(error_) { - // @ts-expect-error TS(2339) FIXME: Property 'payload' does not exist on type '{ start... Remove this comment to see the full error message - const { payload, startTime } = this.analytics + /** Will be called on the end of an action to track the metrics */ + async onEnd(error_?: unknown) { + const { payload = {}, startTime } = this.analytics const duration = getDuration(startTime) const status = error_ === undefined ? 'success' : 'error' @@ -430,7 +372,6 @@ export default class BaseCommand extends Command { } catch {} if (error_ !== undefined) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'string | Error' is not assignabl... Remove this comment to see the full error message error(error_ instanceof Error ? error_ : format(error_), { exit: false }) exit(1) } @@ -504,25 +445,18 @@ export default class BaseCommand extends Command { return accessToken } - /** - * Adds some data to the analytics payload - * @param {Record} payload - */ - // @ts-expect-error TS(7006) FIXME: Parameter 'payload' implicitly has an 'any' type. - setAnalyticsPayload(payload) { - // @ts-expect-error TS(2339) FIXME: Property 'payload' does not exist on type '{ start... Remove this comment to see the full error message - const newPayload = { ...this.analytics.payload, ...payload } - // @ts-expect-error TS(2322) FIXME: Type '{ payload: any; startTime: bigint; }' is not... Remove this comment to see the full error message - this.analytics = { ...this.analytics, payload: newPayload } + /** Adds some data to the analytics payload */ + setAnalyticsPayload(payload: Record) { + this.analytics = { + ...this.analytics, + payload: { ...this.analytics.payload, ...payload }, + } } /** * Initializes the options and parses the configuration needs to be called on start of a command function - * @param {BaseCommand} actionCommand The command of the action that is run (`this.` gets the parent command) - * @private */ - // @ts-expect-error TS(7006) FIXME: Parameter 'actionCommand' implicitly has an 'any' ... Remove this comment to see the full error message - async init(actionCommand) { + private async init(actionCommand: BaseCommand) { debug(`${actionCommand.name()}:init`)('start') const flags = actionCommand.opts() // here we actually want to use the process.cwd as we are setting the workingDir @@ -551,8 +485,7 @@ export default class BaseCommand extends Command { }) }) const frameworks = await this.project.detectFrameworks() - /** @type { string|undefined} */ - let packageConfig = flags.config ? resolve(flags.config) : undefined + let packageConfig: string | undefined = flags.config ? resolve(flags.config) : undefined // check if we have detected multiple projects inside which one we have to perform our operations. // only ask to select one if on the workspace root if ( @@ -577,17 +510,19 @@ export default class BaseCommand extends Command { const state = new StateConfig(this.workingDir) const [token] = await getToken(flags.auth) - const apiUrlOpts = { + const apiUrlOpts: { + userAgent: string + scheme?: string + host?: string + pathPrefix?: string + } = { userAgent: USER_AGENT, } if (process.env.NETLIFY_API_URL) { const apiUrl = new URL(process.env.NETLIFY_API_URL) - // @ts-expect-error TS(2339) FIXME: Property 'scheme' does not exist on type '{ userAg... Remove this comment to see the full error message apiUrlOpts.scheme = apiUrl.protocol.slice(0, -1) - // @ts-expect-error TS(2339) FIXME: Property 'host' does not exist on type '{ userAgen... Remove this comment to see the full error message apiUrlOpts.host = apiUrl.host - // @ts-expect-error TS(2339) FIXME: Property 'pathPrefix' does not exist on type '{ us... Remove this comment to see the full error message apiUrlOpts.pathPrefix = process.env.NETLIFY_API_URL === `${apiUrl.protocol}//${apiUrl.host}` ? '/api/v1' : apiUrl.pathname } @@ -613,7 +548,8 @@ export default class BaseCommand extends Command { certificateFile: flags.httpProxyCertificateFilename, }) const apiOpts = { ...apiUrlOpts, agent } - const api = new NetlifyAPI(token || '', apiOpts) + // TODO: remove typecast once we have proper types for the API + const api = new NetlifyAPI(token || '', apiOpts) as NetlifyOptions['api'] // If a user passes a site name as an option instead of a site ID to options.site, the siteInfo object // will only have the property siteInfo.id. Checking for one of the other properties ensures that we can do @@ -631,7 +567,6 @@ export default class BaseCommand extends Command { // ================================================== // Perform analytics reporting // ================================================== - // @ts-expect-error TS(7006) FIXME: Parameter 'framework' implicitly has an 'any' type... Remove this comment to see the full error message const frameworkIDs = frameworks?.map((framework) => framework.id) if (frameworkIDs?.length !== 0) { this.setAnalyticsPayload({ frameworks: frameworkIDs }) @@ -639,7 +574,6 @@ export default class BaseCommand extends Command { this.setAnalyticsPayload({ monorepo: Boolean(this.project.workspace), packageManager: this.project.packageManager?.name, - // @ts-expect-error TS(7031) FIXME: Binding element 'id' implicitly has an 'any' type. buildSystem: this.project.buildSystems.map(({ id }) => id), }) @@ -687,23 +621,21 @@ export default class BaseCommand extends Command { debug(`${this.name()}:init`)('end') } - /** - * Find and resolve the Netlify configuration - * @param {object} config - * @param {string} config.cwd - * @param {string|null=} config.token - * @param {*} config.state - * @param {boolean=} config.offline - * @param {string=} config.configFilePath An optional path to the netlify configuration file e.g. netlify.toml - * @param {string=} config.packagePath - * @param {string=} config.repositoryRoot - * @param {string=} config.host - * @param {string=} config.pathPrefix - * @param {string=} config.scheme - * @returns {ReturnType} - */ - // @ts-expect-error TS(7023) FIXME: 'getConfig' implicitly has return type 'any' becau... Remove this comment to see the full error message - async getConfig(config) { + /** Find and resolve the Netlify configuration */ + async getConfig(config: { + cwd: string + token?: string | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state?: any + offline?: boolean + /** An optional path to the netlify configuration file e.g. netlify.toml */ + configFilePath?: string + packagePath?: string + repositoryRoot?: string + host?: string + pathPrefix?: string + scheme?: string + }): ReturnType { // the flags that are passed to the command like `--debug` or `--offline` const flags = this.opts() @@ -754,10 +686,15 @@ export default class BaseCommand extends Command { * Returns the context that should be used in case one hasn't been explicitly * set. The default context is `dev` most of the time, but some commands may * wish to override that. - * - * @returns {'production' | 'dev'} */ - getDefaultContext() { + getDefaultContext(): 'production' | 'dev' { return this.name() === 'serve' ? 'production' : 'dev' } + + /** + * Retrieve feature flags for this site + */ + getFeatureFlag(flagName: string): T { + return this.netlify.siteInfo.feature_flags?.[flagName] || null + } } diff --git a/src/commands/dev/dev.ts b/src/commands/dev/dev.ts index 62eda3e7a43..50f1da5a745 100644 --- a/src/commands/dev/dev.ts +++ b/src/commands/dev/dev.ts @@ -29,6 +29,7 @@ import { getGeoCountryArgParser } from '../../utils/validation.js' import BaseCommand from '../base-command.js' import { createDevExecCommand } from './dev-exec.js' +import { type DevConfig } from './types.js' /** * @@ -90,7 +91,6 @@ export const dev = async (options: OptionValues, command: BaseCommand) => { const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify config.dev = { ...config.dev } config.build = { ...config.build } - /** @type {import('./types.js').DevConfig} */ const devConfig = { framework: '#auto', autoLaunch: Boolean(options.open), @@ -99,7 +99,7 @@ export const dev = async (options: OptionValues, command: BaseCommand) => { ...(config.build.base && { base: config.build.base }), ...config.dev, ...options, - } + } as DevConfig let { env } = cachedConfig diff --git a/src/commands/link/link.ts b/src/commands/link/link.ts index cce6a46a7bb..30889cb2525 100644 --- a/src/commands/link/link.ts +++ b/src/commands/link/link.ts @@ -211,7 +211,6 @@ or run ${chalk.cyanBright('netlify sites:create')} to create a site.`) } catch (error_) { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. if (error_.status === 404) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'Error' is not assignable to para... Remove this comment to see the full error message error(new Error(`Site ID '${siteId}' not found`)) } else { // @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message @@ -225,7 +224,6 @@ or run ${chalk.cyanBright('netlify sites:create')} to create a site.`) } if (!site) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'Error' is not assignable to para... Remove this comment to see the full error message error(new Error(`No site found`)) } @@ -285,7 +283,6 @@ export const link = async (options: OptionValues, command: BaseCommand) => { } catch (error_) { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. if (error_.status === 404) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'Error' is not assignable to para... Remove this comment to see the full error message error(new Error(`Site id ${options.id} not found`)) } else { // @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message @@ -315,7 +312,6 @@ export const link = async (options: OptionValues, command: BaseCommand) => { } catch (error_) { // @ts-expect-error TS(2571) FIXME: Object is of type 'unknown'. if (error_.status === 404) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'Error' is not assignable to para... Remove this comment to see the full error message error(new Error(`${options.name} not found`)) } else { // @ts-expect-error TS(2345) FIXME: Argument of type 'unknown' is not assignable to pa... Remove this comment to see the full error message @@ -324,7 +320,6 @@ export const link = async (options: OptionValues, command: BaseCommand) => { } if (results.length === 0) { - // @ts-expect-error TS(2345) FIXME: Argument of type 'Error' is not assignable to para... Remove this comment to see the full error message error(new Error(`No sites found named ${options.name}`)) } const [firstSiteData] = results diff --git a/src/commands/serve/serve.ts b/src/commands/serve/serve.ts index 762ab246662..d136d55c4bf 100644 --- a/src/commands/serve/serve.ts +++ b/src/commands/serve/serve.ts @@ -25,12 +25,12 @@ import { generateInspectSettings, startProxyServer } from '../../utils/proxy-ser import { runBuildTimeline } from '../../utils/run-build.js' import type { ServerSettings } from '../../utils/types.js' import BaseCommand from '../base-command.js' +import { type DevConfig } from '../dev/types.js' export const serve = async (options: OptionValues, command: BaseCommand) => { const { api, cachedConfig, config, repositoryRoot, site, siteInfo, state } = command.netlify config.dev = { ...config.dev } config.build = { ...config.build } - /** @type {import('../dev/types').DevConfig} */ const devConfig = { ...(config.functionsDirectory && { functions: config.functionsDirectory }), ...(config.build.publish && { publish: config.build.publish }), @@ -40,7 +40,7 @@ export const serve = async (options: OptionValues, command: BaseCommand) => { // Override the `framework` value so that we start a static server and not // the framework's development server. framework: '#static', - } + } as DevConfig let { env } = cachedConfig diff --git a/src/commands/types.d.ts b/src/commands/types.d.ts index dcb4c643b85..f776e22570e 100644 --- a/src/commands/types.d.ts +++ b/src/commands/types.d.ts @@ -3,6 +3,9 @@ import type { NetlifyAPI } from 'netlify' import StateConfig from '../utils/state-config.js' +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type $TSFixMe = any; + export type NetlifySite = { root?: string configPath?: string @@ -11,21 +14,32 @@ export type NetlifySite = { set id(id: string): void } +type PatchedConfig = NetlifyTOML & { + functionsDirectory?: string + build: NetlifyTOML['build'] & { + functionsSource?: string + } + dev: NetlifyTOML['dev'] & { + functions?: string + } +} + /** * The netlify object inside each command with the state */ export type NetlifyOptions = { - api: NetlifyAPI - apiOpts: unknown + // poorly duck type the missing api functions + api: NetlifyAPI & Record Promise<$TSFixMe>> + apiOpts: $TSFixMe repositoryRoot: string /** Absolute path of the netlify configuration file */ configFilePath: string /** Relative path of the netlify configuration file */ relConfigFilePath: string site: NetlifySite - siteInfo: unknown - config: NetlifyTOML - cachedConfig: Record - globalConfig: unknown + siteInfo: $TSFixMe + config: PatchedConfig + cachedConfig: Record + globalConfig: $TSFixMe state: StateConfig } diff --git a/src/utils/command-helpers.ts b/src/utils/command-helpers.ts index ee53421d7f8..5c884e28b51 100644 --- a/src/utils/command-helpers.ts +++ b/src/utils/command-helpers.ts @@ -184,23 +184,16 @@ export const warn = (message = '') => { log(` ${bang} Warning: ${message}`) } -/** - * throws an error or log it - * @param {unknown} message - * @param {object} [options] - * @param {boolean} [options.exit] - */ -export const error = (message = '', options = {}) => { +/** Throws an error or logs it */ +export const error = (message: Error | string = '', options: { exit?: boolean } = {}) => { const err = - // @ts-expect-error TS(2358) FIXME: The left-hand side of an 'instanceof' expression m... Remove this comment to see the full error message message instanceof Error ? message : // eslint-disable-next-line unicorn/no-nested-ternary typeof message === 'string' ? new Error(message) - : /** @type {Error} */ { message, stack: undefined, name: 'Error' } + : { message, stack: undefined, name: 'Error' } - // @ts-expect-error TS(2339) FIXME: Property 'exit' does not exist on type '{}'. if (options.exit === false) { const bang = chalk.red(BANG) if (process.env.DEBUG) { diff --git a/src/utils/detect-server-settings.ts b/src/utils/detect-server-settings.ts index 18bc5a1904e..5fe75bea9cb 100644 --- a/src/utils/detect-server-settings.ts +++ b/src/utils/detect-server-settings.ts @@ -2,14 +2,19 @@ import { readFile } from 'fs/promises' import { EOL } from 'os' import { dirname, relative, resolve } from 'path' -import { getFramework, getSettings } from '@netlify/build-info' +import { Project, Settings, getFramework, getSettings } from '@netlify/build-info' +import type { OptionValues } from 'commander' import getPort from 'get-port' +import BaseCommand from '../commands/base-command.js' +import { type DevConfig } from '../commands/dev/types.js' + import { detectFrameworkSettings } from './build-info.js' import { NETLIFYDEVWARN, chalk, log } from './command-helpers.js' import { acquirePort } from './dev.js' import { getInternalFunctionsDir } from './functions/functions.js' import { getPluginsToAutoInstall } from './init/utils.js' +import { BaseServerSettings, ServerSettings } from './types.js' /** @param {string} str */ // @ts-expect-error TS(7006) FIXME: Parameter 'str' implicitly has an 'any' type. @@ -180,11 +185,8 @@ const handleStaticServer = async ({ devConfig, flags, workingDir }) => { /** * Retrieves the settings from a framework - * @param {import('@netlify/build-info').Settings} [settings] - * @returns {import('./types.js').BaseServerSettings | undefined} */ -// @ts-expect-error TS(7006) FIXME: Parameter 'settings' implicitly has an 'any' type. -const getSettingsFromDetectedSettings = (settings) => { +const getSettingsFromDetectedSettings = (command: BaseCommand, settings?: Settings) => { if (!settings) { return } @@ -196,7 +198,7 @@ const getSettingsFromDetectedSettings = (settings) => { framework: settings.framework.name, env: settings.env, pollingStrategies: settings.pollingStrategies, - plugins: getPluginsToAutoInstall(settings.plugins_from_config_file, settings.plugins_recommended), + plugins: getPluginsToAutoInstall(command, settings.plugins_from_config_file, settings.plugins_recommended), } } @@ -265,32 +267,29 @@ const mergeSettings = async ({ devConfig, frameworkSettings = {}, workingDir }) /** * Handles a forced framework and retrieves the settings for it - * @param {object} config - * @param {import('../commands/dev/types.js').DevConfig} config.devConfig - * @param {import('@netlify/build-info').Project} config.project - * @param {string} config.workingDir - * @param {string=} config.workspacePackage - * @returns {Promise} */ -// @ts-expect-error TS(7031) FIXME: Binding element 'devConfig' implicitly has an 'any... Remove this comment to see the full error message -const handleForcedFramework = async ({ devConfig, project, workingDir, workspacePackage }) => { +const handleForcedFramework = async (options: { + command: BaseCommand + devConfig: DevConfig + project: Project + workingDir: string + workspacePackage?: string +}): Promise => { // this throws if `devConfig.framework` is not a supported framework - const framework = await getFramework(devConfig.framework, project) - const settings = await getSettings(framework, project, workspacePackage || '') - const frameworkSettings = getSettingsFromDetectedSettings(settings) - return mergeSettings({ devConfig, workingDir, frameworkSettings }) + const framework = await getFramework(options.devConfig.framework, options.project) + const settings = await getSettings(framework, options.project, options.workspacePackage || '') + const frameworkSettings = getSettingsFromDetectedSettings(options.command, settings) + return mergeSettings({ devConfig: options.devConfig, workingDir: options.workingDir, frameworkSettings }) } /** * Get the server settings based on the flags and the devConfig - * @param {import('../commands/dev/types.js').DevConfig} devConfig - * @param {import('commander').OptionValues} flags - * @param {import('../commands/base-command.js').default} command - * @returns {Promise} */ - -// @ts-expect-error TS(7006) FIXME: Parameter 'devConfig' implicitly has an 'any' type... Remove this comment to see the full error message -const detectServerSettings = async (devConfig, flags, command) => { +const detectServerSettings = async ( + devConfig: DevConfig, + flags: OptionValues, + command: BaseCommand, +): Promise => { validateProperty(devConfig, 'framework', 'string') /** @type {Partial} */ @@ -304,7 +303,7 @@ const detectServerSettings = async (devConfig, flags, command) => { const runDetection = !hasCommandAndTargetPort(devConfig) const frameworkSettings = runDetection - ? getSettingsFromDetectedSettings(await detectFrameworkSettings(command, 'dev')) + ? getSettingsFromDetectedSettings(command, await detectFrameworkSettings(command, 'dev')) : undefined if (frameworkSettings === undefined && runDetection) { log(`${NETLIFYDEVWARN} No app server detected. Using simple static server`) @@ -325,6 +324,7 @@ const detectServerSettings = async (devConfig, flags, command) => { validateFrameworkConfig({ devConfig }) // this is when the user explicitly configures a framework, e.g. `framework = "gatsby"` settings = await handleForcedFramework({ + command, devConfig, project: command.project, workingDir: command.workingDir, diff --git a/src/utils/init/utils.ts b/src/utils/init/utils.ts index 0e8bfb3f1f0..f349693f0d6 100644 --- a/src/utils/init/utils.ts +++ b/src/utils/init/utils.ts @@ -1,9 +1,12 @@ import { writeFile } from 'fs/promises' import path from 'path' +import { NetlifyConfig } from '@netlify/build' +import { Settings } from '@netlify/build-info' import cleanDeep from 'clean-deep' import inquirer from 'inquirer' +import BaseCommand from '../../commands/base-command.js' import { fileExistsAsync } from '../../lib/fs.js' import { normalizeBackslash } from '../../lib/path.js' import { detectBuildSettings } from '../build-info.js' @@ -11,43 +14,44 @@ import { chalk, error as failAndExit, log, warn } from '../command-helpers.js' import { getRecommendPlugins, getUIPlugins } from './plugins.js' -// these plugins represent runtimes that are -// expected to be "automatically" installed. Even though -// they can be installed on package/toml, we always -// want them installed in the site settings. When installed -// there our build will automatically install the latest without -// user management of the versioning. -const pluginsToAlwaysInstall = new Set(['@netlify/plugin-nextjs']) +const formatTitle = (title: string) => chalk.cyan(title) /** * Retrieve a list of plugins to auto install - * @param {string[]=} pluginsInstalled - * @param {string[]=} pluginsRecommended + * @param pluginsToAlwaysInstall these plugins represent runtimes that are + * expected to be "automatically" installed. Even though + * they can be installed on package/toml, we always + * want them installed in the site settings. When installed + * there our build will automatically install the latest without + * user management of the versioning. + * @param pluginsInstalled + * @param pluginsRecommended * @returns */ -export const getPluginsToAutoInstall = (pluginsInstalled = [], pluginsRecommended = []) => - pluginsRecommended.reduce( +export const getPluginsToAutoInstall = ( + command: BaseCommand, + pluginsInstalled: string[] = [], + pluginsRecommended: string[] = [], +) => { + const nextRuntime = '@netlify/plugin-nextjs' + const pluginsToAlwaysInstall = new Set([nextRuntime]) + return pluginsRecommended.reduce( (acc, plugin) => pluginsInstalled.includes(plugin) && !pluginsToAlwaysInstall.has(plugin) ? acc : [...acc, plugin], - - /** @type {string[]} */ [], + [] as string[], ) - +} /** - * - * @param {Partial} settings - * @param {*} config - * @param {import('../../commands/base-command.js').default} command */ -// @ts-expect-error TS(7006) FIXME: Parameter 'settings' implicitly has an 'any' type. -const normalizeSettings = (settings, config, command) => { - const plugins = getPluginsToAutoInstall(settings.plugins_from_config_file, settings.plugins_recommended) +const normalizeSettings = (settings: Settings, config: NetlifyConfig, command: BaseCommand) => { + const plugins = getPluginsToAutoInstall(command, settings.plugins_from_config_file, settings.plugins_recommended) const recommendedPlugins = getRecommendPlugins(plugins, config) return { defaultBaseDir: settings.baseDirectory ?? command.project.relativeBaseDirectory ?? '', defaultBuildCmd: config.build.command || settings.buildCommand, defaultBuildDir: settings.dist, + // @ts-expect-error types need to be fixed on @netlify/build defaultFunctionsDir: config.build.functions || 'netlify/functions', recommendedPlugins, } @@ -106,7 +110,7 @@ export const getBuildSettings = async ({ command, config }) => { await normalizeSettings(setting, config, command) if (recommendedPlugins.length !== 0 && setting.framework?.name) { - log(`Configuring ${formatTitle(setting.framework?.name)} runtime...`) + log(`Configuring ${formatTitle(setting.framework.name)} runtime...`) log() } @@ -210,12 +214,6 @@ export const formatErrorMessage = ({ error, message }) => { return `${message} with error: ${chalk.red(errorMessage)}` } -/** - * @param {string} title - */ -// @ts-expect-error TS(7006) FIXME: Parameter 'title' implicitly has an 'any' type. -const formatTitle = (title) => chalk.cyan(title) - // @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 createDeployKey = async ({ api }) => { try { From e3f654eb140ebf27ee019939da8e8188cd8dac70 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Tue, 12 Dec 2023 19:40:23 -0600 Subject: [PATCH 2/5] refactor: replace got with node-fetch on scheduled-functions.test.ts (#6233) replace got with node-fetch related to #5695 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../commands/dev/scheduled-functions.test.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/integration/commands/dev/scheduled-functions.test.ts b/tests/integration/commands/dev/scheduled-functions.test.ts index 7475332d072..27f62494e0e 100644 --- a/tests/integration/commands/dev/scheduled-functions.test.ts +++ b/tests/integration/commands/dev/scheduled-functions.test.ts @@ -1,27 +1,23 @@ import { describe, expect, test } from 'vitest' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import got from '../../utils/got.js' +import fetch from 'node-fetch' import { pause } from '../../utils/pause.js' describe('scheduled functions', () => { setupFixtureTests('dev-server-with-functions', { devServer: true }, () => { test('should emulate next_run for scheduled functions', async ({ devServer }) => { - const response = await got(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const response = await fetch(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc`, {}) - expect(response.statusCode).toBe(200) + expect(response.status).toBe(200) }) }) setupFixtureTests('dev-server-with-functions', { devServer: true }, () => { test('should detect file changes to scheduled function', async ({ devServer, fixture }) => { - const { body } = await got(`http://localhost:${devServer.port}/.netlify/functions/ping`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const body = await fetch(`http://localhost:${devServer.port}/.netlify/functions/ping`, {}).then((res) => + res.text(), + ) expect(body).toBe('ping') @@ -43,10 +39,9 @@ describe('scheduled functions', () => { const DETECT_FILE_CHANGE_DELAY = 500 await pause(DETECT_FILE_CHANGE_DELAY) - const { body: warning } = await got(`http://localhost:${devServer.port}/.netlify/functions/ping`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const warning = await fetch(`http://localhost:${devServer.port}/.netlify/functions/ping`, {}).then((res) => + res.text(), + ) expect(warning).toContain('Your function returned `body`') }) From 8e71e4298c4bfcdf167026018887999568c8405c Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Wed, 13 Dec 2023 08:20:38 -0600 Subject: [PATCH 3/5] refactor: replace got with node-fetch on functions-invoke.test.ts (#6232) replace got with node-fetch related to #5695 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../functions-invoke/functions-invoke.test.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/integration/commands/functions-invoke/functions-invoke.test.ts b/tests/integration/commands/functions-invoke/functions-invoke.test.ts index cc30f216a82..fa63e84d25c 100644 --- a/tests/integration/commands/functions-invoke/functions-invoke.test.ts +++ b/tests/integration/commands/functions-invoke/functions-invoke.test.ts @@ -1,4 +1,4 @@ -import got from 'got' +import fetch from 'node-fetch' import { describe, expect, test } from 'vitest' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' @@ -39,26 +39,25 @@ describe('functions:invoke command', () => { ) test.concurrent('should serve helpful tips and tricks', async ({ devServer, fixture }) => { - const plainTextResponse = await got(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc-body`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const textResponse = await fetch(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc-body`, {}) + + const bodyPlainTextResponse = await textResponse.text() const youReturnedBodyRegex = /.*Your function returned `body`. Is this an accident\?.*/ - expect(plainTextResponse.body).toMatch(youReturnedBodyRegex) - expect(plainTextResponse.body).toMatch(/.*You performed an HTTP request.*/) - expect(plainTextResponse.statusCode).toBe(200) + expect(bodyPlainTextResponse).toMatch(youReturnedBodyRegex) + expect(bodyPlainTextResponse).toMatch(/.*You performed an HTTP request.*/) + expect(textResponse.status).toBe(200) - const htmlResponse = await got(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc-body`, { - throwHttpErrors: false, - retry: { limit: 0 }, + const htmlResponse = await fetch(`http://localhost:${devServer.port}/.netlify/functions/scheduled-isc-body`, { headers: { accept: 'text/html', }, }) - expect(htmlResponse.body).toMatch(/.* Date: Wed, 13 Dec 2023 08:31:31 -0600 Subject: [PATCH 4/5] refactor: replace got with node-fetch on rules-proxy.test.ts (#6236) replace got with node-fetch related to #5695 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- tests/integration/rules-proxy.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/rules-proxy.test.ts b/tests/integration/rules-proxy.test.ts index ac76a9f8ee7..7db083951c7 100644 --- a/tests/integration/rules-proxy.test.ts +++ b/tests/integration/rules-proxy.test.ts @@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, test } from 'vitest' import { createRewriter, getWatchers } from '../../src/utils/rules-proxy.js' -import got from './utils/got.js' +import fetch from 'node-fetch' import { createSiteBuilder, SiteBuilder } from './utils/site-builder.ts' describe('rules-proxy', () => { @@ -48,7 +48,9 @@ describe('rules-proxy', () => { }) test('should apply re-write rule based on _redirects file', async () => { - const response = await got(`http://localhost:${(server?.address() as net.AddressInfo).port}/something`).json() + const response = await fetch(`http://localhost:${(server?.address() as net.AddressInfo).port}/something`).then( + (res) => res.json(), + ) expect(response.from).toBe('/something') expect(response.to).toBe('/ping') From 8834fbc4f6ce12d35b5c570ce15a4ed5a2d7d409 Mon Sep 17 00:00:00 2001 From: Angel Mendez Date: Wed, 13 Dec 2023 13:41:17 -0600 Subject: [PATCH 5/5] refactor: replace got with node-fetch on v2-api.test.ts (#6246) * refactor: run tests concurrently * refactor: replace got with node-fetch on v2-api.test.ts related to #5695 --------- Co-authored-by: Sarah Etter --- tests/integration/commands/dev/v2-api.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/integration/commands/dev/v2-api.test.ts b/tests/integration/commands/dev/v2-api.test.ts index 7eaa4979535..367857fbbda 100644 --- a/tests/integration/commands/dev/v2-api.test.ts +++ b/tests/integration/commands/dev/v2-api.test.ts @@ -5,7 +5,6 @@ import { gte } from 'semver' import { describe, expect, test } from 'vitest' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import got from '../../utils/got.js' const siteInfo = { account_id: 'mock-account-id', @@ -26,16 +25,13 @@ const setup = async ({ fixture }) => { await execa('npm', ['install'], { cwd: fixture.directory }) } -describe.runIf(gte(version, '18.13.0'))('v2 api', () => { +describe.runIf(gte(version, '18.13.0')).concurrent('v2 api', () => { setupFixtureTests('dev-server-with-v2-functions', { devServer: true, mockApi: { routes }, setup }, () => { test('should successfully be able to run v2 functions', async ({ devServer }) => { - const response = await got(`http://localhost:${devServer.port}/.netlify/functions/ping`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const response = await fetch(`http://localhost:${devServer.port}/.netlify/functions/ping`) - expect(response.statusCode).toBe(200) - expect(response.body).toBe('pong') + expect(response.status).toBe(200) + expect(await response.text()).toBe('pong') }) test('supports streamed responses', async ({ devServer }) => {