The chompfile.toml
defines Chomp tasks as a list of Task objects of the form:
chompfile.toml
version = 0.1
[[task]]
name = 'echo'
run = 'echo "Chomp"'
Running chomp echo
will output the echo command.
Tasks support the following optional properties:
- name:
String
, the unique task name string. - target:
String
, the file path created or modified by this task. Singular sugar for a singletargets: [String]
. - targets:
String[]
, the list of file paths created or modified by this task, identical totarget
when there is a single target. - dep:
String
, the task names or file paths this task depends on. Singular sugar for a singledeps: [String]
. - deps:
String[]
, the task names of file paths this task depends on, identical todep
when there is a single dependency. - serial:
Boolean
, whether task dependencies should be processed in serial order. Defaults to false for parallel task processing. - invalidation:
"always" | "mtime" (default) | "not-found"
, the task caching invalidation rules. By default a task is cached based on its target path having an mtime greater than its dependencies per "make" semantics."always"
never caches, and"not-found"
will never rerun the task if the target exists. - display:
"none" | "init-status" | "init-only" | "status-only" | "dot"
, defaults to"init-status"
. Useful to reduce noise in the output log. Init is the note that the task has begun, while status is the note of task success or caching. Task errors will always be reported even withdisplay: 'none'
."dot"
outputs a dot for each run only, for a test-like output when used alongsidestdio = 'stderr-only'
. - echo:
Boolean
, defaults to false - whether to echo the executed command of the task. - stdio:
"none" | "no-stdin" | "stdout-only" | "stderr-only" | "all"
, defaults to"all"
where stderr and stdout are piped to the main process output and stdin is also accepted. Set to"no-stdin"
to disable the stdin for tasks."stdout-only"
and"stderr-only"
will output only those streams. - engine:
"node" | "deno" | "cmd" (default)
, the execution engine to use for therun
string. Fornode
ordeno
it is a Node.js or Deno program source string as if executed in the current directory. - run:
String
, the source code string to run in theengine
. - cwd:
String
, the working directory to use for theengine
execution. - env:
{ [key: String]: String }
, custom environment variables to set for theengine
execution. - env-default:
{ [key: String]: String }
, custom default environment variables to set for theengine
execution, only if not already present in the system environment. - env-replace:
Boolean
, defaults totrue
. Whether to support${{VAR}}
style static environment variable replacements in theenv
andenv-default
environment variable declarations and therun
script of Shell engine tasks. - template:
String
, a registered template name to use for task generation as a template task. - template-options:
{ [option: String]: any }
, the dictionary of options to apply to thetemplate
template generation, as defined by the template itself. - validation:
"none" | "ok-only" | "targets-only" | "ok-targets (default)
, Validation check to determine task success condition. The default is to check the defined targets all exist and the task exited with a success status code."ok-only"
just verifies the status code,"targets-only"
just verifies the targets, and"none"
always treats the task as successful.
Chomp tasks are primarily characterized by their "run"
and "engine"
pair, "run"
representing the source code of a task execution in the "engine"
execution environment. Currently supported engines include the shell execution (the default), Node.js (engine = 'node'
) or Deno (engine = 'deno'
).
There are two ways to execute in Chomp:
- Execute a task by name -
chomp [name]
orchomp :[name]
where[name]
is thename
field of the task being run. - Execute a task by target file path -
chomp [target]
where[target]
is the local file path to generate relative to the Chompfile being run.
chompfile.toml
version = 0.1
[[task]]
name = 'my-task'
target = 'output.txt'
run = 'cat "Chomp Chomp" > output.txt'
Chomp Chomp
into the file at output.txt
, defining this file as a target output path of the task so that the task is cached.This task writes the text Chomp Chomp
into the file at output.txt
, defining this as a target file output of the task.
$ chomp my-task
$ chomp :my-task
$ chomp output.txt
🞂 output.txt
√ output.txt [3.8352ms]
:
prefix) or by target path.The leading :
can be useful to disambiguate task names from file names when necessary. Setting a name
on a task is completely optional.
Once the task has been called, with the target file already existing it will treat it as cached and skip subsequent executions:
$ chomp my-task
● output.txt [cached]
A task is considered to have succeeded if it completes with a zero exit code, and the target or targets expected of the task all exist.
If the spawned process returns a non-zero exit code the task and all its parents will be marked as failed.
If after completion, any of the targets defined for the task still do not exist, then the task is also marked as failed.
The default engine
is the shell environment - PowerShell on Windows or Bash on posix machines.
Common commands like echo
, pwd
, cat
, rm
, cp
, cd
, as well as operators like $(cmd)
, >
, >>
, |
form a subset of shared behaviours that can work when scripting between all platforms. With some care and testing, it is possible to write cross-platform shell task scripts. For PowerShell 5, Chomp will execute PowerShell in UTF-8 mode (applying to >
, >>
and |
), although a BOM will still be output when writing a new file with >
. Since &&
and ||
are not supported in Powershell, multiline scripts and ;
are preferred instead.
For example, here is an SWC task (assuming Babel is installed via npm install @swc/core @swc/cli -D
):
chompfile.toml
version = 0.1
[[task]]
name = 'build:swc'
target = 'lib/app.js'
dep = 'src/app.ts'
run = 'swc $DEP -o $TARGET --source-maps'
src/app.ts
into a JS module lib/app.js
, and supporting configuration in an .swcrc
file.The above works without having to reference the full node_modules/.bin/swc
command prefix since node_modules/.bin
is automatically included in the Chomp spawned PATH
, relative to the Chompfile itself.
In addition to the run
property, two other useful task properties are env
and cwd
which allow customizing the exact execution environment.
In PowerShell, defined environment variables in the task env
are in addition made available as local variables supporting output via $NAME
instead of $Env:Name
for better cross-compatibility with posix shells. This process is explicit only - system-level environment variables are not given this treatment though.
In addition, static environment variable replacements are available via ${{VAR}}
, with optional spacing. Replacements that cannot be resolved to a known environment variable will be replaced with an empty string. Static replacements are available for environment variables and the shell engine run command. Set env-replace = false
to disable static environment variable replacement for a given task.
chompfile.toml
version = 0.1
[[task]]
name = 'env-vars'
run = '''
${{ECHO}} $PARAM1 $PARAM2
'''
[task.env]
PARAM1 = 'Chomp'
[task.default-env]
ECHO = 'echo'
PARAM2 = '${{ PARAM1 }}'
${{VAR}}
provides static replacements.On both Posix and Windows, chomp env-vars
will output: Chomp Chomp
, unless the system has overrides of the CMD
or PARAM2
environment variables to alternative values.
default-env
permits the definition of default environment variables which are only set to the default values if these environment variables are not already set in the system environment or via the global Chompfile environment variables. Just like env
, all variables in default-env
are also defined as PowerShell local variables, even when they are already set in the environment and the default does not apply.
The following task-level environment variables are always defined:
TARGET
: The path to the primary target (the interpolation target or first target).TARGETS
: The:
-separated list of target paths for multiple targets.DEP
: The path to the primary dependency (the interpolation dependency or first dependency file).DEPS
: The:
-separated list of expanded dependency paths.MATCH
When using task interpolation this provides the matched interpolation replacement value (although theTARGET
will always be the fully substituted interpolation target for interpolation tasks).
The PATH
environment variable is automatically extended to include .bin
in the current folder as well as node_modules/.bin
in the Chompfile folder.
The "node"
engine allows writing a Node.js program in the run
field of a task. This is a useful way to encapsulate cross-platform build scripts which aren't possible with cross-platform shell scripting.
For example, the same SWC task in Node.js can be written:
_chompfile.toml_ls
version = 0.1
[[task]]
name = 'build:swc'
target = 'lib/app.js'
dep = 'src/app.ts'
engine = 'node'
run = '''
import swc from '@swc/core';
import { readFileSync, writeFileSync } from 'fs';
import { basename } from 'path';
const input = readFileSync(process.env.DEP, 'utf8');
const { code, map } = await swc.transform(input, {
filename: process.env.DEP,
sourceMaps: true,
jsc: {
parser: {
syntax: "typescript",
},
transform: {},
},
});
writeFileSync(process.env.TARGET, code + '\n//# sourceMappingURL=' + basename(process.env.TARGET) + '.map');
writeFileSync(process.env.TARGET + '.map', JSON.stringify(map));
'''
It is usually preferable to write tasks using shell scripts since they can be much faster than bootstrapping Node.js or Deno, and can more easily support batching.
It is usually easier to use the existing
chomp:swc
experimental template extension instead of writing your own custom task for SWC.
Just like the "node"
engine, the "deno"
engine permits using JS to create build scripts.
The primary benefits being URL import support (no need for package management for tasks) and TypeScript type support (although unfortunately no editor plugins for Chompfiles means it doesn't translate to author time currently). Using a CDN like JSPM.dev (importing eg https://jspm.dev/[pkg]
etc) can be useful for these scripts to load npm packages.
By default the Deno engine will run with full permissions since that is generally the nature of build scripts.
Chomp works best when each task builds a single file target, instead of having a large monolithic build.
To extend the previous example to build all of src
into lib
, we use task interpolation with a #
or ##
, which means the same thing as a *
or **/*
glob respectively, but it retains the important property of being a reversible mapping which is necessary for tracing task invalidations.
Replacing app
with ##
in the previous SWC Shell example, we can achieve the full folder build:
chompfile.toml
version = 0.1
[[task]]
name = 'build:swc'
target = 'lib/##.js'
dep = 'src/##.ts'
run = 'swc $DEP -o $TARGET --source-maps'
.ts
files in src
into JS modules in lib
.By treating each file as a separate build, we get natural build parallelization and caching where only files changed in src
cause rebuilds.
Just like any other target, interpolation targets can be built directly (or even with globbing):
$ chomp lib/app.js
lib/app.js
- no other files in src
need to be checked other than src/app.js
.Only a single interpolation dep
and target
can be defined (with the #
interpolation character), although additional dependencies or targets may be defined in addition by using the deps
array instead, for example to make each compilation depend on the npm install:
chompfile.toml
version = 0.1
[[task]]
name = 'npm:install'
run = 'npm install'
[[task]]
name = 'build:swc'
target = 'lib/##.js'
deps = ['src/##.js', 'npm:install']
run = 'swc $DEP -o $TARGET --source-maps'
$DEP
and $TARGET
will always be the primary dependency and target (the interpolation item or the first in the list). Additional dependencies and targets can always be defined.While Chomp is not designed to be a test runner, it can easily provide many the features of one.
Tests can be run with interpolation. Since interpolation expands a glob of dependencies to operate on, this same technique can be used to create targetless tests:
chompfile.toml
version = 0.1
[[task]]
name = 'test:unit:#'
display = 'status'
stdio = 'stderr-only'
dep = ['test/unit/#.js', 'dist/build.js']
run = 'node $DEP'
In the above, all files test/unit/**/*.js
will be expanded by the test:unit
test resulting in a separate task run for each file. Since no targets
are defined, the task is always invalidated and re-run.
Using the display
and stdio
options it is also possible to hide any test output and the command init logs in the reporter.
By using #
in the name
of the task, individual test or test patterns can be run by name or using glob patterns:
$ chomp --watch test:unit:some-test test:unit:some-suite-*
The above would run the tests test/unit/some-test.js
, and all test/unit/some-suite-*.js
, watching the full build graph and every unit test file for changes and rerunning the tests on change.
Alternatively all unit tests can be run by passing the empty string replacement:
$ chomp test:unit:
$ chomp test:unit:**/*
test:unit:#
, running all the unit tests.Using the deps
and targets
properties (which are interchangeable with their singular forms dep
and target
for a single list item), task dependence graphs are built.
When processing a task, the task graph is constructed and processed in graph order where a task will not begin until its dependencies have completed processing.
Dependencies of tasks are always treated as being parallel - to ensure one task always happens before another the best way is usually to treat it as a dependency. For example by having a test task depend on the build target.
Task parallelization can be controlled by the -j
flag to set the maximum number of parallel child processes to spawn.
For example, here is a build that compiles with SWC, then builds into a single file with RollupJS:
chompfile.toml
version = 0.1
[[task]]
name = 'npm:install'
run = 'npm install'
[[task]]
name = 'build:swc'
target = 'lib/##.js'
deps = ['src/##.js', 'npm:install']
run = 'swc $DEP -o $TARGET --source-maps'
[[task]]
name = 'build:rollup'
dep = 'lib/**/*.js'
target = 'dist/app.js'
run = 'rollup lib/app.js -d dist -m'
Following the task graph from the top, since build:rollup
depends on all deps in lib
, this will make it depend on all the separate file interpolation jobs of build:swc
and in turn their dependence. With each of the build:swc
tasks depending on npm:install
, this task is always run first. Then only once the npm install
is completed successfully, the compilation of all src/**/*.ts
into lib/#.js
will happen with full parallelization in the task graph.
Task dependency inputs can themselves be the result of targets of other tasks. Build order is fully determined by the graph in this way.
Taking the previous example and running:
$ chomp build:rollup --watch
will build the dist/app.js
file and then continue watching all of the input files in src/**/*.ts
as well as the package.json
. A change to any of these files will then trigger a granular live rebuild of only the changed TypeScript file or files.
As a convenience a simple local static file server is also provided:
$ chomp build-rollup --serve
This behaves identically to the watched rebuilds above, but will also serve the folder on localhost for browser and URL tests. This may seem outside of the expected features for a task runner, but it is actually closely associated with the watched rebuild events - a websocket protocol for in-browser hot-reloading is a planned future addition.
Server configuration can be controlled via the serve
options in the Chompfile or the --server-root
and --port
flags.
By separating monolithic builds into sub-compilations on the file system this enables caching, parallelization, finer-grained generic build control and comprehensive incremental builds with watcher support. Replacing monolithic JS build systems with make-style file caching all the commonly expected features of JS dev workflows can still be maintained.
Tasks are cached when the modified time of their targets
is more recent than the modified time of their deps
per standard Make-style semantics.
For example, if we change the npm task definition from the previous example to define the dep
as the package.json
and the target
as the package-lock.json
:
chompfile.toml
version = 0.1
[[task]]
name = 'npm:install'
run = 'npm install'
target = 'package-lock.json'
dep = 'package.json'
The npm install
operation will now be treated as cached and skipped, unless the package.json
has been more recently modified than the package-lock.json
.
The invalidation rule is a binary rule indicating whether or not a given task should rerun or be treated as cached.
The explicit rules of invalidation for this mtime
invalidation are:
- If no targets are defined for a task, it is always invalidated.
- Otherwise, if no deps are defined for a task, it is invalidated only if the targets do not exist.
- Otherwise, if the mtime of any dep is greater than the mtime of any target, the task is invalidated.
Task invalidation can be customized with the invalidation
property on a task:
invalidation = 'mtime'
(default): This is the default invalidation, as per the rules described above.invalidation = 'always'
: The task is always invalidated and rerun, without exception.invalidation = 'not-found'
: The task is only invalidated when not all targets are defined.
In some cases, it can be preferred to write a serial pipeline of steps that should be followed.
This can be achieved by setting serial = true
on the task:
chompfile.toml
version = 0.1
[[task]]
name = 'test'
serial = true
deps = ['test:a', 'test:b', 'test:c']
[[tas]]
name = 'test:a'
run = 'echo a'
[[task]]
name = 'test:b'
run = 'echo b'
[[task]]
name = 'test:c'
run = 'echo c'
test
task executing test:a
then test:b
then testc
in sequence.Running chomp test
with the above, will run each of test:a
, test:b
and test:c
one after the other to completion instead of running their dependence graphs in parallel by default, logging a b c
every time.
Extensions are loaded via the extensions
list in the Chompfile, and can define custom task templates, which can encapsulate the details of a task execution into a simpler definition.
For convenience Chomp provides an experimental core extensions library.
For example, to replace the npm, SWC and RollupJS compilations from the previous examples with their extension templates:
chompfile.toml
version = 0.1
extensions = ['chomp:npm', 'chomp:swc', 'chomp:rollup']
[[task]]
name = 'npm:install'
template = 'npm'
[[task]]
name = 'build:swc'
template = 'swc'
target = 'lib/##.js'
deps = ['src/##.js', 'npm:install']
[task.template-options]
source-maps = true
[[task]]
name = 'build:rollup'
template = 'rollup'
deps = 'lib/**/*.js'
[task.template-options]
outdir = 'dist'
entries = ['lib/app.js']
chomp:npm
, chomp:swc
and chomp:rollup
experimental core extensions allows writing these tasks encapsulated from their implementations.Templates can be loaded from any file path or URL.
Extensions support any https://
URLs or local file paths.
Remote extensions are loaded once and cached locally by Chomp, regardless of cache headers, to ensure the fastest run time.
The remote extension cache can be cleared by running chomp --clear-cache
.
chomp --eject
transforms the Chompfile into the expanded untemplated form without extensions, allowing an opt-out from extension template workflows if it ever feels too magical. In this way templates become a sort of task construction utility.
Read more on writing extensions in the extensions documentation
Chomp extensions can be loaded from any URL or local file path. To write custom templates, create a local extension file local-extension.js
referencing it in the extensions list of the Chompfile:
chompfile.toml
version = 0.1
extensions = ['./local-extension.js']
local-extension.js
Chomp.registerTemplate('npm', function (task) {
return [{
name: task.name,
run: 'npm install',
target: 'package-lock.json',
deps: ['package.json']
}];
});
Chomp.registerTemplate('swc', function (task) {
const { sourceMaps } = task.templateOptions;
return [{
name: task.name,
targets: task.targets,
deps: task.deps,
run: `swc $DEP -o $TARGET${sourceMaps ? ' --source-maps' : ''}`
}];
});
Chomp.registerTemplate('rollup', function (task) {
if (task.targets.length > 0)
throw new Error('Targets is not supported by the Rollup template, use the "outdir" and "entries" template options instead.');
const { outdir, entries } = task.templateOptions;
const targets = entries.map(entry => outdir + '/' + entry.split('/').pop());
return [{
name: task.name,
deps: task.deps,
targets,
run: `rollup ${entries.join(' ')} -d ${outdir} -m`
}];
});
local-extension.js
for the npm
, swc
and rollup
templates.Templates are functions on tasks returning a new list of tasks. All TOML properties apply but with camelCase instead of kebab-case.
PRs to the default Chomp extensions library are welcome, or host your own on your own domain or via an npm CDN. For support on the JSPM CDN, add "type": "script"
to the package.json
of the package to avoid incorrect processing since template extensions are currently scripts and not modules.
Because remote extensions are cached, it is recommended to always use unique URLs with versions when hosting extensions remotely.
See the extensions documentation for the full extensions API.