Skip to content

Commit

Permalink
chore: add infra to support multiple create fuels templates (#2851)
Browse files Browse the repository at this point in the history
* chore: add infra to support multiple `create fuels` templates

* add docs

* disable pr release

* move template name validation to the top

* add default value

* refactor

* pass template name in tests via args

* mock `doesTemplateExist`

* remove param

* add missing testing groups

* refactor

* switch to named params for `generateArgs.ts`

* use `spyOn` to mock

* use `mockReturnValueOnce`
  • Loading branch information
Dhaiwat10 committed Aug 7, 2024
1 parent 0110fd8 commit 4e82ad4
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/wet-elephants-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-fuels": patch
---

chore: add infra to support multiple `create fuels` templates
4 changes: 4 additions & 0 deletions apps/docs/src/guide/creating-a-fuel-dapp/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ bunx --bun create-fuels@{{fuels}} --bun [project-name] [options]

:::

## `--template <template-name>`

Specifies the template to use for your project. The available templates are: `nextjs`.

## `--pnpm`

Notifies the tool to use pnpm as the package manager to install the necessary dependencies.
Expand Down
1 change: 0 additions & 1 deletion packages/create-fuels/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { runScaffoldCli, setupProgram } from './cli';

runScaffoldCli({
program: setupProgram(),
templateName: 'nextjs',
args: process.argv,
})
.then(() => process.exit(0))
Expand Down
22 changes: 17 additions & 5 deletions packages/create-fuels/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import ora from 'ora';
import { join } from 'path';

import { tryInstallFuelUp } from './lib';
import { doesTemplateExist } from './lib/doesTemplateExist';
import { getPackageManager } from './lib/getPackageManager';
import { getPackageVersion } from './lib/getPackageVersion';
import type { ProgramOptions } from './lib/setupProgram';
import { defaultTemplate, templates } from './lib/setupProgram';
import type { Template, ProgramOptions } from './lib/setupProgram';
import { promptForProjectPath } from './prompts';
import { error, log } from './utils/logger';

Expand All @@ -39,18 +41,28 @@ function writeEnvFile(envFilePath: string) {

export const runScaffoldCli = async ({
program,
templateName = 'nextjs',
args = process.argv,
}: {
program: Command;
args: string[];
templateName: string;
}) => {
program.parse(args);
const opts = program.opts<ProgramOptions>();

const templateOfChoice = (opts.template ?? defaultTemplate) as Template;

if (!doesTemplateExist(templateOfChoice)) {
error(`Template '${templateOfChoice}' does not exist.`);
log();
log('Available templates:');
for (const template of templates) {
log(` - ${template}`);
}
process.exit(1);
}

let projectPath = program.args[0] ?? (await promptForProjectPath());

const opts = program.opts<ProgramOptions>();
const verboseEnabled = opts.verbose ?? false;
const packageManager = getPackageManager(opts);

Expand Down Expand Up @@ -93,7 +105,7 @@ export const runScaffoldCli = async ({

await mkdir(projectPath);

const templateDir = join(__dirname, '..', 'templates', templateName);
const templateDir = join(__dirname, '..', 'templates', templateOfChoice);
await cp(templateDir, projectPath, {
recursive: true,
filter: (filename) => !filename.includes('CHANGELOG.md'),
Expand Down
13 changes: 13 additions & 0 deletions packages/create-fuels/src/lib/doesTemplateExist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { doesTemplateExist } from './doesTemplateExist';

/**
* @group node
*/
test('doesTemplateExist should return true if the template exists', () => {
expect(doesTemplateExist('nextjs')).toBeTruthy();
});

test('doesTemplateExist should return false if the template does not exist', () => {
// @ts-expect-error intentionally passing in a non-existent template
expect(doesTemplateExist('non-existent-template')).toBeFalsy();
});
4 changes: 4 additions & 0 deletions packages/create-fuels/src/lib/doesTemplateExist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Template } from './setupProgram';
import { templates } from './setupProgram';

export const doesTemplateExist = (templateName: Template): boolean => templates.has(templateName);
12 changes: 11 additions & 1 deletion packages/create-fuels/src/lib/setupProgram.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,22 @@ import { setupProgram } from './setupProgram';
describe('setupProgram', () => {
test('setupProgram takes in args properly', () => {
const program = setupProgram();
program.parse(['', '', 'test-project-name', '--pnpm', '--npm', '--bun']);
program.parse([
'',
'',
'test-project-name',
'--template',
'nextjs',
'--pnpm',
'--npm',
'--bun',
]);
expect(program.args[0]).toBe('test-project-name');
expect(program.opts().pnpm).toBe(true);
expect(program.opts().npm).toBe(true);
expect(program.opts().bun).toBe(true);
expect(program.opts().install).toBe(true);
expect(program.opts().template).toBe('nextjs');
});

test('setupProgram - no args', () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/create-fuels/src/lib/setupProgram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Command } from 'commander';

import packageJson from '../../package.json';

export type Template = 'nextjs';
export const templates: Set<Template> = new Set(['nextjs']);
export const defaultTemplate: Template = 'nextjs';

export interface ProgramOptions {
contract?: boolean;
predicate?: boolean;
Expand All @@ -11,6 +15,7 @@ export interface ProgramOptions {
bun?: boolean;
verbose?: boolean;
install?: boolean;
template?: Template;
}

export const setupProgram = () => {
Expand All @@ -22,6 +27,7 @@ export const setupProgram = () => {
.option('--bun', 'Use bun to install dependencies')
.option('--verbose', 'Enable verbose logging')
.option('--no-install', 'Do not install dependencies')
.option('--template <template>', 'Specify a template to use', defaultTemplate)
.addHelpCommand()
.showHelpAfterError(true);
return program;
Expand Down
53 changes: 44 additions & 9 deletions packages/create-fuels/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { mkdirSync, readFileSync } from 'fs';
import { join } from 'path';

import { runScaffoldCli, setupProgram } from '../src/cli';
import * as doesTemplateExistMod from '../src/lib/doesTemplateExist';
import { templates } from '../src/lib/setupProgram';

import type { ProjectPaths } from './utils/bootstrapProject';
import {
Expand All @@ -20,7 +22,7 @@ import { filterOriginalTemplateFiles, getAllFiles } from './utils/templateFiles'
* @group node
*/
describe('CLI', { timeout: 15_000 }, () => {
const { error } = mockLogger();
const { error, log } = mockLogger();
let paths: ProjectPaths;

beforeEach(() => {
Expand All @@ -39,11 +41,12 @@ describe('CLI', { timeout: 15_000 }, () => {
});

test('create-fuels extracts the template to the specified directory', async () => {
const args = generateArgv(paths.projectRoot);
const args = generateArgv({ projectName: paths.projectRoot, template: paths.templateName });

vi.spyOn(doesTemplateExistMod, 'doesTemplateExist').mockReturnValueOnce(true);

await runScaffoldCli({
program: setupProgram(),
templateName: paths.templateName,
args,
});

Expand All @@ -56,11 +59,12 @@ describe('CLI', { timeout: 15_000 }, () => {
});

test('create-fuels checks the versions on the fuel-toolchain file', async () => {
const args = generateArgv(paths.projectRoot);
const args = generateArgv({ projectName: paths.projectRoot, template: paths.templateName });

vi.spyOn(doesTemplateExistMod, 'doesTemplateExist').mockReturnValueOnce(true);

await runScaffoldCli({
program: setupProgram(),
templateName: paths.templateName,
args,
});

Expand All @@ -75,11 +79,16 @@ describe('CLI', { timeout: 15_000 }, () => {
});

test('should rewrite for the appropriate package manager', async () => {
const args = generateArgv(paths.projectRoot, 'bun');
const args = generateArgv({
projectName: paths.projectRoot,
packageManager: 'bun',
template: paths.templateName,
});

vi.spyOn(doesTemplateExistMod, 'doesTemplateExist').mockReturnValueOnce(true);

await runScaffoldCli({
program: setupProgram(),
templateName: paths.templateName,
args,
});

Expand All @@ -94,15 +103,16 @@ describe('CLI', { timeout: 15_000 }, () => {
});

test('create-fuels reports an error if the project directory already exists', async () => {
const args = generateArgv(paths.projectRoot);
const args = generateArgv({ projectName: paths.projectRoot, template: paths.templateName });

// Generate the project once
mkdirSync(paths.projectRoot, { recursive: true });

vi.spyOn(doesTemplateExistMod, 'doesTemplateExist').mockReturnValueOnce(true);

// Generate the project again
await runScaffoldCli({
program: setupProgram(),
templateName: paths.templateName,
args,
}).catch((e) => {
expect(e).toBeInstanceOf(Error);
Expand All @@ -112,4 +122,29 @@ describe('CLI', { timeout: 15_000 }, () => {
expect.stringContaining(`A folder already exists at ${paths.projectRoot}`)
);
});

test('create-fuels reports an error if the template does not exist', async () => {
const args = generateArgv({
projectName: paths.projectRoot,
template: 'non-existent-template',
});

vi.spyOn(doesTemplateExistMod, 'doesTemplateExist').mockReturnValueOnce(false);

await runScaffoldCli({
program: setupProgram(),
args,
}).catch((e) => {
expect(e).toBeInstanceOf(Error);
});

expect(error).toHaveBeenCalledWith(
expect.stringContaining(`Template 'non-existent-template' does not exist.`)
);
expect(log).toHaveBeenCalledWith();
expect(log).toHaveBeenCalledWith('Available templates:');
for (const template of templates) {
expect(log).toHaveBeenCalledWith(` - ${template}`);
}
});
});
2 changes: 1 addition & 1 deletion packages/create-fuels/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('`create fuels` package integrity', () => {
`"fuels": "[0-9]+.[0-9]+.[0-9]+-${PUBLISHED_NPM_TAG}-[0-9]+"`
);

const args = generateArgs(paths.projectRoot, packageManager).join(' ');
const args = generateArgs({ projectName: paths.projectRoot, packageManager }).join(' ');
const expectedTemplateFiles = await getAllFiles(paths.templateSource).then((files) =>
filterOriginalTemplateFiles(files).filter(filterForcBuildFiles)
);
Expand Down
28 changes: 22 additions & 6 deletions packages/create-fuels/test/utils/generateArgs.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
export const generateArgs = (projectName?: string, packageManager: string = 'pnpm'): string[] => {
export const generateArgs = ({
projectName,
packageManager = 'pnpm',
template,
}: {
projectName?: string;
packageManager: string;
template?: string;
}): string[] => {
const args = [];
if (packageManager === 'npm') {
args.push('--');
}
if (projectName) {
args.push(projectName);
}
if (template) {
args.push(`--template`);
args.push(template);
}
args.push(`--${packageManager}`);
args.push(`--no-install`);
return args;
};

export const generateArgv = (projectName?: string, packageManager: string = 'pnpm'): string[] => [
'',
'',
...generateArgs(projectName, packageManager),
];
export const generateArgv = ({
projectName,
packageManager = 'pnpm',
template,
}: {
projectName?: string;
packageManager?: string;
template?: string;
}): string[] => ['', '', ...generateArgs({ projectName, packageManager, template })];

0 comments on commit 4e82ad4

Please sign in to comment.