pnpm/meta-updater

extend support to non-json files

ibezkrovnyi opened this issue · 6 comments

Hi, we are using meta-updater for some json files, and this is really powerful and convenient tool, however we are limited to only files with .json extension.

We would like to also process the following files in meta-updater:

  • .npmrc, .gitignore, .browserlistrc (text content)
  • somefile.cjs, prettier.config.js (js content)
  • .gitlab-ci.yaml (yaml content)

JavaScript

And we will definitely know the format for each file, so the simplest change would be to replace the following:

if (!p.endsWith('.json')) continue
const obj = await readJsonFile(fp)

with:

if (!p.endsWith('.json')) continue
const obj = p.endsWith('.json') ? await readJsonFile(fp) : await readFile(fp, 'utf8')

and add import { readFile } from 'fs/promises' to the top of file

Type Checking

Also update parameter can be strongly typed depending if file has or hasn't .json extension: ts playground

It might be even more convenient if we provide a parser/strinigifier for different file types.

Do you mean adding support for more types out of the box (I can think of yaml, yml and jsonc and may be json5) and treat all other file types as pure text ?

either our of the box or passing in the parsers/stringifiers to meta-updater.

I recently discovered this nifty little package that turns out to be very useful and maybe it's powers could be merged into @pnpm/meta-updater?

In the last year I've ditched Yarn and adopted PNPM in all my projects. It would be amazing to have the mrm file fixing/modding abilities baked into PNPM. Utilizing this meta-updater is really nice for me to make sure everything is synced up.

@zkochan Experimented a bit with passing custom format handlers into meta-updater taking into account type safety. Types are flowing everywhere, so minimal typing is required from user.

Types and Custom Formats ('.gitignore' in example below) and Built-in Formats ('json' in example below) are merged.

If this approach will be implemented, it will be possible to add new built-in types into meta-updater anytime later.

Here is what I got: TS Playground

Usage Example 1: Simple usage

export default (workspaceDir: string) => {
  return createUpdateOptions({
    files: {
      "tsconfig.json": {
        format: "json",
        update: (prev, _dir, _manifest) => prev ? prev : {
          compilerOptions: { target: "esnext" },
        },
      },
    },
  });
};

Usage Example 2: With custom format '.gitignore' defined

const gitignoreFormat = {
  async read(filePath: string) {
    return (await fs.readFile(filePath, "utf8")).split("\n");
  },
  update(
    content: string[] | null,
    dir: string,
    manifest: ProjectManifest,
    userUpdateFn: UpdateFunc<string[]>
  ) {
    return userUpdateFn(content, dir, manifest);
  },
  async write(content: string[], filePath: string) {
    const unique = <T extends unknown[]>(array: T) =>
      Array.from(new Set<T[number]>(array)).sort();
    await fs.writeFile(filePath, unique(content).join("\n"), "utf8");
  },
} as const;

export default (workspaceDir: string) => {
  return createUpdateOptions({
    formats: {
      ".gitignore": gitignoreFormat,
    },
    files: {
      "tsconfig.json": {
        format: "json",
        update: (prev, _dir, _manifest) => prev ?? {
          compilerOptions: { target: "esnext" },
        }
      },
      ".prettierignore": {
        format: ".gitignore",
        update: (prev, _dir, _manifest) => prev ?? ["node_modules"],
      },
    },
  });
};

Also potentially we could look into template string direction:

Current version of meta-updater defines '<file>': <update_func>.

Because now we need to also pass format, it is now on object: '<file>': { format: '<format>', update: <update_func> }.

But actually we can write it much shorter: '<file> [format]': <update_func>

export default (workspaceDir: string) => {
  return createUpdateOptions({
    files: {
      "tsconfig.json [json]": (prev, _dir, _manifest) => prev ? prev : {
          compilerOptions: { target: "esnext" },
       },
    },
  });
};

Also we can add second way of determining format - by defined in FormatHandler extension, so usage will look:

This will simplify definition of each file that has clear extension (json/yaml/ts/etc). This won't work with files w/o extension (.gitignore/prettierignore/.eslintrc/etc).

export default (workspaceDir: string) => {
  return createUpdateOptions({
    files: {
      "tsconfig.json": (prev, _dir, _manifest) => prev ? prev : {
          compilerOptions: { target: "esnext" },
       },
    },
  });
};

While json format can be defined as following:

const jsonFormat = {
  extension: '.json', // <-------------------------------------------
  read(filePath: string) {
    return readJsonFile(filePath);
  },
  update(
    content: object,
    dir: string,
    manifest: ProjectManifest,
    userUpdateFn: UpdateFunc<object>
  ) {
    return userUpdateFn(content, dir, manifest);
  },
  async write(content: object, filePath: string) {
    await writeJsonFile(filePath, content, { detectIndent: true });
  },
} as const;

Detecting by extension sounds good. And for the rest maybe the format can be specified by the user.