pkg-types icon indicating copy to clipboard operation
pkg-types copied to clipboard

Package mutation utilities

Open 43081j opened this issue 6 months ago • 6 comments

Describe the feature

To provide a lighter alternative to the npmcli libraries, we could provide the common mutation utilities and leave out the rest:

Features

  • Add/remove dependencies from prod, dev, peer, optional peer
  • Partial updates
  • Basic normalisation
    • Automatically sort dependency lists

Implementation

We can probably also follow a class based approach like npm did:

type DependencyType = 'peer' | 'optionalPeer' | 'dev' | 'prod';

declare class PackageJson {
  addDependency(
    name: string,
    version: string,
    type?: DependencyType
  ): void;
  removeDependency(
    name: string,
    type?: DependencyType
  ): void;
  update(
    partial: Partial<PackageJson>
  ): void;
  save(): Promise<void>;
}

declare function loadPackageJson(path: string): Promise<PackageJson>;

internally, save can do basic normalisation:

  • sort the dependency lists
  • set name to be '' if it isn't set
  • set version to be '' if it isn't set

Given we already have PackageJson (type), we could name this PackageJsonManager or some such thing, and have a convenience constructor/util to new one up from a PackageJson (as that's also what we'd store internally).

Additional information

  • [ ] Would you be willing to help implement this feature?

43081j avatar Aug 04 '25 20:08 43081j

This is now implemented by #240

MichaelDeBoey avatar Aug 19 '25 23:08 MichaelDeBoey

We might still introduce addDependency and removeDependency utils BTW just have to do more research about ecosystem needs.

Feedbacks more than welcome @MichaelDeBoey what you feel missing with current state.

pi0 avatar Aug 19 '25 23:08 pi0

@pi0 As mentioned in https://github.com/unjs/pkg-types/pull/240#issuecomment-3175361169, these utilities could be a perfect alternative for @npmcli/package-json usage in React Router without much changes to the current code

  • https://github.com/remix-run/react-router/blob/a4644d37e4b891486e094479bc7c33dd538dd7d5/packages/react-router-dev/cli/commands.ts
  • https://github.com/remix-run/react-router/blob/a4644d37e4b891486e094479bc7c33dd538dd7d5/packages/react-router-dev/config/config.ts
  • https://github.com/remix-run/indie-stack/blob/56abb93bf81f635b574d9ca23eed05602699458a/remix.init/index.js
  • https://github.com/remix-run/blues-stack/blob/7390646d6f3b6e6cb7ef722e4b61cdbffb997afc/remix.init/index.js
  • https://github.com/remix-run/grunge-stack/blob/c25307668648b4944e5cd7815c78c098d67ea322/remix.init/index.js

Adding convenience methods like removePackageJSONScript could replace code like seen in https://github.com/remix-run/grunge-stack/blob/c25307668648b4944e5cd7815c78c098d67ea322/remix.init/index.js#L51-L64 But I would understand that doing pkg.scripts.scriptToRemove = undefined would be the preferred solution.

MichaelDeBoey avatar Aug 20 '25 10:08 MichaelDeBoey

Apart from normalizing, Utilities for managing dependencies (without involving package managers), particularly, might make real value IMO, like @43081j's initial draft for controlling peer/meta, etc. We could also add some registry-aware features like resolving tags and auto-adding peer deps with a simple fetch to it.

Adding more utils for all the fields might be hard to maintain and add to the package size, while it could be done with simple one-line JS code already.


Currently, we only expose the core utility updatePackage, which allows running any kind of changes in one atomic read/write run and preserving format.

@MichaelDeBoey I think your above example could migrate to something like this. How does it sound to you:

import { updatePackage } from 'pkg-types'

const updatePackageJson = async ({ APP_NAME, packageJson }) => {
  await updatePackage('package.json', pkg => {
    pkg.scripts["format:repo"] = _repoFormatScript
    pkg.name = APP_NAME
  })
};

pi0 avatar Aug 20 '25 17:08 pi0

I think your above example could migrate to something like this. How does it sound to you:

import { updatePackage } from 'pkg-types'

const updatePackageJson = async ({ APP_NAME, packageJson }) => {
  await updatePackage('package.json', pkg => {
    pkg.scripts["format:repo"] = _repoFormatScript
    pkg.name = APP_NAME
  })
};

@pi0 Actually, it doesn't, since pkg.scripts["format:repo"] is supposed to be removed.

The way that's currently done is by getting it as _repoFormatScript and putting the rest in scripts due to the rest operator. After that we overwrite the original pkg.scripts with scripts So it would actually be

import { updatePackage } from 'pkg-types'

const updatePackageJson = async ({ APP_NAME, packageJson }) =>
  updatePackage('package.json', (pkg) => {
    { "format:repo": _repoFormatScript, ...scripts } = pkg.scripts

    pkg.scripts = scripts
    pkg.name = APP_NAME
  });

So the whole destructing thing is what I actually wanted to avoid with a util like removePackageScript. Having something like @43081j suggested where you could just set it to undefined and it would be removed, would be a nice alternative if you ask me

import { updatePackage } from 'pkg-types'

const updatePackageJson = async ({ APP_NAME, packageJson }) =>
  updatePackage('package.json', (pkg) => {
    pkg.scripts["format:repo"] = undefined
    pkg.name = APP_NAME
  });

MichaelDeBoey avatar Aug 20 '25 23:08 MichaelDeBoey

Ah i might have misread your ref code. Yeah you can totally do that and assign = undefined. During JSON serialization it will be dropped.

pi0 avatar Aug 21 '25 09:08 pi0