Skip to content

Commit ba0f3d0

Browse files
Dhaiwat10Torres-ssfmaschad
authored
feat: add new node command to fuels CLI (#2376)
* feat: add new `node` command to `fuels` CLI * add command to readme --------- Co-authored-by: Sérgio Torres <[email protected]> Co-authored-by: Chad Nehemiah <[email protected]>
1 parent 4c3b4c8 commit ba0f3d0

File tree

8 files changed

+171
-0
lines changed

8 files changed

+171
-0
lines changed

Diff for: .changeset/quiet-lobsters-design.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"fuels": patch
3+
---
4+
5+
feat: add new `node` command to `fuels` CLI

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Options:
6666

6767
Commands:
6868
init [options] Create a sample `fuel.config.ts` file
69+
node [options] Start a Fuel node
6970
dev [options] Start a Fuel node and run build + deploy on every file change
7071
build [options] Build Sway programs and generate Typescript for them
7172
deploy [options] Deploy contracts to the Fuel network

Diff for: apps/docs/src/guide/fuels-cli/commands.md

+8
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ The `fuels dev` command does three things:
131131

132132
> _In `dev` mode, every time you update a contract on your Forc `workspace`, we re-generate type definitions and factory classes for it, following your pre-configured [`output`](./config-file.md#output) directory. If it's part of another build system running in dev mode (i.e. `next dev`), you can expect it to re-build / auto-reload as well._
133133
134+
## `fuels node`
135+
136+
```console-vue
137+
npx fuels@{{fuels}} node
138+
```
139+
140+
The `fuels node` command starts a short-lived `fuel-core` node ([docs](./config-file.md#autostartfuelcore)).
141+
134142
## `fuels typegen`
135143

136144
Manually generates type definitions and factory classes from ABI JSON files.

Diff for: packages/fuels/src/cli.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ describe('cli.js', () => {
3434

3535
const init = cmd(Commands.init);
3636
const dev = cmd(Commands.dev);
37+
const node = cmd(Commands.node);
3738
const build = cmd(Commands.build);
3839
const deploy = cmd(Commands.deploy);
3940

4041
expect(init).toBeTruthy();
4142
expect(dev).toBeTruthy();
43+
expect(node).toBeTruthy();
4244
expect(build).toBeTruthy();
4345
expect(deploy).toBeTruthy();
4446

@@ -47,6 +49,7 @@ describe('cli.js', () => {
4749

4850
expect(init?.opts()).toEqual({ path });
4951
expect(dev?.opts()).toEqual({ path });
52+
expect(node?.opts()).toEqual({ path });
5053
expect(build?.opts()).toEqual({ path });
5154
expect(deploy?.opts()).toEqual({ path });
5255
});

Diff for: packages/fuels/src/cli.ts

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { build } from './cli/commands/build';
77
import { deploy } from './cli/commands/deploy';
88
import { dev } from './cli/commands/dev';
99
import { init } from './cli/commands/init';
10+
import { node } from './cli/commands/node';
1011
import { withBinaryPaths } from './cli/commands/withBinaryPaths';
1112
import { withConfig } from './cli/commands/withConfig';
1213
import { withProgram } from './cli/commands/withProgram';
@@ -66,6 +67,11 @@ export const configureCli = () => {
6667
.addOption(pathOption)
6768
.action(withConfig(command, Commands.dev, dev));
6869

70+
(command = program.command(Commands.node))
71+
.description('Start a Fuel node')
72+
.addOption(pathOption)
73+
.action(withConfig(command, Commands.node, node));
74+
6975
(command = program.command(Commands.build))
7076
.description('Build Sway programs and generate Typescript for them')
7177
.addOption(pathOption)

Diff for: packages/fuels/src/cli/commands/node/index.test.ts

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { FSWatcher } from 'chokidar';
2+
3+
import { fuelsConfig } from '../../../../test/fixtures/fuels.config';
4+
import { mockStartFuelCore } from '../../../../test/utils/mockAutoStartFuelCore';
5+
import { mockLogger } from '../../../../test/utils/mockLogger';
6+
import * as loadConfigMod from '../../config/loadConfig';
7+
import type { FuelsConfig } from '../../types';
8+
import * as withConfigMod from '../withConfig';
9+
10+
import { closeAllFileHandlers, configFileChanged, getConfigFilepathsToWatch } from '.';
11+
12+
/**
13+
* @group node
14+
*/
15+
describe('node', () => {
16+
beforeEach(() => {
17+
vi.restoreAllMocks();
18+
});
19+
20+
function mockAll() {
21+
const { autoStartFuelCore, fuelCore, killChildProcess } = mockStartFuelCore();
22+
23+
const onFailure = vi.fn();
24+
25+
const withConfigErrorHandler = vi
26+
.spyOn(withConfigMod, 'withConfigErrorHandler')
27+
.mockReturnValue(Promise.resolve());
28+
29+
const loadConfig = vi
30+
.spyOn(loadConfigMod, 'loadConfig')
31+
.mockReturnValue(Promise.resolve(fuelsConfig));
32+
33+
return {
34+
autoStartFuelCore,
35+
fuelCore,
36+
killChildProcess,
37+
loadConfig,
38+
onFailure,
39+
withConfigErrorHandler,
40+
};
41+
}
42+
43+
test('should call `close` on all file handlers', () => {
44+
const close = vi.fn();
45+
46+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
47+
const handlers: any = [{ close }, { close }, { close }];
48+
49+
closeAllFileHandlers(handlers);
50+
51+
expect(close).toHaveBeenCalledTimes(3);
52+
});
53+
54+
test('should restart everything when config file changes', async () => {
55+
const { log } = mockLogger();
56+
const { autoStartFuelCore, fuelCore, killChildProcess, loadConfig, withConfigErrorHandler } =
57+
mockAll();
58+
59+
const config = structuredClone(fuelsConfig);
60+
const close = vi.fn();
61+
const watchHandlers = [{ close }, { close }] as unknown as FSWatcher[];
62+
63+
await configFileChanged({ config, fuelCore, watchHandlers })('event', 'some/path');
64+
65+
// configFileChanged() internals
66+
expect(log).toHaveBeenCalledTimes(1);
67+
expect(close).toHaveBeenCalledTimes(2);
68+
expect(killChildProcess).toHaveBeenCalledTimes(1);
69+
expect(loadConfig).toHaveBeenCalledTimes(1);
70+
71+
// node() internals
72+
expect(autoStartFuelCore).toHaveBeenCalledTimes(1);
73+
expect(withConfigErrorHandler).toHaveBeenCalledTimes(0); // never error
74+
});
75+
76+
test('should collect only non-null config paths', () => {
77+
const config: FuelsConfig = structuredClone(fuelsConfig);
78+
79+
config.snapshotDir = undefined;
80+
expect(getConfigFilepathsToWatch(config)).toHaveLength(1);
81+
82+
config.snapshotDir = '/some/path/to/chainConfig.json';
83+
expect(getConfigFilepathsToWatch(config)).toHaveLength(2);
84+
});
85+
});

Diff for: packages/fuels/src/cli/commands/node/index.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { watch, type FSWatcher } from 'chokidar';
2+
3+
import { loadConfig } from '../../config/loadConfig';
4+
import type { FuelsConfig } from '../../types';
5+
import { error, log } from '../../utils/logger';
6+
import type { FuelCoreNode } from '../dev/autoStartFuelCore';
7+
import { autoStartFuelCore } from '../dev/autoStartFuelCore';
8+
import { withConfigErrorHandler } from '../withConfig';
9+
10+
export type NodeState = {
11+
config: FuelsConfig;
12+
watchHandlers: FSWatcher[];
13+
fuelCore?: FuelCoreNode;
14+
};
15+
16+
export const getConfigFilepathsToWatch = (config: FuelsConfig) => {
17+
const configFilePathsToWatch: string[] = [config.configPath];
18+
if (config.snapshotDir) {
19+
configFilePathsToWatch.push(config.snapshotDir);
20+
}
21+
return configFilePathsToWatch;
22+
};
23+
24+
export const closeAllFileHandlers = (handlers: FSWatcher[]) => {
25+
handlers.forEach((h) => h.close());
26+
};
27+
28+
export const configFileChanged = (state: NodeState) => async (_event: string, path: string) => {
29+
log(`\nFile changed: ${path}`);
30+
31+
closeAllFileHandlers(state.watchHandlers);
32+
state.fuelCore?.killChildProcess();
33+
34+
try {
35+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
36+
await node(await loadConfig(state.config.basePath));
37+
} catch (err: unknown) {
38+
await withConfigErrorHandler(<Error>err, state.config);
39+
}
40+
};
41+
42+
export const node = async (config: FuelsConfig) => {
43+
const fuelCore = await autoStartFuelCore(config);
44+
45+
const configFilePaths = getConfigFilepathsToWatch(config);
46+
47+
try {
48+
const watchHandlers: FSWatcher[] = [];
49+
const options = { persistent: true, ignoreInitial: true, ignored: '**/out/**' };
50+
const state = { config, watchHandlers, fuelCore };
51+
52+
// watch: fuels.config.ts and snapshotDir
53+
watchHandlers.push(watch(configFilePaths, options).on('all', configFileChanged(state)));
54+
} catch (err: unknown) {
55+
error(err);
56+
throw err;
57+
}
58+
};

Diff for: packages/fuels/src/cli/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum Commands {
66
dev = 'dev',
77
init = 'init',
88
versions = 'versions',
9+
node = 'node',
910
}
1011

1112
export type CommandEvent =
@@ -28,6 +29,10 @@ export type CommandEvent =
2829
| {
2930
type: Commands.versions;
3031
data: unknown;
32+
}
33+
| {
34+
type: Commands.node;
35+
data: unknown;
3136
};
3237

3338
export type DeployedContract = {

0 commit comments

Comments
 (0)