Build System
The Backstage build system is a collection of build and development tools that help you lint, test, develop and finally release your Backstage projects. The purpose of the build system is to provide an out-of-the-box solution that works well with Backstage and lets you focus on building an app rather than having to spend time setting up your own tooling.
The build system setup is part of the @backstage/cli, and already included in any project that you create using @backstage/create-app. It is similar to for example react-scripts, which is the tooling you get with create-react-app. The Backstage build system relies heavily on existing open source tools from the JavaScript and TypeScript ecosystem, such as Webpack, Rollup, Jest, and ESLint.
Design Considerations
There are a couple of core beliefs and constraints that guided the design of the Backstage build system. The first and most important is that we put the development experience first. If we need to cut corners or add complexity we do so in other areas, but the experience of firing up an editor and iterating on some code should always be as smooth as possible.
In addition, there are a number of hard and soft requirements:
- Monorepos - The build system should support multi-package workspaces
- Publishing - It should be possible to build and publish individual packages
- Scale - It should scale to hundreds of large packages without excessive wait times
- Reloads - The development flow should support quick on-save hot reloads
- Simple - Usage should be simple and configuration should be kept minimal
- Universal - Development towards both web applications, isomorphic packages, and Node.js
- Modern - The build system targets modern environments
- Editors - Type checking and linting should be available within most editors
During the design of the build system this collection of requirements was not
something that was supported by existing tools like for example react-scripts.
The requirements of scaling in combination of monorepo, publishing, and editor
support led us to adopting our own specialized setup.
Structure
We can divide the development flow within Backstage into a couple of different steps:
- Formatting - Applies a consistent formatting to your source code
- Linting - Analyzes your source code for potential problems
- Type Checking - Verifies that TypeScript types are valid
- Testing - Runs different levels of test suites towards your project
- Building - Compiles the source code in an individual package
- Bundling - Combines a package and all of its dependencies into a production-ready bundle
These steps are generally kept isolated from each other, with each step focusing on its specific task. For example, we do not do linting or type checking together with the building or bundling. This is so that we can provide more flexibility and avoid duplicate work, improving performance. It is strongly recommended that as a part of developing within Backstage you use a code editor or IDE that has support for formatting, linting, and type checking.
Let's dive into a detailed look at each of these steps and how they are implemented in a typical Backstage app.
Package Roles
Package roles were introduced in March 2022. To migrate existing projects, see the migration guide.
The Backstage build system uses the concept of package roles in order to help keep
configuration lean, provide utility and tooling, and enable optimizations. A package
role is a single string that identifies what the purpose of a package is, and it's
defined in the package.json of each package like this:
{
"name": "my-package",
"backstage": {
"role": "<role>"
},
...
}
These are the available roles that are currently supported by the Backstage build system:
| Role | Description | Example |
|---|---|---|
| frontend | Bundled frontend application | packages/app |
| backend | Bundled backend application | packages/backend |
| cli | Package used as a command-line interface | @backstage/cli, @backstage/codemods |
| web-library | Web library for use by other packages | @backstage/plugin-catalog-react |
| node-library | Node.js library for use by other packages | @backstage/plugin-techdocs-node |
| common-library | Isomorphic library for use by other packages | @backstage/plugin-permission-common |
| frontend-plugin | Backstage frontend plugin | @backstage/plugin-scaffolder |
| frontend-plugin-module | Backstage frontend plugin module | @backstage/plugin-analytics-module-ga |
| backend-plugin | Backstage backend plugin | @backstage/plugin-auth-backend |
| backend-plugin-module | Backstage backend plugin module | @backstage/plugin-search-backend-module-pg |
Most of the steps that we cover below have an accompanying command that is intended to be used as a package script. The commands are all available under the backstage-cli package category, and many of the commands will behave differently depending on the role of the package. The commands are intended to be used like this:
{
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
...
}
}
Formatting
The formatting setup lives completely within each Backstage application and is
not part of the CLI. In an app created with @backstage/create-app the
formatting is handled by prettier, but each application
can choose their own formatting rules and switch to a different formatter if
desired.
Linting
The Backstage CLI includes a lint command, which is a thin wrapper around
eslint. It adds a few options that can't be set through configuration, such as
including the .ts and .tsx extensions in the set of linted files. The lint
command simply provides a sane default and is not intended to be customizable.
If you want to supply more advanced options you can invoke eslint directly
instead.
In addition to the lint command, the Backstage CLI also includes a set of base
ESLint configurations, one for frontend and one for backend packages. These lint
configurations in turn build on top of the lint rules from
@spotify/web-scripts.
In a standard Backstage setup, each individual package has its own lint configuration, along with a root configuration that applies to the entire project. The configuration in each package starts out as a standard configuration that is determined based on the package role, but it can be customized to fit the needs of each package.
A minimal .eslintrc.js configuration now looks like this:
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
But you can provide custom overrides for each package using the optional second argument:
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
ignorePatterns: ['templates/'],
rules: {
'jest/expect-expect': 'off',
},
});
The configuration factory also provides utilities for extending the configuration in ways that are otherwise very cumbersome to do with plain ESLint, particularly for rules like no-restricted-syntax. These are the extra keys that are available:
| Key | Description |
|---|---|
tsRules | Additional rules to apply to TypeScript files |
testRules | Additional rules to apply to tests files |
restrictedImports | Additional paths to add to no-restricted-imports |
restrictedImportPatterns | Additional patterns to add to no-restricted-imports |
restrictedSrcImports | Additional paths to add to no-restricted-imports in src files |
restrictedTestImports | Additional paths to add to no-restricted-imports in test files |
restrictedSyntax | Additional patterns to add to no-restricted-syntax |
restrictedSrcSyntax | Additional patterns to add to no-restricted-syntax in src files |
restrictedTestSyntax | Additional patterns to add to no-restricted-syntax in test files |
Type Checking
Just like formatting, the Backstage CLI does not have its own command for type checking. It does however have a base configuration with both recommended defaults as well as some required settings for the build system to work.
Perhaps the most notable part about the TypeScript setup in Backstage projects is that the entire project is one big compilation unit. This is due to performance optimization as well as ease of use, since breaking projects down into smaller pieces has proven to both lead to a more complicated setup, as well as type checking of the entire project being an order of magnitude slower. In order to make this setup work, the entrypoint of each package needs to point to the TypeScript source files, which in turn causes some complications during publishing that we'll talk about in that section.
The type checking is generally configured to be incremental for local
development, with the output stored in the dist-types folder in the repo root.
This provides a significant speedup when running tsc multiple times locally,
but it does make the initial run a little bit slower. Because of the slower
initial run we disable incremental type checking in the tcs:full Yarn script
that is included by default in any Backstage app and is intended for use in CI.
Another optimization that is used by default is to skip the checking of library
types, this means that TypeScript will not verify that types within
node_modules are sound. Disabling this check significantly speeds up type
checking, but in the end it is still an important check that should not be
completely omitted, it's simply unlikely to catch issues that are introduced
during local development. What we opt for instead is to include the check in CI
through the tsc:full script, which will run a full type check, including
node_modules.
For the two reasons mentioned above, it is highly recommended to use the
tsc:full script to run type checking in CI.