A system for managing JS/TS project dev tools
- @jcoreio/toolchain
- Project goals
- How-to
- Creating a new project
- Migrating an existing project to
@jcoreio/toolchain
- Installing
@jcoreio/toolchain
in an empty project - Upgrading
@jcoreio/toolchain
- Specify
main
,module
,exports
, andbin
and link package locally - Run build scripts
- Exclude files from build output
- Customize Git hooks
- Disable ESM build
- Disable source map output
- Configure transpilation options
- Run scripts before or after toolchain scripts
- Disable scripts like
build:smoke-test
that are run byprepublish
- Load chai plugins, customize mocha, etc.
- Change mocha default specs
- Define multiple test targets
- Create dual CJS+ESM packages
- Current limitations
- Make it easy to keep standalone project dev dependencies and configuration up-to-date with the ecosystem
- Help us migrate all of our packages to ESM
- Make it easy to migrate a project to different systems (e.g. switching to typescript, or possibly in the future switching from mocha to another test runner, or from CircleCI to GitHub actions)
- Make it easy to set up new standalone projects with all the dev tools and config we use to ensure quality and publish packages
In the parent dir of where you want to create your project directory, run:
pnpm --package=@jcoreio/toolchain dlx tc create
In your project dir, run:
pnpm --package=@jcoreio/toolchain dlx tc init
This does a bunch of things:
- Switches the project from
yarn
ornpm
topnpm
- Installs the applicable
@jcore/toolchain
packages - Updates managed dev dependencies
- Adds config files for managed dev tools to the project
- Removes obsolete dev dependencies, config files, and things in package.json
that have been common in projects before
@jcoreio/toolchain
- Formats files and autofixes eslint errors
I plan to make tc init
work better for this use case, but right now the
process is:
- Manually install the relevant
@jcoreio/toolchain*
packages - Run
tc migrate
- Run
tc upgrade [version]
Since the build output is in the dist
directory, you should have relative paths
to ./dist
in your package.json
:
{
"main": "./dist/index.js",
"bin": "./dist/index.js"
That way, if you link your package root to another project locally, requiring/running it will work.
tc build
strips the ./dist/
out of these paths in the output dist/package.json
that actually gets published:
{
"main": "./index.js",
"bin": "./index.js"
@jcoreio/toolchain
adds a toolchain
script to your package.json
(also tc
for short):
$ pnpm toolchain
Usage: toolchain <command> <arguments...>
Available commands:
build build dist directory
check check format, types (if applicable), and lint
ci:browse open CircleCI page in browser
clean remove build output
coverage run tests with code coverage
format format files with prettier
init install toolchains and migrate
install-git-hooks install git hooks
lint check files with eslint
lint:fix autofix eslint errors
migrate update dependencies and config, fix lint errors and format
open:coverage open code coverage report
preinstall run this script before installing toolchains in a project
prepublish run check, coverage, and build
release run automated release
test run tests
upgrade upgrade toolchains and migrate
version print version of @jcoreio/toolchain
Configure the buildIgnore
option in your toolchain.config.cjs
. buildIgnore
takes an array of glob patterns. **
is supported, though brace expansion (e.g.
*.{ts,tsx}
) is not currently.
Example:
/* eslint-env node, es2018 */
module.exports = {
cjsBabelEnv: { forceAllTransforms: true },
outputEsm: false,
buildIgnore: ['src/**/__tests__'],
}
Edit githooks.cjs
. The default added by toolchain init
is:
/* eslint-env node, es2018 */
module.exports = {
...require('@jcoreio/toolchain/githooks.cjs'),
}
If you jump to @jcoreio/toolchain/githooks.cjs
, you'll see:
module.exports = {
'pre-commit': 'lint-staged',
}
Each hook can be a shell command string or a (possibly async) function.
toolchain init
/toolchain install-git-hooks
essentially does
git config core.hooksPath node_modules/@jcoreio/toolchain/githooks
,
which contains the scripts that invoke what's configured in your githooks.cjs
.
Set outputEsm: false
in toolchain.config.cjs
:
/* eslint-env node, es2018 */
module.exports = {
cjsBabelEnv: { forceAllTransforms: true },
outputEsm: false,
}
Add sourceMaps: false
to toolchain.config.cjs
:
/* eslint-env node, es2018 */
module.exports = {
sourceMaps: false,
// ...
}
This will also prevent the published package from including src/**
.
You can put options for @babel/preset-env
in cjsBabelEnv
/esmBabelEnv
in toolchain.config.cjs
. The default options are:
/* eslint-env node, es2018 */
module.exports = {
cjsBabelEnv: { forceAllTransforms: true },
esmBabelEnv: { targets: { node: 16 } },
}
Similar to package.json
scripts, you can add pre*
and post*
scripts to your
toolchain.config.cjs
. However, the script can be a shell command string or an
object with props { description: string, run: () => any }
:
/* eslint-env node, es2018 */
module.exports = {
cjsBabelEnv: { forceAllTransforms: true },
esmBabelEnv: { targets: { node: 16 } },
scripts: {
pretest: 'echo test',
postbuild: {
description: 'runs after build',
run: async () => {
// do something...
},
},
},
}
In toolchain.config.cjs
:
module.exports = {
...
scripts: {
'build:smoke-test': false,
},
}
Edit .mocharc.cjs
. For example to add your own configuration script that loads chai plugins:
/* eslint-env node, es2018 */
const base = require('@jcoreio/toolchain-mocha/.mocharc.cjs')
module.exports = {
...base,
require: [...base.require, 'test/configure.js'],
}
Edit .mocharc.cjs
. It's recommended to use the getSpecs
helper to avoid running
all specs by default if specific specs are passed on the command line. It's kind of
a bug that the Mocha CLI doesn't override the specs from config by default...
/* eslint-env node, es2018 */
const base = require('@jcoreio/toolchain-mocha/.mocharc.cjs')
const { getSpecs } = require('@jcoreio/toolchain-mocha')
module.exports = {
...base,
spec: getSpecs(['src/**/*.spec.js']),
}
If you define test:unit
, test:integration
etc scripts, @jcoreio/toolchain-mocha
will automatically create coverage:*
scripts for them, and reconfigure the test
script to run the test:*
scripts in sequence.
To be precise, it looks for scripts matching /^test\W/
, so the names test-unit
and
test/foo
etc. would also work.
Example toolchain.config.cjs
:
/* eslint-env node, es2018 */
const execa = require('@jcoreio/toolchain/util/execa.cjs')
module.exports = {
scripts: {
'test:unit': {
description: 'run unit tests',
run: (args = []) =>
execa('mocha', ['--config', '.mocharc-unit.cjs', ...args]),
},
'pretest:integration': 'docker compose up -d',
'test:integration': {
description: 'run integration tests',
run: (args = []) =>
execa('mocha', ['--config', '.mocharc-integration.cjs', ...args]),
},
},
}
As long as you use @jcoreio/toolchain-esnext
and don't have outputEsm: false
in
your toolchain.config.cjs
, tc build
will output both .cjs
and .mjs
files.
There will be a tc test:esm
command available that runs all your tests in ESM mode
so you can make sure the ESM works.
Although ESM requires explicit file extensions for relative imports, you should still omit them from your source and test code so that the build and test scripts work for both CJS and ESM. The toolchain will use a babel plugin to add the necessary extensions to your import paths when building and testing.
Right now the build doesn't output source maps or source files in the published package, but we should probably make it do that.