Choosing The Right Build Framework For Web Development

Rich Searle
XRLO — eXtended Reality Lowdown
8 min readSep 29, 2021
Decisions, decisions! Photo by Victoriano Izquierdo on Unsplash

With an ever-expanding web ecosystem and the number of options for web frameworks, it’s important to make a few initial decisions regarding the architecture of a new web project, these decisions need to be informed by the requirements of the project and the team. Monorepos are not new, but with the scale of web projects ever-growing, our processes and tooling also need to adapt.

Nx, the Smart, Extensible Build Framework. Credit: nx.dev

This won’t be an exhaustive list of features or a “getting started with Nx” article, for that, there is plenty of existing documentation available online. The purpose of this article is to discuss how, here at REWIND, we came to adopt Nx, and a brief look into a few of our favourite features.

What is Nx?

Nx, created by Nrwl.io, is described as a “Smart, Extensible Build Framework”. It facilitates a large proportion of the architecture and processes for a monorepo workspace in 2021, it improves on some of the monorepo concepts from Yarn Workspaces and Lerna, with a few tweaks and a huge amount of features related to common tooling and workspace management. In a nutshell, Nx is a tool belt for common project tasks, from managing dependencies, templating project architecture, to common tools for testing frameworks and review. As a workspace grows it is not uncommon for these tools to be introduced into a project, with Nx they can be set up within seconds with minimal configuration.

We first heard about Nx from our colleagues over at Magnopus who have recently incorporated the tool in some of their projects and workflow. After an extensive look into features and other alternatives, Nx became our monorepo manager of choice. Nx is designed to contain different technologies/frameworks within the same workspace; currently, there are core generators for React, Angular and Node applications, with plenty of other community options. In addition to Apps, we have self-contained libraries that also have plenty of customisable options. As all these applications and libraries live within the same workspace, both applications and libraries can be built, tested and consumed in different ways, while supporting different language features or workflow.

Alongside these application templates, Nrwl maintains plugins for tooling with the likes of Jest, Cypress for testing, Typescript, ESLint, Prettier for linting and Storybook. With the addition of a plethora of community plugins, there are plenty of options for integrations into less common frameworks and tools.

React, Angular, Jest, Cypress, Typescript, ESLint, Prettier and Storybook are a handful of the NX extensions that are maintained by the Nx team and are available with minimal setup. Credit: nx.dev

This core fundamental of Nx fits nicely with our workflow here at REWIND, functionality can be developed independently of each other, be switched, removed or changed based on requirements, but also keep our codebase structured in a way that makes it easier for future engineers to contribute to any of the applications or libraries within the workspace.

Nx commands And VSCode Plugin

There’s a whole host of built-in Nx CLI commands that cover common lifecycle functions, they include building, testing, creating libraries and running tools. Not only does the CLI tool cover a wide range of workspace tooling and automation, but they are also highly configurable with support for a variety of frameworks and tools out of the box.

There is also a great VSCode plugin that also allows you easy access to all the nx commands via a simple Graphical User Interface (GUI) within code.

The official Nx Console extension for VSCode, A GUI for the underlying Nx Commands of a workspace. Credit: REWIND

Workspace Structure And Affected Commands

An Example workspace’s dependency graph built with Nx’s intuitive depth-graph tool. Credit: REWIND

Let’s look at an example structure of a workspace, with the concepts of apps and libs in mind, let’s assume we have two apps (the frontend, and a backend-API service), our frontend app depends on a UI library, and a library with some common functionality shared with the API also includes a user library which in turn is split into sign-up and profile systems.

By encouraging developers to think in a modular way from a project’s inception, it allows us to develop in a way that satisfies our project process and reduces unwanted de-coupling in the future.

With this project structure in mind, let’s make a change to our user sign-up library. During the development phase, we can prefix commands with “affected” to only perform tasks on libraries that have changed, and any dependents. Let’s try an affected:test for this scenario.

$ yarn nx affected:test
yarn run v1.22.10
$ /home/rich/workspace/node_modules/.bin/nx affected:test
> NX NOTE Affected criteria defaulted to --base=master --head=HEAD
> NX Running target test for 3 project(s):
- user-signup
- user-system
- backend-api

Nx has calculated that only the backend application, user-system and the edited user-signup libs have changed or are dependent on libraries with changes. Therefore only running the tests for these apps and libs. Our frontend app and a few libraries are left alone and save time and resources. Right now this may only save seconds or minutes, but scaled up with a workspace containing dozens of apps and libraries the use of affected commands will not only greatly reduce building, testing, and deployment time, but also serves as an indicator of what applications or deployments will be affected by changes, useful for informing our pipelines and services.

Generators And Workspace Commands

Up until this point, a lot of what we have discussed has been utilising Nx’s built-in generators and executors. However, by introducing our own we can reduce repetitive tasks within our workspace. Custom generators and executors have a set design within Nx, but are not limited in their possibilities.

Let’s create a simple workspace generator. In the following scenario, we are working on a library consisting of a simple ECS system. One use for Nx’s workspace generators could be the creation of our components, not only do we wish to template the component file with a few default functions and properties, but there are also additional tasks to consider. We may want to automatically register the generated component to a world-class within the library. Let’s start by using the Nx command to generate a new workspace generator:

nx generate @nrwl/workspace:workspace-generator ecs-component

From here, we can now populate our template files and add any additional functionality required in the generated code. Let’s start by updating the component template, assuming we are extending a base Component class, and we want to boiler-plate some common functions.

Our Component template:

export class <%= componentName %> extends Component {
constructor() {
}
update() {
}
destroy() {
}
}
export default <%= componentName %>;

Now, let’s handle our additional requirements. Let’s assume we have a basic ECS class that handles the registration of components and systems. With something like the following, we can see that for the new component to be registered, we will need to import that class and add the component to the main register function.

export class ECS {
constructor() {
this.register();
}
register() {
// $components
} registerComponent(component: any) {
//ECS system register code.
...
}
}

Nothing fancy here, and obviously barebones, but let’s look at tweaking our generator code to automatically apply the changes discussed above.

export default async function (host: Tree, schema: ECSComponentSchema) {  //apply our defaults and other required params
const schemaWithDefaults = {
componentName: schema.componentName,
fileName: schema.fileName || schema.componentName,
registerToECS:schema.registerToECS || true,
directory: schema.directory || "components/"+schema.componentName
}
//load AST for our ECS register class.
const registerPath = "./libs/ecs/src/lib/register.ts";
const registerSource = host.read(registerPath, 'utf-8');
const sourceFile = ts.createSourceFile(
registerPath,
registerSource,
ts.ScriptTarget.Latest,
true
);
//define our changes
let registerChange = appendToFunction(sourceFile, "register", `this.registerComponent(${schemaWithDefaults.componentName});`);
let importChange = addImport(sourceFile, `import {${schemaWithDefaults.componentName}} from './${schemaWithDefaults.directory}/${schemaWithDefaults.fileName}';`)
//apply the changes
let changes = applyChangesToString(registerSource,[...registerChange,...importChange]);
//write changes
host.write(registerPath, changes);
//generate the templated files.
generateFiles(host, joinPathFragments(__dirname, './files'), './libs/ecs/src/lib/'+schemaWithDefaults.directory, {"tmpl":"",...schemaWithDefaults});
await formatFiles(host);
}
function appendToFunction(sourceFile: ts.SourceFile, functionName:string, textToAppend:string):StringChange[]{
const functions = findNodes(
sourceFile,
ts.SyntaxKind.MethodDeclaration
) as ts.MethodDeclaration[];
for (const expr of functions) {
if (expr.name.getText(sourceFile) === functionName) {
return [
{
type: ChangeType.Insert,
index: expr.getChildren()[expr.getChildCount() - 1].end - 1,
text: textToAppend,
},
];
}
}
return [];
}

I won’t go into too much detail about the above code, but the default exported function that was generated for us above have been updated to the following;

  1. Apply some default values to our configuration based on user input.
  2. Generate some changes to our register class but using some built-in AST helpers from within NX, one to append some code to a function, and one to add the import to the top of the file.
  3. Finally, generate our components class from the template we defined above.

Additionally, you can populate a schema.json file for your commands to be accessible and configurable through the VSCode extension. But for the purpose of demonstrating our command, we can execute it, and pass out component name via:

nx workspace-generator ecs-component-generator --componentName=GravityComponent

And there we have our generated component, with registration within our custom system. This is only just scratching the surface of what generators are capable of. Nx generators are not limited just to code templating and generation, these workspace commands could be used for a variety of purposes. Want to generate changelogs automatically? Maybe we want to generate a large amount of test data? The possibilities are endless.

Conclusion

As always, the requirements of a project could lead to technical decisions between process and library choice. There is no denying that Nx comes with a ton of features out of the box that cover a range of tooling that is essential to larger-scale web projects. Jest, Cyprus and Storybook also benefit not only the Engineering team here at REWIND, but also benefit the Design team for reviewing UI, and the QA team for tracking and identifying issues.

With Generators and Executors, we can reduce the number of repetitive tasks in an extensible way while still keeping uniformity with similar tools. Combine these with the affected build commands and we have a build, test and deploy that are only executed when needed, reducing valuable computation time and resources within our continuous integration.

If you would like to explore any of the code discussed in this article further, you can find our example workspace on REWIND’s Github account.

--

--