Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve fuels init directory detection #3638

Merged
5 changes: 5 additions & 0 deletions .changeset/serious-pens-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"fuels": patch
---

fix: improve `fuels init` directory detection
88 changes: 59 additions & 29 deletions packages/fuels/src/cli/commands/init/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FuelError } from '@fuel-ts/errors';
import { type Command } from 'commander';
import { existsSync, writeFileSync } from 'fs';
import { globSync } from 'glob';
import { join, relative, resolve } from 'path';

import { findPrograms } from '../../config/forcUtils';
import { renderFuelsConfigTemplate } from '../../templates/fuels.config';
import { log } from '../../utils/logger';

Expand All @@ -25,47 +25,77 @@ export function init(program: Command) {

const [contracts, scripts, predicates] = ['contracts', 'scripts', 'predicates'].map(
(optionName) => {
const pathOrGlob: string = options[optionName];
if (!pathOrGlob) {
const pathsOrGlobs: string[] = options[optionName];
if (!pathsOrGlobs) {
return undefined;
}
const expanded = globSync(pathOrGlob, { cwd: path });
const relatives = expanded.map((e) => relative(path, e));
return relatives;

const selectedSwayType = optionName.slice(0, -1);

const programs = pathsOrGlobs.flatMap((pathOrGlob) =>
findPrograms(pathOrGlob, { cwd: path })
);
const programDirs = programs
.filter(({ swayType }) => swayType === selectedSwayType)
.map(({ path: programPath }) => relative(path, programPath));
return programDirs;
}
);

// Check that at least one of the options is informed
const noneIsInformed = ![workspace, contracts, scripts, predicates].find((v) => v !== undefined);

if (noneIsInformed) {
// mimicking commander property validation
// eslint-disable-next-line no-console
console.log(`error: required option '-w, --workspace <path>' not specified\r`);
process.exit(1);
} else {
const fuelsConfigPath = join(path, 'fuels.config.ts');
}

if (existsSync(fuelsConfigPath)) {
throw new FuelError(
FuelError.CODES.CONFIG_FILE_ALREADY_EXISTS,
`Config file exists, aborting.\n ${fuelsConfigPath}`
);
// Ensure that every program that is defined, has at least one program
const programLengths = [contracts, scripts, predicates]
.filter(Boolean)
.map((programs) => programs?.length);
if (programLengths.some((length) => length === 0)) {
const [contractLength, scriptLength, predicateLength] = programLengths;

const message = ['error: unable to detect program/s'];
if (contractLength === 0) {
message.push(`- contract/s detected ${contractLength}`);
}
if (scriptLength === 0) {
message.push(`- script/s detected ${scriptLength}`);
}
if (predicateLength === 0) {
message.push(`- predicate/s detected ${predicateLength}`);
}

const renderedConfig = renderFuelsConfigTemplate({
workspace,
contracts,
scripts,
predicates,
output,
forcPath,
fuelCorePath,
autoStartFuelCore,
fuelCorePort,
});

writeFileSync(fuelsConfigPath, renderedConfig);

log(`Config file created at:\n\n ${fuelsConfigPath}\n`);
// eslint-disable-next-line no-console
console.log(message.join('\r\n'));
process.exit(1);
}

const fuelsConfigPath = join(path, 'fuels.config.ts');

if (existsSync(fuelsConfigPath)) {
throw new FuelError(
FuelError.CODES.CONFIG_FILE_ALREADY_EXISTS,
`Config file exists, aborting.\n ${fuelsConfigPath}`
);
}

const renderedConfig = renderFuelsConfigTemplate({
workspace,
contracts,
scripts,
predicates,
output,
forcPath,
fuelCorePath,
autoStartFuelCore,
fuelCorePort,
});

writeFileSync(fuelsConfigPath, renderedConfig);

log(`Config file created at:\n\n ${fuelsConfigPath}\n`);
}
19 changes: 18 additions & 1 deletion packages/fuels/src/cli/config/forcUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FuelError } from '@fuel-ts/errors';
import { readFileSync, existsSync, writeFileSync } from 'fs';
import { globSync } from 'glob';
import camelCase from 'lodash.camelcase';
import { join } from 'path';
import { dirname, join } from 'path';
import toml from 'toml';

import type { FuelsConfig } from '../types';
Expand Down Expand Up @@ -152,3 +153,19 @@ export const getStorageSlotsPath = (contractPath: string, { buildMode }: FuelsCo
const projectName = getContractName(contractPath);
return join(contractPath, `/out/${buildMode}/${projectName}-storage_slots.json`);
};

export const findPrograms = (pathOrGlob: string, opts?: { cwd?: string }) => {
const pathWithoutGlob = pathOrGlob.replace(/[/][*]*$/, '').replace(opts?.cwd ?? '', '');
const absolutePath = join(opts?.cwd ?? '', pathWithoutGlob);
const allTomlPaths = globSync(`${absolutePath}/**/*.toml`);

return (
allTomlPaths
// Filter out the workspace
.map((path) => ({ path, isWorkspace: readForcToml(path).workspace !== undefined }))
.filter(({ isWorkspace }) => !isWorkspace)
// Parse the sway type and filter out the library
.map(({ path }) => ({ path: dirname(path), swayType: readSwayType(dirname(path)) }))
.filter(({ swayType }) => swayType !== SwayType.library)
);
};
16 changes: 16 additions & 0 deletions packages/fuels/src/cli/config/loadConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ describe('loadConfig', () => {
expect(config.predicates.length).toEqual(0);
});

test(`should resolve a single contract`, async () => {
await runInit({
root: paths.root,
output: paths.outputDir,
forcPath: paths.forcPath,
fuelCorePath: paths.fuelCorePath,
contracts: 'workspace/contracts/bar/*',
});

const config = await loadConfig(paths.root);

expect(config.contracts.length).toEqual(1);
expect(config.scripts.length).toEqual(0);
expect(config.predicates.length).toEqual(0);
});

test(`should resolve only scripts`, async () => {
await runInit({
root: paths.root,
Expand Down
145 changes: 119 additions & 26 deletions packages/fuels/test/features/init.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import chalk from 'chalk';
import { existsSync, readFileSync } from 'fs';
import { existsSync } from 'fs';

import { Commands } from '../../src';
import { mockCheckForUpdates } from '../utils/mockCheckForUpdates';
Expand All @@ -10,6 +10,7 @@ import {
runInit,
resetDiskAndMocks,
resetConfigAndMocks,
loadFuelsConfig,
} from '../utils/runCommands';

/**
Expand Down Expand Up @@ -39,14 +40,14 @@ describe('init', () => {
});

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();
const fuelsContents = readFileSync(paths.fuelsConfigPath, 'utf-8');
expect(fuelsContents).toMatch(`workspace: './workspace',`);
expect(fuelsContents).toMatch(`output: './output',`);
expect(fuelsContents).not.toMatch(`forcPath: 'fuels-forc',`);
expect(fuelsContents).not.toMatch(`fuelCorePath: 'fuels-core',`);
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
workspace: './workspace',
output: './output',
});
});

it('should run `init` command with --contracts', async () => {
it('should run `init` command with --contracts [absolute paths]', async () => {
await runInit({
root: paths.root,
contracts: [paths.contractsBarDir, paths.contractsFooDir],
Expand All @@ -59,25 +60,109 @@ describe('init', () => {
];

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();
const fuelsContents = readFileSync(paths.fuelsConfigPath, 'utf-8');
expect(fuelsContents).toMatch(/contracts:/);
expect(fuelsContents).toMatch(relativeBarDir);
expect(fuelsContents).toMatch(relativeFooDir);
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
contracts: [relativeBarDir, relativeFooDir],
output: './output',
});
});

it('should run `init` command with --contracts [glob path - multiple matches]', async () => {
await runInit({
root: paths.root,
contracts: [`${paths.contractsDir}/*`],
output: paths.outputDir,
});

const relativeContractPaths = [
paths.upgradableChunkedContractPath,
paths.upgradableContractPath,
paths.contractsFooDir,
paths.contractsBarDir,
].map((path) => path.replace(paths.workspaceDir, 'workspace'));

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
contracts: relativeContractPaths,
output: './output',
});
});

it('should run `init` command with --contracts [glob path - single path]', async () => {
await runInit({
root: paths.root,
contracts: [`${paths.contractsBarDir}/*`],
output: paths.outputDir,
});

const [relativeBarDir] = [paths.contractsBarDir.replace(paths.workspaceDir, 'workspace')];

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();

const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
contracts: [relativeBarDir],
output: './output',
});
});

it('should run `init` command with --contracts [glob path - multiple contracts]', async () => {
await runInit({
root: paths.root,
contracts: [`${paths.workspaceDir}/*`],
output: paths.outputDir,
});

const relativeContractPaths = [
paths.upgradableChunkedContractPath,
paths.upgradableContractPath,
paths.contractsFooDir,
paths.contractsBarDir,
].map((path) => path.replace(paths.workspaceDir, 'workspace'));

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();

const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
contracts: relativeContractPaths,
output: './output',
});
});

it('should run `init` command with --contracts [no matches - log error]', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const exit = vi.spyOn(process, 'exit').mockResolvedValue({} as never);

await runInit({
root: paths.root,
contracts: [`${paths.predicatesDir}/*`],
output: paths.outputDir,
});

expect(logSpy).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledWith(
['error: unable to detect program/s', '- contract/s detected 0'].join('\r\n')
);
expect(exit).toHaveBeenCalledTimes(1);
expect(exit).toHaveBeenCalledWith(1);
});

it('should run `init` command with --predicates', async () => {
await runInit({
root: paths.root,
predicates: paths.predicateDir,
predicates: paths.predicatesDir,
output: paths.outputDir,
});

const relativePredicateDir = paths.predicateDir.replace(paths.workspaceDir, 'workspace');

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();
const fuelsContents = readFileSync(paths.fuelsConfigPath, 'utf-8');
expect(fuelsContents).toMatch(/predicates:/);
expect(fuelsContents).toMatch(relativePredicateDir);
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
predicates: [relativePredicateDir],
output: './output',
});
});

it('should run `init` command with --scripts', async () => {
Expand All @@ -87,12 +172,14 @@ describe('init', () => {
output: paths.outputDir,
});

const relativeScriptDir = paths.scriptsDir.replace(paths.workspaceDir, 'workspace');
const relativeScriptDir = paths.scriptDir.replace(paths.workspaceDir, 'workspace');

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();
const fuelsContents = readFileSync(paths.fuelsConfigPath, 'utf-8');
expect(fuelsContents).toMatch(/scripts:/);
expect(fuelsContents).toMatch(relativeScriptDir);
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
scripts: [relativeScriptDir],
output: './output',
});
});

it('should run `init` command using custom binaries', async () => {
Expand All @@ -105,11 +192,13 @@ describe('init', () => {
});

expect(existsSync(paths.fuelsConfigPath)).toBeTruthy();
const fuelsContents = readFileSync(paths.fuelsConfigPath, 'utf-8');
expect(fuelsContents).toMatch(`workspace: './workspace',`);
expect(fuelsContents).toMatch(`output: './output',`);
expect(fuelsContents).toMatch(`forcPath: 'fuels-forc',`);
expect(fuelsContents).toMatch(`fuelCorePath: 'fuels-core',`);
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
workspace: './workspace',
output: './output',
forcPath: 'fuels-forc',
fuelCorePath: 'fuels-core',
});
});

it('should run `init` command with custom fuel-core-port', async () => {
Expand All @@ -120,8 +209,12 @@ describe('init', () => {
fuelCorePort: '1234',
});

const fuelsContents = readFileSync(paths.fuelsConfigPath, 'utf-8');
expect(fuelsContents).toMatch(`fuelCorePort: 1234,`);
const fuelsConfig = await loadFuelsConfig(paths.fuelsConfigPath);
expect(fuelsConfig).toEqual({
fuelCorePort: 1234,
output: './output',
workspace: './workspace',
});
});

it('should run `init` command and throw for existent config file', async () => {
Expand Down
Loading
Loading