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..008c1d40654 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,11 +84,31 @@ export default createConfig({ }, // #endregion deployConfig-fn - // #region onSuccess - onSuccess: (event: CommandEvent, config: FuelsConfig) => { - console.log('fuels:onSuccess', { event, config }); + // #region onBuild + onBuild: (data: unknown, config: FuelsConfig) => { + console.log('fuels:onBuild', { data, config }); }, - // #endregion onSuccess + // #endregion onBuild + + // #region onDeploy + // #import { DeployedContract, FuelsConfig }; + + onDeploy: (data: DeployedContract[], config: FuelsConfig) => { + console.log('fuels:onDeploy', { data, config }); + }, + // #endregion onDeploy + + // #region onDev + onDev: (data: unknown, config: FuelsConfig) => { + console.log('fuels:onDev', { data, config }); + }, + // #endregion onDev + + // #region onNode + onNode: (data: unknown, config: FuelsConfig) => { + console.log('fuels:onNode', { data, config }); + }, + // #endregion onNode // #region onFailure onFailure: (error: Error, config: FuelsConfig) => { diff --git a/apps/docs/src/guide/fuels-cli/config-file.md b/apps/docs/src/guide/fuels-cli/config-file.md index 169e7e18c6b..b338bb0d577 100644 --- a/apps/docs/src/guide/fuels-cli/config-file.md +++ b/apps/docs/src/guide/fuels-cli/config-file.md @@ -121,16 +121,49 @@ 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 +- `data` — The data (always `null` for `onBuild`) - `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: + +- `data` — The data (an array of deployed contracts) +- `config` — The loaded config (`fuels.config.ts`) + +<<< @../../../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: + +- `data` — The data (always `null` for `onDev`) +- `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: + +- `data` — The data (always `null` for `onNode`) +- `config` — The loaded config (`fuels.config.ts`) + +<<< @../../../demo-fuels/fuels.config.full.ts#onNode{ts:line-numbers} ## `onFailure` 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..af87873cff8 --- /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(null, config); + }); +}); diff --git a/packages/fuels/src/cli/commands/build/index.ts b/packages/fuels/src/cli/commands/build/index.ts index 48f1738d67b..9d5bc62bd13 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?.(null, 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..63fee708272 --- /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(expectedContracts, config); + }); +}); diff --git a/packages/fuels/src/cli/commands/deploy/index.ts b/packages/fuels/src/cli/commands/deploy/index.ts index e8f1c6174bb..c1ebbf91324 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?.(contracts, config); 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..1a71477cf0e 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(null, 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..41a84affb4e 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?.(null, 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..ba8413e419d 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).toHaveBeenCalledTimes(1); 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..387f019f008 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?.(null, 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 27f47028ac2..4a4c9d88f89 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); }); 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..e351dc1ddde 100644 --- a/packages/fuels/src/cli/commands/withConfig.ts +++ b/packages/fuels/src/cli/commands/withConfig.ts @@ -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..91ff1ffa150 100644 --- a/packages/fuels/src/cli/types.ts +++ b/packages/fuels/src/cli/types.ts @@ -50,6 +50,11 @@ export type OptionsFunction = ( options: ContractDeployOptions ) => DeployContractOptions | Promise; +export type FuelsEventListener = ( + data: Extract['data'], + config: FuelsConfig +) => void; + export type UserFuelsConfig = { /** Relative directory path to Forc workspace */ workspace?: string; @@ -109,11 +114,36 @@ 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 data - The event that triggered this execution + * @param config - The loaded `fuels.config.ts` + */ + onBuild?: FuelsEventListener; + + /** + * Function callback, will be called after a successful deploy operation + * + * @param data - The event that triggered this execution + * @param config - The loaded `fuels.config.ts` + */ + onDeploy?: FuelsEventListener; + + /** + * Function callback, will be called after a successful dev operation + * + * @param data - The event that triggered this execution + * @param config - The loaded `fuels.config.ts` + */ + onDev?: FuelsEventListener; + + /** + * Function callback, will be called after a successful Node refresh operation + * + * @param data - The event that triggered this execution * @param config - The loaded `fuels.config.ts` */ - onSuccess?: (event: CommandEvent, config: FuelsConfig) => void; + onNode?: FuelsEventListener; /** * Function callback, will be called in case of errors