@@ -7,7 +7,7 @@ import type {
7
7
SpreadElement ,
8
8
StringLiteral ,
9
9
} from '@babel/types' ;
10
- import type { Parser } from 'prettier' ;
10
+ import type { Parser , ParserOptions } from 'prettier' ;
11
11
import { parsers as babelParsers } from 'prettier/plugins/babel' ;
12
12
13
13
/**
@@ -198,109 +198,166 @@ function sortAst(
198
198
return ast ;
199
199
}
200
200
201
- export const parsers = {
202
- json : {
203
- ...babelParsers . json ,
204
- async parse ( text , options : any ) {
205
- const jsonRootAst = await babelParsers . json . parse ( text , options ) ;
206
-
207
- // The Prettier JSON parser wraps the AST in a 'JsonRoot' node
208
- // This ast variable is the real document root
209
- const ast = jsonRootAst . node ;
210
-
211
- const { jsonRecursiveSort, jsonSortOrder } = options ;
212
-
213
- // Only objects are intended to be sorted by this plugin
214
- // Arrays are considered only in recursive mode, so that we
215
- // can get to nested objected.
216
- if (
217
- ! (
218
- ast . type === 'ObjectExpression' ||
219
- ( ast . type === 'ArrayExpression' && jsonRecursiveSort )
220
- )
221
- ) {
222
- return jsonRootAst ;
223
- }
201
+ /**
202
+ * JSON sorting options. See README for details.
203
+ */
204
+ type SortJsonOptions = {
205
+ jsonRecursiveSort : boolean ;
206
+ jsonSortOrder : Record < string , CategorySort | null > ;
207
+ } ;
224
208
225
- let sortCompareFunction : ( a : string , b : string ) => number = lexicalSort ;
226
- if ( jsonSortOrder ) {
227
- let parsedCustomSort ;
228
- try {
229
- parsedCustomSort = JSON . parse ( jsonSortOrder ) ;
230
- } catch ( error ) {
231
- // @ts -expect-error Error cause property not yet supported by '@types/node' (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/61827)
232
- throw new Error ( `Failed to parse sort order option as JSON` , {
233
- cause : error ,
234
- } ) ;
235
- }
209
+ /**
210
+ * Parse JSON sort options from Prettier options.
211
+ *
212
+ * @param prettierOptions - Prettier options.
213
+ * @returns JSON sort options.
214
+ */
215
+ function parseOptions ( prettierOptions : ParserOptions ) : SortJsonOptions {
216
+ const jsonRecursiveSort = prettierOptions . jsonRecursiveSort ?? false ;
236
217
237
- if (
238
- Array . isArray ( parsedCustomSort ) ||
239
- typeof parsedCustomSort !== 'object'
240
- ) {
241
- throw new Error ( `Invalid custom sort order; must be an object` ) ;
242
- }
218
+ if ( typeof jsonRecursiveSort !== 'boolean' ) {
219
+ throw new Error (
220
+ `Invalid 'jsonRecursiveSort' option; expected boolean, got '${ typeof prettierOptions . jsonRecursiveSort } '` ,
221
+ ) ;
222
+ }
243
223
244
- for ( const categorySort of Object . values ( parsedCustomSort ) ) {
245
- if ( ! allowedCategorySortValues . includes ( categorySort as any ) ) {
246
- throw new Error (
247
- `Invalid custom sort entry: value must be one of '${ String (
248
- allowedCategorySortValues ,
249
- ) } ', got '${ String ( categorySort ) } '`,
250
- ) ;
251
- }
252
- }
253
- const customSort = parsedCustomSort as Record <
254
- string ,
255
- null | CategorySort
256
- > ;
257
-
258
- const evaluateSortEntry = ( value : string , entry : string ) : boolean => {
259
- const regexRegex = / ^ \/ ( .+ ) \/ ( [ i m s u ] * ) $ / u;
260
- if ( entry . match ( regexRegex ) ) {
261
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
262
- const [ , regexSpec , flags ] : string [ ] = entry . match ( regexRegex ) ! ;
263
- // "regexSpec" guaranteed to be defined because of capture group. False positive for unnecessary type assertion.
264
- const regex = new RegExp ( regexSpec as string , flags ) ;
265
- return Boolean ( value . match ( regex ) ) ;
266
- }
267
- return value === entry ;
268
- } ;
269
-
270
- const sortEntries = Object . keys ( customSort ) ;
271
-
272
- sortCompareFunction = ( a : string , b : string ) : number => {
273
- const aIndex = sortEntries . findIndex ( evaluateSortEntry . bind ( null , a ) ) ;
274
- const bIndex = sortEntries . findIndex ( evaluateSortEntry . bind ( null , b ) ) ;
275
-
276
- if ( aIndex === - 1 && bIndex === - 1 ) {
277
- return lexicalSort ( a , b ) ;
278
- } else if ( bIndex === - 1 ) {
279
- return - 1 ;
280
- } else if ( aIndex === - 1 ) {
281
- return 1 ;
282
- } else if ( aIndex === bIndex ) {
283
- // Sort entry guaranteed to be non-null because index was found
284
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
285
- const sortEntry = sortEntries [ aIndex ] ! ;
286
- // Guaranteed to be defined because `sortEntry` is derived from `Object.keys`
287
- const categorySort = customSort [ sortEntry ] as null | CategorySort ;
288
- const categorySortFunction =
289
- categorySort === null
290
- ? lexicalSort
291
- : categorySortFunctions [ categorySort ] ;
292
- return categorySortFunction ( a , b ) ;
293
- }
294
- return aIndex - bIndex ;
295
- } ;
224
+ const rawJsonSortOrder = prettierOptions . jsonSortOrder ?? null ;
225
+ if ( rawJsonSortOrder !== null && typeof rawJsonSortOrder !== 'string' ) {
226
+ throw new Error (
227
+ `Invalid 'jsonSortOrder' option; expected string, got '${ typeof prettierOptions . rawJsonSortOrder } '` ,
228
+ ) ;
229
+ }
230
+
231
+ let jsonSortOrder = null ;
232
+ if ( rawJsonSortOrder !== null ) {
233
+ try {
234
+ jsonSortOrder = JSON . parse ( rawJsonSortOrder ) ;
235
+ } catch ( error ) {
236
+ // @ts -expect-error Error cause property not yet supported by '@types/node' (see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/61827)
237
+ throw new Error ( `Failed to parse sort order option as JSON` , {
238
+ cause : error ,
239
+ } ) ;
240
+ }
241
+
242
+ if ( Array . isArray ( jsonSortOrder ) || typeof jsonSortOrder !== 'object' ) {
243
+ throw new Error ( `Invalid custom sort order; must be an object` ) ;
244
+ }
245
+
246
+ for ( const categorySort of Object . values ( jsonSortOrder ) ) {
247
+ if ( ! allowedCategorySortValues . includes ( categorySort as any ) ) {
248
+ throw new Error (
249
+ `Invalid custom sort entry: value must be one of '${ String (
250
+ allowedCategorySortValues ,
251
+ ) } ', got '${ String ( categorySort ) } '`,
252
+ ) ;
296
253
}
297
- const sortedAst = sortAst ( ast , jsonRecursiveSort , sortCompareFunction ) ;
254
+ }
255
+ }
256
+
257
+ return { jsonRecursiveSort, jsonSortOrder } ;
258
+ }
298
259
299
- return {
300
- ...jsonRootAst ,
301
- node : sortedAst ,
302
- } ;
303
- } ,
260
+ /**
261
+ * Create sort compare function from a custom JSON sort order configuration.
262
+ *
263
+ * @param jsonSortOrder - JSON sort order configuration.
264
+ * @returns A sorting function for comparing Object keys.
265
+ */
266
+ function createSortCompareFunction (
267
+ jsonSortOrder : Record < string , CategorySort | null > ,
268
+ ) : ( a : string , b : string ) => number {
269
+ const evaluateSortEntry = ( value : string , entry : string ) : boolean => {
270
+ const regexRegex = / ^ \/ ( .+ ) \/ ( [ i m s u ] * ) $ / u;
271
+ if ( entry . match ( regexRegex ) ) {
272
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
273
+ const [ , regexSpec , flags ] : string [ ] = entry . match ( regexRegex ) ! ;
274
+ // "regexSpec" guaranteed to be defined because of capture group. False positive for unnecessary type assertion.
275
+ const regex = new RegExp ( regexSpec as string , flags ) ;
276
+ return Boolean ( value . match ( regex ) ) ;
277
+ }
278
+ return value === entry ;
279
+ } ;
280
+
281
+ const sortEntries = Object . keys ( jsonSortOrder ) ;
282
+
283
+ return ( a : string , b : string ) : number => {
284
+ const aIndex = sortEntries . findIndex ( evaluateSortEntry . bind ( null , a ) ) ;
285
+ const bIndex = sortEntries . findIndex ( evaluateSortEntry . bind ( null , b ) ) ;
286
+
287
+ if ( aIndex === - 1 && bIndex === - 1 ) {
288
+ return lexicalSort ( a , b ) ;
289
+ } else if ( bIndex === - 1 ) {
290
+ return - 1 ;
291
+ } else if ( aIndex === - 1 ) {
292
+ return 1 ;
293
+ } else if ( aIndex === bIndex ) {
294
+ // Sort entry guaranteed to be non-null because index was found
295
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
296
+ const sortEntry = sortEntries [ aIndex ] ! ;
297
+ // Guaranteed to be defined because `sortEntry` is derived from `Object.keys`
298
+ const categorySort = jsonSortOrder [ sortEntry ] as null | CategorySort ;
299
+ const categorySortFunction =
300
+ categorySort === null
301
+ ? lexicalSort
302
+ : categorySortFunctions [ categorySort ] ;
303
+ return categorySortFunction ( a , b ) ;
304
+ }
305
+ return aIndex - bIndex ;
306
+ } ;
307
+ }
308
+
309
+ /**
310
+ * Prettier JSON parsers.
311
+ */
312
+ type JsonParser = 'json' ;
313
+
314
+ /**
315
+ * Create a JSON sorting parser based upon the specified Prettier parser.
316
+ *
317
+ * @param parser - The Prettier JSON parser to base the sorting on.
318
+ * @returns The JSON sorting parser.
319
+ */
320
+ function createParser (
321
+ parser : JsonParser ,
322
+ ) : ( text : string , options : ParserOptions ) => Promise < any > {
323
+ return async ( text : string , prettierOptions : ParserOptions ) : Promise < any > => {
324
+ const { jsonRecursiveSort, jsonSortOrder } = parseOptions ( prettierOptions ) ;
325
+
326
+ const jsonRootAst = await babelParsers [ parser ] . parse ( text , prettierOptions ) ;
327
+
328
+ // The Prettier JSON parser wraps the AST in a 'JsonRoot' node
329
+ // This ast variable is the real document root
330
+ const ast = jsonRootAst . node ;
331
+
332
+ // Only objects are intended to be sorted by this plugin
333
+ // Arrays are considered only in recursive mode, so that we
334
+ // can get to nested objected.
335
+ if (
336
+ ! (
337
+ ast . type === 'ObjectExpression' ||
338
+ ( ast . type === 'ArrayExpression' && jsonRecursiveSort )
339
+ )
340
+ ) {
341
+ return jsonRootAst ;
342
+ }
343
+
344
+ let sortCompareFunction : ( a : string , b : string ) => number = lexicalSort ;
345
+ if ( jsonSortOrder ) {
346
+ sortCompareFunction = createSortCompareFunction ( jsonSortOrder ) ;
347
+ }
348
+ const sortedAst = sortAst ( ast , jsonRecursiveSort , sortCompareFunction ) ;
349
+
350
+ return {
351
+ ...jsonRootAst ,
352
+ node : sortedAst ,
353
+ } ;
354
+ } ;
355
+ }
356
+
357
+ export const parsers = {
358
+ json : {
359
+ ...babelParsers . json ,
360
+ parse : createParser ( 'json' ) ,
304
361
} ,
305
362
} as Record < string , Parser > ;
306
363
0 commit comments