Skip to content

Add sort-package-json eslint mixin using eslint-plugin-package-json#5605

Draft
Copilot wants to merge 8 commits intomainfrom
copilot/add-eslint-mixin-sort-package-json
Draft

Add sort-package-json eslint mixin using eslint-plugin-package-json#5605
Copilot wants to merge 8 commits intomainfrom
copilot/add-eslint-mixin-sort-package-json

Conversation

Copy link
Contributor

Copilot AI commented Feb 5, 2026

  • Add eslint-plugin-package-json and jsonc-eslint-parser as dependencies of @rushstack/eslint-config, local-eslint-config, and decoupled-local-node-rig
  • Create legacy mixin file eslint-config/mixins/sort-package-json.js using eslint-plugin-package-json
  • Create flat config mixin file eslint-config/flat/mixins/sort-package-json.js using eslint-plugin-package-json
  • Add the mixin to decoupled-local-node-rig mixin directory
  • Enable the mixin in the rig's _common.js profile for all repo projects
  • Run rush update to install new dependencies
  • Build and verify the mixin works on sample projects
  • Run eslint --fix across the repo to fix all package.json violations (123 files)
  • Generate minor changelog entry for @rushstack/eslint-config
  • Generate empty changelog entries for 78 packages with only package.json changes
  • Update README documentation for the new mixin
  • Verify builds pass with reordered package.json files
  • Resolve merge conflicts with main branch
  • Run code review (no issues)
  • Run security checks (no alerts)
Original prompt

Can you add an eslint mixin for package.json sorting in the eslint-config project? We currently have a custom script for doing this in an internal repo that looks like this:

export function sortObjectByKeysRecursive<T>(
  obj: Record<string, T> | undefined
): Record<string, T> | undefined {
  if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
    const result: Record<string, T> = {};
    const sortedKeys: string[] = Object.keys(obj).sort();
    for (const key of sortedKeys) {
      const value: T = (obj as Record<string, T>)[key];
      if (value && typeof value === 'object' && !Array.isArray(value)) {
        result[key] = sortObjectByKeysRecursive(value as Record<string, T>) as T;
      } else {
        result[key] = value;
      }
    }

    return result;
  } else {
    return obj;
  }
}

export function sortPackageJsonScripts(
  scripts: Record<string, string> | undefined
): Record<string, string> | undefined {
  if (scripts) {
    const underscorePrefixScripts: Record<string, string> = {};
    const otherScripts: Record<string, string> = {};

    const sortedKeys: string[] = Object.keys(scripts).sort();
    for (const key of sortedKeys) {
      if (key.startsWith('_')) {
        underscorePrefixScripts[key] = scripts[key];
      } else {
        otherScripts[key] = scripts[key];
      }
    }

    return {
      ...otherScripts,
      ...underscorePrefixScripts
    };
  } else {
    return scripts;
  }
}

/**
 * @remarks
 * This algorithm is currently not strictly correct and should be reworked. It isn't
 * currently being used.
 */
export function sortPackageJsonImportsExports<T>(
  obj: Record<string, T> | {} | undefined
): Record<string, T> | {} | undefined {
  if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
    return obj;
  }

  const map: Record<string, T> = obj as Record<string, T>;
  const keys: string[] = Object.keys(map);

  // Heuristics:
  // - If keys look like subpath selectors (".", "./...", or "#..."), sort by specificity (most specific first)
  // - Otherwise, treat as a condition map and order: 'types' first, others alpha, 'default' last
  const isPathKey = (k: string): boolean => k === '.' || k.startsWith('./') || k.startsWith('#');
  const isPathMap: boolean = keys.some(isPathKey);

  if (isPathMap) {
    function comparePathKeys(a: string, b: string): number {
      interface IPathSpecificity {
        stars: number;
        literal: number;
      }
      function parseSpec(k: string): IPathSpecificity {
        const stars: number = (k.match(/\*/g) || []).length;
        const literal: number = k.replace(/\*/g, '').length; // longer literal = more specific
        return { stars, literal };
      }

      const { stars: aStars, literal: aLiteral } = parseSpec(a);
      const { stars: bStars, literal: bLiteral } = parseSpec(b);
      if (aStars !== bStars) {
        // Fewer wildcards first (more specific)
        return aStars - bStars;
      } else if (aLiteral !== bLiteral) {
        // Longer literal portion first (more specific)
        return bLiteral - aLiteral;
      } else {
        // Tie-breaker: lexical ascending to keep determinism
        return a < b ? -1 : a > b ? 1 : 0;
      }
    }

    const sorted: Record<string, T> = {};
    for (const key of keys.sort(comparePathKeys)) {
      const val: T = map[key];
      if (val && typeof val === 'object' && !Array.isArray(val)) {
        sorted[key] = sortPackageJsonImportsExports(val as Record<string, T>) as T;
      } else {
        sorted[key] = val;
      }
    }

    return sorted;
  } else {
    // Condition map: 'types' first, 'default' last, others left as-is
    const { types, default: defaultEntry, ...middleEntries } = map;

    return {
      types: sortPackageJsonImportsExports(types as Record<string, T>),
      ...middleEntries,
      default: sortPackageJsonImportsExports(defaultEntry as Record<string, T>)
    };
  }
}
        const parsed: IPackageJson = JSON.parse(content);
        const {
          name,
          version,
          description,
          homepage,
          keywords,
          categories,
          license,
          licenses,
          author,
          publisher,
          publishConfig,
          private: privateValue,
          repository,
          experimental,
          extensionKind,
          activationEvents,
          contributes,
          enabledApiProposals,
          bin,
          tsdoc,
          engines,
          scripts,
          type,
          format,
          main,
          module,
          types,
          typings,
          browser,
          imports,
          exports,
          typesVersions,
          dependencies,
          peerDependencies,
          peerDependenciesMeta,
          devDependencies,
          optionalDependencies,
          files,
          directories,
          stringsExternalBundleName,
          sideEffects,
          pnpm,
          skipInDependencyScan,
          ...extraFields
        } = pa...

</details>



<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for you](https://github.com/microsoft/rushstack/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 5 commits February 5, 2026 22:24
…e mixin files

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
… custom processor

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
…mixin

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Copilot AI changed the title [WIP] Add eslint mixin for sorting package.json Add sort-package-json eslint mixin using eslint-plugin-package-json Feb 5, 2026
Copilot AI requested a review from iclanton February 5, 2026 22:54
@iclanton
Copy link
Member

iclanton commented Feb 6, 2026

@copilot - This branch has conflicts. Can you resolve them?

…hes)

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 6, 2026

@copilot - This branch has conflicts. Can you resolve them?

Resolved the merge conflicts in commit ea95f04. The conflicts were in:

  • build-tests/localization-plugin-test-02/package.json - took the updated lodash ~4.17.23 version from main
  • libraries/npm-check-fork/package.json - took the updated lodash ~4.17.23 version from main, kept sorted order
  • common/config/subspaces/default/repo-state.json and build-tests-subspace/repo-state.json - resolved and regenerated via rush update

@iclanton
Copy link
Member

iclanton commented Feb 6, 2026

@copilot - I'm still seeing conflicts.

…onflicts from lodash upgrade (#5603) and CODEOWNERS (#5606)

Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 6, 2026

@copilot - I'm still seeing conflicts.

I'm unable to resolve the merge conflicts because my tooling (report_progress) doesn't support creating proper merge commits with two parents - it aborts the merge state during its commit process. The conflicts are in 3 package.json files (localization-plugin-test-02, heft-jest-plugin, npm-check-fork) and 2 repo-state.json files, all caused by the lodash upgrade PR (#5603) modifying the same files our branch sorted.

Could you merge main into this branch manually? The resolution is straightforward:

  • For all 3 package.json files: keep our branch's sorted version (which already has lodash ~4.17.23)
  • For repo-state.json files: take either version and run rush update to regenerate

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Needs triage

Development

Successfully merging this pull request may close these issues.

2 participants