diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 3aa1984f042..9e4ab077948 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -27,7 +27,7 @@ jobs: .delta.dependencyCount - name: Get TypeScript conversion progress run: - grep -r --exclude-dir="node_modules" --include="*.mts" "@ts-expect-error" . | wc -l | xargs > + grep -r --exclude-dir="node_modules" --include="*.ts" "@ts-expect-error" . | wc -l | xargs > .delta.tsConversion && echo " (Number of ts-expect-error directives)" >> .delta.tsConversion - name: Save PR number if: github.event_name == 'pull_request' diff --git a/CHANGELOG.md b/CHANGELOG.md index 334a700c58a..277f62a51b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,25 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +## [17.10.1](https://github.com/netlify/cli/compare/v17.10.0...v17.10.1) (2023-12-08) + + +### Bug Fixes + +* use correct file extensions for transpilation ([#6256](https://github.com/netlify/cli/issues/6256)) ([8308097](https://github.com/netlify/cli/commit/8308097334fc76117cee4798772abf18ff5a7e4d)) + +## [17.10.0](https://github.com/netlify/cli/compare/v17.9.0...v17.10.0) (2023-12-06) + + +### Features + +* add blobs upload step ([#6223](https://github.com/netlify/cli/issues/6223)) ([33177fc](https://github.com/netlify/cli/commit/33177fc0608952196f40249a3e0fae1e42af25a0)) + + +### Bug Fixes + +* **deps:** update netlify packages ([#6244](https://github.com/netlify/cli/issues/6244)) ([b3f857a](https://github.com/netlify/cli/commit/b3f857a939ed78e726545092c2bbb2d80ae5105e)) + ## [17.9.0](https://github.com/netlify/cli/compare/v17.8.1...v17.9.0) (2023-12-04) diff --git a/package-lock.json b/package-lock.json index a6805ce593f..ee3ede96d94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,20 @@ { "name": "netlify-cli", - "version": "17.9.0", + "version": "17.10.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "netlify-cli", - "version": "17.9.0", + "version": "17.10.1", "hasInstallScript": true, "license": "MIT", "dependencies": { "@bugsnag/js": "7.20.2", "@fastify/static": "6.10.2", "@netlify/blobs": "6.3.1", - "@netlify/build": "29.29.4", - "@netlify/build-info": "7.11.1", + "@netlify/build": "29.31.0", + "@netlify/build-info": "7.11.3", "@netlify/config": "20.10.0", "@netlify/edge-bundler": "10.1.3", "@netlify/local-functions-proxy": "1.1.1", @@ -2287,9 +2287,9 @@ } }, "node_modules/@netlify/build": { - "version": "29.29.4", - "resolved": "https://registry.npmjs.org/@netlify/build/-/build-29.29.4.tgz", - "integrity": "sha512-Df0f5M4FN3uUrYImvVOY6SRON+YgsWwBoeJKsNUso3EIAPjCXNLoGYExH1LE/LmS6MgDbYQcm/jKndhTVY0sIA==", + "version": "29.31.0", + "resolved": "https://registry.npmjs.org/@netlify/build/-/build-29.31.0.tgz", + "integrity": "sha512-XtUXnj3LtdWxcgP4kz1k0creF9bZa7eGNh85B89sw4V99Mtsn1Sl89yAZJkEH6rtuGJIs1eiQ261nlhUZ6+C6Q==", "dependencies": { "@bugsnag/js": "^7.0.0", "@honeycombio/opentelemetry-node": "^0.5.0", @@ -2367,9 +2367,9 @@ } }, "node_modules/@netlify/build-info": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@netlify/build-info/-/build-info-7.11.1.tgz", - "integrity": "sha512-JnsFgY22eOT++VRGoLHQDWzp1cvWFgsxiIk+/y3VgdmUQdugeKn5HeUYfWkBOau4xTCu53MKhtuVm8DkLpH19g==", + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/@netlify/build-info/-/build-info-7.11.3.tgz", + "integrity": "sha512-lnJsJcoFSZIIB+4tOU/rdPjPn2+TP896B9y23z5THyACeN/e2I0Y7Y+d+BzHgFDcJBwqFpIehEs2jf8wPb9CHw==", "dependencies": { "@bugsnag/js": "^7.20.0", "dot-prop": "^7.2.0", @@ -26448,9 +26448,9 @@ "integrity": "sha512-JjLz3WW7Wp6NVwQtDxPpWio4L3u9pnnDXnQ7Q16zgAFE9IA1rSjZVSsyOQrtkiBQIxaJ1Zr5eky8vrXJ5mdRWg==" }, "@netlify/build": { - "version": "29.29.4", - "resolved": "https://registry.npmjs.org/@netlify/build/-/build-29.29.4.tgz", - "integrity": "sha512-Df0f5M4FN3uUrYImvVOY6SRON+YgsWwBoeJKsNUso3EIAPjCXNLoGYExH1LE/LmS6MgDbYQcm/jKndhTVY0sIA==", + "version": "29.31.0", + "resolved": "https://registry.npmjs.org/@netlify/build/-/build-29.31.0.tgz", + "integrity": "sha512-XtUXnj3LtdWxcgP4kz1k0creF9bZa7eGNh85B89sw4V99Mtsn1Sl89yAZJkEH6rtuGJIs1eiQ261nlhUZ6+C6Q==", "requires": { "@bugsnag/js": "^7.0.0", "@honeycombio/opentelemetry-node": "^0.5.0", @@ -26751,9 +26751,9 @@ } }, "@netlify/build-info": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@netlify/build-info/-/build-info-7.11.1.tgz", - "integrity": "sha512-JnsFgY22eOT++VRGoLHQDWzp1cvWFgsxiIk+/y3VgdmUQdugeKn5HeUYfWkBOau4xTCu53MKhtuVm8DkLpH19g==", + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/@netlify/build-info/-/build-info-7.11.3.tgz", + "integrity": "sha512-lnJsJcoFSZIIB+4tOU/rdPjPn2+TP896B9y23z5THyACeN/e2I0Y7Y+d+BzHgFDcJBwqFpIehEs2jf8wPb9CHw==", "requires": { "@bugsnag/js": "^7.20.0", "dot-prop": "^7.2.0", diff --git a/package.json b/package.json index c4497327f62..3fbe9eaffe0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "netlify-cli", "description": "Netlify command line tool", - "version": "17.9.0", + "version": "17.10.1", "author": "Netlify Inc.", "type": "module", "engines": { @@ -81,8 +81,8 @@ "@bugsnag/js": "7.20.2", "@fastify/static": "6.10.2", "@netlify/blobs": "6.3.1", - "@netlify/build": "29.29.4", - "@netlify/build-info": "7.11.1", + "@netlify/build": "29.31.0", + "@netlify/build-info": "7.11.3", "@netlify/config": "20.10.0", "@netlify/edge-bundler": "10.1.3", "@netlify/local-functions-proxy": "1.1.1", diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 02014944969..3eef4ff9118 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -39,8 +39,12 @@ import openBrowser from '../utils/open-browser.js' import StateConfig from '../utils/state-config.js' import { identify, reportError, track } from '../utils/telemetry/index.js' -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type $FIXME = any +import { type NetlifyOptions } from './types.js' + +type Analytics = { + startTime: bigint + payload?: Record +} // load the autocomplete plugin inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt) @@ -65,21 +69,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))) } @@ -87,13 +81,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 && @@ -117,9 +106,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}`, @@ -142,19 +129,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. @@ -168,25 +148,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)) @@ -238,33 +209,23 @@ export default class BaseCommand extends Command { }) } - /** @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) => { @@ -298,13 +259,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)) || @@ -314,15 +270,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) { @@ -334,25 +283,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 @@ -363,7 +307,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) @@ -409,14 +353,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' @@ -434,7 +373,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) } @@ -508,24 +446,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 */ - async init(actionCommand: C) { + 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 @@ -554,8 +486,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 ( @@ -580,17 +511,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 } @@ -617,7 +550,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 @@ -635,7 +569,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 }) @@ -643,7 +576,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), }) @@ -691,21 +623,21 @@ export default class BaseCommand extends Command { debug(`${this.name()}:init`)('end') } - /** - * Find and resolve the Netlify configuration - */ + /** Find and resolve the Netlify configuration */ async getConfig(config: { cwd: string token?: string | null - state: $FIXME + // 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 - }): Promise> { + }): ReturnType { // the flags that are passed to the command like `--debug` or `--offline` const flags = this.opts() @@ -756,10 +688,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/lib/functions/netlify-function.ts b/src/lib/functions/netlify-function.ts index 8acd316709c..f4f55501771 100644 --- a/src/lib/functions/netlify-function.ts +++ b/src/lib/functions/netlify-function.ts @@ -121,7 +121,7 @@ export default class NetlifyFunction { } if (extension === '.js') { - return '.js' + return '.mjs' } } diff --git a/src/lib/functions/runtimes/js/builders/zisi.ts b/src/lib/functions/runtimes/js/builders/zisi.ts index e85de6f0429..1d08ab35112 100644 --- a/src/lib/functions/runtimes/js/builders/zisi.ts +++ b/src/lib/functions/runtimes/js/builders/zisi.ts @@ -175,7 +175,7 @@ export default async function handler({ config, directory, errorExit, func, meta featureFlags.zisi_pure_esm_mjs = true } else { // We must use esbuild for certain file extensions. - const mustTranspile = ['.js', '.ts', '.mts', '.cts'].includes(path.extname(func.mainFile)) + const mustTranspile = ['.mjs', '.ts', '.mts', '.cts'].includes(path.extname(func.mainFile)) const mustUseEsbuild = hasTypeModule || mustTranspile if (mustUseEsbuild && !functionsConfig['*'].nodeBundler) { diff --git a/src/utils/command-helpers.ts b/src/utils/command-helpers.ts index e3e4f6d1f7b..a53f9846858 100644 --- a/src/utils/command-helpers.ts +++ b/src/utils/command-helpers.ts @@ -185,23 +185,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 { diff --git a/tests/integration/commands/deploy/deploy.test.js b/tests/integration/commands/deploy/deploy.test.js index a092e147fa2..0bdfbd110c4 100644 --- a/tests/integration/commands/deploy/deploy.test.js +++ b/tests/integration/commands/deploy/deploy.test.js @@ -495,7 +495,7 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co export default async () => new Response("Internal V2 API") export const config = { path: "/internal-v2-func" } `, - path: '.netlify/functions-internal/func-4.js', + path: '.netlify/functions-internal/func-4.mjs', }) .buildAsync() @@ -824,11 +824,15 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co await builder .withNetlifyToml({ config: { - build: { publish: 'dist', functions: 'functions' }, + build: { functions: 'functions', publish: 'dist' }, }, }) .withContentFile({ - path: 'dist/.netlify/blobs/deploy/hello', + path: 'dist/index.html', + content: 'get blob', + }) + .withContentFile({ + path: '.netlify/blobs/deploy/hello', content: 'hello from the blob', }) .withPackageJson({ @@ -843,21 +847,21 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co path: 'functions/read-blob.ts', content: ` import { getDeployStore } from "@netlify/blobs" - import { Config, Context } from "@netlify/functions" - - export default async (req: Request, context: Context) => { + import { Config } from "@netlify/functions" + + export default async () => { const store = getDeployStore() const blob = await store.get('hello') - + return new Response(blob) } - + export const config: Config = { path: "/read-blob" - } + } `, }) - .buildAsync() + .build() await execa.command('npm install', { cwd: builder.directory }) const { deploy_url: deployUrl } = await callCli( diff --git a/tests/integration/commands/dev/dev-miscellaneous.test.js b/tests/integration/commands/dev/dev-miscellaneous.test.js index 0e9be56a01b..37d25b7fe70 100644 --- a/tests/integration/commands/dev/dev-miscellaneous.test.js +++ b/tests/integration/commands/dev/dev-miscellaneous.test.js @@ -895,7 +895,7 @@ describe.concurrent('commands/dev-miscellaneous', () => { }) }) - test('should respect in-source configuration from internal edge functions', async (t) => { + test.skip('should respect in-source configuration from internal edge functions', async (t) => { await withSiteBuilder('site-with-internal-edge-functions', async (builder) => { const publicDir = 'public' await builder @@ -948,7 +948,7 @@ describe.concurrent('commands/dev-miscellaneous', () => { }) }) - test('Serves edge functions with import maps coming from the `functions.deno_import_map` config property and from the internal manifest', async (t) => { + test.skip('Serves edge functions with import maps coming from the `functions.deno_import_map` config property and from the internal manifest', async (t) => { await withSiteBuilder('site-with-edge-functions-and-import-maps', async (builder) => { const internalEdgeFunctionsDir = path.join('.netlify', 'edge-functions') diff --git a/tests/integration/commands/dev/edge-functions.test.ts b/tests/integration/commands/dev/edge-functions.test.ts index 39392d3deb2..3438a3a54a3 100644 --- a/tests/integration/commands/dev/edge-functions.test.ts +++ b/tests/integration/commands/dev/edge-functions.test.ts @@ -37,7 +37,7 @@ const setup = async ({ fixture }) => { describe.skipIf(isWindows)('edge functions', () => { setupFixtureTests('dev-server-with-edge-functions', { devServer: true, mockApi: { routes } }, () => { - test('should run edge functions in correct order', async ({ devServer }) => { + test.skip('should run edge functions in correct order', async ({ devServer }) => { const response = await got(`http://localhost:${devServer.port}/ordertest`, { throwHttpErrors: false, retry: { limit: 0 }, diff --git a/tests/integration/commands/dev/redirects.test.ts b/tests/integration/commands/dev/redirects.test.ts index 7a3e55c480b..a4fc93aa45d 100644 --- a/tests/integration/commands/dev/redirects.test.ts +++ b/tests/integration/commands/dev/redirects.test.ts @@ -1,19 +1,16 @@ import { describe, expect, test } from 'vitest' import { FixtureTestContext, setupFixtureTests } from '../../utils/fixture.js' -import got from '../../utils/got.js' +import fetch from 'node-fetch' describe('redirects', () => { setupFixtureTests('dev-server-with-functions', { devServer: true }, () => { test('should send original query params to functions', async ({ devServer }) => { - const response = await got(`http://localhost:${devServer.port}/with-params?param2=world&other=1`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const response = await fetch(`http://localhost:${devServer.port}/with-params?param2=world&other=1`, {}) - expect(response.statusCode).toBe(200) + expect(response.status).toBe(200) - const result = JSON.parse(response.body) + const result = await response.json() expect(result.queryStringParameters).not.toHaveProperty('param1') expect(result.queryStringParameters).toHaveProperty('param2', 'world') expect(result.queryStringParameters).toHaveProperty('other', '1') @@ -22,14 +19,11 @@ describe('redirects', () => { test('should send original query params to functions when using duplicate parameters', async ({ devServer, }) => { - const response = await got(`http://localhost:${devServer.port}/api/echo?param=hello¶m=world`, { - throwHttpErrors: false, - retry: { limit: 0 }, - }) + const response = await fetch(`http://localhost:${devServer.port}/api/echo?param=hello¶m=world`, {}) - expect(response.statusCode).toBe(200) + expect(response.status).toBe(200) - const result = JSON.parse(response.body) + const result = await response.json() expect(result.queryStringParameters).toHaveProperty('param', 'hello, world') expect(result.multiValueQueryStringParameters).toHaveProperty('param', ['hello', 'world']) }) 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.js index c2a016796e6..ed430628f8f 100644 --- a/tests/integration/commands/functions-with-args/functions-with-args.test.js +++ b/tests/integration/commands/functions-with-args/functions-with-args.test.js @@ -494,7 +494,7 @@ exports.handler = async () => ({ }) }) - test('Serves functions from the internal functions directory', async (t) => { + test.skip('Serves functions from the internal functions directory', async (t) => { await withSiteBuilder('function-internal', async (builder) => { const bundlerConfig = args.includes('esbuild') ? { node_bundler: 'esbuild' } : {} @@ -610,7 +610,7 @@ exports.handler = async () => ({ }) }) - test('Serves functions with a `.js` extension', async (t) => { + test('Serves functions with a `.mjs` extension', async (t) => { await withSiteBuilder('function-mjs', async (builder) => { const bundlerConfig = args.includes('esbuild') ? { node_bundler: 'esbuild' } : {} @@ -623,7 +623,7 @@ exports.handler = async () => ({ }, }) .withContentFile({ - path: 'functions/hello.js', + path: 'functions/hello.mjs', content: ` const handler = async () => { return { diff --git a/tests/integration/frameworks/hugo.test.ts b/tests/integration/frameworks/hugo.test.ts index 9990ea3060b..61ec0d9dec7 100644 --- a/tests/integration/frameworks/hugo.test.ts +++ b/tests/integration/frameworks/hugo.test.ts @@ -1,11 +1,11 @@ import { expect, test } from 'vitest' import { FixtureTestContext, setupFixtureTests } from '../utils/fixture.js' -import got from '../utils/got.js' +import fetch from 'node-fetch' setupFixtureTests('hugo-site', { devServer: true }, () => { test('should not infinite redirect when -d flag is passed', async ({ devServer: { url } }) => { - const response = await got(`${url}/`).text() + const response = await fetch(`${url}/`).then((res) => res.text()) expect(response).toContain('Home page!') })