Description
We need to refactor the way our commands work so the steps are more easily reused and developers can access the commands programmatically.
User Stories
As an Integration Engineer
I want to use the Frontity commands programmatically
so that I can integrate Frontity in my CI/hosting/deploy tools.
As an Integration Engineer
I want to use env variables to configure Frontity commands
so that I can integrate Frontity in my CI/hosting/deploy tools
As a core developer
I want to resuse steps in different commands
so that we don’t have to maintain duplicate code
As a core developer
I want to use commands in different interfaces
so that we can release more than one interface
Proposed implementation
- Logic is isolated in steps, which are the smaller unit of code.
- Multiple steps form a command.
- Commands can be called directly or by interfaces.
Steps -> Commands -> Interfaces
Steps
Steps are small functions that isolate logic that can be used by one or more commands. They should do only one task. They can return either the result of that task or a promise that resolves to the result in case the task is asynchronous. They throw an error if something goes wrong.
Some examples:
isDirEmpty
ensureDirExists
createPackageJson
createFrontitySettings
cloneNpmPackage
installDependencies
downloadFavicon
createIndexJs
subscribeToNewsletter
Each step should receive the arguments it needs to execute the task. It should not receive the whole options of the command, because can be used by more than one command.
export const downloadFavicon = async (path: string) => {
const response = await fetch(faviconUrl);
const fileStream = createWriteStream(resolvePath(path, "favicon.ico"));
response.body.pipe(fileStream);
await new Promise(resolve => fileStream.on("finish", resolve));
};
Commands
Commands wrap more steps together and can be used directly or by interfaces.
This is the list of the current commands:
dev
build
serve
create
create-package
subscribe
info
These are the commands we want to add in the short term:
deploy
install-package
rename-package
update-package
They will be exported in frontity/commands
(that means we have to rename the current commands.ts
file to cli.ts
).
Commands are async functions that resolve when all the steps are finished. They receive an options
object as first parameter.
import { create, build } from "frontity/commands";
await create({
name: "my-frontity-project",
starter: "@frontity/mars-theme",
typescript: false
});
await build({
buildFolder: "/public",
target: "module",
mode: "production"
});
Parameters can be optional and need a default value.
When some parameter is not included, the command will check for an environment variable with the same name and the FRONTITY
prefix. If it is not present, it will use the default.
// This build will use "/build" because it's the internal default
await build();
// This build will use "/public" because options take precedence.
process.env.FRONTITY_BUILD_FOLDER = "/dist";
await build({
buildFolder: "/public",
});
// This build will use "/dist" because options is empty.
process.env.FRONTITY_BUILD_FOLDER = "/dist";
await build();
All commands reside in the frontity
package except dev
, build
and serve
which are in the @frontity/core
package.
To reduce the size of frontity
and avoid download unrelated packages when people use npx frontity create
, the dependencies of all the steps used by the create
command will be "dependencies"
in the frontity
package, and the rest will be "devDependencies"
in frontity
and "dependencies"
in @frontity/core
. That’s because once the Frontity project is created, @frontity/core
is mandatory.
Commands need to be able to communicate their internal progress to the interfaces.
This is the part I’m not 100% sure about, but maybe we could include some type of callback that it is called when steps finish.
const buildFinished = await build(options, ({ awaiting, started, finished }) => {
// Update React state.
setAwaiting(remaining);
setStarted(started);
setFinished(finished);
const total = awaiting.length + started.length + finished.length;
setProgress(finished.length/total);
});
It’s important to note that a command can execute several steps in parallel.
If a step fails, they should bubble up the error. They may also have a mechanism to revert the changes they’ve done so far. For example: delete the folder they have created or revert the changes to a file.
Interfaces
Interfaces provide a way for users to gather the options needed for each command and then execute those commands.
Right now we have two in mind.
- Frontity CLI (current)
- Frontity Admin UI (coming)
The frontity
package will include the CLI for now, but it’s good if we start thinking that the same commands will be used by the Admin UI as well.
Frontity CLI
The cli will prompt for the options of that command:
> npx frontity create
- Name of your project? "my-frontity-project"
- Which starter theme to use? "@frontity/mars-theme"
- Do you want to use TypeScript? Y/n
If an option is passed using arguments, it will be used to populate the placeholder:
> npx frontity create --starter @frontity/mars-theme
- Name of your project? "my-frontity-project"
- Which starter theme to use? # (@frontity/mars-theme)
Only in the case of the presence of the arg --no-prompt
, the cli won’t prompt for any option and it will execute the command directly.
In that case, it will use the options provided by the arguments and it will leave the rest not defined so the command can use env variables if they are present.
> npx frontity create "my-project" --typescript --no-prompt
[/] Creating your project...
// This will run:
await create({
name: "my-project",
typescript: true
})
Command options object properties, cli params and env variable names must be easily inferrable from each other.
> npx frontity create --typescript
await create({
typescript: true
});
process.env.FRONTITY_TYPESCRIPT = true;
> npx frontity build --build-folder public
await create({
buildFolder: "public"
});
process.env.FRONTITY_BUILD_FOLDER = "public";
Next steps
This is a mixed list of issues and next features for reference. I’ll create new features for some of the items.
- Migrate all the steps to individual files and refactor commands and interface folders.
- Fix the
create
steps to be able to include more starter themes. - Refactor commands to work with the new callback function.
- Add support for env variables.
- Migrate current commands to Ink.
- Add e2e testing (in mac, win and linux with both npm and yarn).
- Fix typescript in
create
. - Improve
create
with additional questions about: git, eslint, prettier, gutenberg…