Adding more tasks
This section continues the tutorial project from the Hello World tutorial.
Heft's architecture is designed around plugin packages. Heft ships with a collection of official plugin packages for the most common build tasks. Their source code can be found in the rushstack/heft-plugins which is a great reference, if you want to create your own Heft plugins.
Continuing our tutorial, let's enable the two most common plugins: Jest for unit tests and ESlint for style checking.
Adding unit tests to your project
First, we need to install the TypeScript typings for Jest. These steps continue the my-app project from the Hello World tutorial. Recall that this project is not using Rush yet, so we will invoke PNPM directly to add the dependency to our package.json file (instead of using rush add):
cd my-app
# Because @types packages don't follow SemVer, it's a good idea to use --save-exact
pnpm install --save-dev --save-exact @types/heft-jest
pnpm install --save-dev @rushstack/heft-jest-pluginAdd a
"test"
section to your Heft config file, producing this result:config/heft.json
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
"phasesByName": {
// Define a phase whose name is "build"
"build": {
"phaseDescription": "This phase compiles the project source code.",
// Before invoking the compiler, delete the "dist" and "lib" folders
"cleanFiles": [{ "sourcePath": "dist" }, { "sourcePath": "lib" }],
"tasksByName": {
// Define a task whose name is "typescript"
"typescript": {
"taskPlugin": {
// This task will invoke the TypeScript plugin
"pluginPackage": "@rushstack/heft-typescript-plugin"
}
}
}
},
// Define a phase whose name is "test"
"test": {
"phaseDescription": "This phase runs the project's unit tests.",
// This phase requires the "build" phase to be run first
"phaseDependencies": ["build"],
"tasksByName": {
// Define a task whose name is "jest"
"jest": {
"taskPlugin": {
// This task will invoke the Jest plugin
"pluginPackage": "@rushstack/heft-jest-plugin"
}
}
}
}
}
}For complete descriptions of these settings, see heft.json template.
If you run
heft --help
you should now seetest
andtest-watch
command-line actions because our second phase was named"test"
.Since Jest's API consists of global variables, we need to load them globally (whereas most other
@types
packages are loaded viaimport
statements in your source code). Update your tsconfig.json file to say"types": ["heft-jest", "node"]
instead of just"types": ["node"]
. The result should look like this:my-app/tsconfig.json
{
"$schema": "http://json.schemastore.org/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDirs": ["src/"],
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"declarationMap": true,
"inlineSources": true,
"experimentalDecorators": true,
"strict": true,
"useUnknownInCatchVariables": false,
"esModuleInterop": true,
"noEmitOnError": false,
"allowUnreachableCode": false,
"types": ["heft-jest", "node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "lib"]
}Next, we need to add the jest.config.json config file. The presence of this file causes Heft to invoke the Jest test runner. Heft expects a specific file path config/jest.config.json. For most cases, your Jest configuration should simply extend Heft's standard preset as shown below:
my-app/config/jest.config.json
{
"extends": "@rushstack/heft-jest-plugin/includes/jest-shared.config.json",
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 50,
"functions": 50,
"lines": 50,
"statements": 50
}
}
}Note: For web projects, you probably want to use
@rushstack/heft-jest-plugin/includes/jest-web.config.json
instead ofjest-shared.config.json
to support dual outputs aslib-commonjs
andlib
folders. See the Jest plugin documentation for details.Now we need to add a unit test. Jest supports quite a lot of features, but for this tutorial we'll create a trivial test file. The
.test.ts
file extension causes Heft to look for unit tests in this file:my-app/src/example.test.ts
describe('Example Test', () => {
it('correctly runs a test', () => {
expect(true).toBeTruthy();
});
});To run the test, we need to use the
heft test
action, becauseheft build
normally skips testing to speed up development.# View the command line help
heft test --help
# Build the project and run tests
heft test --verbose
# Run Jest in watch mode
heft test-watchWow,
heft test --help
has quite a lot of command-line parameters! Where did they come from? They were added by the Jest plugin's heft-plugin.json manifest file because we loaded that plugin in our phase.(What happens if two different plugins define the same command-line parameter? Heft includes a sophisticated disambiguation mechanism, for example allowing you to use
--jest:update-snapshots
instead of--update-snapshots
if some other plugin also defines a--update-snapshots
parameter.)We should update our package.json script so that
pnpm run test
will run the Jest tests:my-app/package.json
{
. . .
"scripts": {
"build": "heft build --clean",
"test": "heft test --clean",
"start": "node lib/start.js"
},
. . .
}
Note: Do not invoke the
jest
command line directly. Doing so would run the tests that it finds inlib/**/*.js
, but it will not invoke Heft's other tasks needed to update those output files.
That's it for setting up Jest! Further information, including instructions for debugging tests, can be found in the Jest plugin reference and the heft-node-jest-tutorial sample project.
Enabling linting
To ensure best practices and catch common mistakes, let's also enable the @rushstack/eslint-config standard ruleset. First we need to add a few more NPM dependencies to our package.json file.
cd my-app
# Add the ESLint engine
pnpm install --save-dev eslint
# Add Heft's plugin for ESLint
pnpm install --save-dev @rushstack/heft-lint-plugin
# Add Rush Stack's all-in-one lint ruleset
pnpm install --save-dev @rushstack/eslint-configUpdate your Heft config file to add a task that loads
@rushstack/heft-lint-plugin
during theheft build
phase:config/heft.json
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
"phasesByName": {
// Define a phase whose name is "build"
"build": {
"phaseDescription": "Compiles the project source code",
// Before invoking the compiler, delete the "dist" and "lib" folders
"cleanFiles": [{ "sourcePath": "dist" }, { "sourcePath": "lib" }],
"tasksByName": {
// Define a task whose name is "typescript"
"typescript": {
"taskPlugin": {
// This task will invoke the TypeScript plugin
"pluginPackage": "@rushstack/heft-typescript-plugin"
}
},
// Define a task whose name is "lint"
"lint": {
// This task should run after "typescript" has completed
// because Heft optimizes ESLint by reusing the TypeScript
// compiler's AST analysis
"taskDependencies": ["typescript"],
"taskPlugin": {
// This task will invoke the ESLint plugin
"pluginPackage": "@rushstack/heft-lint-plugin"
}
}
}
},
// Define a phase whose name is "test"
"test": {
// This phase requires the "build" phase to be run first
"phaseDependencies": ["build"],
"tasksByName": {
// Define a task whose name is "jest"
"jest": {
"taskPlugin": {
// This task will invoke the Jest plugin
"pluginPackage": "@rushstack/heft-jest-plugin"
}
}
}
}
}
}For complete descriptions of these settings, see heft.json template.
Next, create the .eslintrc.js config file. For this tutorial we'll just use the official Rush Stack ruleset:
my-app/.eslintrc.js
// This is a workaround for https://github.com/eslint/eslint/issues/3458
require('@rushstack/eslint-config/patch/modern-module-resolution');
module.exports = {
extends: ['@rushstack/eslint-config/profile/node'],
parserOptions: { tsconfigRootDir: __dirname }
};Note: If your project uses the React framework, you should also extend from the
"@rushstack/eslint-config/mixins/react"
mixin. See the documentation for details about@rushstack/eslint-config
"profiles" and "mixins".To test it out, try updating your start.ts source file to introduce a lint issue:
my-app/src/start.ts
console.log('Hello, world!');
export function f() {
// <--- oops
}When you run
pnpm run build
, you should see a log message like this:-------------------- Finished (3.555s) --------------------
Encountered 1 warning
[build:lint] src/start.ts:3:8 - (@typescript-eslint/explicit-function-return-type) Missing return type on function.To fix the problem, fix the code to add the missing return type, and it should now build successfully:
my-app/src/start.ts
console.log('Hello, world!');
export function f(): void {
// <--- okay
}The
@rushstack/eslint-config
ruleset is designed to work together with the Prettier code formatter. To set that up, see the Enabling Prettier article on the Rush website.
That's it for ESLint! More detail can be found in the Lint plugin reference.