From c0d00b22ebd408fef107d2dce6d5773b01482a08 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 6 Dec 2020 00:10:31 +0100 Subject: [PATCH 1/3] Allow customizing where type imports go --- README.md | 8 +- examples/.eslintrc.js | 102 ++++++++++++++ ...groups.type-imports-first-in-each-group.ts | 14 ++ examples/groups.type-imports-first-sorted.ts | 14 ++ examples/groups.type-imports-first.ts | 14 ++ .../groups.type-imports-last-in-each-group.ts | 14 ++ examples/groups.type-imports-last-sorted.ts | 14 ++ examples/groups.type-imports-last.ts | 14 ++ src/imports.js | 2 + test/__snapshots__/examples.test.js.snap | 130 ++++++++++++++++++ test/imports.test.js | 70 ++++++++++ 11 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 examples/groups.type-imports-first-in-each-group.ts create mode 100644 examples/groups.type-imports-first-sorted.ts create mode 100644 examples/groups.type-imports-first.ts create mode 100644 examples/groups.type-imports-last-in-each-group.ts create mode 100644 examples/groups.type-imports-last-sorted.ts create mode 100644 examples/groups.type-imports-last.ts diff --git a/README.md b/README.md index 33fcd63..1ba8a42 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Easy autofixable import sorting. - ✅️ Runs via `eslint --fix` – no new tooling - ✅️ Also sorts exports where possible - ✅️ Handles comments -- ✅️ Handles [Flow type imports] \(via [babel-eslint]) +- ✅️ Handles type imports/exports - ✅️ [TypeScript] friendly \(via [@typescript-eslint/parser]) - ✅️ [Prettier] friendly - ✅️ [eslint-plugin-import] friendly @@ -16,9 +16,7 @@ Easy autofixable import sorting. This is for those who use `eslint --fix` (autofix) a lot and want to completely forget about sorting imports! [@typescript-eslint/parser]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser -[babel-eslint]: https://github.com/babel/babel-eslint [eslint-plugin-import]: https://github.com/benmosher/eslint-plugin-import/ -[flow type imports]: https://flow.org/en/docs/types/modules/ [no-require]: https://github.com/lydell/eslint-plugin-simple-import-sort/#does-it-support-require [prettier]: https://prettier.io/ [typescript]: https://www.typescriptlang.org/ @@ -257,7 +255,7 @@ In other words, the imports/exports within groups are sorted alphabetically, cas There’s one addition to the alphabetical rule: Directory structure. Relative imports/exports of files higher up in the directory structure come before closer ones – `"../../utils"` comes before `"../utils"`, which comes before `"."`. (In short, `.` and `/` sort before any other (non-whitespace, non-control) character. `".."` and similar sort like `"../,"` (to avoid the “shorter prefix comes first” sorting concept).) -If both `import type` _and_ regular imports are used for the same source, the type imports come first. Same thing for `export type`. +If both `import type` _and_ regular imports are used for the same source, the type imports come first. Same thing for `export type`. (You can move type imports to their own group, as mentioned in [custom grouping].) ### Example @@ -397,6 +395,8 @@ Imports that don’t match any regex are grouped together last. Side effect imports have `\u0000` prepended to their `from` string. You can match them with `"^\\u0000"`. +Type imports have `\u0000` appended to their `from` string. You can match them with `"\\u0000$"` – but you probably need more than that to avoid them also being matched by other groups. + The inner arrays are joined with one newline; the outer arrays are joined with two (creating a blank line). Every group is sorted internally as mentioned in [Sort order]. Side effect imports are always placed first in the group and keep their internal order. It’s recommended to keep side effect imports in their own group. diff --git a/examples/.eslintrc.js b/examples/.eslintrc.js index 2251c35..6c50581 100644 --- a/examples/.eslintrc.js +++ b/examples/.eslintrc.js @@ -118,6 +118,108 @@ module.exports = { ], }, }, + { + files: ["groups.type-imports-first.ts"], + parser: "@typescript-eslint/parser", + rules: { + imports: [ + "error", + { + // The default grouping, but with type imports first as a separate group. + groups: [["\u0000$"], ["^\\u0000"], ["^@?\\w"], ["^"], ["^\\."]], + }, + ], + }, + }, + { + files: ["groups.type-imports-last.ts"], + parser: "@typescript-eslint/parser", + rules: { + imports: [ + "error", + { + // The default grouping, but with type imports last as a separate group. + groups: [["^\\u0000"], ["^@?\\w"], ["^"], ["^\\."], ["^.+\u0000$"]], + }, + ], + }, + }, + { + files: ["groups.type-imports-first-sorted.ts"], + parser: "@typescript-eslint/parser", + rules: { + imports: [ + "error", + { + // The default grouping, but with type imports first as a separate + // group, sorting that group like non-type imports are grouped. + groups: [ + ["^@?\\w.*\\u0000$", "\\u0000$", "^\\..*\\u0000$"], + ["^\\u0000"], + ["^@?\\w"], + ["^"], + ["^\\."], + ], + }, + ], + }, + }, + { + files: ["groups.type-imports-last-sorted.ts"], + parser: "@typescript-eslint/parser", + rules: { + imports: [ + "error", + { + // The default grouping, but with type imports last as a separate + // group, sorting that group like non-type imports are grouped. + groups: [ + ["^\\u0000"], + ["^@?\\w"], + ["^"], + ["^\\."], + ["^@?\\w.*\\u0000$", "^[^.].*\\u0000$", "^\\..*\\u0000$"], + ], + }, + ], + }, + }, + { + files: ["groups.type-imports-first-in-each-group.ts"], + parser: "@typescript-eslint/parser", + rules: { + imports: [ + "error", + { + // The default grouping, but with type imports first in each group. + groups: [ + ["^\\u0000"], + ["^@?\\w.*\\u0000$", "^@?\\w"], + ["(?<=\\u0000)$", "^"], + ["^\\..*\\u0000$", "^\\."], + ], + }, + ], + }, + }, + { + files: ["groups.type-imports-last-in-each-group.ts"], + parser: "@typescript-eslint/parser", + rules: { + imports: [ + "error", + { + // The default grouping, but with type imports last in each group. + groups: [ + ["^\\u0000"], + ["^@?\\w", "^@?\\w.*\\u0000$"], + ["(? diff --git a/test/__snapshots__/examples.test.js.snap b/test/__snapshots__/examples.test.js.snap index 7af78d3..c15ce39 100644 --- a/test/__snapshots__/examples.test.js.snap +++ b/test/__snapshots__/examples.test.js.snap @@ -135,6 +135,136 @@ import react from "react"; `; +exports[`examples groups.type-imports-first.ts 1`] = ` +import type { Page } from "./page"; +import type { Css } from "./styles"; +import type { Config } from "/config"; +import type { AppRouter } from "@/App"; +import type { Component } from "react"; +import type { Store } from "redux"; + +import "./polyfills"; + +import type { Story } from "@storybook/react"; +import { storiesOf } from "@storybook/react"; +import react from "react"; + +import config from "/config"; +import App from "@/App"; + +import page from "./page"; +import styles from "./styles"; + +`; + +exports[`examples groups.type-imports-first-in-each-group.ts 1`] = ` +import "./polyfills"; + +import type { Story } from "@storybook/react"; +import type { Component } from "react"; +import type { Store } from "redux"; +import { storiesOf } from "@storybook/react"; +import react from "react"; + +import type { Config } from "/config"; +import type { AppRouter } from "@/App"; +import config from "/config"; +import App from "@/App"; + +import type { Page } from "./page"; +import type { Css } from "./styles"; +import page from "./page"; +import styles from "./styles"; + +`; + +exports[`examples groups.type-imports-first-sorted.ts 1`] = ` +import type { Story } from "@storybook/react"; +import type { Component } from "react"; +import type { Store } from "redux"; +import type { Config } from "/config"; +import type { AppRouter } from "@/App"; +import type { Page } from "./page"; +import type { Css } from "./styles"; + +import "./polyfills"; + +import { storiesOf } from "@storybook/react"; +import react from "react"; + +import config from "/config"; +import App from "@/App"; + +import page from "./page"; +import styles from "./styles"; + +`; + +exports[`examples groups.type-imports-last.ts 1`] = ` +import "./polyfills"; + +import { storiesOf } from "@storybook/react"; +import react from "react"; + +import config from "/config"; +import App from "@/App"; + +import page from "./page"; +import styles from "./styles"; + +import type { Page } from "./page"; +import type { Css } from "./styles"; +import type { Config } from "/config"; +import type { AppRouter } from "@/App"; +import type { Story } from "@storybook/react"; +import type { Component } from "react"; +import type { Store } from "redux"; + +`; + +exports[`examples groups.type-imports-last-in-each-group.ts 1`] = ` +import "./polyfills"; + +import { storiesOf } from "@storybook/react"; +import react from "react"; +import type { Story } from "@storybook/react"; +import type { Component } from "react"; +import type { Store } from "redux"; + +import config from "/config"; +import App from "@/App"; +import type { Config } from "/config"; +import type { AppRouter } from "@/App"; + +import page from "./page"; +import styles from "./styles"; +import type { Page } from "./page"; +import type { Css } from "./styles"; + +`; + +exports[`examples groups.type-imports-last-sorted.ts 1`] = ` +import "./polyfills"; + +import { storiesOf } from "@storybook/react"; +import react from "react"; + +import config from "/config"; +import App from "@/App"; + +import page from "./page"; +import styles from "./styles"; + +import type { Story } from "@storybook/react"; +import type { Component } from "react"; +import type { Store } from "redux"; +import type { Config } from "/config"; +import type { AppRouter } from "@/App"; +import type { Page } from "./page"; +import type { Css } from "./styles"; + +`; + exports[`examples ignore.js 1`] = ` // First off, imports that are only used for side effects stay in the input // order. These won’t be sorted: diff --git a/test/imports.test.js b/test/imports.test.js index 604f9df..2abc2c8 100644 --- a/test/imports.test.js +++ b/test/imports.test.js @@ -1637,6 +1637,41 @@ const flowTests = { errors: 1, }, + // `groups` – type imports. + { + options: [{ groups: [["^\\w"], ["^\\."], ["^.*\\u0000$"]] }], + code: input` + |import '@/'; + |import c from '@/'; + |import type C from '@/'; + |import b from './'; + |import type B from './'; + |import './'; + |import a from 'a'; + |import typeof A from 'a'; + |import 'a'; + |import {} from 'a'; + `, + output: (actual) => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'a'; + |import {} from 'a'; + | + |import b from './'; + | + |import type B from './'; + |import type C from '@/'; + |import typeof A from 'a'; + | + |import '@/'; + |import './'; + |import 'a'; + |import c from '@/'; + `); + }, + errors: 1, + }, + // https://github.com/graphql/graphql-js/blob/64b194c6c9b9aaa1c139f1b7c3692a6ef851928e/src/execution/execute.js#L10-L69 { code: input` @@ -1826,6 +1861,41 @@ const typescriptTests = { }, errors: 1, }, + + // `groups` – type imports. + { + options: [{ groups: [["^\\w"], ["^\\."], ["^.*\\u0000$"]] }], + code: input` + |import '@/'; + |import c from '@/'; + |import type C from '@/'; + |import b from './'; + |import type B from './'; + |import './'; + |import a from 'a'; + |import type A from 'a'; + |import 'a'; + |import {} from 'a'; + `, + output: (actual) => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'a'; + |import {} from 'a'; + | + |import b from './'; + | + |import type B from './'; + |import type C from '@/'; + |import type A from 'a'; + | + |import '@/'; + |import './'; + |import 'a'; + |import c from '@/'; + `); + }, + errors: 1, + }, ], }; From 1f696ba8ce8142626d8a7c0e175235329b661aa9 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Sun, 6 Dec 2020 00:21:30 +0100 Subject: [PATCH 2/3] Add real-word test cases with type imports --- test/imports.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/imports.test.js b/test/imports.test.js index 2abc2c8..e2afb67 100644 --- a/test/imports.test.js +++ b/test/imports.test.js @@ -1896,6 +1896,44 @@ const typescriptTests = { }, errors: 1, }, + + // `groups` – real-world example from issue #61 based on: + // https://github.com/polkadot-js/apps/blob/074b245a725873f7c3c16fc83b80fb9c02351a65/packages/apps/src/Endpoints/Group.tsx + // https://github.com/polkadot-js/apps/blob/074b245a725873f7c3c16fc83b80fb9c02351a65/.eslintrc.js#L31-L39 + { + options: [ + { + groups: [ + ["^[^@.].*\\u0000$", "^[^/.]"], + ["^@polkadot.*\\u0000$", "^@polkadot"], + ["^\\..*\\u0000$", "^\\."], + ], + }, + ], + code: input` + |import React, { useCallback } from 'react'; + |import styled from 'styled-components'; + | + |import { Icon } from '@polkadot/react-components'; + |import type { ThemeProps } from '@polkadot/react-components/types'; + | + |import Network from './Network'; + |import type { Group } from './types'; + `, + output: (actual) => { + expect(actual).toMatchInlineSnapshot(` + |import React, { useCallback } from 'react'; + |import styled from 'styled-components'; + | + |import type { ThemeProps } from '@polkadot/react-components/types'; + |import { Icon } from '@polkadot/react-components'; + | + |import type { Group } from './types'; + |import Network from './Network'; + `); + }, + errors: 1, + }, ], }; From 5dd83b98abbf86e7ddb48d0b444cf4050827abe6 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Tue, 8 Dec 2020 21:49:20 +0100 Subject: [PATCH 3/3] Tests and docs fixes --- README.md | 4 ++-- examples/.eslintrc.js | 6 +++--- test/__snapshots__/examples.test.js.snap | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ba8a42..5d2a391 100644 --- a/README.md +++ b/README.md @@ -393,9 +393,9 @@ Each `import` is matched against _all_ regexes on the `from` string. The import Imports that don’t match any regex are grouped together last. -Side effect imports have `\u0000` prepended to their `from` string. You can match them with `"^\\u0000"`. +Side effect imports have `\u0000` _prepended_ to their `from` string (starts with `\u0000`). You can match them with `"^\\u0000"`. -Type imports have `\u0000` appended to their `from` string. You can match them with `"\\u0000$"` – but you probably need more than that to avoid them also being matched by other groups. +Type imports have `\u0000` _appended_ to their `from` string (ends with `\u0000`). You can match them with `"\\u0000$"` – but you probably need more than that to avoid them also being matched by other groups. The inner arrays are joined with one newline; the outer arrays are joined with two (creating a blank line). diff --git a/examples/.eslintrc.js b/examples/.eslintrc.js index 6c50581..ab4c7fa 100644 --- a/examples/.eslintrc.js +++ b/examples/.eslintrc.js @@ -126,7 +126,7 @@ module.exports = { "error", { // The default grouping, but with type imports first as a separate group. - groups: [["\u0000$"], ["^\\u0000"], ["^@?\\w"], ["^"], ["^\\."]], + groups: [["^.*\\u0000$"], ["^\\u0000"], ["^@?\\w"], ["^"], ["^\\."]], }, ], }, @@ -139,7 +139,7 @@ module.exports = { "error", { // The default grouping, but with type imports last as a separate group. - groups: [["^\\u0000"], ["^@?\\w"], ["^"], ["^\\."], ["^.+\u0000$"]], + groups: [["^\\u0000"], ["^@?\\w"], ["^"], ["^\\."], ["^.+\\u0000$"]], }, ], }, @@ -154,7 +154,7 @@ module.exports = { // The default grouping, but with type imports first as a separate // group, sorting that group like non-type imports are grouped. groups: [ - ["^@?\\w.*\\u0000$", "\\u0000$", "^\\..*\\u0000$"], + ["^@?\\w.*\\u0000$", "^[^.].*\\u0000$", "^\\..*\\u0000$"], ["^\\u0000"], ["^@?\\w"], ["^"], diff --git a/test/__snapshots__/examples.test.js.snap b/test/__snapshots__/examples.test.js.snap index c15ce39..20c3387 100644 --- a/test/__snapshots__/examples.test.js.snap +++ b/test/__snapshots__/examples.test.js.snap @@ -140,12 +140,12 @@ import type { Page } from "./page"; import type { Css } from "./styles"; import type { Config } from "/config"; import type { AppRouter } from "@/App"; +import type { Story } from "@storybook/react"; import type { Component } from "react"; import type { Store } from "redux"; import "./polyfills"; -import type { Story } from "@storybook/react"; import { storiesOf } from "@storybook/react"; import react from "react";