diff --git a/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap b/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap index 5b793dd52f904..91d6c6f0faf3c 100644 --- a/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap +++ b/crates/oxc_formatter/tests/ir_transform/_sort-imports-tests.ref.snap @@ -1,40 +1,40 @@ // @ts-nocheck // vim: set filetype=typescript: -describe("alphabetical", () => { +describe('natural', () => { let options = { - type: "alphabetical", - order: "asc", - } as const; + type: 'natural', + order: 'asc', + } as const - it("groups all type imports together when specific type groups not configured", async () => { + it('groups all type imports together when specific type groups not configured', async () => { await valid({ options: [ { ...options, groups: [ - "type", - ["builtin", "external"], - "internal", - ["parent", "sibling", "index"], + 'type', + ['builtin', 'external'], + 'internal', + ['parent', 'sibling', 'index'], ], }, ], code: dedent` import type { T } from '../t' - import type { U } from '~/u' import type { V } from 'v' + import type { U } from '~/u' `, - }); + }) await invalid({ options: [ { ...options, groups: [ - "type", - ["builtin", "external"], - "internal", - ["parent", "sibling", "index"], + 'type', + ['builtin', 'external'], + 'internal', + ['parent', 'sibling', 'index'], ], }, ], @@ -47,26 +47,26 @@ describe("alphabetical", () => { `, output: dedent` import type { T } from '../t' - import type { U } from '~/u' import type { V } from 'v' + import type { U } from '~/u' `, - }); - }); + }) + }) - it("groups style imports separately when configured", async () => { + it('groups style imports separately when configured', async () => { await valid({ options: [ { ...options, groups: [ - "type", - ["builtin", "external"], - "internal-type", - "internal", - ["parent-type", "sibling-type", "index-type"], - ["parent", "sibling", "index"], - "style", - "unknown", + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'style', + 'unknown', ], }, ], @@ -76,23 +76,23 @@ describe("alphabetical", () => { import styles from '../s.css' import './t.css' `, - }); - }); + }) + }) - it("groups side-effect imports separately when configured", async () => { + it('groups side-effect imports separately when configured', async () => { await valid({ options: [ { ...options, groups: [ - "type", - ["builtin", "external"], - "internal-type", - "internal", - ["parent-type", "sibling-type", "index-type"], - ["parent", "sibling", "index"], - "side-effect", - "unknown", + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'side-effect', + 'unknown', ], }, ], @@ -103,15 +103,15 @@ describe("alphabetical", () => { import '../c.js' import './d' `, - }); - }); + }) + }) - it("groups builtin types separately from other type imports", async () => { + it('groups builtin types separately from other type imports', async () => { await valid({ options: [ { ...options, - groups: ["builtin-type", "type"], + groups: ['builtin-type', 'type'], }, ], code: dedent` @@ -119,22 +119,22 @@ describe("alphabetical", () => { import a from 'a' `, - }); - }); + }) + }) - it("handles imports with semicolons correctly", async () => { + it('handles imports with semicolons correctly', async () => { await invalid({ options: [ { ...options, groups: [ - "type", - ["builtin", "external"], - "internal-type", - "internal", - ["parent-type", "sibling-type", "index-type"], - ["parent", "sibling", "index"], - "unknown", + 'type', + ['builtin', 'external'], + 'internal-type', + 'internal', + ['parent-type', 'sibling-type', 'index-type'], + ['parent', 'sibling', 'index'], + 'unknown', ], }, ], @@ -147,177 +147,91 @@ describe("alphabetical", () => { import a from 'a'; import b from './index'; `, - }); - }); + }) + }) - it("supports custom import groups with primary and secondary categories", async () => { - await invalid({ + it('preserves side-effect import order when sorting disabled', async () => { + await valid({ options: [ { ...options, - groups: [ - "type", - "primary", - "secondary", - ["builtin", "external"], - "internal-type", - "internal", - ["parent-type", "sibling-type", "index-type"], - ["parent", "sibling", "index"], - "unknown", - ], - customGroups: { - value: { - primary: ["t", "@a/.+"], - secondary: "@b/.+", - }, - type: { - primary: ["t", "@a/.+"], - }, - }, + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, }, ], - output: dedent` - import a1 from '@a/a1' - import a2 from '@a/a2' - import type { T } from 't' - - import b1 from '@b/b1' - import b2 from '@b/b2' - import b3 from '@b/b3' - - import { c } from 'c' - `, code: dedent` - import type { T } from 't' + import a from 'aaaa' - import a1 from '@a/a1' - import a2 from '@a/a2' - import b1 from '@b/b1' - import b2 from '@b/b2' - import b3 from '@b/b3' - import { c } from 'c' + import 'bbb' + import './cc' + import '../d' `, - }); - }); + }) - it("supports custom groups for value imports only", async () => { - await invalid({ + await valid({ options: [ { ...options, - customGroups: { - value: { - primary: ["a"], - }, - }, - groups: ["type", "primary"], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, }, ], - output: dedent` - import type { A } from 'a' - - import { a } from 'a' - `, code: dedent` - import type { A } from 'a' - import { a } from 'a' + import 'c' + import 'bb' + import 'aaa' `, - }); - }); - - it("handles hash symbol in internal patterns correctly", async () => { - await valid({ - code: dedent` - import type { T } from 'a' - - import { a } from 'a' + }) - import type { S } from '#b' - - import { b1, b2 } from '#b' - import c from '#c' - - import { d } from '../d' - `, + await invalid({ options: [ { ...options, - internalPattern: ["#.+"], + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: false, }, ], - }); - - await invalid({ output: dedent` - import type { T } from 'a' - - import { a } from 'a' - - import type { S } from '#b' - - import { b1, b2 } from '#b' - import c from '#c' - - import { d } from '../d' - `, - code: dedent` - import type { T } from 'a' - - import { a } from 'a' - - import type { S } from '#b' - import c from '#c' - import { b1, b2 } from '#b' + import a from 'aaaa' + import e from 'e' - import { d } from '../d' + import './cc' + import 'bbb' + import '../d' `, - options: [ - { - ...options, - internalPattern: ["#.+"], - }, - ], - }); - }); - - it("recognizes Bun built-in modules when configured", async () => { - await valid({ - options: [ - { - ...options, - groups: ["builtin", "external", "unknown"], - newlinesBetween: "never", - environment: "bun", - }, - ], code: dedent` - import { expect } from 'bun:test' - import { a } from 'a' + import './cc' + import 'bbb' + import e from 'e' + import a from 'aaaa' + import '../d' `, - }); + }) + }) + it('sorts side-effect imports when sorting enabled', async () => { await invalid({ options: [ { ...options, - groups: ["builtin", "external", "unknown"], - newlinesBetween: "never", - environment: "bun", + groups: ['external', 'side-effect', 'unknown'], + sortSideEffects: true, }, ], output: dedent` - import { expect } from 'bun:test' - import { a } from 'a' + import 'aaa' + import 'bb' + import 'c' `, code: dedent` - import { a } from 'a' - import { expect } from 'bun:test' + import 'c' + import 'bb' + import 'aaa' `, - }); - }); + }) + }) - it("preserves original order when side-effect imports are not grouped", async () => { + it('preserves original order when side-effect imports are not grouped', async () => { await invalid({ output: dedent` import "./z-side-effect.scss"; @@ -338,13 +252,13 @@ describe("alphabetical", () => { options: [ { ...options, - groups: ["unknown"], + groups: ['unknown'], }, ], - }); - }); + }) + }) - it("groups side-effect imports together without sorting them", async () => { + it('groups side-effect imports together without sorting them', async () => { await invalid({ output: dedent` import "./z-side-effect.scss"; @@ -366,13 +280,13 @@ describe("alphabetical", () => { options: [ { ...options, - groups: ["side-effect", "unknown"], + groups: ['side-effect', 'unknown'], }, ], - }); - }); + }) + }) - it("groups side-effect and style imports together in same group without sorting", async () => { + it('groups side-effect and style imports together in same group without sorting', async () => { await invalid({ output: dedent` import "./z-side-effect.scss"; @@ -394,13 +308,13 @@ describe("alphabetical", () => { options: [ { ...options, - groups: [["side-effect", "side-effect-style"], "unknown"], + groups: [['side-effect', 'side-effect-style'], 'unknown'], }, ], - }); - }); + }) + }) - it("separates side-effect and style imports into distinct groups without sorting", async () => { + it('separates side-effect and style imports into distinct groups without sorting', async () => { await invalid({ output: dedent` import './b-side-effect' @@ -423,13 +337,13 @@ describe("alphabetical", () => { options: [ { ...options, - groups: ["side-effect", "side-effect-style", "unknown"], + groups: ['side-effect', 'side-effect-style', 'unknown'], }, ], - }); - }); + }) + }) - it("groups style side-effect imports separately without sorting", async () => { + it('groups style side-effect imports separately without sorting', async () => { await invalid({ output: dedent` import "./z-side-effect"; @@ -451,18 +365,18 @@ describe("alphabetical", () => { options: [ { ...options, - groups: ["side-effect-style", "unknown"], + groups: ['side-effect-style', 'unknown'], }, ], - }); - }); + }) + }) - it("ignores fallback sorting for side-effect imports", async () => { + it('ignores fallback sorting for side-effect imports', async () => { await valid({ options: [ { - groups: ["side-effect", "side-effect-style"], - fallbackSort: { type: "alphabetical" }, + groups: ['side-effect', 'side-effect-style'], + fallbackSort: { type: 'alphabetical' }, }, ], code: dedent` @@ -472,1523 +386,350 @@ describe("alphabetical", () => { import 'b.css'; import 'a.css'; `, - }); - }); + }) + }) - it("handles custom spacing rules between consecutive groups", async () => { + it('handles newlines and comments after fixes', async () => { await invalid({ - options: [ - { - ...options, - groups: [ - "a", - { newlinesBetween: "always" }, - "b", - { newlinesBetween: "always" }, - "c", - { newlinesBetween: "never" }, - "d", - { newlinesBetween: "ignore" }, - "e", - ], - customGroups: { - value: { - a: "a", - b: "b", - c: "c", - d: "d", - e: "e", - }, - }, - newlinesBetween: "always", - }, - ], output: dedent` - import { A } from 'a' - - import { B } from 'b' - - import { C } from 'c' - import { D } from 'd' - + import { a } from './a' // Comment after - import { E } from 'e' + import { b } from 'b' + import { c } from 'c' `, code: dedent` - import { A } from 'a' - import { B } from 'b' - - - import { C } from 'c' - - import { D } from 'd' - + import { b } from 'b' + import { a } from './a' // Comment after - import { E } from 'e' + import { c } from 'c' `, - }); - }); - - it.each([ - [ - "enforces spacing when global option is 2 and group option is never", - 2, - "never", - ], - ["enforces spacing when global option is 2 and group option is 0", 2, 0], - [ - "enforces spacing when global option is 2 and group option is ignore", - 2, - "ignore", - ], - [ - "enforces spacing when global option is never and group option is 2", - "never", - 2, - ], - ["enforces spacing when global option is 0 and group option is 2", 0, 2], - [ - "enforces spacing when global option is ignore and group option is 2", - "ignore", - 2, - ], - ])( - "%s", - async (_description, globalNewlinesBetween, groupNewlinesBetween) => { - await invalid({ - options: [ - { - ...options, - customGroups: { - value: { - unusedGroup: "X", - a: "a", - b: "b", - }, - }, - groups: [ - "a", - "unusedGroup", - { newlinesBetween: groupNewlinesBetween }, - "b", - ], - newlinesBetween: globalNewlinesBetween, - }, - ], - output: dedent` - import { a } from 'a'; - - - import { b } from 'b'; - `, - code: dedent` - import { a } from 'a'; - import { b } from 'b'; - `, - }); - }, - ); - - it.each([ - [ - "removes spacing when never option exists between groups regardless of global setting always", - "always", - ], - [ - "removes spacing when never option exists between groups regardless of global setting 2", - 2, - ], - [ - "removes spacing when never option exists between groups regardless of global setting ignore", - "ignore", - ], - [ - "removes spacing when never option exists between groups regardless of global setting never", - "never", - ], - [ - "removes spacing when never option exists between groups regardless of global setting 0", - 0, - ], - ])("%s", async (_description, globalNewlinesBetween) => { - await invalid({ options: [ { - ...options, - groups: [ - "a", - { newlinesBetween: "never" }, - "unusedGroup", - { newlinesBetween: "never" }, - "b", - { newlinesBetween: "always" }, - "c", - ], - customGroups: { - value: { - unusedGroup: "X", - a: "a", - b: "b", - c: "c", - }, - }, - newlinesBetween: globalNewlinesBetween, + groups: ['unknown', 'external'], + newlinesBetween: 'always', }, ], - output: dedent` - import { a } from 'a'; - import { b } from 'b'; - `, + }) + }) + + it('supports style imports with query parameters', async () => { + await valid({ code: dedent` - import { a } from 'a'; + import b from './b.css?raw' + import c from './c.css' - import { b } from 'b'; + import a from './a.js' `, - }); - }); - - it.each([ - [ - "preserves existing spacing when ignore and never options are combined", - "ignore", - "never", - ], - [ - "preserves existing spacing when ignore and 0 options are combined", - "ignore", - 0, - ], - [ - "preserves existing spacing when never and ignore options are combined", - "never", - "ignore", - ], - [ - "preserves existing spacing when 0 and ignore options are combined", - 0, - "ignore", - ], - ])( - "%s", - async (_description, globalNewlinesBetween, groupNewlinesBetween) => { - await valid({ - options: [ - { - ...options, - customGroups: { - value: { - unusedGroup: "X", - a: "a", - b: "b", - }, - }, - groups: [ - "a", - "unusedGroup", - { newlinesBetween: groupNewlinesBetween }, - "b", - ], - newlinesBetween: globalNewlinesBetween, - }, - ], - code: dedent` - import { a } from 'a'; - - import { b } from 'b'; - `, - }); - - await valid({ - options: [ - { - ...options, - customGroups: { - value: { - unusedGroup: "X", - a: "a", - b: "b", - }, - }, - groups: [ - "a", - "unusedGroup", - { newlinesBetween: groupNewlinesBetween }, - "b", - ], - newlinesBetween: globalNewlinesBetween, - }, - ], - code: dedent` - import { a } from 'a'; - import { b } from 'b'; - `, - }); - }, - ); - - it.each([ - [ - "ignores newline fixes between different partitions with never option", - "never", - ], - ["ignores newline fixes between different partitions with 0 option", 0], - ])("%s", async (_description, newlinesBetween) => { - await invalid({ options: [ { ...options, - customGroups: [ - { - elementNamePattern: "a", - groupName: "a", - }, - ], - groups: ["a", "unknown"], - partitionByComment: true, - newlinesBetween, + groups: ['style', 'unknown'], }, ], - output: dedent` - import a from 'a'; + }) - // Partition comment - - import { b } from './b'; - import { c } from './c'; - `, - code: dedent` - import a from 'a'; - - // Partition comment - - import { c } from './c'; - import { b } from './b'; - `, - }); - }); - - it("allows partitioning by comment patterns", async () => { await invalid({ output: dedent` - // Part: A - // Not partition comment - import bbb from './bbb'; - import cc from './cc'; - import d from './d'; - // Part: B - import aaaa from './aaaa'; - import e from './e'; - // Part: C - // Not partition comment - import fff from './fff'; - import gg from './gg'; + import b from './b.css?raw' + import c from './c.css' + + import a from './a.js' `, code: dedent` - // Part: A - import cc from './cc'; - import d from './d'; - // Not partition comment - import bbb from './bbb'; - // Part: B - import aaaa from './aaaa'; - import e from './e'; - // Part: C - import gg from './gg'; - // Not partition comment - import fff from './fff'; + import a from './a.js' + import b from './b.css?raw' + import c from './c.css' `, options: [ { ...options, - partitionByComment: "^Part", + groups: ['style', 'unknown'], }, ], - }); - }); + }) + }) - it("supports regex patterns for partition comments", async () => { - await valid({ - code: dedent` - import e from './e' - import f from './f' - // I am a partition comment because I don't have f o o - import a from './a' - import b from './b' - `, + it('prioritizes index types over sibling types', async () => { + await invalid({ options: [ { ...options, - partitionByComment: ["^(?!.*foo).*$"], + groups: ['index-type', 'sibling-type'], }, ], - }); - }); + output: dedent` + import type b from './index' + + import type a from './a' + `, + code: dedent` + import type a from './a' - it("ignores block comments when line comment partitioning is enabled", async () => { + import type b from './index' + `, + }) + }) + + it('prioritizes specific type selectors over generic type group', async () => { await invalid({ options: [ { ...options, - partitionByComment: { - line: true, - }, + groups: [ + [ + 'index-type', + 'internal-type', + 'external-type', + 'sibling-type', + 'builtin-type', + ], + 'type', + ], }, ], output: dedent` - /* Comment */ - import a from './a' - import b from './b' + import type b from './b' + import type c from './index' + import type d from 'd' + import type e from 'timers' + + import type a from '../a' `, code: dedent` - import b from './b' - /* Comment */ - import a from './a' + import type a from '../a' + + import type b from './b' + import type c from './index' + import type d from 'd' + import type e from 'timers' `, - }); - }); + }) + }) - it("treats all line comments as partition boundaries when enabled", async () => { - await valid({ + it('prioritizes index imports over sibling imports', async () => { + await invalid({ options: [ { ...options, - partitionByComment: { - line: true, - }, + groups: ['index', 'sibling'], }, ], + output: dedent` + import b from './index' + + import a from './a' + `, code: dedent` - import b from './b' - // Comment import a from './a' + + import b from './index' `, - }); - }); + }) + }) - it("supports multiple line comment patterns for partitioning", async () => { - await valid({ + it('prioritizes style side-effects over generic side-effects', async () => { + await invalid({ options: [ { ...options, - partitionByComment: { - line: ["a", "b"], - }, + groups: ['side-effect-style', 'side-effect'], }, ], + output: dedent` + import 'style.css' + + import 'something' + `, code: dedent` - import c from './c' - // b - import b from './b' - // a - import a from './a' + import 'something' + + import 'style.css' `, - }); - }); + }) + }) - it("supports regex patterns for line comment partitioning", async () => { - await valid({ + it('prioritizes side-effects over style imports with default exports', async () => { + await invalid({ options: [ { ...options, - partitionByComment: { - line: ["^(?!.*foo).*$"], - }, + groups: ['side-effect', 'style'], }, ], + output: dedent` + import 'something' + + import style from 'style.css' + `, code: dedent` - import b from './b' - // I am a partition comment because I don't have f o o - import a from './a' + import style from 'style.css' + + import 'something' `, - }); - }); + }) + }) - it("ignores line comments when block comment partitioning is enabled", async () => { + it('prioritizes style imports over other import types', async () => { await invalid({ options: [ { ...options, - partitionByComment: { - block: true, - }, + groups: [ + 'style', + [ + 'index', + 'internal', + 'subpath', + 'external', + 'sibling', + 'builtin', + 'parent', + 'tsconfig-path', + ], + ], + tsconfigRootDir: '.', }, ], output: dedent` - // Comment - import a from './a' + import style from 'style.css' + + import subpath from '#subpath' + import tsConfigPath from '$path' + import a from '../a' import b from './b' + import c from './index' + import d from 'd' + import e from 'timers' `, code: dedent` + import a from '../a' import b from './b' - // Comment - import a from './a' - `, - }); - }); - - it("treats all block comments as partition boundaries when enabled", async () => { - await valid({ - options: [ - { - ...options, - partitionByComment: { - block: true, - }, - }, - ], - code: dedent` - import b from './b' - /* Comment */ - import a from './a' - `, - }); - }); - - it("supports multiple block comment patterns for partitioning", async () => { - await valid({ - options: [ - { - ...options, - partitionByComment: { - block: ["a", "b"], - }, - }, - ], - code: dedent` - import c from './c' - /* b */ - import b from './b' - /* a */ - import a from './a' - `, - }); - }); - - it("supports regex patterns for block comment partitioning", async () => { - await valid({ - options: [ - { - ...options, - partitionByComment: { - block: ["^(?!.*foo).*$"], - }, - }, - ], - code: dedent` - import b from './b' - /* I am a partition comment because I don't have f o o */ - import a from './a' - `, - }); - }); - - it("prioritizes index types over sibling types", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["index-type", "sibling-type"], - }, - ], - output: dedent` - import type b from './index' - - import type a from './a' - `, - code: dedent` - import type a from './a' - - import type b from './index' - `, - }); - }); - - it("prioritizes specific type selectors over generic type group", async () => { - await invalid({ - options: [ - { - ...options, - groups: [ - [ - "index-type", - "internal-type", - "external-type", - "sibling-type", - "builtin-type", - ], - "type", - ], - }, - ], - output: dedent` - import type b from './b' - import type c from './index' - import type d from 'd' - import type e from 'timers' - - import type a from '../a' - `, - code: dedent` - import type a from '../a' - - import type b from './b' - import type c from './index' - import type d from 'd' - import type e from 'timers' - `, - }); - }); - - it("prioritizes index imports over sibling imports", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["index", "sibling"], - }, - ], - output: dedent` - import b from './index' - - import a from './a' - `, - code: dedent` - import a from './a' - - import b from './index' - `, - }); - }); - - it("prioritizes style side-effects over generic side-effects", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["side-effect-style", "side-effect"], - }, - ], - output: dedent` - import 'style.css' - - import 'something' - `, - code: dedent` - import 'something' - - import 'style.css' - `, - }); - }); - - it("prioritizes side-effects over style imports with default exports", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["side-effect", "style"], - }, - ], - output: dedent` - import 'something' - - import style from 'style.css' - `, - code: dedent` - import style from 'style.css' - - import 'something' - `, - }); - }); - - it("prioritizes style imports over other import types", async () => { - await invalid({ - options: [ - { - ...options, - groups: [ - "style", - [ - "index", - "internal", - "subpath", - "external", - "sibling", - "builtin", - "parent", - "tsconfig-path", - ], - ], - tsconfigRootDir: ".", - }, - ], - output: dedent` - import style from 'style.css' - - import a from '../a' - import b from './b' - import c from './index' - import subpath from '#subpath' - import tsConfigPath from '$path' - import d from 'd' - import e from 'timers' - `, - code: dedent` - import a from '../a' - import b from './b' - import c from './index' - import subpath from '#subpath' - import tsConfigPath from '$path' - import d from 'd' - import e from 'timers' - - import style from 'style.css' - `, - before: () => { - mockReadClosestTsConfigByPathWith({ - paths: { - $path: ["./path"], - }, - }); - }, - }); - }); - - it("prioritizes external imports over generic import group", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["external", "import"], - }, - ], - output: dedent` - import b from 'b' - - import a from './a' - `, - code: dedent` - import a from './a' - - import b from 'b' - `, - }); - }); - - it("prioritizes side-effect imports over value imports", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["side-effect-import", "external", "value-import"], - sortSideEffects: true, - }, - ], - output: dedent` - import "./z" - - import f from 'f' - `, - code: dedent` - import f from 'f' - - import "./z" - `, - }); - }); - - it("prioritizes default imports over named imports", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["default-import", "external", "named-import"], - }, - ], - output: dedent` - import z, { z } from "./z" - - import f from 'f' - `, - code: dedent` - import f from 'f' - - import z, { z } from "./z" - `, - }); - }); - - it("prioritizes wildcard imports over named imports", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["wildcard-import", "external", "named-import"], - }, - ], - output: dedent` - import z, * as z from "./z" - - import f from 'f' - `, - code: dedent` - import f from 'f' - - import z, * as z from "./z" - `, - }); - }); - - it.each([ - ["filters on element name pattern with string", "hello"], - ["filters on element name pattern with array", ["noMatch", "hello"]], - [ - "filters on element name pattern with regex object", - { pattern: "HELLO", flags: "i" }, - ], - [ - "filters on element name pattern with array containing regex", - ["noMatch", { pattern: "HELLO", flags: "i" }], - ], - ])("%s", async (_description, elementNamePattern) => { - await invalid({ - options: [ - { - customGroups: [ - { - groupName: "importsStartingWithHello", - elementNamePattern, - }, - ], - groups: ["importsStartingWithHello", "unknown"], - }, - ], - output: dedent` - import hello from 'helloImport' - - import a from 'a' - `, - code: dedent` - import a from 'a' - - import hello from 'helloImport' - `, - }); - }); - - it("sorts custom groups by overriding type and order settings", async () => { - await invalid({ - options: [ - { - customGroups: [ - { - groupName: "reversedExternalImportsByLineLength", - selector: "external", - type: "line-length", - order: "desc", - }, - ], - groups: ["reversedExternalImportsByLineLength", "unknown"], - newlinesBetween: "ignore", - type: "alphabetical", - order: "asc", - }, - ], - output: dedent` - import dddd from 'dddd' - import ccc from 'ccc' - import eee from 'eee' - import bb from 'bb' - import ff from 'ff' - import a from 'a' - import g from 'g' - import h from './h' - import i from './i' - import jjjjj from './jjjjj' - `, - code: dedent` - import a from 'a' - import bb from 'bb' - import ccc from 'ccc' - import dddd from 'dddd' - import jjjjj from './jjjjj' - import eee from 'eee' - import ff from 'ff' - import g from 'g' - import h from './h' - import i from './i' - `, - }); - }); - - it("sorts custom groups using fallback sort settings", async () => { - await invalid({ - options: [ - { - customGroups: [ - { - fallbackSort: { - type: "alphabetical", - order: "asc", - }, - elementNamePattern: "^foo", - type: "line-length", - groupName: "foo", - order: "desc", - }, - ], - type: "alphabetical", - groups: ["foo"], - order: "asc", - }, - ], - output: dedent` - import fooBar from 'fooBar' - import fooZar from 'fooZar' - `, - code: dedent` - import fooZar from 'fooZar' - import fooBar from 'fooBar' - `, - }); - }); - - it("preserves order for custom groups with unsorted type", async () => { - await invalid({ - options: [ - { - customGroups: [ - { - groupName: "unsortedExternalImports", - selector: "external", - type: "unsorted", - }, - ], - groups: ["unsortedExternalImports", "unknown"], - newlinesBetween: "ignore", - }, - ], - output: dedent` - import b from 'b' - import a from 'a' - import d from 'd' - import e from 'e' - import c from 'c' - import something from './something' - `, - code: dedent` - import b from 'b' - import a from 'a' - import d from 'd' - import e from 'e' - import something from './something' - import c from 'c' - `, - }); - }); - - it("sorts custom group blocks with complex selectors", async () => { - await invalid({ - options: [ - { - customGroups: [ - { - anyOf: [ - { - selector: "external", - }, - { - selector: "sibling", - modifiers: ["type"], - }, - ], - groupName: "externalAndTypeSiblingImports", - }, - ], - groups: [["externalAndTypeSiblingImports", "index"], "unknown"], - newlinesBetween: "ignore", - }, - ], - output: dedent` - import type c from './c' - import type d from './d' - import i from './index' - import a from 'a' - import e from 'e' - import b from './b' - `, - code: dedent` - import a from 'a' - import b from './b' - import type c from './c' - import type d from './d' - import e from 'e' - import i from './index' - `, - }); - }); - - it.each([ - ["adds spacing inside custom groups when always option is used", "always"], - ["adds spacing inside custom groups when 1 option is used", 1], - ])("%s", async (_description, newlinesInside) => { - await invalid({ - options: [ - { - customGroups: [ - { - selector: "external", - groupName: "group1", - newlinesInside, - }, - ], - groups: ["group1"], - }, - ], - output: dedent` - import a from 'a' - - import b from 'b' - `, - code: dedent` - import a from 'a' - import b from 'b' - `, - }); - }); - - it.each([ - ["removes spacing inside custom groups when never option is used", "never"], - ["removes spacing inside custom groups when 0 option is used", 0], - ])("%s", async (_description, newlinesInside) => { - await invalid({ - options: [ - { - customGroups: [ - { - selector: "external", - groupName: "group1", - newlinesInside, - }, - ], - type: "alphabetical", - groups: ["group1"], - }, - ], - output: dedent` - import a from 'a' - import b from 'b' - `, - code: dedent` - import a from 'a' - - import b from 'b' - `, - }); - }); - - it("detects TypeScript import-equals dependencies", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["unknown"], - }, - ], - output: dedent` - import { aImport } from "b"; - import a = aImport.a1.a2; - `, - code: dedent` - import a = aImport.a1.a2; - import { aImport } from "b"; - `, - }); - - await invalid({ - options: [ - { - ...options, - groups: ["unknown"], - }, - ], - output: dedent` - import * as aImport from "b"; - import a = aImport.a1.a2; - `, - code: dedent` - import a = aImport.a1.a2; - import * as aImport from "b"; - `, - }); - - await invalid({ - options: [ - { - ...options, - groups: ["unknown"], - }, - ], - output: dedent` - import aImport from "b"; - import a = aImport.a1.a2; - `, - code: dedent` - import a = aImport.a1.a2; - import aImport from "b"; - `, - }); - - await invalid({ - options: [ - { - ...options, - groups: ["unknown"], - }, - ], - output: dedent` - import aImport = require("b") - import a = aImport.a1.a2; - `, - code: dedent` - import a = aImport.a1.a2; - import aImport = require("b") - `, - }); - }); - - it("prioritizes dependencies over group configuration", async () => { - await valid({ - options: [ - { - ...options, - customGroups: [ - { - groupName: "importsStartingWithA", - elementNamePattern: "^a", - }, - { - groupName: "importsStartingWithB", - elementNamePattern: "^b", - }, - ], - groups: ["importsStartingWithA", "importsStartingWithB"], - }, - ], - code: dedent` - import aImport from "b"; - import a = aImport.a1.a2; - `, - }); - - await invalid({ - options: [ - { - ...options, - customGroups: [ - { - groupName: "importsStartingWithA", - elementNamePattern: "^a", - }, - { - groupName: "importsStartingWithB", - elementNamePattern: "^b", - }, - ], - groups: ["importsStartingWithA", "importsStartingWithB"], - }, - ], - output: dedent` - import aImport from "b"; - import a = aImport.a1.a2; - `, - code: dedent` - import a = aImport.a1.a2; - import aImport from "b"; - `, - }); - }); - - it("prioritizes dependencies over comment-based partitions", async () => { - await invalid({ - output: dedent` - import aImport from "b"; - - // Part: 1 - import a = aImport.a1.a2; - `, - code: dedent` - import a = aImport.a1.a2; - - // Part: 1 - import aImport from "b"; - `, - options: [ - { - ...options, - partitionByComment: "^Part", - }, - ], - }); - }); - - it("treats @ symbol pattern as internal imports", async () => { - await invalid({ - options: [ - { - ...options, - groups: ["external", "internal"], - newlinesBetween: "always", - }, - ], - output: dedent` - import { b } from 'b' + import c from './index' + import subpath from '#subpath' + import tsConfigPath from '$path' + import d from 'd' + import e from 'timers' - import { a } from '@/a' - `, - code: dedent` - import { b } from 'b' - import { a } from '@/a' + import style from 'style.css' `, - }); - }); + before: () => { + mockReadClosestTsConfigByPathWith({ + paths: { + $path: ['./path'], + }, + }) + }, + }) + }) - it("reports missing comments above import groups", async () => { + it('prioritizes external imports over generic import group', async () => { await invalid({ options: [ { ...options, - groups: [ - { commentAbove: "Comment above a" }, - "external", - { commentAbove: "Comment above b" }, - "unknown", - ], + groups: ['external', 'import'], }, ], output: dedent` - // Comment above a - import { a } from "a"; + import b from 'b' - // Comment above b - import { b } from "./b"; + import a from './a' `, code: dedent` - import { a } from "a"; + import a from './a' - import { b } from "./b"; + import b from 'b' `, - }); - }); + }) + }) - it("reports missing comments for single import groups", async () => { + it('prioritizes side-effect imports over value imports', async () => { await invalid({ options: [ { ...options, - groups: [{ commentAbove: "Comment above" }, "unknown"], + groups: ['side-effect-import', 'external', 'value-import'], + sortSideEffects: true, }, ], output: dedent` - // Comment above - import { a } from "a"; - `, - code: dedent` - import { a } from "a"; - `, - }); - }); - - it("ignores shebangs and top-level comments when adding group comments", async () => { - await invalid({ - output: dedent` - #!/usr/bin/node - // Some disclaimer - - // Comment above - import a from "a"; - import b from "b"; - `, - options: [ - { - ...options, - groups: [{ commentAbove: "Comment above" }, "external"], - }, - ], - code: dedent` - #!/usr/bin/node - // Some disclaimer + import "./z" - import b from "b"; - import a from "a"; + import f from 'f' `, - }); - }); - - it.each([ - ["detects existing line comment with extra spaces", "// Comment above "], - [ - "detects existing line comment with different case", - "// comment above ", - ], - [ - "detects existing block comment with standard format", - dedent` - /** - * Comment above - */ - `, - ], - [ - "detects existing block comment with surrounding text", - dedent` - /** - * Something before - * CoMmEnT ABoVe - * Something after - */ - `, - ], - ])("%s", async (_description, comment) => { - await valid({ - options: [ - { - ...options, - groups: ["external", { commentAbove: "Comment above" }, "unknown"], - }, - ], code: dedent` - import a from "a"; + import f from 'f' - ${comment} - import b from "./b"; + import "./z" `, - }); - }); + }) + }) - it("removes and repositions invalid auto-added comments", async () => { + it('prioritizes default imports over named imports', async () => { await invalid({ options: [ { ...options, - groups: [ - { commentAbove: "external" }, - "external", - { commentAbove: "sibling" }, - "sibling", - { commentAbove: "internal" }, - "internal", - ], + groups: ['default-import', 'external', 'named-import'], }, ], output: dedent` - // external - import a from "a"; - - // sibling - import b from './b'; + import z, { z } from "./z" - // internal - import c from '~/c'; - import d from '~/d'; + import f from 'f' `, code: dedent` - import d from '~/d'; - // internal - import c from '~/c'; - - // sibling - import b from './b'; + import f from 'f' - // external - import a from "a"; + import z, { z } from "./z" `, - }); - }); + }) + }) - it("handles complex scenarios with multiple error types and comment management", async () => { + it('prioritizes wildcard imports over named imports', async () => { await invalid({ - code: dedent` - #!/usr/bin/node - // Some disclaimer - - // Comment above c - // external - import c from './c'; // Comment after c - // Comment above a - // internal or sibling - import a from "a"; // Comment after a - // Comment above b - // external - import b from '~/b'; // Comment after b - `, options: [ { ...options, - groups: [ - { commentAbove: "external" }, - "external", - { - commentAbove: "internal or sibling", - newlinesBetween: "always", - }, - ["internal", "sibling"], - ], - newlinesBetween: "never", + groups: ['wildcard-import', 'external', 'named-import'], }, ], output: dedent` - #!/usr/bin/node - // Some disclaimer - - // Comment above a - // external - import a from "a"; // Comment after a + import z, * as z from "./z" - // internal or sibling - // Comment above c - import c from './c'; // Comment after c - // Comment above b - import b from '~/b'; // Comment after b + import f from 'f' `, - }); - }); -}); - -describe("unsorted", () => { - let options = { - type: "unsorted", - order: "asc", - } as const; - - it("preserves original order when sorting is disabled", async () => { - await valid({ code: dedent` - import { b } from 'b'; - import { c } from 'c'; - import { a } from 'a'; - `, - options: [options], - }); - }); - - it("enforces group order regardless of sorting settings", async () => { - await invalid({ - options: [ - { - ...options, - customGroups: { - value: { - a: "^a", - b: "^b", - }, - }, - groups: ["b", "a"], - }, - ], - output: dedent` - import { ba } from 'ba' - import { bb } from 'bb' + import f from 'f' - import { ab } from 'ab' - import { aa } from 'aa' - `, - code: dedent` - import { ab } from 'ab' - import { aa } from 'aa' - import { ba } from 'ba' - import { bb } from 'bb' + import z, * as z from "./z" `, - }); - }); + }) + }) - it("enforces spacing rules between import groups", async () => { + it('treats @ symbol pattern as internal imports', async () => { await invalid({ options: [ { ...options, - customGroups: { - value: { - a: "^a", - b: "^b", - }, - }, - newlinesBetween: "never", - groups: ["b", "a"], - }, - ], - errors: [ - { - data: { - right: "a", - left: "b", - }, - messageId: "extraSpacingBetweenImports", + groups: ['external', 'internal'], + newlinesBetween: 'always', }, ], output: dedent` import { b } from 'b' - import { a } from 'a' + + import { a } from '@/a' `, code: dedent` import { b } from 'b' - - import { a } from 'a' + import { a } from '@/a' `, - }); - }); -}); + }) + }) +}) describe("misc", () => { - it("supports combination of predefined and custom groups", async () => { - await valid({ - options: [ - { - groups: [ - "side-effect-style", - "external-type", - "internal-type", - "builtin-type", - "sibling-type", - "parent-type", - "side-effect", - "index-type", - "internal", - "external", - "sibling", - "unknown", - "builtin", - "parent", - "index", - "style", - "type", - "myCustomGroup1", - ], - customGroups: { - type: { - myCustomGroup1: "x", - }, - }, - }, - ], - code: dedent` - import type { T } from 't' - - // @ts-expect-error missing types - import { t } from 't' - `, - }); - }); - - it("preserves order of side-effect imports", async () => { - await valid(dedent` - import './index.css' - import './animate.css' - import './reset.css' - `); - }); - it("recognizes Node.js built-in modules with node: prefix", async () => { await valid({ code: dedent` @@ -2058,155 +799,6 @@ describe("misc", () => { }); }); - it("handles complex projects with many custom groups", async () => { - await valid({ - options: [ - { - customGroups: { - value: { - validators: ["^~/validators/.+"], - composable: ["^~/composable/.+"], - components: ["^~/components/.+"], - services: ["^~/services/.+"], - widgets: ["^~/widgets/.+"], - stores: ["^~/stores/.+"], - logics: ["^~/logics/.+"], - assets: ["^~/assets/.+"], - utils: ["^~/utils/.+"], - pages: ["^~/pages/.+"], - ui: ["^~/ui/.+"], - }, - }, - groups: [ - ["builtin", "external"], - "internal", - "stores", - "services", - "validators", - "utils", - "logics", - "composable", - "ui", - "components", - "pages", - "widgets", - "assets", - "parent", - "sibling", - "side-effect", - "index", - "style", - "unknown", - ], - type: "line-length", - }, - ], - code: dedent` - import { useCartStore } from '~/stores/cartStore.ts' - import { useUserStore } from '~/stores/userStore.ts' - - import { getCart } from '~/services/cartService.ts' - - import { connect } from '~/utils/ws.ts' - import { formattingDate } from '~/utils/dateTime.ts' - - import { useFetch } from '~/composable/useFetch.ts' - import { useDebounce } from '~/composable/useDebounce.ts' - import { useMouseMove } from '~/composable/useMouseMove.ts' - - import ComponentA from '~/components/ComponentA.vue' - import ComponentB from '~/components/ComponentB.vue' - import ComponentC from '~/components/ComponentC.vue' - - import CartComponentA from './cart/CartComponentA.vue' - import CartComponentB from './cart/CartComponentB.vue' - `, - }); - - await invalid({ - options: [ - { - customGroups: { - value: { - validators: ["~/validators/.+"], - composable: ["~/composable/.+"], - components: ["~/components/.+"], - services: ["~/services/.+"], - widgets: ["~/widgets/.+"], - stores: ["~/stores/.+"], - logics: ["~/logics/.+"], - assets: ["~/assets/.+"], - utils: ["~/utils/.+"], - pages: ["~/pages/.+"], - ui: ["~/ui/.+"], - }, - }, - groups: [ - ["builtin", "external"], - "internal", - "stores", - "services", - "validators", - "utils", - "logics", - "composable", - "ui", - "components", - "pages", - "widgets", - "assets", - "parent", - "sibling", - "side-effect", - "index", - "style", - "unknown", - ], - type: "line-length", - }, - ], - output: dedent` - import { useUserStore } from '~/stores/userStore.ts' - import { useCartStore } from '~/stores/cartStore.ts' - - import { getCart } from '~/services/cartService.ts' - - import { connect } from '~/utils/ws.ts' - import { formattingDate } from '~/utils/dateTime.ts' - - import { useFetch } from '~/composable/useFetch.ts' - import { useDebounce } from '~/composable/useDebounce.ts' - import { useMouseMove } from '~/composable/useMouseMove.ts' - - import ComponentA from '~/components/ComponentA.vue' - import ComponentB from '~/components/ComponentB.vue' - import ComponentC from '~/components/ComponentC.vue' - - import CartComponentA from './cart/CartComponentA.vue' - import CartComponentB from './cart/CartComponentB.vue' - `, - code: dedent` - import CartComponentA from './cart/CartComponentA.vue' - import CartComponentB from './cart/CartComponentB.vue' - - import { connect } from '~/utils/ws.ts' - import { getCart } from '~/services/cartService.ts' - - import { useUserStore } from '~/stores/userStore.ts' - import { formattingDate } from '~/utils/dateTime.ts' - - import { useFetch } from '~/composable/useFetch.ts' - import { useCartStore } from '~/stores/cartStore.ts' - import { useDebounce } from '~/composable/useDebounce.ts' - import { useMouseMove } from '~/composable/useMouseMove.ts' - - import ComponentA from '~/components/ComponentA.vue' - import ComponentB from '~/components/ComponentB.vue' - import ComponentC from '~/components/ComponentC.vue' - `, - }); - }); - it("treats empty named imports as regular imports not side-effects", async () => { await valid({ code: dedent` @@ -2225,22 +817,6 @@ describe("misc", () => { }); }); - it("ignores dynamic require statements", async () => { - await valid({ - code: dedent` - const path = require(path); - const myFileName = require('the-filename'); - const file = require(path.join(myDir, myFileName)); - const other = require('./other.js'); - `, - options: [ - { - groups: ["builtin", "external", "side-effect"], - newlinesBetween: "never", - }, - ], - }); - }); describe("validates compatibility between sortSideEffects and groups configuration", () => { function createRule( @@ -2324,28 +900,6 @@ describe("misc", () => { options: [{}], }); - await invalid({ - output: dedent` - import { b } from './b' - import { c } from './c' - // eslint-disable-next-line - import { a } from './a' - import { d } from './d' - `, - code: dedent` - import { d } from './d' - import { c } from './c' - // eslint-disable-next-line - import { a } from './a' - import { b } from './b' - `, - options: [ - { - partitionByComment: true, - }, - ], - }); - await invalid({ output: dedent` import { b } from './b' diff --git a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs index f10ecb9b05c1c..7dff3dd55c1ba 100644 --- a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs @@ -300,6 +300,23 @@ fn should_stop_grouping_when_other_statements_appear() { assert_format( r#" import type { V } from "v"; + +export type { U } from "u"; + +import type { T1, T2 } from "t"; +"#, + r#"{ "experimentalSortImports": {} }"#, + r#" +import type { V } from "v"; + +export type { U } from "u"; + +import type { T1, T2 } from "t"; +"#, + ); + assert_format( + r#" +import type { V } from "v"; export type { U } from "u"; import type { T1, T2 } from "t"; "#, @@ -739,6 +756,20 @@ import "aaa"; import "aaa"; import "bb"; import "c"; +"#, + ); + assert_format( + r#" +import "./index.css" +import "./animate.css" +import "./reset.css" + +"#, + r#"{ "experimentalSortImports": {} }"#, + r#" +import "./index.css"; +import "./animate.css"; +import "./reset.css"; "#, ); }