Skip to content

Commit 2be5ffe

Browse files
Boshenclaude
andauthored
fix: preserve exports and imports key order (#69)
The order of condition keys in `exports` and `imports` has meaning per the Node.js spec (first-match resolution semantics). Stop sorting these fields to avoid changing package resolution behavior. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 72e6126 commit 2be5ffe

File tree

3 files changed

+9
-71
lines changed

3 files changed

+9
-71
lines changed

README.md

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ A Rust implementation that sorts package.json files according to well-establishe
2222

2323
- **Sorts top-level fields** according to npm ecosystem conventions (138 predefined fields)
2424
- **Preserves all data** - only reorders fields, never modifies values
25+
- **Respects semantics** - `exports` and `imports` fields preserve their key order (first-match resolution)
2526
- **Fast and safe** - pure Rust implementation with no unsafe code
2627
- **Idempotent** - sorting multiple times produces the same result
2728
- **Handles edge cases** - unknown fields sorted alphabetically, private fields (starting with `_`) sorted last
@@ -134,14 +135,8 @@ Fields are sorted into 12 logical groups, followed by unknown fields alphabetica
134135
"module": "./dist/index.mjs",
135136
"browser": "./dist/browser.js",
136137
"types": "./dist/index.d.ts",
137-
"exports": {
138-
".": {
139-
"types": "./dist/index.d.ts",
140-
"import": "./dist/index.mjs",
141-
"require": "./dist/index.cjs",
142-
"default": "./dist/index.mjs",
143-
},
144-
},
138+
"imports": { /* preserved as-is (order has meaning per spec) */ },
139+
"exports": { /* preserved as-is (order has meaning per spec) */ },
145140
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" },
146141

147142
// 6. Scripts

src/lib.rs

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -226,63 +226,6 @@ fn sort_people_object(obj: Map<String, Value>) -> Map<String, Value> {
226226
sort_object_by_key_order(obj, &["name", "email", "url"])
227227
}
228228

229-
fn sort_exports(obj: Map<String, Value>) -> Map<String, Value> {
230-
let obj_len = obj.len();
231-
let mut paths = Vec::new();
232-
let mut types_conds = Vec::new();
233-
let mut other_conds = Vec::new();
234-
let mut default_cond = None;
235-
236-
for (key, value) in obj {
237-
if key.starts_with('.') {
238-
paths.push((key, value));
239-
} else if key == "default" {
240-
default_cond = Some((key, value));
241-
} else if key == "types" || key.starts_with("types@") {
242-
types_conds.push((key, value));
243-
} else {
244-
other_conds.push((key, value));
245-
}
246-
}
247-
248-
let mut result = Map::with_capacity(obj_len);
249-
250-
// Add in order: paths, types, others, default
251-
for (key, value) in paths {
252-
let transformed = match value {
253-
Value::Object(nested) => Value::Object(sort_exports(nested)),
254-
_ => value,
255-
};
256-
result.insert(key, transformed);
257-
}
258-
259-
for (key, value) in types_conds {
260-
let transformed = match value {
261-
Value::Object(nested) => Value::Object(sort_exports(nested)),
262-
_ => value,
263-
};
264-
result.insert(key, transformed);
265-
}
266-
267-
for (key, value) in other_conds {
268-
let transformed = match value {
269-
Value::Object(nested) => Value::Object(sort_exports(nested)),
270-
_ => value,
271-
};
272-
result.insert(key, transformed);
273-
}
274-
275-
if let Some((key, value)) = default_cond {
276-
let transformed = match value {
277-
Value::Object(nested) => Value::Object(sort_exports(nested)),
278-
_ => value,
279-
};
280-
result.insert(key, transformed);
281-
}
282-
283-
result
284-
}
285-
286229
fn sort_object_keys(obj: Map<String, Value>, options: &SortOptions) -> Map<String, Value> {
287230
// Storage for categorized keys with their values and ordering information
288231
let mut known: Vec<(usize, String, Value)> = Vec::new(); // (order_index, key, value)
@@ -361,7 +304,7 @@ fn sort_object_keys(obj: Map<String, Value>, options: &SortOptions) -> Map<Strin
361304
61 => "fesm2020",
362305
62 => "esnext",
363306
63 => "imports",
364-
64 => "exports" => transform_value(value, sort_exports),
307+
64 => "exports",
365308
65 => "publishConfig" => transform_value(value, sort_object_alphabetically),
366309
// Scripts
367310
66 => "scripts" => if options.sort_scripts { transform_value(value, sort_object_alphabetically) } else { value },

tests/snapshots/integration_test__sort_package_json.snap

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,20 @@ expression: result
8686
"types": "./dist/index.d.ts",
8787
"exports": {
8888
".": {
89-
"types": "./dist/index.d.ts",
89+
"default": "./dist/index.js",
9090
"require": "./dist/index.cjs",
91-
"import": "./dist/index.esm.js",
92-
"default": "./dist/index.js"
91+
"types": "./dist/index.d.ts",
92+
"import": "./dist/index.esm.js"
9393
},
9494
"./paths-should-keep-b_a": {
9595
"b": "./index.cjs",
9696
"a": "./index.js"
9797
},
9898
"./package.json": "./package.json",
9999
"./utils": {
100-
"types": "./dist/utils.d.ts",
101100
"import": "./dist/utils.esm.js",
102-
"default": "./dist/utils.js"
101+
"default": "./dist/utils.js",
102+
"types": "./dist/utils.d.ts"
103103
}
104104
},
105105
"publishConfig": {

0 commit comments

Comments
 (0)