Configuration Files

This section will walk you through the configuration files required by the various tools used by ts. If you are already familiar with configuring these tools, this process should be straightforward.

ts takes one of two approaches to configuration, depending on the tool:

For tools that support JavaScript configuration files that export an object or function, ts will provide a higher-order function which uses a base configuration with reasonable defaults, but will allow you to invoke it with your own configuration function to set any overrides you may need. The two configurations are then recursively merged.

For tools that leverage an extends option (ie: ESLint), ts provides an appropriate plugin/preset, and additional configuration may be applied according to the tool's configuration schema.


nr is used to manage and coordinate tasks, keeping your package.json "scripts" section terse while still giving you the flexibility to write custom scripts using a JavaScript or TypeScript configuration file. To configure NR, create nr.config.ts in your project root. ts provides several useful default package scripts which you can choose to overwrite or add to per your project's needs.

nr.config.ts
import { withDefaultPackageScripts } from '@darkobits/ts';

export default withDefaultPackageScripts();

To define additional package scripts or to overwrite one of the defaults, pass an nr configuration function to withDefaultPackageScripts:

nr.config.ts
import { withDefaultPackageScripts } from '@darkobits/ts';

export default withDefaultPackageScripts(({ command, script }) => {
  script('awesome', [
    command('awesome-cmd', {
      args: ['awesome', { sauce: true }]
    })
  ], {
    description: 'Do awesome things.'
  })
});

Once you have created this file, you can produce a list of all package scripts by running:

npx nr --scripts

Take a moment to familiarize yourself with the base scripts provided by ts. It is also possible to create an alias to an nr script in package.json. At the very least, it is recommended that you alias the prepare script provided by ts, which will npm install:

package.json
{
  "scripts": {
    "prepare": "nr prepare"
  }
}

nr supports partial string matching for script names. For example, to run the script build.watch, you could issue the command nr b.w. ✨

Complete documentation for nr can be found here.


Vite (and likely your IDE) need a TypeScript configuration file to be present for language features to work. In your project root, create tsconfig.json. Then, extend the base TypeScript configuration from ts:

tsconfig.json
{
  "extends": "@darkobits/ts/tsconfig.json",
  "compilerOptions": {
    "baseUrl": "src",
    "outDir": "dist"
  }
}

ts uses baseUrl and outDir to configure other tools, including Vite and ESLint. It is therefore crucial that these two options are set correctly.


Vite is used to compile your project. ts also uses plugins that type-check and lint your project as well. To configure Vite, create vite.config.ts in your project root. Then, default-export one of the Vite configuration presets from ts.

Preset: Library

The library configuration preset is suitable for backend servers, CLIs, or Node libraries.

  • Source files will not be bundled into a single output file; the output directory structure will resemble that of the source directory.

  • Dependencies will be externalized.

  • Output format (CJS or ESM) will be inferred from the type field in package.json.

  • Source and output directories will be inferred from tsconfig.json.

  • Shebangs will be preserved in files that have them.

  • Any other files in the source directory will be copied to the output directory as-is, similar to Babel's copyFiles feature.

  • Test files will be excluded from the build.

  • Output will not be minified.

A minimal configuration file would include the following:

vite.config.ts
import { vite } from '@darkobits/ts';

export default vite.library();

To customize Vite's configuration, you may pass either an object or a function to the preset. The object form is suitable for simple, synchronous configuration modifications:

vite.config.ts
import { vite } from '@darkobits/ts';

export default vite.library({
  clearScreen: true,
  publicDir: 'assets'
});

If more advanced configuration is necessary, use a function. This function may be asynchronous. It will be passed a context object with the following properties:

vite.config.ts
import { vite } from '@darkobits/ts';

export default vite.library(async context => {
  const {
    // Vite configuration object created by the library preset.
    config,
    // Forwarded directly from Vite.
    // See: https://vitejs.dev/config/#conditional-config
    command, mode, ssrBuild,
    // Computed project root directory.
    root,
    // Root directory + srcDir from tsconfig.json.
    srcDir,
    // Root directory + outDir from tsconfig.json.
    outDir,
    // Parsed/normalized package.json.
    packageJson,
    // Parsed tsconfig.json.
    tsConfig,
    // Path to the tsconfig file being used.
    tsConfigPath
    // Common glob patterns for different file types.
    patterns,
  } = context;
});

When your configuration function is called, ts will have already applied the preset's configuration. This entire configuration object is available to you to modify in-place. Or, you may prefer to return a new configuration object. If that's the case, the value you return will be recursively merged with the config object provided by the preset.

Whether you decide to modify context.config in-place or return a new object is up to you. Below is an example of using both approaches to add a new plugin:

vite.config.ts
import { vite } from '@darkobits/ts';
import awesomeVitePlugin from 'awesome-vite-plugin';

// Option A: Modify config.plugins in-place.
export default vite.library(({ config }) => {
  config.plugins.push(awesomeVitePlugin({
    // Optional plugin configuration.
  }));
});

// Option B: Return a new configuration object.
export default vite.library(() => {
  return {
    plugins: [
      awesomeVitePlugin({
        // Optional plugin configuration.
      })
    ]
  };
});

When returning a new object, arrays are merged by concatenating them.

Build Targets

ts will configure Vite's config.build.lib.formats setting to output ESM when "type": "module" is set in the project's package.json. Otherwise, Vite will be configured to output CommonJS. If necessary, you may override this behavior using the methods described above.

For more information on configuring Vite, consult the Vite configuration documentation.


Vitest is used for unit-testing your project. By default, Vitest will use your existing Vite configuration file with additional options for Vitest under the test key. Configuration presets provided by ts ship with sensible defaults for Vitest, but these settings may be customized:

vite.config.ts
import { vite } from '@darkobits/ts';

export default vite.library({
  test: {
    // Add custom Vitest configuration here.
    coverage: {
      lines: 90,
      functions: 80
    }
  }
});

For more information on configuring Vitest, consult the Vitest configuration documentation.


ESLint is used to reduce bugs by encouraging best practices and consistent code style. ESLint may be configured using either the legacy configuration file format or the newer flat configuration format.

Flat Configuration

Create eslint.config.js in your project root. This will be an ESM file, so if your project does not have "type": "module" set in package.json, you will need to use the .mjs extension. Then, export the preset provided by @darkobits/eslint-plugin:

eslint.config.js
export { ts as default } from '@darkobits/eslint-plugin';

If you need to add your own rules or other configuration, you will need to export an array:

eslint.config.js
import { ts } from '@darkobits/eslint-plugin';

export default [
  {
    // This config will be applied before the preset.
  },
  ...ts,
  {
    // This config will be applied after the preset.
  }
];

Legacy Configuration

Create .eslintrc.js in your project root. If your project has "type": "module" set in package.json, you must use the .cjs extension as ESLint does not support ESM in legacy configuration files. Then, extend the configuration preset from @darkobits/eslint-plugin, provided by ts:

.eslintrc.js
module.exports = {
  extends: 'plugin:@darkobits/ts'
};

For more information on configuring ESLint, consult the ESLint configuration documentation.

Last updated