Skip to content

Commit f7b3d59

Browse files
Copiloticlanton
andcommitted
Add sort-package-json processor to @rushstack/eslint-plugin and create mixin files
Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
1 parent d8326c1 commit f7b3d59

6 files changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// This mixin enables linting of package.json files to ensure consistent property ordering.
5+
// For more information please see the README.md for @rushstack/eslint-config.
6+
//
7+
// IMPORTANT: Mixins must be included in your ESLint configuration AFTER the profile
8+
9+
const rushstackEslintPlugin = require('@rushstack/eslint-plugin');
10+
11+
module.exports = [
12+
{
13+
files: ['**/package.json'],
14+
plugins: {
15+
'@rushstack': rushstackEslintPlugin
16+
},
17+
processor: '@rushstack/sort-package-json'
18+
}
19+
];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// This mixin enables linting of package.json files to ensure consistent property ordering.
5+
// For more information please see the README.md for @rushstack/eslint-config.
6+
module.exports = {
7+
plugins: ['@rushstack/eslint-plugin'],
8+
9+
overrides: [
10+
{
11+
files: ['package.json'],
12+
processor: '@rushstack/sort-package-json'
13+
}
14+
]
15+
};

eslint/eslint-plugin/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See LICENSE in the project root for license information.
33

44
import type { TSESLint } from '@typescript-eslint/utils';
5+
import type { Linter } from 'eslint';
56

67
import { hoistJestMock } from './hoist-jest-mock';
78
import { noBackslashImportsRule } from './no-backslash-imports';
@@ -14,9 +15,11 @@ import { normalizedImportsRule } from './normalized-imports';
1415
import { typedefVar } from './typedef-var';
1516
import { importRequiresChunkNameRule } from './import-requires-chunk-name';
1617
import { pairReactDomRenderUnmountRule } from './pair-react-dom-render-unmount';
18+
import { sortPackageJsonProcessor } from './sort-package-json';
1719

1820
interface IPlugin {
1921
rules: { [ruleName: string]: TSESLint.RuleModule<string, unknown[]> };
22+
processors: { [processorName: string]: Linter.Processor };
2023
}
2124

2225
const plugin: IPlugin = {
@@ -53,6 +56,10 @@ const plugin: IPlugin = {
5356

5457
// Full name: "@rushstack/pair-react-dom-render-unmount"
5558
'pair-react-dom-render-unmount': pairReactDomRenderUnmountRule
59+
},
60+
61+
processors: {
62+
'sort-package-json': sortPackageJsonProcessor
5663
}
5764
};
5865

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { Linter } from 'eslint';
5+
6+
interface IPackageJson {
7+
[key: string]: unknown;
8+
name?: string;
9+
version?: string;
10+
description?: string;
11+
homepage?: string;
12+
keywords?: string[];
13+
categories?: string[];
14+
license?: string;
15+
licenses?: unknown;
16+
author?: unknown;
17+
publisher?: unknown;
18+
publishConfig?: unknown;
19+
private?: boolean;
20+
repository?: unknown;
21+
experimental?: unknown;
22+
extensionKind?: unknown;
23+
activationEvents?: unknown;
24+
contributes?: unknown;
25+
enabledApiProposals?: unknown;
26+
bin?: Record<string, string>;
27+
tsdoc?: unknown;
28+
engines?: Record<string, string>;
29+
scripts?: Record<string, string>;
30+
type?: string;
31+
format?: unknown;
32+
main?: string;
33+
module?: string;
34+
types?: string;
35+
typings?: string;
36+
browser?: unknown;
37+
imports?: unknown;
38+
exports?: unknown;
39+
typesVersions?: unknown;
40+
dependencies?: Record<string, string>;
41+
peerDependencies?: Record<string, string>;
42+
peerDependenciesMeta?: Record<string, unknown>;
43+
devDependencies?: Record<string, string>;
44+
optionalDependencies?: Record<string, string>;
45+
files?: string[];
46+
directories?: unknown;
47+
sideEffects?: unknown;
48+
pnpm?: unknown;
49+
}
50+
51+
function sortObjectByKeysRecursive<T>(obj: Record<string, T> | undefined): Record<string, T> | undefined {
52+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
53+
const result: Record<string, T> = {};
54+
const sortedKeys: string[] = Object.keys(obj).sort();
55+
for (const key of sortedKeys) {
56+
const value: T = obj[key];
57+
if (value && typeof value === 'object' && !Array.isArray(value)) {
58+
result[key] = sortObjectByKeysRecursive(value as Record<string, T>) as T;
59+
} else {
60+
result[key] = value;
61+
}
62+
}
63+
64+
return result;
65+
} else {
66+
return obj;
67+
}
68+
}
69+
70+
function sortPackageJsonScripts(
71+
scripts: Record<string, string> | undefined
72+
): Record<string, string> | undefined {
73+
if (scripts) {
74+
const underscorePrefixScripts: Record<string, string> = {};
75+
const otherScripts: Record<string, string> = {};
76+
77+
const sortedKeys: string[] = Object.keys(scripts).sort();
78+
for (const key of sortedKeys) {
79+
if (key.startsWith('_')) {
80+
underscorePrefixScripts[key] = scripts[key];
81+
} else {
82+
otherScripts[key] = scripts[key];
83+
}
84+
}
85+
86+
return {
87+
...otherScripts,
88+
...underscorePrefixScripts
89+
};
90+
} else {
91+
return scripts;
92+
}
93+
}
94+
95+
/**
96+
* Compute the sorted package.json content from the parsed object.
97+
*/
98+
function computeSortedPackageJson(parsed: IPackageJson): string {
99+
const {
100+
name,
101+
version,
102+
description,
103+
homepage,
104+
keywords,
105+
categories,
106+
license,
107+
licenses,
108+
author,
109+
publisher,
110+
publishConfig,
111+
private: privateValue,
112+
repository,
113+
experimental,
114+
extensionKind,
115+
activationEvents,
116+
contributes,
117+
enabledApiProposals,
118+
bin,
119+
tsdoc,
120+
engines,
121+
scripts,
122+
type,
123+
format,
124+
main,
125+
module: moduleValue,
126+
types,
127+
typings,
128+
browser,
129+
imports,
130+
exports: exportsValue,
131+
typesVersions,
132+
dependencies,
133+
peerDependencies,
134+
peerDependenciesMeta,
135+
devDependencies,
136+
optionalDependencies,
137+
files,
138+
directories,
139+
sideEffects,
140+
pnpm,
141+
...extraFields
142+
} = parsed;
143+
144+
const newPackageJson: {} = {
145+
name,
146+
version,
147+
description,
148+
homepage,
149+
keywords,
150+
categories,
151+
license,
152+
licenses,
153+
author,
154+
publisher,
155+
publishConfig,
156+
private: privateValue,
157+
repository,
158+
experimental,
159+
extensionKind,
160+
activationEvents,
161+
contributes,
162+
enabledApiProposals,
163+
bin: sortObjectByKeysRecursive(bin as Record<string, string> | undefined),
164+
tsdoc,
165+
engines: sortObjectByKeysRecursive(engines as Record<string, string> | undefined),
166+
scripts: sortPackageJsonScripts(scripts),
167+
type,
168+
format,
169+
main,
170+
module: moduleValue,
171+
types,
172+
typings,
173+
browser,
174+
imports,
175+
exports: exportsValue,
176+
typesVersions,
177+
dependencies: sortObjectByKeysRecursive(dependencies),
178+
peerDependencies: sortObjectByKeysRecursive(peerDependencies),
179+
peerDependenciesMeta: sortObjectByKeysRecursive(
180+
peerDependenciesMeta as Record<string, Record<string, unknown>> | undefined
181+
),
182+
devDependencies: sortObjectByKeysRecursive(devDependencies),
183+
optionalDependencies: sortObjectByKeysRecursive(optionalDependencies),
184+
files,
185+
directories,
186+
sideEffects,
187+
pnpm,
188+
...extraFields
189+
};
190+
191+
return JSON.stringify(newPackageJson, undefined, 2) + '\n';
192+
}
193+
194+
// Store original content keyed by filename for use in postprocess
195+
const originalContentMap: Map<string, string> = new Map();
196+
197+
const sortPackageJsonProcessor: Linter.Processor = {
198+
meta: {
199+
name: '@rushstack/sort-package-json',
200+
version: '0.0.0'
201+
},
202+
203+
supportsAutofix: true,
204+
205+
preprocess(text: string, filename: string): Linter.ProcessorFile[] {
206+
originalContentMap.set(filename, text);
207+
// Return a minimal valid JS file so ESLint doesn't report parse errors
208+
return [{ text: '', filename: '0.js' }];
209+
},
210+
211+
postprocess(messages: Linter.LintMessage[][], filename: string): Linter.LintMessage[] {
212+
const originalContent: string | undefined = originalContentMap.get(filename);
213+
originalContentMap.delete(filename);
214+
215+
if (!originalContent) {
216+
return [];
217+
}
218+
219+
let parsed: IPackageJson;
220+
try {
221+
parsed = JSON.parse(originalContent);
222+
} catch {
223+
// If the JSON is invalid, don't report any sorting issues
224+
return [];
225+
}
226+
227+
const sortedContent: string = computeSortedPackageJson(parsed);
228+
229+
if (originalContent !== sortedContent) {
230+
return [
231+
{
232+
ruleId: '@rushstack/sort-package-json',
233+
message: 'package.json properties are not sorted correctly.',
234+
line: 1,
235+
column: 1,
236+
severity: 1,
237+
nodeType: null as never,
238+
fix: {
239+
range: [0, originalContent.length] as [number, number],
240+
text: sortedContent
241+
}
242+
}
243+
];
244+
}
245+
246+
return [];
247+
}
248+
};
249+
250+
export { sortPackageJsonProcessor, computeSortedPackageJson };
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
// IMPORTANT: Mixins must be included in your ESLint configuration AFTER the profile
5+
6+
const sortPackageJsonMixin = require('@rushstack/eslint-config/flat/mixins/sort-package-json');
7+
8+
module.exports = [...sortPackageJsonMixin];

rigs/decoupled-local-node-rig/profiles/default/includes/eslint/flat/profile/_common.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ const nodeImportResolverPath = require.resolve('eslint-import-resolver-node');
1212

1313
module.exports = {
1414
localCommonConfig: [
15+
{
16+
files: ['**/package.json'],
17+
plugins: {
18+
'@rushstack': rushstackEslintPlugin
19+
},
20+
processor: '@rushstack/sort-package-json'
21+
},
1522
{
1623
files: ['**/*.ts', '**/*.tsx'],
1724
plugins: {

0 commit comments

Comments
 (0)