Skip to content

Commit 880101e

Browse files
committed
feat: sort exports field
1 parent 43ee7e7 commit 880101e

File tree

4 files changed

+281
-1
lines changed

4 files changed

+281
-1
lines changed

index.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ const sortObjectBy = (comparator, deep) => {
3737

3838
return over
3939
}
40+
const objectGroupBy =
41+
// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax -- will enable later
42+
Object.groupBy ||
43+
((array, callback) => {
44+
const result = Object.create(null)
45+
for (const value of array) {
46+
const key = callback(value)
47+
result[key] ??= []
48+
result[key].push(value)
49+
}
50+
return result
51+
})
4052
const sortObject = sortObjectBy()
4153
const sortURLObject = sortObjectBy(['type', 'url'])
4254
const sortPeopleObject = sortObjectBy(['name', 'email', 'url'])
@@ -250,6 +262,52 @@ const sortScripts = onObject((scripts, packageJson) => {
250262
return sortObjectKeys(scripts, order)
251263
})
252264

265+
const sortExports = onObject((exports) => {
266+
const { paths = [], conditions = [] } = objectGroupBy(
267+
Object.keys(exports),
268+
(key) => (key.startsWith('.') ? 'paths' : 'conditions'),
269+
)
270+
271+
// Move `types` to top
272+
{
273+
const index = conditions.indexOf('types')
274+
if (index !== -1) {
275+
conditions.splice(index, 1)
276+
conditions.unshift('types')
277+
}
278+
}
279+
280+
// Move `default` to bottom
281+
{
282+
const index = conditions.indexOf('default')
283+
if (index !== -1) {
284+
conditions.splice(index, 1)
285+
conditions.push('default')
286+
}
287+
}
288+
289+
// Move `module-sync` above `require`
290+
{
291+
const requireConditionIndex = conditions.indexOf('require')
292+
293+
if (requireConditionIndex !== -1) {
294+
const moduleSyncConditionIndex = conditions.indexOf(
295+
'module-sync',
296+
requireConditionIndex,
297+
)
298+
299+
if (moduleSyncConditionIndex !== -1) {
300+
conditions.splice(moduleSyncConditionIndex, 1)
301+
conditions.splice(requireConditionIndex, 1, 'module-sync', 'require')
302+
}
303+
}
304+
}
305+
306+
return Object.fromEntries(
307+
[...paths, ...conditions].map((key) => [key, sortExports(exports[key])]),
308+
)
309+
})
310+
253311
// fields marked `vscode` are for `Visual Studio Code extension manifest` only
254312
// https://code.visualstudio.com/api/references/extension-manifest
255313
// Supported fields:
@@ -288,7 +346,7 @@ const fields = [
288346
{ key: 'sideEffects' },
289347
{ key: 'type' },
290348
{ key: 'imports' },
291-
{ key: 'exports' },
349+
{ key: 'exports', over: sortExports },
292350
{ key: 'main' },
293351
{ key: 'svelte' },
294352
{ key: 'umd:main' },

tests/exports.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
import: './import.mjs',
12+
types: './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+
default: './types.d.ts',
27+
import: './import.mjs',
28+
}
29+
30+
test(`'default' condition should be last${titleSuffix}`, macro.sortObject, {
31+
path: 'exports',
32+
expect: 'snapshot',
33+
value: deep ? { './deep': exports } : exports,
34+
})
35+
}
36+
37+
{
38+
const exports = {
39+
unknown: './unknown.unknown',
40+
require: './require.cjs',
41+
'./path-not-really-makes-no-sense': {},
42+
'module-sync': './module-sync.mjs',
43+
}
44+
45+
test(
46+
`'module-sync' condition should before 'require'${titleSuffix}`,
47+
macro.sortObject,
48+
{
49+
path: 'exports',
50+
expect: 'snapshot',
51+
value: deep ? { './deep': exports } : exports,
52+
},
53+
)
54+
}
55+
}

tests/snapshots/exports.js.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
"import": "./import.mjs",␊
17+
"types": "./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+
"unknown": "./unknown.unknown",␊
26+
"import": "./import.mjs"␊
27+
}␊
28+
}`,
29+
pretty: true,
30+
}
31+
32+
## 'default' condition should be last
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+
"default": "./types.d.ts",␊
42+
"import": "./import.mjs"␊
43+
}␊
44+
}`,
45+
options: undefined,
46+
output: `{␊
47+
"exports": {␊
48+
"./path-not-really-makes-no-sense": {},␊
49+
"unknown": "./unknown.unknown",␊
50+
"import": "./import.mjs",␊
51+
"default": "./types.d.ts"␊
52+
}␊
53+
}`,
54+
pretty: true,
55+
}
56+
57+
## 'module-sync' condition should before 'require'
58+
59+
> Should sort `exports` as object.
60+
61+
{
62+
input: `{␊
63+
"exports": {␊
64+
"unknown": "./unknown.unknown",␊
65+
"require": "./require.cjs",␊
66+
"./path-not-really-makes-no-sense": {},␊
67+
"module-sync": "./module-sync.mjs"␊
68+
}␊
69+
}`,
70+
options: undefined,
71+
output: `{␊
72+
"exports": {␊
73+
"./path-not-really-makes-no-sense": {},␊
74+
"unknown": "./unknown.unknown",␊
75+
"module-sync": "./module-sync.mjs",␊
76+
"require": "./require.cjs"␊
77+
}␊
78+
}`,
79+
pretty: true,
80+
}
81+
82+
## 'types' condition should be first(deep)
83+
84+
> Should sort `exports` as object.
85+
86+
{
87+
input: `{␊
88+
"exports": {␊
89+
"./deep": {␊
90+
"unknown": "./unknown.unknown",␊
91+
"./path-not-really-makes-no-sense": {},␊
92+
"import": "./import.mjs",␊
93+
"types": "./types.d.ts"␊
94+
}␊
95+
}␊
96+
}`,
97+
options: undefined,
98+
output: `{␊
99+
"exports": {␊
100+
"./deep": {␊
101+
"./path-not-really-makes-no-sense": {},␊
102+
"types": "./types.d.ts",␊
103+
"unknown": "./unknown.unknown",␊
104+
"import": "./import.mjs"␊
105+
}␊
106+
}␊
107+
}`,
108+
pretty: true,
109+
}
110+
111+
## 'default' condition should be last(deep)
112+
113+
> Should sort `exports` as object.
114+
115+
{
116+
input: `{␊
117+
"exports": {␊
118+
"./deep": {␊
119+
"unknown": "./unknown.unknown",␊
120+
"./path-not-really-makes-no-sense": {},␊
121+
"default": "./types.d.ts",␊
122+
"import": "./import.mjs"␊
123+
}␊
124+
}␊
125+
}`,
126+
options: undefined,
127+
output: `{␊
128+
"exports": {␊
129+
"./deep": {␊
130+
"./path-not-really-makes-no-sense": {},␊
131+
"unknown": "./unknown.unknown",␊
132+
"import": "./import.mjs",␊
133+
"default": "./types.d.ts"␊
134+
}␊
135+
}␊
136+
}`,
137+
pretty: true,
138+
}
139+
140+
## 'module-sync' condition should before 'require'(deep)
141+
142+
> Should sort `exports` as object.
143+
144+
{
145+
input: `{␊
146+
"exports": {␊
147+
"./deep": {␊
148+
"unknown": "./unknown.unknown",␊
149+
"require": "./require.cjs",␊
150+
"./path-not-really-makes-no-sense": {},␊
151+
"module-sync": "./module-sync.mjs"␊
152+
}␊
153+
}␊
154+
}`,
155+
options: undefined,
156+
output: `{␊
157+
"exports": {␊
158+
"./deep": {␊
159+
"./path-not-really-makes-no-sense": {},␊
160+
"unknown": "./unknown.unknown",␊
161+
"module-sync": "./module-sync.mjs",␊
162+
"require": "./require.cjs"␊
163+
}␊
164+
}␊
165+
}`,
166+
pretty: true,
167+
}

tests/snapshots/exports.js.snap

679 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)