Convention-based helper functions for people authoring npm packages.
npm install @maranomynet/libtools@maranomynet/libtools Helper functions for people authoring npm packages. They are opinionated and
eccentric, but they work surprisingly well.
```
npm install --save-dev @maranomynet/libtools
yarn add --dev @maranomynet/libtools
bun add --dev @maranomynet/libtools
Contents:
- Features
- Build / Publish Helpers.
- buildNpmLib
- updatePkgVersion
- getPkgVersion
- publishToNpm
- distFolder
- Code Quality Helpers.
- errorCheckSources
- typeCheckSources
- lintSources
- formatSources
- Misc Utilities
- args Object
- argStrings Object
- shell$
- Script and Package Binary Runner
- Logging and Errors
- prompt
- promptYN
- Type Testing Helpers
- Type Expect
- Type Equals
- Type Extends
- Type NotExtends
- Contributing
- Change Log
---
> NOTE:
> This "features" chapter is a bit rough. It's a work in progress. The rest of
> this readme, however, is quite complete.
These functions are convention-based and opinionated. They are designed to
help you use TypeScript to author npm packages that are easy to maintain,
build and publish.
The published packages are dual-format (CommonJS and ES modules) and include
type definitions, and are extremely lightweight and free of unnecessary
dev-related files.
The project must contain a file called tsconfig.build.json with includeexclude
and fields that describe the files to be treated as entrypints andexports
added to the published package.json's field.
If your package contains a pkg.bin field, the buildNpmLib function willpkg.exports
exclude its contents from the field.
Your package.json should be set to private: true and contain anpmPackageJson field with overrides for the dist package.json.
The project's CHANGELOG.md MUST follow the same format as the one in this
project.
The .gitignore file must also contain the following line:
`sh`
/_npm-lib
Example pkg.npmPackageJson:
`json`
"npmPackageJson": {
"type": null,
"private": null,
"scripts": null,
"devDependencies": null,
"engines": null,
"sideEffects": false,
}
(NOTE: Fields with null are removed from the published dist package.json
file.)
Example tsconfig.build.json:
`json`
{
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ES2018",
"resolveJsonModule": false,
"noEmit": false,
"declaration": true
},
"include": ["src//.ts", "src//.tsx"],
"exclude": [
"src/*/.test.*",
"src/*/.privates.*",
"src/*/_/*",
"src/*/.d.ts"
]
}
Example pgk.scripts
`json`
"scripts": {
"dev": "bun install && bun test --watch",
"build": "bun scripts/build.ts",
"publish:lib": "bun scripts/publish.ts",
"prepublishOnly": "echo \"\nRun 'bun run publish:lib' to publish this package\n\n\" && exit 1",
"check": "bun scripts/checkErrors.ts"
},
(This example uses the bun runtime, but you can easily use yarn or npm
if you prefer.)
---
---
Syntax: buildNpmLib(opts?: BuildNpmLibOpts): Promise
Reads ./tsconfig.build.json for include and exclude patterns and uses themdistFolder
as entry points to build the CommonJS and ESM versions of the library into the folder.
`ts
import { buildNpmLib } from '@maranomynet/libtools';
await buildNpmLib(); // Exits on errors.
`
BuildNpmLibOpts:
- srcDir?: string — (Default: 'src') postProcess
The source folder where the build entry points are located.
- ?: (jsFileContents: string, fileName: string, type: 'cjs' | 'esm') => string | undefined | Promiseundefined
— (Default: ) .js
A function to post-process the tsc built files. It should return the.js
new/updated content for the file, or undefined if no changes weretype
made.
- ?: 'esm' | 'commonjs' | 'both' — (Default: 'both') 'esm'
The type of module to build: , 'commonjs', or 'both'.root
- ?: string — (Default: '.') pkgJsonSuffix
The root folder of the project/package.
- ?: string — (Default: '') package.json
Optional suffix to append to the file before the .json filechangelogSuffix
extension.
- ?: string — (Default: '') CHANGELOG.md
Optional suffix to append to the file before the .md filereadmeSuffix
extension.
- ?: string — (Default: '') README.md
Optional suffix to append to the file before the .md file
extension.
---
Syntax: updatePkgVersion(opts?: UpdatePkgVersionOpts): Promise
Auto-updates the package.json and CHANGELOG.md files with a new versionBREAKING
and release date, based on the "## Upcoming..." entries in the changelog, and
their conventional commit prefixees (, feat:, fix:, docs:)
Prompts the user to confirm the new version number, before writing any changes
to disk.
Exits if any problems are found.
`ts
import { updatePkgVersion } from '@maranomynet/libtools';
await updatePkgVersion(); // Exits on errors.
// Now you can build and publish the package!!
`
UpdatePkgVersionOpts:
- preReleaseName?: string — (Default: '') 'beta.1'
Optional pre-release name to append to the version number. (e.g. )buildName
- ?: string — (Default: '') 'build-999'
Optional build-number/build-name to append to the version number. (e.g.
)offerDateShift
- ?: boolean — (Default: false) root
Should the user be offered to shift the release date N days into the future.
- ?: string — (Default: '.') changelogSuffix
The root directory of the project/package.
- ?: string — (Default: '') CHANGELOG.md
Optional suffix to append to the file before the .md filepkgJsonSuffix
extension.
- ?: string — (Default: '') package.json
Optional suffix to append to the file before the .json fileversionKey
extension.
- ?: string — (Default: 'version') pkg.*
Optional custom key to read.
---
Syntax: getPkgVersion(options?: PackageVersionOpts): Promise
Reads the current version field from the ./package.json file and returns
it.
Errors (but does not exit) if the string does not roughly match the semver
format.
`ts
import { getPkgVersion } from '@maranomynet/libtools';
const version: string = await getPkgVersion();
`
PackageVersionOpts:
- root?: string — (Default: '.') pkgJsonSuffix
The root directory of the project/package.
- ?: string — (Default: '') package.json
Optional suffix to append to the file before the .json fileversionKey
extension.
- ?: string — (Default: 'version') pkg.*
Optional custom key to read.
---
Syntax: publishToNpm(opts?: PublishToNpmOpts): Promise
Publishes the library to npm (using npm publish) and commits theCANGELOG.md and package.json changes to the local git repo.
Exits if any problems are found.
`ts
import { publishToNpm } from '@maranomynet/libtools';
// First, update the package version and run build and tests, etc.
await publishToNpm(); // Exits on errors.
`
PublishToNpmOpts:
- showName?: boolean — (Default: false) root
Should the package name be displayed in the "release:" commit message.
- ?: string — (Default: '.') pkgJsonSuffix
The root directory of the project/package.
- ?: string — (Default: '') package.json
Optional suffix to append to the file before the .json filechangelogSuffix
extension.
- ?: string — (Default: '') CHANGELOG.md
Optional suffix to append to the file before the .md file
extension.
---
Syntax: distFolder: '_npm-lib'
The directory where the built package will be placed.
`ts
import { distFolder } from '@maranomynet/libtools';
console.log(distFolder); // '_npm-lib'
`
This directory is not configurable, and should be added to your .gitignoresearch.exclude
file and VSCode's setting:
`json`
"search.exclude": {
"_npm-lib/*": true
},
---
This package offers a few tools to ensure code quality. They assume you have
ESLint and Prettier installed and configured.
Shared options:
- continueOnError — (Default: false)
Controls whether the check should hard exit on errors, or merely reject the
promise to allow you to handle the error (and possibly exit) manually.
Syntax:
errorCheckSources(opts?: { tsWorkspaces?: Array
Error-checks the project's sources using ESLint and the TypeScript compiler.
It ignores warnings, but exits if errors are found. Does NOT auto-fix
anything.
Exra Options:
- tsWorkspaces?: Array — (Default: []) tsconfig.json
An array additional TypeScript workspaces to type-check.\
Can be either a relative path to a tsconfig file, or a folder that containings
a file called .
`ts
import { errorCheckSources } from '@maranomynet/libtools';
await errorCheckSources(); // Exits on errors.
// or...
await errorCheckSources({ continueOnError: true }).catch((err) => {
// do something custom
});
// multi-workspace:
await errorCheckSources({
tsWorkspaces: ['api-server', './tsconfig.testserver.json'],
});
// Runs tsc for:
// - ./tsconfig.json (<-- always checked!)./api-server/tsconfig.json
// - ./tsconfig.testserver.json
// - `
---
Syntax:
typeCheckSources(opts?: { tsWorkspaces?: Array
Type-checks the project's sources using TypeScript's tsc.
Has the same options as errorCheckSources, plus:
- watch?: boolean — (Default: false) true
If , the type-checker will watch the files for changes.
`ts
import { typeCheckSources } from '@maranomynet/libtools';
await typeCheckSources(); // Exits on errors.
// or...
typeCheckSources({ watch: true }); // Does not exit on errors.
// multi-workspace:
await typeCheckSources({
tsWorkspaces: ['api-server', './tsconfig.testserver.json'],
watch: true,
});
// typechecks and watches all workspaces
`
---
Syntax: lintSources(opts?: { continueOnError?: boolean }): Promise
Lints the project's sources using ESLint and Prettier. Reports all warnings
and errors, but DOES NOT EXIT. Does NOT auto-fix anyting.
`ts
import { lintSources } from '@maranomynet/libtools';
await lintSources();
`
---
Syntax:
formatSources(opts?: { continueOnError?: boolean }): Promise
Formats auto-fixes the project's sources using Prettier and ESLint. Auto-fixes
all auto-fixable issues, but does NOT report anything. Exits if errors are
found.
`ts
import { formatSources } from '@maranomynet/libtools';
await formatSources(); // Exits on errors.
// or...
await formatSources({ continueOnError: true }).catch((err) => {
// do something custom
});
`
---
---
Syntax: Record
The command line arguments passed to the script, parsed into an object where
the keys are the argument names and the values are the argument values.
For example, if you call your script like this:
`sh`
bun my-script.ts --foo=bar --baz --smu=false
…then in my-script.ts:
`ts
import { args } from '@maranomynet/libtools';
console.log(args);
// {
// foo: 'bar',
// baz: true,
// smu: false
// }
`
---
Syntax: Record
Filtered convenience clone of args with all boolean values removed.
So, if you call your script like this:
`sh`
bun my-script.ts --foo=bar --baz --smu=false
…then in my-script.ts:
`ts
import { argStrings } from '@maranomynet/libtools';
console.log(argStrings);
// {
// foo: 'bar',
// }
`
The argument parsing is currenly very simple and stupid:
1. Spaces are used to separate arguments.
2. Quotation marks CAN NOT be used to group arguments.
3. Equals signs (=) between key and value must NOT have any spaces aroundtrue
them.
4. Literal or false (case-insensitive) values are converted to aboolean
.
Syntax:
shell$(cmd: string | Array
A wrapper around Node.js' child_process.exec command that returns a promise
and pipes the output to the current process' stdout and stderr.
If you pass an array of commands, they will be joined with ' && ' (after
filtering out all falsy values).
If handleError is true, the process will simply throw (i.e. reject theprocess
Promise) instead of exiting the with code 1.
If handleError is a callback, it will be called with the error object before
the promise resolves.
`ts
import { shell$ } from '@maranomynet/libtools';
await shell$('NAME=World; echo "Hello ${NAME}!"');
// Logs: "Hello World!"
const dir = 'some-dir';
// These commands are joined with ' && ' before execution
await shell$([
mkdir ${dir},cd ${dir}
,echo "Hello World!" > hello.txt
,cd -
null, // Falsy values are ignored/filtered
,
]);
// Don't exit the process on error, just log it
await shell$('bad_command boom!', true).catch(logError);
// Shorthand version that also does not exit the process
await shell$('bad_command boom!', logError);
`
There are different ways of running scripts and package binaries, depending on
whether you're using npm, yarn or bun.
Libtools tries to auto-detect which runner you're using, based on the presence
of bun.lockb and yarn.lock files — falling back to npm as a default.
`ts
import {
runner,
runScript,
runPkgBin,
setRunner,
} from '@maranomynet/libtools';
console.log(runner); // ??? (auto-detected for your project, defaults to 'npm')
setRunner('npm'); // Force "npm" as the runner (for example)
console.log(runner); // 'npm'
console.log(runScript); // 'npm run '
console.log(runPkgBin); // 'npm exec -- '
`
The runScript string is a prefix that can be used to run a package.jsonrunPkgBin
script using the current runner, whereas executes the package
binary of an installed dependency. For example:
`ts
import { ruScript, runPkgBin, shell$ } from '@maranomynet/libtools';
await shell$(runScript + 'test'); // runs pkg.scripts.test
await shell$(runPkgBin + 'vitest --watch'); // runs node_modules/.bin/vitest
`
This package also includes a few convenience functions for handling thrown
errors, rejected Promises and other script failures.
`ts
import {
exit1,
logThenExit1,
logError,
ignoreError,
} from '@maranomynet/libtools';
const rejected = Promise.reject(new Error('Oops!'));
rejected.catch(exit1); // Immediate process.exit(1)`
// or...
rejected.catch(logThenExit1); // Logs the error, then exits with code 1
// or...
rejected.catch(logError); // Console logs the error and then continues
// or...
rejected.catch(ignoreError); // Ignores the error and continues
Syntax: prompt(question: string): Promise
Prompts the user with a question and returns a promise that resolves to the
user's answer (trimmed).
`ts
import { prompt } from '@maranomynet/libtools';
const name: string = await prompt('What is your name?');
// What is your name? ▍
`
Syntax:
promptYN(question: string, defAnswer?: 'y'|'n'): Promise
Prompts the user with a question and returns a promise that resolves to truefalse
if the user enters "y" or "Y" and if the user enters "n" or "N".
`ts
import { promptYN } from '@maranomynet/libtools';
const userAccepted: boolean = await promptYN('Do you want to continue?');
// Do you want to continue? [Y]n ▍
const deleteAll = await promptYN('Delete all the things?', 'n');
// Delete all the things? y[N] ▍
`
---
---
Expects T to be true
`ts
import type { Expect } from '@reykjavik/hanna-utils';
type OK = Expect
type Fails = Expect
`
---
Returns true if types A and B are equal (and neither is any)
`ts
import type { Equals, Expect } from '@reykjavik/hanna-utils';
type OK = Expect
type Fails = Expect
`
---
Returns true if type A extends type B (and neither is any)
`ts
import type { Extends, Expect } from '@reykjavik/hanna-utils';
type OK = Expect
type Fails = Expect
`
---
Returns true if type A does NOT extend type B (and neither is any)
`ts
import type { NotExtends, Expect } from '@reykjavik/hanna-utils';
type OK = Expect
type Fails = Expect
type FailsAlso = Expect
``
---
This project uses the Bun runtime for development (tests,
build, etc.)
PRs are welcoms!
---
See
CHANGELOG.md