diff --git a/README.md b/README.md index 99d2139..57a081b 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,11 @@ Easy autofixable import sorting. - ✔️ Runs via `eslint --fix` – no new tooling - ✔️ Handles comments - ✔️ Handles [Flow type imports] \(via [babel-eslint]) -- ✔️ Handles [webpack loader syntax] - ✔️ [TypeScript] friendly \(via [@typescript-eslint/parser]) - ✔️ [Prettier] friendly - ✔️ [eslint-plugin-import] friendly - ✔️ `git diff` friendly - ✔️ 100% code coverage -- ☯️ No configuration - ❌ [Does not support `require`][no-require] This is for those who use `eslint --fix` (autofix) a lot and want to completely @@ -29,6 +27,7 @@ forget about sorting imports! - [Usage](#usage) - [Example configuration](#example-configuration) - [Sort order](#sort-order) +- [Custom grouping](#custom-grouping) - [Comment and whitespace handling](#comment-and-whitespace-handling) - [FAQ](#faq) - [Does it support `require`?](#does-it-support-require) @@ -213,10 +212,13 @@ Then, each chunk is _grouped_ into sections with a blank line between each. 1. `import "./setup"`: Side effect imports. (These are not sorted internally.) 2. `import react from "react"`: Packages (npm packages and Node.js builtins). -3. `import a from "/a"`: Absolute imports, full URLs and other imports (such as - Vue-style `@/foo` ones). +3. `import a from "/a"`: Absolute imports and other imports such as Vue-style + `@/foo`. 4. `import a from "./a"`: Relative imports. +Note: The above groups are very loosely defined. See [Custom grouping] for more +information. + Within each section, the imports are sorted alphabetically on the `from` string (see also [“Why sort on `from`?”][sort-from]). Keep it simple! It helps looking at the code here: @@ -242,13 +244,10 @@ structure come before closer ones – `"../../utils"` comes before `"../utils"`. Perhaps surprisingly though, `".."` would come before `"../../utils"` (since shorter substrings sort before longer strings). For that reason there’s one addition to the alphabetical rule: `"."` and `".."` are treated as `"./"` and -`"../"`. Also, within the absolute imports group, imports starting with an ASCII -letter or digit come first, separating them from those starting with symbols. +`"../"`. -[webpack loader syntax] is stripped before sorting, so `"loader!a"` sorts before -`"b"`. If two sources are equal after stripping the loader syntax, the one with -loader syntax comes last. Similarly, if both `import type` _and_ regular imports -are used for the same source, the [Flow type imports] come first. +If both `import type` _and_ regular imports are used for the same source, the +[Flow type imports] come first. Example: @@ -263,9 +262,9 @@ import "./global.css"; import type A from "an-npm-package"; import a from "an-npm-package"; import fs from "fs"; - -// Absolute imports, full URLs and other imports. import b from "https://example.com/script.js"; + +// Absolute imports and other imports. import Error from "@/components/error.vue"; import c from "/"; import d from "/home/user/foo"; @@ -278,7 +277,6 @@ import typeof C from "../types"; import g from "."; import h from "./constants"; import i from "./styles"; -import j from "html-loader!./text.html"; // Regardless of group, imported items are sorted like this: import { @@ -304,6 +302,97 @@ Workaround to make the next section to appear in the table of contents. ``` --> +## Custom grouping + +For a long time, this plugin used to have no options, which helped keeping it +simple. + +While the human alphabetical sorting and comment handling seems to work for a +lot of people, grouping of imports is more difficult. Projects differ too much +to have a one-size-fits-all grouping. + +However, the default grouping is fine for many use cases! Don’t bother learning +how custom grouping works unless you _really_ need it. + +> If you’re looking at custom grouping because you want to move `src/Button`, +> `@company/Button` and similar – also consider using names that do not look +> like npm packages, such as `@/Button` and `~company/Button`. Then you won’t +> need to customize the grouping at all, and as a bonus things might be less +> confusing for other people working on the code base. +> +> In the future, it would be cool if the plugin could automatically detect your +> “first party”/“absolute” imports for TypeScript projects by reading your +> tsconfig.json – see [issue #31]. + +There is **one** option (and I would really like it to stay that way!) called +`groups` that allows you to: + +- Move `src/Button`, `@company/Button` and similar out of the (third party) + “packages” group, into their own group. +- Move `react` first. +- Remove blank lines between groups. +- Make a separate group for Node.js builtins. +- Make a separate group for style imports. +- Separate `./` and `../` imports. +- Not use groups at all and only sort alphabetically. + +`groups` is an array of arrays of strings: + +```ts +type Options = { + groups: Array>; +}; +``` + +Each string is a regex (with the `u` flag) and defines a group. (Remember to +escape backslashes – it’s `"\\w"`, not `"\w"`, for example.) + +Each `import` is matched against _all_ regexes on the `from` string. The import +ends up in the group with **the longest match.** In case of a tie, the first +matching group wins. + +> If an import ends up in the wrong group – try making the desired group regex +> match more of the `from` string, or use negative lookahead (`(?!x)`) to +> exclude things from other groups. + +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"`. + +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 sorted too – but will keep their internal order. It’s recommended to +keep side effect imports in their own group. + +These are the default groups: + +```js +[ + // Side effect imports. + ["^\\u0000"], + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + ["^@?\\w"], + // Absolute imports and other imports such as Vue-style `@/foo`. + // Anything that does not start with a dot. + ["^[^.]"], + // Relative imports. + // Anything that starts with a dot. + ["^\\."], +]; +``` + +The astute reader might notice that the above regexes match more than their +comments say. For example, `"@config"` and `"_internal"` are matched as +packages, but none of them are valid npm package names. `".foo"` is matched as a +relative import, but what does `".foo"` even mean? There’s little gain in having +more specific rules, though. So keep it simple! + +See the [examples] for inspiration. + ## Comment and whitespace handling When an import is moved through sorting, it’s comments are moved with it. @@ -332,11 +421,11 @@ import b from "b"; // b2 // comment before import chunk // a1 /* a2 - */ import a /* a3 */ from "a"; /* a4 */ + */ import a /* a3 */ from "a"; /* a4 */ // b1 import b from "b"; // b2 /* c1 */ import c from "c"; // c2 - /* not-a +/* not-a */ // comment after import chunk ``` @@ -574,6 +663,7 @@ You can need [Node.js] 10 and npm 6. [@typescript-eslint/parser]: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser [babel-eslint]: https://github.com/babel/babel-eslint [comment-handling]: #comment-and-whitespace-handling +[custom grouping]: #custom-grouping [doctoc]: https://github.com/thlorenz/doctoc/ [eslint-fix]: https://eslint.org/docs/user-guide/command-line-interface#--fix [eslint-plugin-import]: https://github.com/benmosher/eslint-plugin-import/ @@ -589,6 +679,7 @@ You can need [Node.js] 10 and npm 6. [import/no-duplicates]: https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md [import/order]: https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/order.md [intl.collator]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator +[issue #31]: https://github.com/lydell/eslint-plugin-simple-import-sort/issues/31 [jest]: https://jestjs.io/ [lines-around-comment]: https://eslint.org/docs/rules/lines-around-comment [no-require]: #does-it-support-require @@ -604,5 +695,4 @@ You can need [Node.js] 10 and npm 6. [travis-badge]: https://travis-ci.com/lydell/eslint-plugin-simple-import-sort.svg?branch=master [travis-link]: https://travis-ci.com/lydell/eslint-plugin-simple-import-sort [typescript]: https://www.typescriptlang.org/ -[webpack loader syntax]: https://webpack.js.org/concepts/loaders/#inline diff --git a/examples/.eslintrc.js b/examples/.eslintrc.js index 3608073..3a118b9 100644 --- a/examples/.eslintrc.js +++ b/examples/.eslintrc.js @@ -53,7 +53,7 @@ module.exports = { rules: { "import/first": "error", "import/newline-after-import": "error", - "import/no-duplicates": "error" + "import/no-duplicates": "error", }, }, { @@ -65,6 +65,59 @@ module.exports = { "import/no-duplicates": "error", }, }, + { + files: ["groups.custom.js"], + rules: { + sort: [ + "error", + { + groups: [ + // Node.js builtins. You could also generate this regex if you use a `.js` config. + // For example: `^(${require("module").builtinModules.join("|")})(/|$)` + [ + "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)", + ], + // Packages. `react` related packages come first. + ["^react", "^@?\\w"], + // Internal packages. + ["^(@|@company|@ui|components|utils|config|vendored-lib)(/.*|$)"], + // Side effect imports. + ["^\\u0000"], + // Parent imports. Put `..` last. + ["^\\.\\.(?!/?$)", "^\\.\\./?$"], + // Other relative imports. Put same-folder imports and `.` last. + ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], + // Style imports. + ["^.+\\.s?css$"], + ], + }, + ], + }, + }, + { + files: ["groups.no-blank-lines.js"], + rules: { + sort: [ + "error", + { + // The default grouping, but with no blank lines. + groups: [["^\\u0000", "^@?\\w", "^[^.]", "^\\."]], + }, + ], + }, + }, + { + files: ["groups.none.js"], + rules: { + sort: [ + "error", + { + // No grouping, only alphabetical sorting. + groups: [], + }, + ], + }, + }, { // These files are used in README.md. files: ["readme-*.js"], diff --git a/examples/groups.custom.js b/examples/groups.custom.js new file mode 100644 index 0000000..d6370ac --- /dev/null +++ b/examples/groups.custom.js @@ -0,0 +1,26 @@ +import fs from "fs"; +import assert from "assert"; +import react from "react"; +import Select from "react-select" +import _ from "lodash" +import {providers} from "./providers" +import tomorrow from "./time/tomorrow" +import now from "./time/now" +import {PRODUCT_NAMES} from "." +import {CATEGORIES} from "../" +import {truncate, removeWhitespace} from "../../utils"; +import cssGlobals from "../../css/globals" +import styles from "./styles.scss"; +import circleSyles from "./circle.scss"; +import "./global.scss" +import {name} from "@company/config" +import Button from "@ui/Button"; +import Label from "components/Label" +import {pluralize} from "utils/string" +import {API_URL} from "config"; +import VendoredLib from "vendored-lib" +import createWrapper from "vendored-lib/lib/create-wrapper" +import Textarea from "@/ui/Textarea" +import "./local-polyfill" +import "polyfill-package" +import "../../alert.css" diff --git a/examples/groups.no-blank-lines.js b/examples/groups.no-blank-lines.js new file mode 100644 index 0000000..c29336c --- /dev/null +++ b/examples/groups.no-blank-lines.js @@ -0,0 +1,10 @@ +import React from "react"; +import Button from "../Button"; + +import styles from "./styles.css"; +import { User } from "../../types"; +import { getUser } from "../../api"; + +import PropTypes from "prop-types"; +import classnames from "classnames"; +import { truncate, formatNumber } from "../../utils"; diff --git a/examples/groups.none.js b/examples/groups.none.js new file mode 100644 index 0000000..15ab067 --- /dev/null +++ b/examples/groups.none.js @@ -0,0 +1,5 @@ +import react from "react"; +import { storiesOf } from "@storybook/react"; +import App from "@/App"; +import styles from "./styles"; +import config from "/config"; diff --git a/examples/readme-order.js b/examples/readme-order.js index 34a2d73..8b62b54 100644 --- a/examples/readme-order.js +++ b/examples/readme-order.js @@ -7,9 +7,9 @@ import "./global.css"; import type A from "an-npm-package"; import a from "an-npm-package"; import fs from "fs"; - -// Absolute imports, full URLs and other imports. import b from "https://example.com/script.js"; + +// Absolute imports and other imports. import Error from "@/components/error.vue" import c from "/"; import d from "/home/user/foo"; @@ -22,7 +22,6 @@ import typeof C from "../types"; import g from "."; import h from "./constants"; import i from "./styles"; -import j from "html-loader!./text.html"; // Regardless of group, imported items are sorted like this: import { diff --git a/package-lock.json b/package-lock.json index 6c10603..40f0de7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -529,51 +529,57 @@ "dev": true }, "@typescript-eslint/experimental-utils": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.6.0.tgz", - "integrity": "sha512-34BAFpNOwHXeqT+AvdalLxOvcPYnCxA5JGmBAFL64RGMdP0u65rXjii7l/nwpgk5aLEE1LaqF+SsCU0/Cb64xA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.8.0.tgz", + "integrity": "sha512-jZ05E4SxCbbXseQGXOKf3ESKcsGxT8Ucpkp1jiVp55MGhOvZB2twmWKf894PAuVQTCgbPbJz9ZbRDqtUWzP8xA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.6.0", + "@typescript-eslint/typescript-estree": "2.8.0", "eslint-scope": "^5.0.0" - }, - "dependencies": { - "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - } } }, "@typescript-eslint/parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.6.0.tgz", - "integrity": "sha512-AvLejMmkcjRTJ2KD72v565W4slSrrzUIzkReu1JN34b8JnsEsxx7S9Xx/qXEuMQas0mkdUfETr0j3zOhq2DIqQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.8.0.tgz", + "integrity": "sha512-NseXWzhkucq+JM2HgqAAoKEzGQMb5LuTRjFPLQzGIdLthXMNUfuiskbl7QSykvWW6mvzCtYbw1fYWGa2EIaekw==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.6.0", - "@typescript-eslint/typescript-estree": "2.6.0", + "@typescript-eslint/experimental-utils": "2.8.0", + "@typescript-eslint/typescript-estree": "2.8.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.6.0.tgz", - "integrity": "sha512-A3lSBVIdj2Gp0lFEL6in2eSPqJ33uAc3Ko+Y4brhjkxzjbzLnwBH22CwsW2sCo+iwogfIyvb56/AJri15H0u5Q==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.8.0.tgz", + "integrity": "sha512-ksvjBDTdbAQ04cR5JyFSDX113k66FxH1tAXmi+dj6hufsl/G0eMc/f1GgLjEVPkYClDbRKv+rnBFuE5EusomUw==", "dev": true, "requires": { "debug": "^4.1.1", - "glob": "^7.1.4", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", "is-glob": "^4.0.1", "lodash.unescape": "4.0.1", - "semver": "^6.3.0" + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "abab": { @@ -607,9 +613,9 @@ } }, "acorn-jsx": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", - "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", "dev": true }, "acorn-walk": { @@ -1040,11 +1046,6 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, - "builtins": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", - "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" - }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -1834,9 +1835,9 @@ } }, "eslint-plugin-jest": { - "version": "23.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.0.2.tgz", - "integrity": "sha512-fkxcvOJm0hC/jbJqYJjtuC9mvpTJqXd0Nixx7joVQvJoBQuXk/ws3+MtRYzD/4TcKSgvr21uuSLdwSxKJKC2cg==", + "version": "23.0.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz", + "integrity": "sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw==", "dev": true, "requires": { "@typescript-eslint/experimental-utils": "^2.5.0" @@ -1863,18 +1864,18 @@ } }, "eslint-plugin-vue": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-5.2.3.tgz", - "integrity": "sha512-mGwMqbbJf0+VvpGR5Lllq0PMxvTdrZ/ZPjmhkacrCHbubJeJOt+T6E3HUzAifa2Mxi7RSdJfC9HFpOeSYVMMIw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.0.1.tgz", + "integrity": "sha512-5tgFPcxGDKjfVB/6Yi56bKiWxygUibfZmzSh26Np3kuwAk/lfaGbVld+Yt+MPgD84ppvcachtiL4/winsXLjXA==", "dev": true, "requires": { - "vue-eslint-parser": "^5.0.0" + "vue-eslint-parser": "^6.0.5" } }, "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", "dev": true, "requires": { "esrecurse": "^4.1.0", @@ -2963,9 +2964,9 @@ "dev": true }, "handlebars": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.5.tgz", - "integrity": "sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4842,9 +4843,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, "prettier-linter-helpers": { @@ -6028,6 +6029,15 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -6059,15 +6069,15 @@ "dev": true }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.2.tgz", + "integrity": "sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==", "dev": true }, "uglify-js": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.4.tgz", - "integrity": "sha512-9Yc2i881pF4BPGhjteCXQNaXx1DCwm3dtOyBaG2hitHjLWOczw/ki8vD1bqyT3u6K0Ms/FpCShkmfg+FtlOfYA==", + "version": "3.6.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.9.tgz", + "integrity": "sha512-pcnnhaoG6RtrvHJ1dFncAe8Od6Nuy30oaJ82ts6//sGSXOP5UjBMEthiProjXmMNHOfd93sqlkztifFMcb+4yw==", "dev": true, "optional": true, "requires": { @@ -6270,14 +6280,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "validate-npm-package-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", - "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", - "requires": { - "builtins": "^1.0.3" - } - }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -6317,15 +6319,15 @@ } }, "vue-eslint-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz", - "integrity": "sha512-JlHVZwBBTNVvzmifwjpZYn0oPWH2SgWv5dojlZBsrhablDu95VFD+hriB1rQGwbD+bms6g+rAFhQHk6+NyiS6g==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-6.0.5.tgz", + "integrity": "sha512-Bvjlx7rH1Ulvus56KHeLXOjEi3JMOYTa1GAqZr9lBQhd8weK8mV7U7V2l85yokBZEWHJQjLn6X3nosY8TzkOKg==", "dev": true, "requires": { - "debug": "^4.1.0", + "debug": "^4.1.1", "eslint-scope": "^4.0.0", "eslint-visitor-keys": "^1.0.0", - "espree": "^4.1.0", + "espree": "^5.0.0", "esquery": "^1.0.1", "lodash": "^4.17.11" }, @@ -6336,13 +6338,23 @@ "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==", "dev": true }, + "eslint-scope": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", + "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, "espree": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-4.1.0.tgz", - "integrity": "sha512-I5BycZW6FCVIub93TeVY1s7vjhP9CY6cXCznIRfiig7nRviKZYdRnj/sHEWC6A7WE9RDWOFq9+7OsWSYz8qv2w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", + "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", "dev": true, "requires": { - "acorn": "^6.0.2", + "acorn": "^6.0.7", "acorn-jsx": "^5.0.0", "eslint-visitor-keys": "^1.0.0" } diff --git a/package.json b/package.json index c02877b..abffc33 100644 --- a/package.json +++ b/package.json @@ -31,24 +31,21 @@ "test": "npm run eslint && npm run coverage", "prepublishOnly": "npm test" }, - "dependencies": { - "validate-npm-package-name": "^3.0.0" - }, "devDependencies": { - "@typescript-eslint/parser": "2.6.0", + "@typescript-eslint/parser": "2.8.0", "babel-eslint": "10.0.3", "cross-spawn": "7.0.1", "doctoc": "1.4.0", "eslint": "6.6.0", "eslint-config-lydell": "14.0.0", "eslint-plugin-import": "2.18.2", - "eslint-plugin-jest": "23.0.2", + "eslint-plugin-jest": "23.0.4", "eslint-plugin-markdown": "1.0.1", "eslint-plugin-prettier": "3.1.1", - "eslint-plugin-vue": "5.2.3", + "eslint-plugin-vue": "6.0.1", "jest": "24.9.0", - "prettier": "1.18.2", - "typescript": "3.6.4" + "prettier": "1.19.1", + "typescript": "3.7.2" }, "peerDependencies": { "eslint": ">=5.0.0" diff --git a/src/sort.js b/src/sort.js index 6a592db..d4a56c8 100644 --- a/src/sort.js +++ b/src/sort.js @@ -1,12 +1,40 @@ "use strict"; -const validateNpmPackageName = require("validate-npm-package-name"); +const defaultGroups = [ + // Side effect imports. + ["^\\u0000"], + // Packages. + // Things that start with a letter (or digit or underscore), or `@` followed by a letter. + ["^@?\\w"], + // Absolute imports and other imports such as Vue-style `@/foo`. + // Anything that does not start with a dot. + ["^[^.]"], + // Relative imports. + // Anything that starts with a dot. + ["^\\."], +]; module.exports = { meta: { type: "layout", fixable: "code", - schema: [], + schema: [ + { + type: "object", + properties: { + groups: { + type: "array", + items: { + type: "array", + items: { + type: "string", + }, + }, + }, + }, + additionalProperties: false, + }, + ], docs: { url: "https://github.com/lydell/eslint-plugin-simple-import-sort#sort-order", @@ -16,10 +44,14 @@ module.exports = { }, }, create: context => { + const { groups: rawGroups = defaultGroups } = context.options[0] || {}; + const outerGroups = rawGroups.map(groups => + groups.map(item => RegExp(item, "u")) + ); return { Program: node => { for (const imports of extractImportChunks(node)) { - maybeReportSorting(imports, context); + maybeReportSorting(imports, context, outerGroups); } }, }; @@ -48,10 +80,10 @@ function extractImportChunks(programNode) { return chunks; } -function maybeReportSorting(imports, context) { +function maybeReportSorting(imports, context, outerGroups) { const sourceCode = context.getSourceCode(); const items = getImportItems(imports, sourceCode); - const sorted = printSortedImports(items, sourceCode); + const sorted = printSortedImports(items, sourceCode, outerGroups); const { start } = items[0]; const { end } = items[items.length - 1]; @@ -69,42 +101,54 @@ function maybeReportSorting(imports, context) { } } -function printSortedImports(importItems, sourceCode) { - const sideEffectImports = []; - const packageImports = []; - const relativeImports = []; - const restImports = []; +function printSortedImports(importItems, sourceCode, outerGroups) { + const itemGroups = outerGroups.map(groups => + groups.map(regex => ({ regex, items: [] })) + ); + const rest = []; for (const item of importItems) { - if (item.group === "sideEffect") { - sideEffectImports.push(item); - } else if (item.group === "package") { - packageImports.push(item); - } else if (item.group === "relative") { - relativeImports.push(item); + const { originalSource } = item.source; + const source = item.isSideEffectImport + ? `\0${originalSource}` + : originalSource; + const [matchedGroup] = flatMap(itemGroups, groups => + groups.map(group => [group, group.regex.exec(source)]) + ).reduce( + ([group, longestMatch], [nextGroup, nextMatch]) => + nextMatch != null && + (longestMatch == null || nextMatch[0].length > longestMatch[0].length) + ? [nextGroup, nextMatch] + : [group, longestMatch], + [undefined, undefined] + ); + if (matchedGroup == null) { + rest.push(item); } else { - restImports.push(item); + matchedGroup.items.push(item); } } - const sortedItems = [ - sideEffectImports, - sortImportItems(packageImports), - sortImportItems(restImports), - sortImportItems(relativeImports), - ]; + const sortedItems = itemGroups + .concat([[{ regex: /^/, items: rest }]]) + .map(groups => groups.filter(group => group.items.length > 0)) + .filter(groups => groups.length > 0) + .map(groups => groups.map(group => sortImportItems(group.items))); const newline = guessNewline(sourceCode); const sorted = sortedItems - .filter(items => items.length > 0) - .map(items => items.map(item => item.code).join(newline)) + .map(groups => + groups + .map(groupItems => groupItems.map(item => item.code).join(newline)) + .join(newline) + ) .join(newline + newline); // Edge case: If the last import (after sorting) ends with a line comment and // there’s code (or a multiline block comment) on the same line, add a newline // so we don’t accidentally comment stuff out. - const flattened = [].concat(...sortedItems); + const flattened = flatMap(sortedItems, groups => [].concat(...groups)); const lastSortedItem = flattened[flattened.length - 1]; const lastOriginalItem = importItems[importItems.length - 1]; const nextToken = lastSortedItem.needsNewline @@ -192,14 +236,14 @@ function getImportItems(passedImports, sourceCode) { const [start] = all[0].range; const [, end] = all[all.length - 1].range; - const { group, source } = getGroupAndSource(importNode, sourceCode); + const source = getSource(importNode); return { node: importNode, code, start: start - indentation.length, end: end + trailingSpaces.length, - group, + isSideEffectImport: isSideEffectImport(importNode, sourceCode), source, index: importIndex, needsNewline: @@ -729,21 +773,21 @@ function getTrailingSpaces(node, sourceCode) { } function sortImportItems(items) { - return items.slice().sort( - (itemA, itemB) => - // First compare the `from` part, excluding webpack loader syntax. - compare(itemA.source.source, itemB.source.source) || - // The `.source` has been slightly tweaked. To stay fully deterministic, - // also sort on the original value. - compare(itemA.source.originalSource, itemB.source.originalSource) || - // Then put Flow type imports before regular ones. - compare(itemA.source.importKind, itemB.source.importKind) || - // Then sort by webpack loader syntax (no loaders comes first). - compare(itemA.source.webpack, itemB.source.webpack) || - // Keep the original order if the sources are the same. It's not worth - // trying to compare anything else, and you can use `import/no-duplicates` - // to get rid of the problem anyway. - itemA.index - itemB.index + return items.slice().sort((itemA, itemB) => + // If both items are side effect imports, keep their original order. + itemA.isSideEffectImport && itemB.isSideEffectImport + ? 0 + : // First compare the `from` part. + compare(itemA.source.source, itemB.source.source) || + // The `.source` has been slightly tweaked. To stay fully deterministic, + // also sort on the original value. + compare(itemA.source.originalSource, itemB.source.originalSource) || + // Then put Flow type imports before regular ones. + compare(itemA.source.importKind, itemB.source.importKind) || + // Keep the original order if the sources are the same. It's not worth + // trying to compare anything else, and you can use `import/no-duplicates` + // to get rid of the problem anyway. + itemA.index - itemB.index ); } @@ -795,36 +839,6 @@ function isSideEffectImport(importNode, sourceCode) { ); } -const PACKAGE_REGEX = /^(?:@[^/]+\/)?[^/]+/; - -// import fs from "fs"; -// import { compose } from "lodash/fp"; -// import { storiesOf } from '@storybook/react'; -function isPackageImport(source) { - const match = PACKAGE_REGEX.exec(source); - if (match == null) { - return false; - } - const { errors = [], warnings = [] } = validateNpmPackageName(match[0]); - return ( - errors.length === 0 && - warnings.filter(warning => !warning.includes("core module")).length === 0 - ); -} - -// import a from "." -// import a from "./x" -// import a from ".." -// import a from "../x" -function isRelativeImport(source) { - return ( - source === "." || - source === ".." || - source.startsWith("./") || - source.startsWith("../") - ); -} - function isIdentifier(node) { return node.type === "Identifier"; } @@ -849,51 +863,21 @@ function isNewline(node) { return node.type === "Newline"; } -// Parse the `from` part of an import, stripping away webpack loader syntax: -// -// import x from "webpack-loader!./index.js" -// ^^^^^^^^^^ -// Loader syntax documentation: https://webpack.js.org/concepts/loaders/#inline -function getGroupAndSource(importNode, sourceCode) { - const rawSource = importNode.source.value; - const index = rawSource.lastIndexOf("!"); - const [source, webpack] = - index >= 0 - ? [rawSource.slice(index + 1), rawSource.slice(0, index + 1)] - : [rawSource, ""]; - const group = isSideEffectImport(importNode, sourceCode) - ? "sideEffect" - : isPackageImport(source) - ? "package" - : isRelativeImport(source) - ? "relative" - : "rest"; +function getSource(importNode) { + const source = importNode.source.value; return { - group, - source: { - source: - group === "relative" - ? // Due to "." sorting before "/" by default, relative imports are - // automatically sorted in a logical manner for us: Imports from files - // further up come first, with deeper imports last. There’s one - // exception, though: When the `from` part ends with one or two dots: - // "." and "..". Those are supposed to sort just like "./", "../". So - // add in the slash for them. (No special handling is done for cases - // like "./a/.." because nobody writes that anyway.) - source === "." || source === ".." - ? `${source}/` - : source - : group === "rest" - ? // This makes ASCII letters and digits sort first. When using - // `Intl.Collator`, `\t` followed by a letter sorts first (while `\0` - // followed by a letter comes a lot later!). - source.replace(/^[a-z\d]/i, "\t$&") - : source, - originalSource: source, - importKind: getImportKind(importNode), - webpack, - }, + source: + // Due to "." sorting before "/" by default, relative imports are + // automatically sorted in a logical manner for us: Imports from files + // further up come first, with deeper imports last. There’s one + // exception, though: When the `from` part ends with one or two dots: + // "." and "..". Those are supposed to sort just like "./", "../". So + // add in the slash for them. (No special handling is done for cases + // like "./a/.." because nobody writes that anyway.) + source === "." || source === ".." ? `${source}/` : source, + originalSource: source, + importKind: getImportKind(importNode), }; } diff --git a/test/__snapshots__/examples.test.js.snap b/test/__snapshots__/examples.test.js.snap index d8a9aac..7cd999d 100644 --- a/test/__snapshots__/examples.test.js.snap +++ b/test/__snapshots__/examples.test.js.snap @@ -65,6 +65,63 @@ bar(); `; +exports[`examples groups.custom.js 1`] = ` +import assert from "assert"; +import fs from "fs"; + +import react from "react"; +import Select from "react-select" +import _ from "lodash" + +import Textarea from "@/ui/Textarea" +import {name} from "@company/config" +import Button from "@ui/Button"; +import Label from "components/Label" +import {API_URL} from "config"; +import {pluralize} from "utils/string" +import VendoredLib from "vendored-lib" +import createWrapper from "vendored-lib/lib/create-wrapper" + +import "./local-polyfill" +import "polyfill-package" + +import cssGlobals from "../../css/globals" +import {removeWhitespace,truncate} from "../../utils"; +import {CATEGORIES} from "../" + +import now from "./time/now" +import tomorrow from "./time/tomorrow" +import {providers} from "./providers" +import {PRODUCT_NAMES} from "." + +import circleSyles from "./circle.scss"; +import "./global.scss" +import "../../alert.css" +import styles from "./styles.scss"; + +`; + +exports[`examples groups.no-blank-lines.js 1`] = ` +import classnames from "classnames"; +import PropTypes from "prop-types"; +import React from "react"; +import { getUser } from "../../api"; +import { User } from "../../types"; +import { formatNumber,truncate } from "../../utils"; +import Button from "../Button"; +import styles from "./styles.css"; + +`; + +exports[`examples groups.none.js 1`] = ` +import styles from "./styles"; +import App from "@/App"; +import { storiesOf } from "@storybook/react"; +import config from "/config"; +import react from "react"; + +`; + 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: @@ -213,9 +270,9 @@ import "./global.css"; import type A from "an-npm-package"; import a from "an-npm-package"; import fs from "fs"; - -// Absolute imports, full URLs and other imports. import b from "https://example.com/script.js"; + +// Absolute imports and other imports. import Error from "@/components/error.vue"; import c from "/"; import d from "/home/user/foo"; @@ -228,7 +285,6 @@ import typeof C from "../types"; import g from "."; import h from "./constants"; import i from "./styles"; -import j from "html-loader!./text.html"; // Regardless of group, imported items are sorted like this: import { // First, Flow type imports. diff --git a/test/sort.test.js b/test/sort.test.js index 618d3c3..1d5174a 100644 --- a/test/sort.test.js +++ b/test/sort.test.js @@ -818,39 +818,11 @@ const baseTests = expect => ({ errors: 1, }, - // Webpack loader syntax - { - code: input` - |import x1 from "webpack-loader!b" - |import x2 from "webpack-loader!./c" - |import x3 from "webpack-loader!/d" - |import x4 from 'loader1!loader2?query!loader3?{"key":"value!"}!a' - |import x5 from "webpack-loader!b" - |import x6 from "other-loader!b" - |import x7 from "b" - `, - output: actual => { - expect(actual).toMatchInlineSnapshot(` - |import x4 from 'loader1!loader2?query!loader3?{"key":"value!"}!a' - |import x7 from "b" - |import x6 from "other-loader!b" - |import x1 from "webpack-loader!b" - |import x5 from "webpack-loader!b" - | - |import x3 from "webpack-loader!/d" - | - |import x2 from "webpack-loader!./c" - `); - }, - errors: 1, - }, - // Special characters sorting order. { code: input` |import {} from ""; |import {} from "."; - |import {} from "loader!."; |import {} from ".//"; |import {} from "./"; |import {} from "./B"; // B1 @@ -905,21 +877,17 @@ const baseTests = expect => ({ |import {} from "@storybook/react"; |import {} from "@storybook/react/something"; |import {} from "1"; + |import {} from "1*"; + |import {} from "a*"; |import {} from "async"; + |import {} from "Fs"; |import {} from "fs"; |import {} from "fs/something"; + |import {} from "http://example.com/script.js"; + |import {} from "https://example.com/script.js"; |import {} from "lodash/fp"; |import {} from "react"; | - |import {} from ""; - |import {} from "1*"; - |import {} from "a*"; - |import {} from "Fs"; - |import {} from "http://example.com/script.js"; - |import {} from "https://example.com/script.js"; - |import {} from "..."; - |import {} from ".../"; - |import {} from ".a"; |import {} from "@/components/Alert" |import {} from "@/components/error.vue" |import {} from "/"; @@ -928,6 +896,8 @@ const baseTests = expect => ({ |import {} from "#/test" |import {} from "~/test" | + |import {} from "..."; + |import {} from ".../"; |import {} from ".."; |import {} from "../"; |import {} from "../.."; @@ -939,7 +909,6 @@ const baseTests = expect => ({ |import {} from "../a/../"; |import {} from "../a/../b"; |import {} from "."; - |import {} from "loader!."; |import {} from "./"; |import {} from ".//"; |import {} from "./A"; @@ -955,6 +924,9 @@ const baseTests = expect => ({ |import img1 from "./img1"; |import img2 from "./img2"; |import img10 from "./img10"; + |import {} from ".a"; + | + |import {} from ""; `); }, errors: 1, @@ -1082,7 +1054,7 @@ const baseTests = expect => ({ |// a | |import a from "a" -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` |// a @@ -1108,7 +1080,7 @@ const baseTests = expect => ({ |\r |import a from "a"\r |after();\r -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` |// a @@ -1181,7 +1153,7 @@ const baseTests = expect => ({ |// final | |; -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` |import @@ -1224,7 +1196,7 @@ const baseTests = expect => ({ |import { | | } from "specifiers-empty" -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` |import { @@ -1242,7 +1214,7 @@ const baseTests = expect => ({ | | b // b | ,a} from "specifiers-line-comment" -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` |import { @@ -1266,7 +1238,7 @@ const baseTests = expect => ({ | | // d | import d from "d" -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` | /* a */ import a from "a"; @@ -1299,7 +1271,7 @@ const baseTests = expect => ({ | // d\r | import d from "d"\r | -`, + `, output: actual => { expect(actual).toMatchInlineSnapshot(` | @@ -1459,6 +1431,141 @@ const baseTests = expect => ({ }, errors: 1, }, + + // `groups` – `u` flag. + { + options: [{ groups: [["^\\p{L}"], ["^\\."]] }], + code: input` + |import b from '.'; + |import a from 'ä'; + `, + output: actual => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'ä'; + | + |import b from '.'; + `); + }, + errors: 1, + }, + + // `groups` – non-matching imports end up last. + { + options: [{ groups: [["^\\w"], ["^\\."]] }], + code: input` + |import c from ''; + |import b from '.'; + |import a from 'a'; + |import d from '@/a'; + `, + output: actual => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'a'; + | + |import b from '.'; + | + |import c from ''; + |import d from '@/a'; + `); + }, + errors: 1, + }, + + // `groups` – first longest match wins. + { + options: [{ groups: [["^\\w"], ["^\\w{2}"], ["^.{2}"]] }], + code: input` + |import c from './'; + |import b from 'bx'; + |import a from 'a'; + `, + output: actual => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'a'; + | + |import b from 'bx'; + | + |import c from './'; + `); + }, + errors: 1, + }, + + // `groups` – side effect imports. + { + options: [{ groups: [["^\\w"], ["^\\."], ["^\\u0000"]] }], + code: input` + |import '@/'; + |import c from '@/'; + |import b from './'; + |import './'; + |import a from 'a'; + |import 'a'; + |import {} from 'a'; + `, + output: actual => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'a'; + |import {} from 'a'; + | + |import b from './'; + | + |import '@/'; + |import './'; + |import 'a'; + | + |import c from '@/'; + `); + }, + errors: 1, + }, + + // `groups` – side effect imports keep internal order but are sorted otherwise. + { + options: [{ groups: [] }], + code: input` + |import b from 'b'; + |import 'c'; + |import d from 'd'; + |import 'a'; + |import '.'; + |import x from './x'; + `, + output: actual => { + expect(actual).toMatchInlineSnapshot(` + |import x from './x'; + |import b from 'b'; + |import 'c'; + |import 'a'; + |import '.'; + |import d from 'd'; + `); + }, + errors: 1, + }, + + // `groups` – no line breaks between inner array items. + { + options: [{ groups: [["^\\w", "^react"], ["^\\."]] }], + code: input` + |import react from 'react'; + |import a from 'a'; + |import webpack from "webpack" + |import Select from 'react-select'; + |import App from './App'; + `, + output: actual => { + expect(actual).toMatchInlineSnapshot(` + |import a from 'a'; + |import webpack from "webpack" + |import react from 'react'; + |import Select from 'react-select'; + | + |import App from './App'; + `); + }, + errors: 1, + }, ], }); @@ -1515,11 +1622,11 @@ const flowTests = { expect(actual).toMatchInlineSnapshot(` |import './global.css'; | - |import react from "react" - | |import typeof A from "A"; + |import react from "react" |import type {X} from "X"; |import type {Z} from "Z"; + | |import type E from "@/B"; |import type C from "/B"; | @@ -1531,36 +1638,6 @@ const flowTests = { errors: 1, }, - // All at once. - { - code: input` - |import A from "webpack!a"; - |import B from "webpack!./a"; - |import type C from "a"; - |import D from "a"; - |import typeof E from "a"; - |import type F from "flow!./a"; - |import type G from "flow!a"; - |import typeof H from "a"; - |import type I from "./a"; - `, - output: actual => { - expect(actual).toMatchInlineSnapshot(` - |import type C from "a"; - |import type G from "flow!a"; - |import typeof E from "a"; - |import typeof H from "a"; - |import D from "a"; - |import A from "webpack!a"; - | - |import type I from "./a"; - |import type F from "flow!./a"; - |import B from "webpack!./a"; - `); - }, - errors: 1, - }, - // https://github.com/graphql/graphql-js/blob/64b194c6c9b9aaa1c139f1b7c3692a6ef851928e/src/execution/execute.js#L10-L69 { code: input`