npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rushstack/heft-config-file

v0.20.10

Published

Configuration file loader for @rushstack/heft

Readme

@rushstack/heft-config-file

A library for loading JSON configuration files in the Heft build system. It supports extends-based inheritance between config files, configurable property merge strategies, automatic path resolution, and JSON schema validation.

Links

Heft is part of the Rush Stack family of projects.


Overview

@rushstack/heft-config-file provides a structured way to load JSON config files that:

  • Extend a parent config file via an "extends" field (including across packages)
  • Merge parent and child properties with configurable inheritance strategies (append, merge, replace, or custom)
  • Resolve paths in property values relative to the config file, project root, or via Node.js module resolution
  • Validate the merged result against a JSON schema
  • Support rigs by falling back to a rig package profile if the project doesn't have its own config file

For config file authors (Heft users)

If you're writing or customizing a config file that uses this system (e.g. heft.json, typescript.json, or a plugin's config file), here's what you need to know.

The extends field

Config files can inherit from a parent file using "extends":

{
  "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json",
  "extends": "@my-company/build-config/config/heft.json"
}

The "extends" value is resolved using Node.js module resolution, so it can be:

  • A relative path: "extends": "../shared/base.json"
  • A package reference: "extends": "@my-company/rig/profiles/default/config/heft.json"

Circular extends chains are detected and will throw an error.

How property inheritance works

When a child config extends a parent, each top-level property is merged according to its inheritance type. The inheritance type is configured by the package that defines the config file schema (not by the user).

The built-in inheritance types are:

| Type | Applies to | Behavior | |------|-----------|---------| | replace | any | Child value completely replaces the parent value (default for objects) | | append | arrays only | Child array elements are appended after parent array elements (default for arrays) | | merge | objects only | Shallow merge: child properties override parent properties, parent-only properties are kept | | custom | any | A custom merge function defined by the loader |

Setting a property to null always removes the parent's value, regardless of inheritance type.

Per-property inline override: $propertyName.inheritanceType

If the schema allows it, you can override the inheritance type for an individual property directly in your config file using the "$<propertyName>.inheritanceType" annotation:

{
  "extends": "./base.json",

  "$plugins.inheritanceType": "append",
  "plugins": [
    { "pluginName": "my-plugin" }
  ],

  "$settings.inheritanceType": "merge",
  "settings": {
    "strict": true
  }
}

These annotations work at any nesting level - you can annotate a nested property the same way:

{
  "extends": "./base.json",

  "$d.inheritanceType": "merge",
  "d": {
    "$g.inheritanceType": "append",
    "g": [{ "h": "B" }],

    "$i.inheritanceType": "replace",
    "i": [{ "j": "B" }]
  }
}

The inline annotation takes precedence over any default set by the loader.

Note: $propertyName.inheritanceType is a loader-level annotation and is stripped from the final config object; it will not appear in the merged result or be validated by the schema.

Path resolution

Properties that represent file system paths may be automatically resolved by the loader. The resolution method is determined by the loader's configuration, not the config file author. The original (unresolved) value is preserved and can be retrieved via the API.


For API consumers (plugin/loader authors)

If you're writing a Heft plugin that needs to load a config file, use ProjectConfigurationFile or NonProjectConfigurationFile.

ProjectConfigurationFile

Use this for config files stored at a known path relative to the project root, with optional rig support.

import { ProjectConfigurationFile, InheritanceType, PathResolutionMethod } from '@rushstack/heft-config-file';

interface IMyPluginConfig {
  outputFolder: string;
  plugins: string[];
  settings?: {
    strict: boolean;
  };
  extends?: string;
}

const loader = new ProjectConfigurationFile<IMyPluginConfig>({
  // Path relative to the project root
  projectRelativeFilePath: 'config/my-plugin.json',

  // Provide either jsonSchemaPath or jsonSchemaObject
  jsonSchemaPath: require.resolve('./schemas/my-plugin.schema.json'),

  // Configure how properties merge when a config file uses "extends"
  propertyInheritance: {
    plugins: { inheritanceType: InheritanceType.append },
    settings: { inheritanceType: InheritanceType.merge }
    // Properties not listed here use the default for their type
  },

  // Optionally override the default inheritance for all arrays or all objects
  propertyInheritanceDefaults: {
    array: { inheritanceType: InheritanceType.append },   // built-in default
    object: { inheritanceType: InheritanceType.replace }  // built-in default
  },

  // Automatically resolve path properties to absolute paths
  jsonPathMetadata: {
    '$.outputFolder': {
      pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile
    }
  }
});

// Load config for a project (throws if not found)
const config = loader.loadConfigurationFileForProject(terminal, projectPath);

// Load config with rig fallback
const config = loader.loadConfigurationFileForProject(terminal, projectPath, rigConfig);

// Returns undefined instead of throwing if the file doesn't exist
const config = loader.tryLoadConfigurationFileForProject(terminal, projectPath, rigConfig);

// Async variants are also available:
// loader.loadConfigurationFileForProjectAsync(...)
// loader.tryLoadConfigurationFileForProjectAsync(...)

When a rigConfig is provided and the project does not have its own config file, the loader falls back to the same relative path inside the rig's profile folder.

NonProjectConfigurationFile

Use this for config files at arbitrary absolute paths (not bound to a project root):

import { NonProjectConfigurationFile } from '@rushstack/heft-config-file';

const loader = new NonProjectConfigurationFile<IMyConfig>({
  jsonSchemaPath: '/path/to/schema.json'
});

const config = loader.loadConfigurationFile(terminal, '/absolute/path/to/config.json');
// Also: tryLoadConfigurationFile, loadConfigurationFileAsync, tryLoadConfigurationFileAsync

JSON schema

Supply either a file path or an inline object:

// From a file path
{ jsonSchemaPath: require.resolve('./schemas/my-plugin.schema.json') }

// Inline
{ jsonSchemaObject: { type: 'object', properties: { ... } } }

Schema validation runs after all inheritance merging, so the schema describes the shape of the final merged result.

Custom validation

For validation logic that JSON schema cannot express, supply a customValidationFunction:

const loader = new ProjectConfigurationFile<IMyPluginConfig>({
  projectRelativeFilePath: 'config/my-plugin.json',
  jsonSchemaPath: require.resolve('./schemas/my-plugin.schema.json'),

  customValidationFunction: (configFile, configFilePath, terminal) => {
    if (configFile.outputFolder === configFile.inputFolder) {
      terminal.writeErrorLine('outputFolder and inputFolder must be different');
      return false;
    }
    return true;
  }
});

The function is called after schema validation. If it returns anything other than true, an error is thrown. The function may also throw its own error to provide a custom message.

Path resolution

Use jsonPathMetadata to automatically resolve string properties that represent file system paths. Keys are JSONPath expressions, so wildcards work for arrays and nested objects:

jsonPathMetadata: {
  // Resolve a specific property
  '$.outputFolder': {
    pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile
  },

  // Resolve all "path" properties inside an array of objects
  '$.plugins.*.path': {
    pathResolutionMethod: PathResolutionMethod.resolvePathRelativeToConfigurationFile
  },

  // Node.js module resolution (like require.resolve)
  '$.loaderPackage': {
    pathResolutionMethod: PathResolutionMethod.nodeResolve
  },

  // Custom resolver
  '$.specialPath': {
    pathResolutionMethod: PathResolutionMethod.custom,
    customResolver: ({ propertyValue, configurationFilePath }) => {
      return myCustomResolve(propertyValue, configurationFilePath);
    }
  }
}

Available resolution methods:

| Method | Behavior | |--------|---------| | resolvePathRelativeToConfigurationFile | path.resolve(configFileDir, value) | | resolvePathRelativeToProjectRoot | path.resolve(projectRoot, value) | | nodeResolve | Node.js require.resolve-style resolution | | custom | Call your own resolver function |

Inspecting source file and original values

After loading, you can query where objects came from and what their pre-resolution values were:

const config = loader.loadConfigurationFileForProject(terminal, projectPath);

// Which config file did this object come from?
const sourceFile = loader.getObjectSourceFilePath(config);
// e.g. "/my-project/config/my-plugin.json"

// What was the raw value of a property before path resolution?
const originalValue = loader.getPropertyOriginalValue({
  parentObject: config,
  propertyName: 'outputFolder'
});
// e.g. "./dist"  (before being resolved to an absolute path)

These methods work on any object that was loaded as part of the config file (including nested objects).

Custom inheritance functions

For cases where the built-in merge strategies aren't enough:

import { InheritanceType } from '@rushstack/heft-config-file';

const loader = new ProjectConfigurationFile<IMyConfig>({
  projectRelativeFilePath: 'config/my-plugin.json',
  jsonSchemaPath: require.resolve('./schemas/my-plugin.schema.json'),
  propertyInheritance: {
    myProperty: {
      inheritanceType: InheritanceType.custom,
      inheritanceFunction: (childValue, parentValue) => {
        // Merge logic here; return the combined result
        return { ...parentValue, ...childValue, extra: 'added' };
      }
    }
  }
});

The function receives (childValue, parentValue) and must return the merged result. It is not called if the child sets the property to null - in that case the property is simply deleted.

Inheritance precedence

When a child config file is merged with its parent, the inheritance type for each property is resolved in this order (highest precedence first):

  1. Inline annotation in the config file: "$myProp.inheritanceType": "append"
  2. propertyInheritance option passed to the loader constructor
  3. propertyInheritanceDefaults option (per type: array or object)
  4. Built-in defaults: append for arrays, replace for objects

Testing

TestUtilities.stripAnnotations removes the internal tracking metadata from a loaded config object, which is useful when writing snapshot tests:

import { TestUtilities } from '@rushstack/heft-config-file';

const config = loader.loadConfigurationFileForProject(terminal, projectPath);
expect(TestUtilities.stripAnnotations(config)).toMatchSnapshot();