Skip to content

Commit 7f22864

Browse files
authored
feat: sort types and default in exports field (#349)
1 parent 9e94971 commit 7f22864

File tree

4 files changed

+356
-1
lines changed

4 files changed

+356
-1
lines changed

index.js

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,22 @@ const sortObjectBy = (comparator, deep) => {
3232

3333
return over
3434
}
35+
const objectGroupBy =
36+
// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax -- Safe
37+
Object.groupBy ||
38+
// Remove this when we drop support for Node.js 20
39+
((array, callback) => {
40+
const result = Object.create(null)
41+
for (const value of array) {
42+
const key = callback(value)
43+
if (result[key]) {
44+
result[key].push(value)
45+
} else {
46+
result[key] = [value]
47+
}
48+
}
49+
return result
50+
})
3551
const sortObject = sortObjectBy()
3652
const sortURLObject = sortObjectBy(['type', 'url'])
3753
const sortPeopleObject = sortObjectBy(['name', 'email', 'url'])
@@ -303,6 +319,44 @@ const sortScripts = onObject((scripts, packageJson) => {
303319
return sortObjectKeys(scripts, order)
304320
})
305321

322+
/*
323+
- Move `types` and versioned type condition to top
324+
- Move `default` condition to bottom
325+
*/
326+
const sortConditions = (conditions) => {
327+
const {
328+
typesConditions = [],
329+
defaultConditions = [],
330+
restConditions = [],
331+
} = objectGroupBy(conditions, (condition) => {
332+
if (condition === 'types' || condition.startsWith('types@')) {
333+
return 'typesConditions'
334+
}
335+
336+
if (condition === 'default') {
337+
return 'defaultConditions'
338+
}
339+
340+
return 'restConditions'
341+
})
342+
343+
return [...typesConditions, ...restConditions, ...defaultConditions]
344+
}
345+
346+
const sortExports = onObject((exports) => {
347+
const { paths = [], conditions = [] } = objectGroupBy(
348+
Object.keys(exports),
349+
(key) => (key.startsWith('.') ? 'paths' : 'conditions'),
350+
)
351+
352+
return Object.fromEntries(
353+
[...paths, ...sortConditions(conditions)].map((key) => [
354+
key,
355+
sortExports(exports[key]),
356+
]),
357+
)
358+
})
359+
306360
// fields marked `vscode` are for `Visual Studio Code extension manifest` only
307361
// https://code.visualstudio.com/api/references/extension-manifest
308362
// Supported fields:
@@ -341,7 +395,7 @@ const fields = [
341395
{ key: 'sideEffects' },
342396
{ key: 'type' },
343397
{ key: 'imports' },
344-
{ key: 'exports' },
398+
{ key: 'exports', over: sortExports },
345399
{ key: 'main' },
346400
{ key: 'svelte' },
347401
{ key: 'umd:main' },

tests/exports.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import test from 'ava'
2+
import { macro } from './_helpers.js'
3+
4+
for (const deep of [false, true]) {
5+
const titleSuffix = deep ? `(deep)` : ''
6+
7+
{
8+
const exports = {
9+
unknown: './unknown.unknown',
10+
'./path-not-really-makes-no-sense': {},
11+
types: './types.d.ts',
12+
'types@<=1': './v1/types.d.ts',
13+
}
14+
15+
test(`'types' condition should be first${titleSuffix}`, macro.sortObject, {
16+
path: 'exports',
17+
expect: 'snapshot',
18+
value: deep ? { './deep': exports } : exports,
19+
})
20+
}
21+
22+
{
23+
const exports = {
24+
unknown: './unknown.unknown',
25+
'./path-not-really-makes-no-sense': {},
26+
'types@<=1': './v1/types.d.ts',
27+
types: './types.d.ts',
28+
}
29+
30+
test(
31+
`'types' condition should be first${titleSuffix} 2`,
32+
macro.sortObject,
33+
{
34+
path: 'exports',
35+
expect: 'snapshot',
36+
value: deep ? { './deep': exports } : exports,
37+
},
38+
)
39+
}
40+
41+
{
42+
const exports = {
43+
unknown: './unknown.unknown',
44+
'./path-not-really-makes-no-sense': {},
45+
default: './default.js',
46+
}
47+
48+
test(`'default' condition should be last${titleSuffix}`, macro.sortObject, {
49+
path: 'exports',
50+
expect: 'snapshot',
51+
value: deep ? { './deep': exports } : exports,
52+
})
53+
}
54+
}
55+
56+
test(`Only 'types'`, macro.sortObject, {
57+
path: 'exports',
58+
expect: 'snapshot',
59+
value: {
60+
types: './types.d.ts',
61+
},
62+
})
63+
64+
test(`Only 'default'`, macro.sortObject, {
65+
path: 'exports',
66+
expect: 'snapshot',
67+
value: {
68+
default: './default.js',
69+
},
70+
})
71+
72+
test(`Well formed`, macro.sortObject, {
73+
path: 'exports',
74+
expect: 'snapshot',
75+
value: {
76+
types: './types.d.ts',
77+
default: './default.js',
78+
},
79+
})

tests/snapshots/exports.js.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Snapshot report for `tests/exports.js`
2+
3+
The actual snapshot is saved in `exports.js.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## 'types' condition should be first
8+
9+
> Should sort `exports` as object.
10+
11+
{
12+
input: `{␊
13+
"exports": {␊
14+
"unknown": "./unknown.unknown",␊
15+
"./path-not-really-makes-no-sense": {},␊
16+
"types": "./types.d.ts",␊
17+
"types@<=1": "./v1/types.d.ts"␊
18+
}␊
19+
}`,
20+
options: undefined,
21+
output: `{␊
22+
"exports": {␊
23+
"./path-not-really-makes-no-sense": {},␊
24+
"types": "./types.d.ts",␊
25+
"types@<=1": "./v1/types.d.ts",␊
26+
"unknown": "./unknown.unknown"␊
27+
}␊
28+
}`,
29+
pretty: true,
30+
}
31+
32+
## 'types' condition should be first 2
33+
34+
> Should sort `exports` as object.
35+
36+
{
37+
input: `{␊
38+
"exports": {␊
39+
"unknown": "./unknown.unknown",␊
40+
"./path-not-really-makes-no-sense": {},␊
41+
"types@<=1": "./v1/types.d.ts",␊
42+
"types": "./types.d.ts"␊
43+
}␊
44+
}`,
45+
options: undefined,
46+
output: `{␊
47+
"exports": {␊
48+
"./path-not-really-makes-no-sense": {},␊
49+
"types@<=1": "./v1/types.d.ts",␊
50+
"types": "./types.d.ts",␊
51+
"unknown": "./unknown.unknown"␊
52+
}␊
53+
}`,
54+
pretty: true,
55+
}
56+
57+
## 'default' condition should be last
58+
59+
> Should sort `exports` as object.
60+
61+
{
62+
input: `{␊
63+
"exports": {␊
64+
"unknown": "./unknown.unknown",␊
65+
"./path-not-really-makes-no-sense": {},␊
66+
"default": "./default.js"␊
67+
}␊
68+
}`,
69+
options: undefined,
70+
output: `{␊
71+
"exports": {␊
72+
"./path-not-really-makes-no-sense": {},␊
73+
"unknown": "./unknown.unknown",␊
74+
"default": "./default.js"␊
75+
}␊
76+
}`,
77+
pretty: true,
78+
}
79+
80+
## 'types' condition should be first(deep)
81+
82+
> Should sort `exports` as object.
83+
84+
{
85+
input: `{␊
86+
"exports": {␊
87+
"./deep": {␊
88+
"unknown": "./unknown.unknown",␊
89+
"./path-not-really-makes-no-sense": {},␊
90+
"types": "./types.d.ts",␊
91+
"types@<=1": "./v1/types.d.ts"␊
92+
}␊
93+
}␊
94+
}`,
95+
options: undefined,
96+
output: `{␊
97+
"exports": {␊
98+
"./deep": {␊
99+
"./path-not-really-makes-no-sense": {},␊
100+
"types": "./types.d.ts",␊
101+
"types@<=1": "./v1/types.d.ts",␊
102+
"unknown": "./unknown.unknown"␊
103+
}␊
104+
}␊
105+
}`,
106+
pretty: true,
107+
}
108+
109+
## 'types' condition should be first(deep) 2
110+
111+
> Should sort `exports` as object.
112+
113+
{
114+
input: `{␊
115+
"exports": {␊
116+
"./deep": {␊
117+
"unknown": "./unknown.unknown",␊
118+
"./path-not-really-makes-no-sense": {},␊
119+
"types@<=1": "./v1/types.d.ts",␊
120+
"types": "./types.d.ts"␊
121+
}␊
122+
}␊
123+
}`,
124+
options: undefined,
125+
output: `{␊
126+
"exports": {␊
127+
"./deep": {␊
128+
"./path-not-really-makes-no-sense": {},␊
129+
"types@<=1": "./v1/types.d.ts",␊
130+
"types": "./types.d.ts",␊
131+
"unknown": "./unknown.unknown"␊
132+
}␊
133+
}␊
134+
}`,
135+
pretty: true,
136+
}
137+
138+
## 'default' condition should be last(deep)
139+
140+
> Should sort `exports` as object.
141+
142+
{
143+
input: `{␊
144+
"exports": {␊
145+
"./deep": {␊
146+
"unknown": "./unknown.unknown",␊
147+
"./path-not-really-makes-no-sense": {},␊
148+
"default": "./default.js"␊
149+
}␊
150+
}␊
151+
}`,
152+
options: undefined,
153+
output: `{␊
154+
"exports": {␊
155+
"./deep": {␊
156+
"./path-not-really-makes-no-sense": {},␊
157+
"unknown": "./unknown.unknown",␊
158+
"default": "./default.js"␊
159+
}␊
160+
}␊
161+
}`,
162+
pretty: true,
163+
}
164+
165+
## Only 'types'
166+
167+
> Should sort `exports` as object.
168+
169+
{
170+
input: `{␊
171+
"exports": {␊
172+
"types": "./types.d.ts"␊
173+
}␊
174+
}`,
175+
options: undefined,
176+
output: `{␊
177+
"exports": {␊
178+
"types": "./types.d.ts"␊
179+
}␊
180+
}`,
181+
pretty: true,
182+
}
183+
184+
## Only 'default'
185+
186+
> Should sort `exports` as object.
187+
188+
{
189+
input: `{␊
190+
"exports": {␊
191+
"default": "./default.js"␊
192+
}␊
193+
}`,
194+
options: undefined,
195+
output: `{␊
196+
"exports": {␊
197+
"default": "./default.js"␊
198+
}␊
199+
}`,
200+
pretty: true,
201+
}
202+
203+
## Well formed
204+
205+
> Should sort `exports` as object.
206+
207+
{
208+
input: `{␊
209+
"exports": {␊
210+
"types": "./types.d.ts",␊
211+
"default": "./default.js"␊
212+
}␊
213+
}`,
214+
options: undefined,
215+
output: `{␊
216+
"exports": {␊
217+
"types": "./types.d.ts",␊
218+
"default": "./default.js"␊
219+
}␊
220+
}`,
221+
pretty: true,
222+
}

tests/snapshots/exports.js.snap

704 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)