diff --git a/index.js b/index.js index 5f77b81..5ac3fbe 100755 --- a/index.js +++ b/index.js @@ -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: @@ -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' }, diff --git a/package.json b/package.json index 7b89ad3..3656180 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/tests/fields.js b/tests/fields.js index 458c8ae..bb553dc 100644 --- a/tests/fields.js +++ b/tests/fields.js @@ -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', + }, +})