Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 201 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,205 @@ const sortScripts = onObject((scripts, packageJson) => {
return sortObjectKeys(scripts, order)
})

/**
* Sorts an array in relative terms defined by the `order`
*
* The effect of relative sort is that keys not in the `order` will be kept
* in the order they were in the original array unless it is shifted to
* accommodate a key in the `order`
*/
const relativeOrderSort = (list, order) => {
const orderMap = new Map(
order.map((key, index) => {
return [key, index]
}),
)
const indexes = list.flatMap((item, i) => {
if (orderMap.has(item)) {
return i
}
return []
})
const sortedIndexes = [...indexes].sort((a, b) => {
const aIndex = orderMap.get(list[a])
const bIndex = orderMap.get(list[b])
return aIndex - bIndex
})

const copy = [...list]
sortedIndexes.forEach((desiredIndex, thisIndex) => {
copy[indexes[thisIndex]] = list[desiredIndex]
})

return copy
}

const withLastKey = (keyName, { [keyName]: keyValue, ...rest }) =>
typeof keyValue !== 'undefined'
? {
...rest,
[keyName]: keyValue,
}
: rest

const withFirstKey = (keyName, { [keyName]: keyValue, ...rest }) =>
typeof keyValue !== 'undefined'
? {
[keyName]: keyValue,
...rest,
}
: rest

const sortConditionObject = (conditionObject) => {
/**
* Sources:
* - WinterCG maintained list of standard runtime keys: https://runtime-keys.proposal.wintercg.org
* - Node.js conditional exports: https://nodejs.org/api/packages.html#conditional-exports
* - Webpack conditions: https://webpack.js.org/guides/package-exports/#conditions
* - Bun condition: https://bun.sh/docs/runtime/modules#importing-packages
* - Bun macro condition: https://bun.sh/docs/bundler/macros#export-condition-macro
*/
const bundlerConditions = ['vite', 'rollup', 'webpack']

const serverVariantConditions = ['react-server']
const edgeConditions = [
'azion',
'edge-light',
'edge-routine',
'fastly',
'lagon',
'netlify',
'wasmer',
'workerd',
]

const referenceSyntaxConditions = [
'svelte',
'asset',
'sass',
'stylus',
'style',
/**
* 'script' condition must come before 'module' condition, as 'script'
* may also be used by bundlers but in more specific conditions than
* 'module'
*/
'script',
'esmodules',
/**
* 'module' condition must come before 'import'. import may include pure node ESM modules
* that are only compatible with node environments, while 'module' may be
* used by bundlers and leverage other bundler features
*/
'module',
'import',
/**
* `module-sync` condition must come before `require` condition and after
* `import`.
*/
'module-sync',
'require',
]

const targetEnvironmentConditions = [
/**
* bun macro condition must come before 'bun'
*/
'macro',
'bun',
'deno',
'browser',
'electron',
'kiesel', // https://runtime-keys.proposal.wintercg.org/#kiesel
'node-addons',
'node',
'moddable', // https://runtime-keys.proposal.wintercg.org/#moddable
'react-native',
'worker',
'worklet',
]

const environmentConditions = ['test', 'development', 'production']

const order = relativeOrderSort(Object.keys(conditionObject), [
/**
* Environment conditions at the top as they are generally used to override
* default behavior based on the environment
*/
...environmentConditions,
/**
* Bundler conditions are generally more important than other conditions
* because they leverage code that will not work outside of the
* bundler environment
*/
...bundlerConditions,
/**
* Edge run-times are often variants of other target environments, so they must come
* before the target environment conditions
*/
...edgeConditions,
/**
* Server variants need to be placed before `referenceSyntaxConditions` and
* `targetEnvironmentConditions` since they may use multiple syntaxes and target
* environments. They should also go after `edgeConditions`
* to allow custom implementations per edge runtime.
*/
...serverVariantConditions,
...targetEnvironmentConditions,
...referenceSyntaxConditions,
])
return withFirstKey(
'types',
withLastKey('default', sortObjectKeys(conditionObject, order)),
)
}

const sortPathLikeObjectWithWildcards = onObject((object) => {
// Replace all '*' with the highest possible unicode character
// To force all wildcards to be at the end, but relative to
// the path they are in
const wildcard = '\u{10FFFF}'
const sortableWildcardPaths = new Map()
const sortablePath = (path) => {
if (sortableWildcardPaths.has(path)) return sortableWildcardPaths.get(path)
const wildcardWeightedPath = path.replace(/\*/g, wildcard)
sortableWildcardPaths.set(path, wildcardWeightedPath)
return wildcardWeightedPath
}
return sortObjectKeys(object, (a, b) => {
return sortablePath(a).localeCompare(sortablePath(b))
})
})

const sortExportsOrImports = onObject((exportOrImports) => {
const exportsWithSortedChildren = Object.fromEntries(
Object.entries(exportOrImports).map(([key, value]) => {
return [key, sortExportsOrImports(value)]
}),
)

const keys = Object.keys(exportsWithSortedChildren)
let isConditionObject = true
let isPathLikeObject = true
for (const key of keys) {
const keyIsPathLike = key.startsWith('.') || key.startsWith('#')

isConditionObject = isConditionObject && !keyIsPathLike
isPathLikeObject = isPathLikeObject && keyIsPathLike
}

if (isConditionObject) {
return sortConditionObject(exportsWithSortedChildren)
}

if (isPathLikeObject) {
return sortPathLikeObjectWithWildcards(exportsWithSortedChildren)
}

// Object is improperly formatted. Leave it alone
return exportOrImports
})

// 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 @@ -306,8 +505,8 @@ const fields = [
/* vscode */ { key: 'publisher' },
{ key: 'sideEffects' },
{ key: 'type' },
{ key: 'imports' },
{ key: 'exports' },
{ key: 'imports', over: sortExportsOrImports },
{ key: 'exports', over: sortExportsOrImports },
{ key: 'main' },
{ key: 'svelte' },
{ key: 'umd:main' },
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"semantic-release": "semantic-release",
"test": "ava && dtslint --localTs node_modules/typescript/lib",
"test-coverage": "nyc ava",
"test-watch": "ava --watch",
"update-snapshots": "ava --update-snapshots"
},
"commitlint": {
Expand Down
125 changes: 125 additions & 0 deletions tests/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,128 @@ test('pnpm', macro.sortObject, {
},
},
})

test('imports', macro.sortObject, {
path: 'imports',
value: {
'#c': './index.js',
'#c/sub': './index.js',
'#c/*': './wild/*.js',
'#a': './sub/index.js',
'#b/sub/*': './wild/*.js',
'#b/*': './wild/*.js',
'#b/sub': './wild/sub-module.js',
},
expect: {
'#a': './sub/index.js',
'#b/sub': './wild/sub-module.js',
'#b/sub/*': './wild/*.js',
'#b/*': './wild/*.js',
'#c': './index.js',
'#c/sub': './index.js',
'#c/*': './wild/*.js',
},
})
test('exports level 1', macro.sortObject, {
path: 'exports',
value: {
'./sub': './sub/index.js',
'./a-wildcard/*': './wild/*.js',
'./a-wildcard/sub': './wild/sub-module.js',
'.': './index.js',
},
expect: {
'.': './index.js',
'./a-wildcard/sub': './wild/sub-module.js',
'./a-wildcard/*': './wild/*.js',
'./sub': './sub/index.js',
},
})

test('exports conditions', macro.sortObject, {
path: 'exports',
value: {
custom: './custom.js',
module: './module.js',
lagon: './lagon.js',
vite: './vite.js',
rollup: './rollup.js',
wasmer: './wasmer.js',
webpack: './webpack.js',
import: './import.js',
types: './types/index.d.ts',
script: './script.js',
node: './node.js',
'edge-light': './edge-light.js',
netlify: './netlify.js',
'react-native': './react-native.js',
stylus: './style.styl',
sass: './style.sass',
esmodules: './esmodules.js',
default: './index.js',
azion: './azion.js',
style: './style.css',
asset: './asset.png',
'react-server': './react-server.js',
browser: './browser.js',
workerd: './workerd.js',
electron: './electron.js',
deno: './deno.js',
fastly: './fastly.js',
worker: './worker.js',
'node-addons': './node-addons.js',
development: './development.js',
bun: './bun.js',
test: './test.js',
require: './require.js',
'edge-routine': './edge-routine.js',
worklet: './worklet.js',
moddable: './moddable.js',
macro: './macro.js',
'module-sync': './module-sync.js',
kiesel: './keisel.js',
production: './production.js',
},
expect: {
types: './types/index.d.ts',
custom: './custom.js',
test: './test.js',
development: './development.js',
production: './production.js',
vite: './vite.js',
rollup: './rollup.js',
webpack: './webpack.js',
azion: './azion.js',
'edge-light': './edge-light.js',
'edge-routine': './edge-routine.js',
fastly: './fastly.js',
lagon: './lagon.js',
netlify: './netlify.js',
wasmer: './wasmer.js',
workerd: './workerd.js',
'react-server': './react-server.js',
macro: './macro.js',
bun: './bun.js',
deno: './deno.js',
browser: './browser.js',
electron: './electron.js',
kiesel: './keisel.js',
'node-addons': './node-addons.js',
node: './node.js',
moddable: './moddable.js',
'react-native': './react-native.js',
worker: './worker.js',
worklet: './worklet.js',
asset: './asset.png',
sass: './style.sass',
stylus: './style.styl',
style: './style.css',
script: './script.js',
esmodules: './esmodules.js',
module: './module.js',
import: './import.js',
'module-sync': './module-sync.js',
require: './require.js',
default: './index.js',
},
})