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 🙏

© 2024 – Pkg Stats / Ryan Hefner

@topoconfig/extends

v0.16.2

Published

Populates `extends` reference in configs

Downloads

1,151

Readme

@topoconfig/extends

Populates extends references in configs

lcov npm (scoped)

Many tools provide extends feature for their configs, but it works a little differently in each place. For example, tsc applies deep merge to compilerOptions, while eslint concatenates elements within the overrides array, among others. As a result, developers have to implement these variances manually for each project, which can be both time-consuming and error-prone. Optimizing this routine process appears to be a practical solution:

const tsconfig = await populate('tsconfig.json', {
  compilerOptions: 'merge'
})

Moreover, now you can resolve a given config just as like tsc, but also do it properly, taking into account ts/issues/56436:

const tsconfig = await populate('tsconfig.json', {
  compilerOptions:               'merge',
  'compilerOptions.paths':       'merge',
  'compilerOptions.typeRoots':   'merge',
  'compilerOptions.typeRoots.*': 'rebase',
  'compilerOptions.outDir':      'rebase',
  'compilerOptions.paths.*.*':   'rebase'
})

Implementation notes

Key features

  • Recursive extras population (extends by default).
  • Multiple sources support
  • Configurable merging rules
    • prop/pattern-specific declarations
    • 5 built-in strategies: populate, ignore, merge, override, rebase
  • Sync and async modes
  • Immutability with prototype transits
  • Easy customization (opinionated)
  • Nodejs, Deno & Bun support

yargs/helpers is the closest one, but the differences are still noticeable:

import {applyExtends} from 'yargs/helpers'

const config = applyExtends({
  extends: './base.config.json',
  foo: 'foo',
  bar: {
    a: 'a'
  }
}, process.cwd())
  • No mjs/esm
  • No immutability
  • No multiple sources
  • No custom merge which is essential for some cases like arrays
  • No custom formats support
  • No async mode
  • No file urls support

Status

Working draft

Install

npm i @topoconfig/extends

Usage

populate

import { populate } from '@topoconfig/extends'

/** Imagine ../base.config.cjs contents
module.export = {
  bar: {
    b: 'b'
  }
}
*/

const config = {
  extends: '../base.config.cjs',
  foo: 'foo',
  bar: {
    a: 'a'
  }
}

const result = await populate(config, {
  bar: 'merge'
})

// returns
{
  foo: 'foo',
  bar: {
    a: 'a',
    b: 'b'
  } // ← bar holds both fields from the base and the current config
}

If the config param is a string it will be treated as a path and loaded.

const result = await populate('tsconfig.json', {
  compilerOptions: 'merge'
})

The sync version is also available. But keep in mind that .mjs (ESM) files cannot be processed in this mode.

import { populateSync } from '@topoconfig/extends'

const result = populateSync({
  extends: '../base.config.cjs',
  foo: 'foo',
  bar: {
    a: 'a'
  }
}, {
  bar: 'merge'
})

The config's extra property may hold objects, strings or string[]. The last two types will be processed via the internal load function. Extra key defaults to extends but can be remapped via merging rules.

const config = {
  extends: [
    '../base.config.cjs',
    {
      // Of cource, nested `extends` will be processed too
      extends: ['../../other.config.mjs']
    }
  ]
}

You can specify how to process config fields obtained from different sources. There are just five strategies: populate, ignore, merge and override. The last one is applied by default.

{
  foo:      'merge',
  bar:      'override',
  baz:      'merge',
  'baz.qu': 'merge',
  cwd:      'ignore',    // do not capture the `cwd` field from the source
  extends:  'populate',
  preset:   'populate',  // now both `preset` and `extends` fields will be populated
  'compilerOptions.typeRoots.*': 'rebase',  // to handle the value as a relative path and resolve it from the root / entry point cwd.
  'compilerOptions.outDir':      'rebase',
  'compilerOptions.paths.*.*':   'rebase'
}

To switch the default behavior use asterisk * as a key:

{
  '*': 'merge'
}

CLI

If you needed this, you definitely know why.

xtends <config.json> [<opts> [<output.json>]]

xtends tsconfig.json '{"compilerOtrions": "merge"}' > resolved.json
xtends prettier.json '{"overrides": "merge"}' resolved.json

Customization

Options define merging rules, but it's also suitable to override some internals:

| Option | Description | Default | |-----------|----------------------------------------------------------------------------|----------------------| | cwd | Current working directory | process.cwd() | | resolve | Utility to reveal resource paths | #resolve | | load | Resource loader | #load | | parse | Parser function. Customize to handle non-std types like .yaml or .toml | #parse | | merge | Merge function. Smth like Object.assign or deepExtend should be ok. | #extend | | prepare | Handler to preprocess data: initialize, validate, clone, etc. | #prepare | | vmap | Value transformer. | #vmap | | rules | Merging rules | {'*': 'override'} |

const opts = {
  cwd: '/foo/bar',
  prepare: lodash.cloneDeep,
  rules: {
    '*': 'merge'
  }
}

yaml

No problem, js-yaml or yaml-js at your service:

import {load as parseYaml} from 'js-yaml'
import {populate} from '@topoconfig/extends'

const config = await populate('tsconfig.yaml', {
  parse({id, contents, ext}) {
    if (ext === '.yaml' || ext === '.yml') 
        return parseYaml(contents)
    if (ext === '.json') 
        return JSON.parse(contents)
    throw new Error(`Unsupported format: ${ext}`)
  }
})

cosmiconfig

Definitely yes! You can use it to find and load configs in various ways:

const raw = {
  a: 'a',
  extends: '../config.extra.in.yaml'
}
const config = await populate(raw, {
  load: async ({id, cwd}) => (await cosmiconfig('foo', {
    searchPlaces: [id]
  }).search(cwd))?.config
})

Or like this:

const {load} = cosmiconfig('foo')
const config = await populate(raw, {
  load: async ({id, cwd}) => (await load(path.resolve(cwd, id)))?.config
})

Or even like this:

import cosmiconfig from 'cosmiconfig'

const config = await populate('cosmiconfig:magic', {
  async load({cwd}) {
    return (await cosmiconfig('foobar').search(cwd))?.config
  }
})

Literally, there is no limitations:

import cosmiconfig from 'cosmiconfig'

const config = await populate('cosmiconfig:magic', {
  resolve({cwd}) {
    return cosmiconfigSync('foobar').search(cwd).filepath
  }
})

Internals

To simplify tweak ups some internals are exposed.

extend

Accepts objects and merges them according to the rules.

import { extend } from '@topoconfig/extends'

const sources = [
    {a: {b: {foo: 'foo'}}},
    {a: {b: {bar: 'bar'}, c: 'c'}},
    {a: {b: {baz: 'baz'}, c: 'C'}}
]
const rules = {
  a: 'merge',
  'a.b': 'merge'
}
const result = extend({sources, rules})
// gives
{
  a: {
    b: {
      foo: 'foo',
      bar: 'bar',
      baz: 'baz'
    },
    c: 'C'
  }
}

merge strategy for arrays means concatenation.

const sources = [
  {a: [1]},
  {a: ['a'], b: 'b'},
  {a: [{foo: 'bar'}], c: 'c'},
]
const rules = {
  a: 'merge',
}
const result = extend({sources, rules})
// returns
{
  a: [1, 'a', {foo: 'bar'}],
  b: 'b',
  c: 'c'
}

resolve

Utility to reveal resource paths.

import { resolve } from '@topoconfig/extends'

const local = resolve({id: '../foo.mjs', cwd: '/some/cwd/'}) // '/some/foo.mjs'
const external = resolve({id: 'foo-pkg', cwd: '/some/cwd/'}) // 'foo-pkg'

load

Resource loader in two flavors: sync and async. It uses import/require api for the standard formats (.json, .js, .cjs, .mjs), and fs.read for the rest.

import { load, loadSync } from '@topoconfig/extends'

const foo = await load({resolved: '/some/cwd/foo.mjs'})
const bar = loadSync({resolved: '/some/bar/bar.json'})

parse

Applies JSON.parse to any input.

export const parse = ({contents}: {id: string, contents: string, ext: string}) => JSON.parse(contents)

prepare

Defaults to internal clone function to ensure immutability.

import { prepare } from '@topoconfig/extends'
const copy = prepare({a: 'a', b() {}}) // {a: 'a', b() {}}

If necessary, you can replace it with a more advanced implementation, such as rfdc.

vmap

Value transformer. It's a good place to apply some custom logic like fields initialization. Default implementation is identity.

const vmap = ({value}) => value

Refs

License

MIT