Skip to content

feat(fmt): add sortNamedImports and sortNamedExports options#33313

Merged
bartlomieju merged 4 commits into
denoland:mainfrom
nikicat:feat/fmt-sort-named-imports-exports
Jun 24, 2026
Merged

feat(fmt): add sortNamedImports and sortNamedExports options#33313
bartlomieju merged 4 commits into
denoland:mainfrom
nikicat:feat/fmt-sort-named-imports-exports

Conversation

@nikicat

@nikicat nikicat commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Adds two deno fmt config options, sortNamedImports and sortNamedExports,
that control how named specifiers are ordered within import/export
statements. Each is a three-way string enum mapping 1:1 onto
dprint-plugin-typescript's SortOrder: caseInsensitive sorts ignoring case,
caseSensitive sorts with uppercase before lowercase (ASCII order), and
maintain preserves the source order.

The default remains caseInsensitive, matching dprint's existing behavior. When
neither option is set the formatter builder is left untouched, so unconfigured
projects see no change in output and nothing is reordered unless explicitly
configured.

{
  "fmt": {
    "sortNamedImports": "maintain",
    "sortNamedExports": "caseSensitive"
  }
}

deno fmt previously always sorted named specifiers case-insensitively, which
conflicts with other tooling that sorts case-sensitively. For example, Biome
sorts alphanumerically with uppercase before lowercase, so running both tools
leaves files in an unstable state where each undoes the other's changes. The
only prior workaround was // deno-fmt-ignore per import or
// deno-fmt-ignore-file, both too coarse. These options let users either
delegate named-specifier ordering to another formatter (maintain) or match its
order directly (caseSensitive) without giving up deno fmt.

An earlier revision exposed these as booleans (true for caseInsensitive,
false for maintain), which left dprint's third variant, caseSensitive,
unreachable, even though that is exactly the order this feature is meant to
interoperate with. The string enum exposes all three states, keeps
caseInsensitive as the default, and avoids an awkward bool to enum migration
later.

The options are wired through FmtOptionsConfig, SerializedFmtConfig,
workspace merging, and the JSON schema, following the existing pattern of
enum-valued fmt options like quoteProps and proseWrap. A new spec exercises
all three modes: maintain preserves an unsorted file under --check, the
default caseInsensitive reorders it, a caseSensitive-ordered file passes
--check under caseSensitive, and that same file fails the default
caseInsensitive check, proving the two sort modes are distinct.

@CLAassistant

CLAassistant commented Apr 17, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@fibibot fibibot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substance walks clean — two Option<bool> fields plumbed through FmtOptionsConfigSerializedFmtConfig → workspace merge → JSON schema → dprint config builder, with a spec test that exercises both the default sort behavior and the disabled-via-config behavior.

What I verified

  • SortOrder::CaseInsensitiveSortOrder::Maintain mapping in cli/tools/fmt.rs:1454-1467 matches the actual dprint-plugin-typescript option semantics. CaseInsensitive is what dprint already uses by default; Maintain preserves source order. The body text says "opt out of dprint's case-insensitive sort," so Maintain is the right value when sort_named_imports: false is set.
  • Default true in the JSON schema means existing deno fmt users see no change in behavior — the only way the new sort behavior fires is if a user explicitly sets false. ✓
  • Workspace merging at libs/config/workspace/mod.rs:2106-2123 uses the standard member_config.or(root_config) pattern, so child workspace members override root. Consistent with how every other FmtOptionsConfig field merges. ✓
  • is_default() updated to include the two new fields — important so FmtOptionsConfig::is_default() returns true when neither is set, preventing spurious config-changed reports. ✓
  • malformed_config/error.out updated to include sortNamedImports/sortNamedExports in the expected one of list — the deserializer correctly rejects unknown fields and lists known ones. ✓
  • Spec test design: preserves_order_when_disabled uses the new config and --check to assert no formatting needed; sorts_by_default uses --no-config and asserts --check fails (exit code 1). Both halves of the toggle exercised cleanly.

Holding COMMENT for two reasons

1. CI is blocked on maintainer authorization

mergeable_state: blocked, and the workflow run from 2026-04-17 is conclusion: "action_required" — a first-time-contributor PR needs a maintainer to authorize the CI workflow before it'll run. Currently only license/cla (passed) and lint title (passed) have status. Build / test / spec shards haven't executed at all.

Without CI, I can't confirm the JSON schema is well-formed (the schema validator runs in CI), the cargo test -p deno_config --lib fmt parse test passes, or that the new spec test actually runs cleanly. Worth pinging a maintainer to authorize the workflow run so CI can land.

2. Escalation: two new public deno.json fmt config fields

This adds sortNamedImports and sortNamedExports as public deno.json config keys. Per fibibot's policy, new user-facing config surface — even one defaulting to true for backwards compat — warrants a maintainer's call on the field naming, the Option<bool> shape (vs e.g. "sortNamedImports": "caseInsensitive" | "maintain" | "none" for future extensibility), and the precedent for letting users opt out of more dprint defaults via similar flags. cc @bartlomieju.

Substance is APPROVE-shape pending those two — happy to come back once CI is unblocked and a maintainer signs off on the surface.

Expose dprint-plugin-typescript's `importDeclaration.sortNamedImports`
and `exportDeclaration.sortNamedExports` settings via `deno.json`.
Both default to `true` (case-insensitive sort, unchanged behavior).

Motivation: `deno fmt` sorts named specifiers case-insensitively, which
conflicts with other tooling that sorts case-sensitively (e.g. Biome's
alphanumeric order). The only prior workaround was
`// deno-fmt-ignore` per import or `// deno-fmt-ignore-file`, both too
coarse. These options let users opt out of `deno fmt`'s sort and
delegate ordering to another tool.

```jsonc
{
  "fmt": {
    "sortNamedImports": false,
    "sortNamedExports": false
  }
}
```
@nikicat
nikicat force-pushed the feat/fmt-sort-named-imports-exports branch from b2a9aca to 762a5f4 Compare June 5, 2026 10:47
@deno-cla-assistant

deno-cla-assistant Bot commented Jun 5, 2026

Copy link
Copy Markdown

Deno Individual Contributor License Agreement

All contributors have signed the CLA. Thank you!

Re-run CLA check


This is an automated message from CLA Assistant

@bartlomieju bartlomieju added this to the 2.9.0 milestone Jun 6, 2026
…mports-exports

# Conflicts:
#	tests/specs/fmt/malformed_config/error.out

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the implementation is clean and well-tested, and the motivation (delegating specifier order to another formatter) is solid. One design suggestion before this lands.

The config currently exposes these as booleans (trueCaseInsensitive, falseMaintain), which drops dprint's third SortOrder variant, CaseSensitive. That matters for this PR's own motivation: Biome sorts case-sensitively (uppercase before lowercase), so with a bool a user still can't make deno fmt produce Biome's order — they can only disable sorting and run both tools.

For comparison, Prettier core doesn't sort named specifiers at all (it delegates to plugins). The Prettier sort plugins model this as two axes — importOrderSortSpecifiers (on/off) plus importOrderCaseSensitive (case sensitivity) — precisely because case-sensitive sorting is a real, wanted mode. dprint already encodes all three states in one SortOrder enum.

So I'd suggest exposing a three-way string enum that maps 1:1 onto dprint, e.g.:

{
  "fmt": {
    "sortNamedImports": "maintain" | "caseSensitive" | "caseInsensitive", // default "caseInsensitive"
    "sortNamedExports": "maintain" | "caseSensitive" | "caseInsensitive"
  }
}

This keeps current behavior as the default, covers the Biome case directly, and avoids a slightly awkward config migration later (bool → enum). It'd follow the existing pattern of enum-valued fmt options like quoteProps/proseWrap rather than the bool fields.

If you'd rather keep it a bool for simplicity, that's defensible, but then it's worth a doc note that case-sensitive ordering isn't reachable. Happy to help with either direction.

Comment thread cli/tools/fmt.rs
);
}

if let Some(sort_named_imports) = options.sort_named_imports {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider mapping the config field directly onto dprint's three-way SortOrder (maintain/caseSensitive/caseInsensitive) instead of boolCaseInsensitive/Maintain. As written the CaseSensitive variant is unreachable, which is the exact ordering Biome uses. See the summary comment for rationale.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done in a4fcc69. Both options are now three-way string enums mapping 1:1 onto dprint's SortOrder:

{
  "fmt": {
    "sortNamedImports": "maintain" | "caseSensitive" | "caseInsensitive",
    "sortNamedExports": "maintain" | "caseSensitive" | "caseInsensitive"
  }
}

caseInsensitive stays the default, so existing behavior is unchanged, and caseSensitive (Biome's order) is now reachable. Modeled it after the existing enum-valued fmt options (quoteProps/proseWrap). The spec test now covers all three modes, including one case that proves caseSensitive produces a different order than the default caseInsensitive.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — done in a4fcc69. Both options are now three-way string enums mapping 1:1 onto dprint's SortOrder:

{
  "fmt": {
    "sortNamedImports": "maintain" | "caseSensitive" | "caseInsensitive",
    "sortNamedExports": "maintain" | "caseSensitive" | "caseInsensitive"
  }
}

caseInsensitive stays the default, so existing behavior is unchanged, and caseSensitive (Biome's order) is now reachable. Modeled it after the existing enum-valued fmt options (quoteProps/proseWrap). The spec test now covers all three modes, including one case that proves caseSensitive produces a different order than the default caseInsensitive.

@bartlomieju bartlomieju removed this from the 2.9.0 milestone Jun 18, 2026
nikicat and others added 2 commits June 21, 2026 23:07
Per review feedback, change both options from a bool to a string enum
that maps 1:1 onto dprint's `SortOrder`:

    "sortNamedImports": "maintain" | "caseSensitive" | "caseInsensitive"
    "sortNamedExports": "maintain" | "caseSensitive" | "caseInsensitive"

The previous bool could only reach `caseInsensitive` (the default) and
`maintain`, leaving dprint's `caseSensitive` ordering unreachable. That
is exactly the order Biome produces (uppercase before lowercase), which
this feature was meant to delegate to. The enum exposes all three states,
keeps `caseInsensitive` as the unchanged default, and follows the pattern
of the existing enum-valued fmt options (quoteProps, proseWrap).

Spec test now covers all three modes and proves caseSensitive is distinct
from the default caseInsensitive ordering.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…mports-exports

# Conflicts:
#	cli/schemas/config-file.v1.json
#	libs/config/deno_json/mod.rs
#	libs/config/workspace/mod.rs
#	tests/specs/fmt/malformed_config/error.out

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@bartlomieju
bartlomieju merged commit e16ad56 into denoland:main Jun 24, 2026
137 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants