Hello world
This walkthrough will get you started with Heft by creating a basic Node.js console project from scratch, adding each task step by step. In practice, you would probably want to use a readymade rig instead. This tutorial's goal is to illustrate the fundamental concepts and architecture of Heft. With this foundation, you can more easily understand complex configurations and troubleshoot any problems that arise.
"Show me the code!"
If you're in a hurry, the heft-node-basic-tutorial and heft-webpack-basic-tutorial folders illustrate a fully worked out example of a simple project that builds using Heft.
The heft-node-rig-tutorial and heft-web-rig-app-tutorial folders show how to accomplish the same result by using Rush Stack rigs, rather than a manual Heft configuration. Rigs enable many projects to share a standard configuration, which greatly reduces maintenance cost of upgrades.
We'll begin by creating a simple standalone project without Rush. (Later, the Interfacing with Rush tutorial will examine what's different when using Heft in a monorepo.)
We'll use the PNPM package manager for this demo. Its command line is very similar to NPM, so you could substitute
npm
forpnpm
in these steps. There are various ways to install PNPM, but the simplest is like this:npm install --global pnpm
Create a new folder my-app with a package.json file for our project, like this:
my-app/package.json
{
"name": "my-app",
"version": "1.0.0",
"description": "A Heft tutorial project",
"license": "MIT",
"main": "lib/start.js",
"typings": "lib/start.d.ts",
"scripts": {
"start": "node lib/start.js"
}
}Create a TypeScript source file that we'll compile.
my-app/src/start.ts
console.log("Hello, world!");
Install @rushstack/heft, @rushstack/heft-typescript-plugin, and typescript as
devDependenices
for your project:cd my-app
pnpm install --save-dev @rushstack/heft
pnpm install --save-dev @rushstack/heft-typescript-plugin
pnpm install --save-dev typescript
# Since this project will use the console.log() API, we also need to add the TypeScript
# typings for Node.js. Typings should always use "--save-exact" version specifiers.
pnpm install --save-dev --save-exact @types/nodeNext we need to create the TypeScript tsconfig.json file. The presence of this file causes Heft to invoke the TypeScript compiler. For now we'll create a simple standalone tsconfig.json file; later we'll demonstrate how to share a reusable configuration across many projects.
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": ["node"],
"module": "commonjs",
"target": "es2017",
"lib": ["es2017"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "lib"]
}Note that
"types": ["node"]
references the@types/node
package that we installed above. This is needed because Node.js is a global environment, so its typings must be loaded globally. Most other@types
packages can be loaded viaimport
statements in your source code.See the TypeScript plugin documentation for more background about TypeScript configuration with Heft.
You can invoke Heft using
./node_modules/.bin/heft
, but it's more convenient to also install it globally so that it's always available in your shellPATH
:# Install the Heft tool globally
npm install --global @rushstack/heftWhat if the globally installed
heft
binary is the wrong version?Just like Rush, Heft implements a "version selector" feature that will automatically discover your local
node_modules
folder and invoke./node_modules/.bin/heft
, ensuring that the correct version is used.Heft is config-driven, which means its behavior within a project folder is defined by data (config files) not code (arbitrary scripts). If you need to extend your build process with program logic, we strongly encourage moving that code into a Heft plugin package, which should be developed as professional software using TypeScript, ESLint, and code reviews. This is more work, but as your monorepo grows in scale, it greatly simplifies maintenance.
Heft's main config file is config/heft.json. Let's start with the simplest possible file:
config/heft.json
{
"$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
}If you run
heft --help
you should see output like this:usage: heft [-h] [--debug] [--unmanaged] <command> ...
Heft is a pluggable build system designed for web projects.
Positional arguments:
<command>
clean Clean the project, removing temporary task folders and
specified clean paths.
run Run a provided selection of Heft phases.
run-watch Run a provided selection of Heft phases in watch mode..
Optional arguments:
-h, --help Show this help message and exit.Now let's expand our configuration by adding a simple phase called
"build"
that invokes a task called"typescript"
to compile your code. (For definitions of these terms, refer to the architecture notes.)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"
}
}
}
}
}
}For complete descriptions of these settings, see heft.json template.
If you run
heft --help
, you will now see that abuild
andbuild-watch
action have been added to your command line, since our phase was called"build"
:usage: heft [-h] [--debug] [--unmanaged] <command> ...
Heft is a pluggable build system designed for web projects.
Positional arguments:
<command>
clean Clean the project, removing temporary task folders and
specified clean paths.
run Run a provided selection of Heft phases.
build Runs to the build phase, including all transitive dependencies.
run-watch Run a provided selection of Heft phases in watch mode..
build-watch
Runs to the build phase, including all transitive dependencies,
in watch mode.
Optional arguments:
-h, --help Show this help message and exit.The
"phaseDescription"
is printed if you runheft build --help
.Let's try invoking Heft's command line to build our project.
# Make sure we're in your project folder
cd my-app
# View the command line help
heft --help
heft build --help
# Build the project
heft build
# To see more detail about what Heft is doing, add you can the "--verbose" flag
heft build --verboseInvoking
heft build --verbose
should produce console output like this:Project: my-app@1.0.0
Project build folder: C:\my-app
Heft version: 0.56.2
Node version: v16.15.1
Executing a maximum of 4 simultaneous tasks...
---- lifecycle started ----
[lifecycle:start] Applying lifecycle plugins
---- build started ----
[build] Applying task plugins
[build:typescript] Loaded plugin from "C:\my-app\node_modules\...\@rushstack\heft-typescript-plugin\lib\TypeScriptPlugin"
[build:typescript] Starting task execution
[build:typescript] Looking for tsconfig at C:/my-app/tsconfig.json
[build:typescript] Resolved "typescript" as a direct devDependency of the project.
[build:typescript] Using TypeScript version 5.1.6
[build:typescript] Configure: 17.34340000152588ms
[build:typescript] I/O Read: 16.67810034751892ms (98 files)
[build:typescript] Parse: 491.7621006965637ms (98 files)
[build:typescript] Program (includes Read + Parse): 581.9542999267578ms
[build:typescript] Analyze: 1135.4448999166489ms
[build:typescript] Bind: 189.5981993675232ms
[build:typescript] Check: 929.5596989393234ms
[build:typescript] Transform: 4.200200080871582ms (2 files)
[build:typescript] Print: 12.058799982070923ms (1 files) (Includes Transform)
[build:typescript] Emit: 12.5ms (Includes Print)
[build:typescript] I/O Write: 0ms (0 files)
[build:typescript] Finished task execution (1964.6486999988556ms)
---- build finished (2.014s) ----
---- lifecycle finished (2.018s) ----
-------------------- Finished (2.02s) --------------------NOTE: When reporting diagnostic messages such as a compile error, Heft prints file paths relative to the project folder. This can be customized using the RUSHSTACK_FILE_ERROR_BASE_FOLDER environment variable.
After the build finishes, confirm that it produced several output files in your
lib
folder:- start.js - the compiled JavaScript code
- start.d.ts - the TypeScript typings, for external libraries that might import this module
- start.js.map and start.d.ts.map - Source map files, which enable tools like debuggers to find the corresponding source code file/line, for a given item in an output file
If you recall, our package.json file has a
"scripts"
section that specifies"start": "node lib/start.js"
. Let's try running the compiled code usingpnpm run
.# Invoke the "start" script from package.json
pnpm run start
# If you have Rush installed, you can also use this slightly shorter equivalent
rushx startYou should see output like this:
> my-app@1.0.0 start C:\my-app
> node lib/start.js
Hello, world!We can also add a
"build"
script to our package.json file:my-app/package.json
{
. . .
"scripts": {
"build": "heft build --clean",
"start": "node lib/start.js"
},
. . .
}With this change, you can also build by invoking
pnpm run build
(orrushx build
). This toolchain-agnostic convention makes it easier for newcomers to guess how to build your project. It will also be useful later when we integrate with Rush.