diff --git a/.changeset/four-seas-stare.md b/.changeset/four-seas-stare.md new file mode 100644 index 00000000000..05b9ad3810f --- /dev/null +++ b/.changeset/four-seas-stare.md @@ -0,0 +1,5 @@ +--- +"fuels": minor +--- + +feat!: separate `onSuccess` events for the Fuels CLI diff --git a/apps/demo-fuels/fuels.config.full.ts b/apps/demo-fuels/fuels.config.full.ts index 9dadb0fcef8..28b01d8831f 100644 --- a/apps/demo-fuels/fuels.config.full.ts +++ b/apps/demo-fuels/fuels.config.full.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { createConfig } from 'fuels'; -import type { CommandEvent, ContractDeployOptions, FuelsConfig } from 'fuels'; +import type { ContractDeployOptions, DeployedContract, FuelsConfig } from 'fuels'; const MY_FIRST_DEPLOYED_CONTRACT_NAME = ''; @@ -84,15 +84,35 @@ export default createConfig({ }, // #endregion deployConfig-fn - // #region onSuccess - onSuccess: (event: CommandEvent, config: FuelsConfig) => { - console.log('fuels:onSuccess', { event, config }); + // #region onBuild + onBuild: (config: FuelsConfig) => { + console.log('fuels:onBuild', { config }); }, - // #endregion onSuccess + // #endregion onBuild + + // #region onDeploy + // #import { DeployedContract, FuelsConfig }; + + onDeploy: (config: FuelsConfig, data: DeployedContract[]) => { + console.log('fuels:onDeploy', { config, data }); + }, + // #endregion onDeploy + + // #region onDev + onDev: (config: FuelsConfig) => { + console.log('fuels:onDev', { config }); + }, + // #endregion onDev + + // #region onNode + onNode: (config: FuelsConfig) => { + console.log('fuels:onNode', { config }); + }, + // #endregion onNode // #region onFailure - onFailure: (error: Error, config: FuelsConfig) => { - console.log('fuels:onFailure', { error, config }); + onFailure: (config: FuelsConfig, error: Error) => { + console.log('fuels:onFailure', { config, error }); }, // #endregion onFailure diff --git a/apps/docs/src/guide/fuels-cli/config-file.md b/apps/docs/src/guide/fuels-cli/config-file.md index 169e7e18c6b..8abc0a2aef4 100644 --- a/apps/docs/src/guide/fuels-cli/config-file.md +++ b/apps/docs/src/guide/fuels-cli/config-file.md @@ -121,16 +121,46 @@ Or use a function for crafting dynamic deployment flows: <<< @../../../demo-fuels/fuels.config.full.ts#deployConfig-fn{ts:line-numbers} -## `onSuccess` +## `onBuild` -Pass a callback function to be called after a successful run. +A callback function that is called after a build event has been successful. Parameters: -- `event` — The event that triggered this execution - `config` — The loaded config (`fuels.config.ts`) -<<< @../../../demo-fuels/fuels.config.full.ts#onSuccess{ts:line-numbers} +<<< @../../../demo-fuels/fuels.config.full.ts#onBuild{ts:line-numbers} + +## `onDeploy` + +A callback function that is called after a deployment event has been successful. + +Parameters: + +- `config` — The loaded config (`fuels.config.ts`) +- `data` — The data (an array of deployed contracts) + +<<< @../../../demo-fuels/fuels.config.full.ts#onDeploy{ts:line-numbers} + +## `onDev` + +A callback function that is called after the [`fuels dev`](./commands.md#fuels-dev) command has successfully restarted. + +Parameters: + +- `config` — The loaded config (`fuels.config.ts`) + +<<< @../../../demo-fuels/fuels.config.full.ts#onDev{ts:line-numbers} + +## `onNode` + +A callback function that is called after the [`fuels node`](./commands.md#fuels-node) command has successfully refreshed. + +Parameters: + +- `config` — The loaded config (`fuels.config.ts`) + +<<< @../../../demo-fuels/fuels.config.full.ts#onNode{ts:line-numbers} ## `onFailure` @@ -138,8 +168,8 @@ Pass a callback function to be called in case of errors. Parameters: -- `error` — Original error object - `config` — The loaded config (`fuels.config.ts`) +- `error` — Original error object <<< @../../../demo-fuels/fuels.config.full.ts#onFailure{ts:line-numbers} diff --git a/packages/fuels/src/cli/commands/build/index.test.ts b/packages/fuels/src/cli/commands/build/index.test.ts new file mode 100644 index 00000000000..2983713940f --- /dev/null +++ b/packages/fuels/src/cli/commands/build/index.test.ts @@ -0,0 +1,48 @@ +import { fuelsConfig } from '../../../../test/fixtures/fuels.config'; +import { mockLogger } from '../../../../test/utils/mockLogger'; + +import { build } from '.'; +import * as buildSwayProgramsMod from './buildSwayPrograms'; +import * as generateTypesMod from './generateTypes'; + +/** + * @group node + */ +describe('build', () => { + const mockAll = () => { + const { log } = mockLogger(); + + const onBuild = vi.fn(); + + const buildSwayPrograms = vi + .spyOn(buildSwayProgramsMod, 'buildSwayPrograms') + .mockResolvedValue(); + const generateTypes = vi.spyOn(generateTypesMod, 'generateTypes').mockResolvedValue(); + + return { + onBuild, + log, + buildSwayPrograms, + generateTypes, + }; + }; + + test('should build sway programs and generate types', async () => { + const { log, buildSwayPrograms, generateTypes } = mockAll(); + + await build(fuelsConfig); + + expect(log).toHaveBeenCalledWith('Building..'); + expect(buildSwayPrograms).toHaveBeenCalled(); + expect(generateTypes).toHaveBeenCalled(); + }); + + test('should call onBuild callback', async () => { + const { onBuild } = mockAll(); + const config = { ...fuelsConfig, onBuild }; + + await build(config); + + expect(onBuild).toHaveBeenCalledWith(config); + }); +}); diff --git a/packages/fuels/src/cli/commands/build/index.ts b/packages/fuels/src/cli/commands/build/index.ts index 48f1738d67b..65cf4799c6d 100644 --- a/packages/fuels/src/cli/commands/build/index.ts +++ b/packages/fuels/src/cli/commands/build/index.ts @@ -13,9 +13,9 @@ export async function build(config: FuelsConfig, program?: Command) { await buildSwayPrograms(config); await generateTypes(config); + config.onBuild?.(config); const options = program?.opts(); - if (options?.deploy) { const fuelCore = await autoStartFuelCore(config); await deploy(config); diff --git a/packages/fuels/src/cli/commands/deploy/index.test.ts b/packages/fuels/src/cli/commands/deploy/index.test.ts new file mode 100644 index 00000000000..5510793eaac --- /dev/null +++ b/packages/fuels/src/cli/commands/deploy/index.test.ts @@ -0,0 +1,40 @@ +import type { Provider } from '@fuel-ts/account'; +import { Wallet } from '@fuel-ts/account'; +import { FUEL_NETWORK_URL } from '@fuel-ts/account/configs'; + +import { fuelsConfig } from '../../../../test/fixtures/fuels.config'; +import type { DeployedContract } from '../../types'; + +import { deploy } from '.'; +import * as createWalletMod from './createWallet'; +import * as saveContractIdsMod from './saveContractIds'; + +/** + * @group node + */ +describe('deploy', () => { + const mockAll = () => { + const onDeploy = vi.fn(); + + const provider = { url: FUEL_NETWORK_URL } as Provider; + const wallet = Wallet.fromPrivateKey('0x01', provider); + const createWallet = vi.spyOn(createWalletMod, 'createWallet').mockResolvedValue(wallet); + + vi.spyOn(saveContractIdsMod, 'saveContractIds').mockResolvedValue(); + + return { + onDeploy, + createWallet, + }; + }; + + test('should call onDeploy callback', async () => { + const { onDeploy } = mockAll(); + const expectedContracts: DeployedContract[] = []; + const config = { ...fuelsConfig, contracts: [], onDeploy }; + + await deploy(config); + + expect(onDeploy).toHaveBeenCalledWith(config, expectedContracts); + }); +}); diff --git a/packages/fuels/src/cli/commands/deploy/index.ts b/packages/fuels/src/cli/commands/deploy/index.ts index e8f1c6174bb..d5e80a1ce00 100644 --- a/packages/fuels/src/cli/commands/deploy/index.ts +++ b/packages/fuels/src/cli/commands/deploy/index.ts @@ -52,6 +52,7 @@ export async function deploy(config: FuelsConfig) { } await saveContractIds(contracts, config.output); + config.onDeploy?.(config, contracts); return contracts; } diff --git a/packages/fuels/src/cli/commands/dev/index.test.ts b/packages/fuels/src/cli/commands/dev/index.test.ts index 802c9d5ddc5..9bbdbb714f9 100644 --- a/packages/fuels/src/cli/commands/dev/index.test.ts +++ b/packages/fuels/src/cli/commands/dev/index.test.ts @@ -30,6 +30,7 @@ describe('dev', () => { function mockAll() { const { autoStartFuelCore, fuelCore, killChildProcess } = mockStartFuelCore(); + const onDev = vi.fn(); const onFailure = vi.fn(); const withConfigErrorHandler = vi @@ -50,6 +51,7 @@ describe('dev', () => { fuelCore, killChildProcess, loadConfig, + onDev, onFailure, withConfigErrorHandler, }; @@ -66,6 +68,15 @@ describe('dev', () => { expect(deploy).toHaveBeenCalledTimes(1); }); + test('should call `onDev` callback on success', async () => { + const { onDev } = mockAll(); + const config: FuelsConfig = { ...fuelsConfig, onDev }; + + await dev(config); + + expect(onDev).toHaveBeenCalledWith(config); + }); + it('dev should handle and log error from `buildAndDeploy`', async () => { const { error } = mockLogger(); diff --git a/packages/fuels/src/cli/commands/dev/index.ts b/packages/fuels/src/cli/commands/dev/index.ts index f56893a3921..bd62eaecbda 100644 --- a/packages/fuels/src/cli/commands/dev/index.ts +++ b/packages/fuels/src/cli/commands/dev/index.ts @@ -3,7 +3,7 @@ import { watch } from 'chokidar'; import { globSync } from 'glob'; import { loadConfig } from '../../config/loadConfig'; -import type { FuelsConfig } from '../../types'; +import { type FuelsConfig } from '../../types'; import { error, log } from '../../utils/logger'; import { build } from '../build'; import { deploy } from '../deploy'; @@ -18,7 +18,10 @@ export const closeAllFileHandlers = (handlers: FSWatcher[]) => { export const buildAndDeploy = async (config: FuelsConfig) => { await build(config); - return deploy(config); + const deployedContracts = await deploy(config); + config.onDev?.(config); + + return deployedContracts; }; export const getConfigFilepathsToWatch = (config: FuelsConfig) => { diff --git a/packages/fuels/src/cli/commands/node/index.test.ts b/packages/fuels/src/cli/commands/node/index.test.ts index 1464f8eb466..659c65c40af 100644 --- a/packages/fuels/src/cli/commands/node/index.test.ts +++ b/packages/fuels/src/cli/commands/node/index.test.ts @@ -20,6 +20,7 @@ describe('node', () => { function mockAll() { const { autoStartFuelCore, fuelCore, killChildProcess } = mockStartFuelCore(); + const onNode = vi.fn(); const onFailure = vi.fn(); const withConfigErrorHandler = vi @@ -35,6 +36,7 @@ describe('node', () => { fuelCore, killChildProcess, loadConfig, + onNode, onFailure, withConfigErrorHandler, }; @@ -53,16 +55,23 @@ describe('node', () => { test('should restart everything when config file changes', async () => { const { log } = mockLogger(); - const { autoStartFuelCore, fuelCore, killChildProcess, loadConfig, withConfigErrorHandler } = - mockAll(); + const { + autoStartFuelCore, + fuelCore, + killChildProcess, + loadConfig, + withConfigErrorHandler, + onNode, + } = mockAll(); - const config = structuredClone(fuelsConfig); + const config = { ...fuelsConfig, onNode }; const close = vi.fn(); const watchHandlers = [{ close }, { close }] as unknown as FSWatcher[]; await configFileChanged({ config, fuelCore, watchHandlers })('event', 'some/path'); // configFileChanged() internals + expect(onNode).toHaveBeenCalledWith(config); expect(log).toHaveBeenCalledTimes(1); expect(close).toHaveBeenCalledTimes(2); expect(killChildProcess).toHaveBeenCalledTimes(1); diff --git a/packages/fuels/src/cli/commands/node/index.ts b/packages/fuels/src/cli/commands/node/index.ts index 8391152bcc6..df762409f4c 100644 --- a/packages/fuels/src/cli/commands/node/index.ts +++ b/packages/fuels/src/cli/commands/node/index.ts @@ -34,6 +34,7 @@ export const configFileChanged = (state: NodeState) => async (_event: string, pa try { // eslint-disable-next-line @typescript-eslint/no-use-before-define await node(await loadConfig(state.config.basePath)); + state.config.onNode?.(state.config); } catch (err: unknown) { await withConfigErrorHandler(err, state.config); } diff --git a/packages/fuels/src/cli/commands/withConfig.test.ts b/packages/fuels/src/cli/commands/withConfig.test.ts index a3b33088ba6..7bef6dcafc7 100644 --- a/packages/fuels/src/cli/commands/withConfig.test.ts +++ b/packages/fuels/src/cli/commands/withConfig.test.ts @@ -21,12 +21,10 @@ describe('withConfig', () => { }); function mockAll(params?: { shouldErrorOnDeploy?: boolean; shouldErrorOnLoadConfig?: boolean }) { - const onSuccess = vi.fn(); const onFailure = vi.fn(); const copyConfig: FuelsConfig = { ...structuredClone(fuelsConfig), - onSuccess, onFailure, }; @@ -57,30 +55,13 @@ describe('withConfig', () => { command, deploy, loadConfig, - onSuccess, onFailure, error, }; } - test('onSuccess hook in config file', async () => { - const { command, deploy, configPath, loadConfig, onSuccess, onFailure } = mockAll({ - shouldErrorOnDeploy: false, - }); - - await withConfig(command, Commands.deploy, deploy)(); - - expect(loadConfig).toHaveBeenCalledTimes(1); - expect(loadConfig.mock.calls[0][0]).toEqual(configPath); - - expect(onSuccess).toHaveBeenCalledTimes(1); - expect(onSuccess.mock.calls[0][0]).toEqual({ data: [], type: 'deploy' }); - - expect(onFailure).toHaveBeenCalledTimes(0); - }); - test('onFailure hook in config file', async () => { - const { command, deploy, error, loadConfig, configPath, onSuccess, onFailure } = mockAll({ + const { command, deploy, error, loadConfig, configPath, onFailure } = mockAll({ shouldErrorOnDeploy: true, }); @@ -89,15 +70,13 @@ describe('withConfig', () => { expect(loadConfig).toHaveBeenCalledTimes(1); expect(loadConfig.mock.calls[0][0]).toEqual(configPath); - expect(onSuccess).toHaveBeenCalledTimes(0); - expect(error).toHaveBeenCalledTimes(1); expect(onFailure).toHaveBeenCalledTimes(1); - expect(onFailure.mock.calls[0][0].toString()).toMatch(/something.+happened/i); + expect(onFailure.mock.calls[0][1].toString()).toMatch(/something.+happened/i); }); test('should handle error when loading config file', async () => { - const { command, deploy, error, loadConfig, configPath, onSuccess } = mockAll({ + const { command, deploy, error, loadConfig, configPath } = mockAll({ shouldErrorOnLoadConfig: true, }); @@ -106,8 +85,6 @@ describe('withConfig', () => { expect(loadConfig).toHaveBeenCalledTimes(1); expect(loadConfig.mock.calls[0][0]).toEqual(configPath); - expect(onSuccess).toHaveBeenCalledTimes(0); - expect(error).toHaveBeenCalledTimes(1); }); }); diff --git a/packages/fuels/src/cli/commands/withConfig.ts b/packages/fuels/src/cli/commands/withConfig.ts index 7dbd9883d74..86ce2484dd5 100644 --- a/packages/fuels/src/cli/commands/withConfig.ts +++ b/packages/fuels/src/cli/commands/withConfig.ts @@ -8,7 +8,7 @@ import { error, log } from '../utils/logger'; export const withConfigErrorHandler = async (err: Error, config?: FuelsConfig) => { error(err.message); if (config) { - await config.onFailure?.(err, config); + await config.onFailure?.(config, err); } }; @@ -33,15 +33,7 @@ export function withConfig( } try { - const eventData = await fn(config, program); - config.onSuccess?.( - { - type: command, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: eventData as any, - }, - config - ); + await fn(config, program); log(`🎉 ${capitalizeString(command)} completed successfully!`); } catch (err: unknown) { await withConfigErrorHandler(err, config); diff --git a/packages/fuels/src/cli/types.ts b/packages/fuels/src/cli/types.ts index 1f4b7b41ef3..72dea7fc8fd 100644 --- a/packages/fuels/src/cli/types.ts +++ b/packages/fuels/src/cli/types.ts @@ -12,7 +12,7 @@ export enum Commands { export type CommandEvent = | { type: Commands.build; - data: unknown; + data: void; } | { type: Commands.deploy; @@ -20,19 +20,19 @@ export type CommandEvent = } | { type: Commands.dev; - data: unknown; + data: void; } | { type: Commands.init; - data: unknown; + data: void; } | { type: Commands.versions; - data: unknown; + data: void; } | { type: Commands.node; - data: unknown; + data: void; }; export type DeployedContract = { @@ -50,6 +50,11 @@ export type OptionsFunction = ( options: ContractDeployOptions ) => DeployContractOptions | Promise; +export type FuelsEventListener = ( + config: FuelsConfig, + data: Extract['data'] +) => void; + export type UserFuelsConfig = { /** Relative directory path to Forc workspace */ workspace?: string; @@ -109,18 +114,40 @@ export type UserFuelsConfig = { forcBuildFlags?: string[]; /** - * Function callback, will be called after a successful run - * @param event - The event that triggered this execution + * Function callback, will be called after a successful build operation + * + * @param config - The loaded `fuels.config.ts` + */ + onBuild?: FuelsEventListener; + + /** + * Function callback, will be called after a successful deploy operation + * * @param config - The loaded `fuels.config.ts` + * @param data - the deployed contracts */ - onSuccess?: (event: CommandEvent, config: FuelsConfig) => void; + onDeploy?: FuelsEventListener; + + /** + * Function callback, will be called after a successful dev operation + * + * @param config - The loaded `fuels.config.ts` + */ + onDev?: FuelsEventListener; + + /** + * Function callback, will be called after a successful Node refresh operation + * + * @param config - The loaded `fuels.config.ts` + */ + onNode?: FuelsEventListener; /** * Function callback, will be called in case of errors - * @param error - Original error object * @param config - Configuration in use + * @param error - Original error object */ - onFailure?: (event: Error, config: FuelsConfig) => void; + onFailure?: (config: FuelsConfig, error: Error) => void; }; export type FuelsConfig = UserFuelsConfig &