Skip to content

Commit

Permalink
feat: sort exports field
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Mar 2, 2025
1 parent 43ee7e7 commit 3156924
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 13 deletions.
60 changes: 59 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ const sortObjectBy = (comparator, deep) => {

return over
}
const objectGroupBy =
// eslint-disable-next-line n/no-unsupported-features/es-builtins, n/no-unsupported-features/es-syntax -- will enable later
Object.groupBy ||
((array, callback) => {
const result = Object.create(null)
for (const value of array) {
const key = callback(value)
result[key] ??= []
result[key].push(value)
}
return result
})
const sortObject = sortObjectBy()
const sortURLObject = sortObjectBy(['type', 'url'])
const sortPeopleObject = sortObjectBy(['name', 'email', 'url'])
Expand Down Expand Up @@ -250,6 +262,52 @@ const sortScripts = onObject((scripts, packageJson) => {
return sortObjectKeys(scripts, order)
})

const sortExports = onObject((exports) => {
const { paths = [], conditions = [] } = objectGroupBy(
Object.keys(exports),
(key) => (key.startsWith('.') ? 'paths' : 'conditions'),
)

// Move `types` to top
{
const index = conditions.indexOf('types')
if (index !== -1) {
conditions.splice(index, 1)
conditions.unshift('types')
}
}

// Move `default` to bottom
{
const index = conditions.indexOf('default')
if (index !== -1) {
conditions.splice(index, 1)
conditions.push('default')
}
}

// Move `module-sync` above `require`
{
const requireConditionIndex = conditions.indexOf('require')

if (requireConditionIndex !== -1) {
const moduleSyncConditionIndex = conditions.indexOf(
'module-sync',
requireConditionIndex,
)

if (moduleSyncConditionIndex !== -1) {
conditions.splice(moduleSyncConditionIndex, 1)
conditions.splice(requireConditionIndex, 1, 'module-sync', 'require')
}
}
}

return Object.fromEntries(
[...paths, ...conditions].map((key) => [key, sortExports(exports[key])]),
)
})

// fields marked `vscode` are for `Visual Studio Code extension manifest` only
// https://code.visualstudio.com/api/references/extension-manifest
// Supported fields:
Expand Down Expand Up @@ -288,7 +346,7 @@ const fields = [
{ key: 'sideEffects' },
{ key: 'type' },
{ key: 'imports' },
{ key: 'exports' },
{ key: 'exports', over: sortExports },
{ key: 'main' },
{ key: 'svelte' },
{ key: 'umd:main' },
Expand Down
13 changes: 1 addition & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,7 @@
"license": "MIT",
"author": "Keith Cirkel <[email protected]> (http://keithcirkel.co.uk/)",
"type": "module",
"exports": {
".": {
"import": {
"types": "./index.d.ts",
"default": "./index.js"
},
"require": {
"types": "./index.d.ts",
"default": "./index.cjs"
}
}
},
"exports": {},
"types": "index.d.ts",
"bin": "cli.js",
"files": [
Expand Down
55 changes: 55 additions & 0 deletions tests/exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import test from 'ava'
import { macro } from './_helpers.js'

for (const deep of [false, true]) {
const titleSuffix = deep ? `(deep)` : ''

{
const exports = {
unknown: './unknown.unknown',
'./path-not-really-makes-no-sense': {},
import: './import.mjs',
types: './types.d.ts',
}

test(`'types' condition should be first${titleSuffix}`, macro.sortObject, {
path: 'exports',
expect: 'snapshot',
value: deep ? { './deep': exports } : exports,
})
}

{
const exports = {
unknown: './unknown.unknown',
'./path-not-really-makes-no-sense': {},
default: './types.d.ts',
import: './import.mjs',
}

test(`'default' condition should be last${titleSuffix}`, macro.sortObject, {
path: 'exports',
expect: 'snapshot',
value: deep ? { './deep': exports } : exports,
})
}

{
const exports = {
unknown: './unknown.unknown',
require: './require.cjs',
'./path-not-really-makes-no-sense': {},
'module-sync': './module-sync.mjs',
}

test(
`'module-sync' condition should before 'require'${titleSuffix}`,
macro.sortObject,
{
path: 'exports',
expect: 'snapshot',
value: deep ? { './deep': exports } : exports,
},
)
}
}
167 changes: 167 additions & 0 deletions tests/snapshots/exports.js.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Snapshot report for `tests/exports.js`

The actual snapshot is saved in `exports.js.snap`.

Generated by [AVA](https://avajs.dev).

## 'types' condition should be first

> Should sort `exports` as object.
{
input: `{␊
"exports": {␊
"unknown": "./unknown.unknown",␊
"./path-not-really-makes-no-sense": {},␊
"import": "./import.mjs",␊
"types": "./types.d.ts"␊
}␊
}`,
options: undefined,
output: `{␊
"exports": {␊
"./path-not-really-makes-no-sense": {},␊
"types": "./types.d.ts",␊
"unknown": "./unknown.unknown",␊
"import": "./import.mjs"␊
}␊
}`,
pretty: true,
}

## 'default' condition should be last

> Should sort `exports` as object.
{
input: `{␊
"exports": {␊
"unknown": "./unknown.unknown",␊
"./path-not-really-makes-no-sense": {},␊
"default": "./types.d.ts",␊
"import": "./import.mjs"␊
}␊
}`,
options: undefined,
output: `{␊
"exports": {␊
"./path-not-really-makes-no-sense": {},␊
"unknown": "./unknown.unknown",␊
"import": "./import.mjs",␊
"default": "./types.d.ts"␊
}␊
}`,
pretty: true,
}

## 'module-sync' condition should before 'require'

> Should sort `exports` as object.
{
input: `{␊
"exports": {␊
"unknown": "./unknown.unknown",␊
"require": "./require.cjs",␊
"./path-not-really-makes-no-sense": {},␊
"module-sync": "./module-sync.mjs"␊
}␊
}`,
options: undefined,
output: `{␊
"exports": {␊
"./path-not-really-makes-no-sense": {},␊
"unknown": "./unknown.unknown",␊
"module-sync": "./module-sync.mjs",␊
"require": "./require.cjs"␊
}␊
}`,
pretty: true,
}

## 'types' condition should be first(deep)

> Should sort `exports` as object.
{
input: `{␊
"exports": {␊
"./deep": {␊
"unknown": "./unknown.unknown",␊
"./path-not-really-makes-no-sense": {},␊
"import": "./import.mjs",␊
"types": "./types.d.ts"␊
}␊
}␊
}`,
options: undefined,
output: `{␊
"exports": {␊
"./deep": {␊
"./path-not-really-makes-no-sense": {},␊
"types": "./types.d.ts",␊
"unknown": "./unknown.unknown",␊
"import": "./import.mjs"␊
}␊
}␊
}`,
pretty: true,
}

## 'default' condition should be last(deep)

> Should sort `exports` as object.
{
input: `{␊
"exports": {␊
"./deep": {␊
"unknown": "./unknown.unknown",␊
"./path-not-really-makes-no-sense": {},␊
"default": "./types.d.ts",␊
"import": "./import.mjs"␊
}␊
}␊
}`,
options: undefined,
output: `{␊
"exports": {␊
"./deep": {␊
"./path-not-really-makes-no-sense": {},␊
"unknown": "./unknown.unknown",␊
"import": "./import.mjs",␊
"default": "./types.d.ts"␊
}␊
}␊
}`,
pretty: true,
}

## 'module-sync' condition should before 'require'(deep)

> Should sort `exports` as object.
{
input: `{␊
"exports": {␊
"./deep": {␊
"unknown": "./unknown.unknown",␊
"require": "./require.cjs",␊
"./path-not-really-makes-no-sense": {},␊
"module-sync": "./module-sync.mjs"␊
}␊
}␊
}`,
options: undefined,
output: `{␊
"exports": {␊
"./deep": {␊
"./path-not-really-makes-no-sense": {},␊
"unknown": "./unknown.unknown",␊
"module-sync": "./module-sync.mjs",␊
"require": "./require.cjs"␊
}␊
}␊
}`,
pretty: true,
}
Binary file added tests/snapshots/exports.js.snap
Binary file not shown.

0 comments on commit 3156924

Please sign in to comment.