├── .all-contributorsrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierrc ├── .releaserc ├── BREAKING_CHANGES.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __tests__ ├── get-package-file-deps.spec.ts ├── get-package-lock-file-deps.spec.ts ├── merge-import-statements.spec.ts ├── optimize-imports-mocks.ts └── optimize-imports.spec.ts ├── assets ├── demo.gif └── logo.png ├── commitlint.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── playground └── sampleDir │ ├── bar │ ├── bar.ts │ └── quux │ │ └── quux.ts │ ├── baz │ ├── baz.ts │ └── qux │ │ └── qux.ts │ ├── foo │ └── foo.ts │ └── main.ts ├── scripts ├── hooks │ └── pre-commit.js ├── post-build.js └── release.js ├── src ├── cliOptions.ts ├── conductor │ ├── categorize-imports.ts │ ├── collect-import-nodes.ts │ ├── collect-non-import-nodes.ts │ ├── conduct.ts │ ├── format-import-statements.ts │ ├── get-files-paths.ts │ ├── get-group-order.ts │ ├── get-import-statement-map.ts │ ├── get-third-party.ts │ ├── merge-import-statements.ts │ ├── organize-imports.ts │ └── sort-import-categories.ts ├── config.ts ├── defaultConfig.ts ├── helpers │ ├── get-lock-file-version.ts │ ├── get-package-file-deps.ts │ ├── get-package-lock-file-deps.ts │ ├── is-custom-import.ts │ ├── is-third-party.ts │ ├── line-ending-detector.ts │ ├── log.ts │ └── parse-json-file.ts ├── index.ts ├── pollyfils.ts ├── types.ts └── version.ts ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "import-conductor", 3 | "projectOwner": "kreuzerk", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "kreuzerk", 15 | "name": "Kevin Kreuzer", 16 | "avatar_url": "https://avatars0.githubusercontent.com/u/5468954?v=4", 17 | "profile": "https://medium.com/@kevinkreuzer", 18 | "contributions": [ 19 | "code", 20 | "design", 21 | "doc", 22 | "ideas", 23 | "infra", 24 | "maintenance", 25 | "test" 26 | ] 27 | }, 28 | { 29 | "login": "shaharkazaz", 30 | "name": "Shahar Kazaz", 31 | "avatar_url": "https://avatars2.githubusercontent.com/u/17194830?v=4", 32 | "profile": "https://github.com/shaharkazaz", 33 | "contributions": [ 34 | "code", 35 | "doc", 36 | "ideas", 37 | "infra", 38 | "maintenance", 39 | "test" 40 | ] 41 | }, 42 | { 43 | "login": "laurenzcodes", 44 | "name": "Robert Laurenz", 45 | "avatar_url": "https://avatars1.githubusercontent.com/u/8169746?v=4", 46 | "profile": "https://github.com/laurenzcodes", 47 | "contributions": [ 48 | "doc" 49 | ] 50 | }, 51 | { 52 | "login": "Lonli-Lokli", 53 | "name": "Lonli-Lokli", 54 | "avatar_url": "https://avatars.githubusercontent.com/u/767795?v=4", 55 | "profile": "https://github.com/Lonli-Lokli", 56 | "contributions": [ 57 | "code", 58 | "test", 59 | "bug" 60 | ] 61 | }, 62 | { 63 | "login": "YuriSS", 64 | "name": "Yuri Santos", 65 | "avatar_url": "https://avatars.githubusercontent.com/u/11182638?v=4", 66 | "profile": "https://github.com/YuriSS", 67 | "contributions": [ 68 | "code" 69 | ] 70 | }, 71 | { 72 | "login": "markoberholzer-es", 73 | "name": "markoberholzer-es", 74 | "avatar_url": "https://avatars.githubusercontent.com/u/64533830?v=4", 75 | "profile": "https://github.com/markoberholzer-es", 76 | "contributions": [ 77 | "code", 78 | "ideas" 79 | ] 80 | }, 81 | { 82 | "login": "mtrefzer", 83 | "name": "Michael Trefzer", 84 | "avatar_url": "https://avatars.githubusercontent.com/u/10129409?v=4", 85 | "profile": "https://github.com/mtrefzer", 86 | "contributions": [ 87 | "code" 88 | ] 89 | } 90 | ], 91 | "contributorsPerLine": 7, 92 | "skipCi": true, 93 | "commitType": "docs" 94 | } 95 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Steps to reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Desktop (please complete the following information):** 20 | - OS: [e.g. iOS] 21 | - import conductor version: [e.g. v2] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the feature** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the use case you need it for** 14 | A clear and concise description of what you need this feature for. 15 | 16 | **Additional context** 17 | Add any other context about the feature request here. 18 | 19 | **New contributors to our project are always welcome** 20 | Let us know if you want to create a pull request for this feature. In case you need help you can always reach out to us. 21 | [] I would like to create a PR 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: feature-branch 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | - '*/*' 7 | - '**' 8 | - '!master' 9 | jobs: 10 | CI: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Install Node & NPM 16 | uses: actions/setup-node@v3 17 | - name: Install node modules 18 | run: npm install 19 | - name: Test 20 | run: npm run test 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | with: 13 | persist-credentials: false 14 | - name: Install Node & NPM 15 | uses: actions/setup-node@v3 16 | - name: Install node_modules 17 | run: npm install 18 | - name: Test 19 | run: npm run test 20 | - name: Build 21 | run: npm run build 22 | - name: Release 23 | uses: cycjimmy/semantic-release-action@v3 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | .DS_Store 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | .idea 65 | dist 66 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 140, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "pkgRoot": "dist", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/changelog", 7 | "@semantic-release/npm", 8 | ["@semantic-release/exec", { 9 | "prepareCmd": "VERSION=${nextRelease.version} npm run bump-version" 10 | }], 11 | ["@semantic-release/git", { 12 | "assets": ["package.json", "CHANGELOG.md"], 13 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 14 | }], 15 | "@semantic-release/github" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /BREAKING_CHANGES.md: -------------------------------------------------------------------------------- 1 | # Import conductor V2 2 | 3 | #### Config Options Changes: 4 | 5 | - `autoMerge` option replaced with `noAutoMerge`, and the option default value is `false`. 6 | - `silent` option replaced with `verbose`, and the option default value is `false`. 7 | - The `-v` option alias now stands for the `verbose` option instead of getting the import conductor version. 8 | - The `-m` option alias (which used for the `autoMerge`) was removed. 9 | - The `-d` option alias now stands for the `dryRun` option instead of `autoAdd`. 10 | 11 | See the updated config options in [this](https://github.com/kreuzerk/import-conductor#options) section of the documentation. 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.6.1](https://github.com/kreuzerk/import-conductor/compare/v2.6.0...v2.6.1) (2023-06-01) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * using npm 9 with lockfileVersion 3 ([eddbbbb](https://github.com/kreuzerk/import-conductor/commit/eddbbbbd04c637a639c505fb38df7a8871728a14)) 7 | 8 | # [2.6.0](https://github.com/kreuzerk/import-conductor/compare/v2.5.0...v2.6.0) (2023-01-11) 9 | 10 | 11 | ### Features 12 | 13 | * switch to fast-glob for performance ([f0dc90a](https://github.com/kreuzerk/import-conductor/commit/f0dc90ad4e60405fbe5391c3c073e0fa2e55d33b)) 14 | 15 | # [2.5.0](https://github.com/kreuzerk/import-conductor/compare/v2.4.1...v2.5.0) (2022-07-01) 16 | 17 | 18 | ### Features 19 | 20 | * better command description ([6569138](https://github.com/kreuzerk/import-conductor/commit/65691388b42b50c5c11c3b6762b0719cb781099f)) 21 | * ordering by configuration ([c39c7e8](https://github.com/kreuzerk/import-conductor/commit/c39c7e854351ec4d8ab6901cb510079d20de1f8b)) 22 | * set default argument ([cd79f13](https://github.com/kreuzerk/import-conductor/commit/cd79f1307eb784b427c3c7076d9d021f7877dae1)) 23 | * teste cases ([ddda79b](https://github.com/kreuzerk/import-conductor/commit/ddda79b0273fac4a6759b4ca9b5591f7fc800ed2)) 24 | * testing no import statement ([3818e0e](https://github.com/kreuzerk/import-conductor/commit/3818e0e5054c36ef52b708b9ba07bb025d9b7260)) 25 | * updated readme with new parameter ([e711b32](https://github.com/kreuzerk/import-conductor/commit/e711b322074b44d1bb941e9833b71e9b2ec51bfd)) 26 | * validating cli argument ([40a918b](https://github.com/kreuzerk/import-conductor/commit/40a918b4409d476fe2c2a31a6e0092e665d9cd58)) 27 | 28 | ## [2.4.1](https://github.com/kreuzerk/import-conductor/compare/v2.4.0...v2.4.1) (2022-06-25) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * 🐛 veryify actions before updating a file ([667a80f](https://github.com/kreuzerk/import-conductor/commit/667a80f22ea084216c299b20a72a0cc99f2d6004)) 34 | * verifyng action before update file ([0ba0c63](https://github.com/kreuzerk/import-conductor/commit/0ba0c635493ea852bb10e8bc8dba3c71c40943b3)) 35 | 36 | # [2.4.0](https://github.com/kreuzerk/import-conductor/compare/v2.3.0...v2.4.0) (2022-04-05) 37 | 38 | 39 | ### Features 40 | 41 | * 🎸 (organize) export organize imports function ([9e9bac5](https://github.com/kreuzerk/import-conductor/commit/9e9bac5f1991813e410f321f50d2b77d4a3a202a)) 42 | 43 | # [2.3.0](https://github.com/kreuzerk/import-conductor/compare/v2.2.6...v2.3.0) (2022-04-05) 44 | 45 | 46 | ### Features 47 | 48 | * 🎸 (conduct) allow conduct usage from code ([12d83ce](https://github.com/kreuzerk/import-conductor/commit/12d83cefc169b2e21c5747b9244d5d32c5e02bba)) 49 | 50 | ## [2.2.6](https://github.com/kreuzerk/import-conductor/compare/v2.2.5...v2.2.6) (2022-04-01) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * 🐛 (vulnearbility) update simple git ([e91e4c1](https://github.com/kreuzerk/import-conductor/commit/e91e4c10025d3b344f18880c19e83d18a2026304)) 56 | 57 | ## [2.2.5](https://github.com/kreuzerk/import-conductor/compare/v2.2.4...v2.2.5) (2021-03-25) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * 🐛 remove accidental debug console.log ([3b2a742](https://github.com/kreuzerk/import-conductor/commit/3b2a742daa641e96c7272d2c23db70e3f86857e6)) 63 | 64 | ## [2.2.4](https://github.com/kreuzerk/import-conductor/compare/v2.2.3...v2.2.4) (2021-03-25) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * 🐛 handle multiple line endings ([6fc41ff](https://github.com/kreuzerk/import-conductor/commit/6fc41ff46224ffff33a0aa47c457667c672e5ac3)) 70 | 71 | ## [2.2.3](https://github.com/kreuzerk/import-conductor/compare/v2.2.2...v2.2.3) (2021-03-24) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * 🐛 handle third party libraries that are not installed ([aab3f11](https://github.com/kreuzerk/import-conductor/commit/aab3f1104de88dbdadd8a84f2cf42f32cd8ee75a)) 77 | 78 | ## [2.2.2](https://github.com/kreuzerk/import-conductor/compare/v2.2.1...v2.2.2) (2021-02-11) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * 🐛 Fixed case with empty separator added ([6419a16](https://github.com/kreuzerk/import-conductor/commit/6419a161dfb30ea8d1e525623577700a8b901f3d)) 84 | 85 | ## [2.2.1](https://github.com/kreuzerk/import-conductor/compare/v2.2.0...v2.2.1) (2020-10-13) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * 🐛 code between import statements gets deleted ([90e3f90](https://github.com/kreuzerk/import-conductor/commit/90e3f90dba286f718037eab8b345519a04629e57)), closes [#40](https://github.com/kreuzerk/import-conductor/issues/40) 91 | 92 | # [2.2.0](https://github.com/kreuzerk/import-conductor/compare/v2.1.0...v2.2.0) (2020-10-11) 93 | 94 | 95 | ### Features 96 | 97 | * 🎸 allow changing the sections separator ([800dbd9](https://github.com/kreuzerk/import-conductor/commit/800dbd9202d1c49bdb2660affc2746afaba884bf)), closes [#34](https://github.com/kreuzerk/import-conductor/issues/34) 98 | 99 | # [2.1.0](https://github.com/kreuzerk/import-conductor/compare/v2.0.3...v2.1.0) (2020-10-11) 100 | 101 | ### Bug Fixes 102 | 103 | - 🐛 cli default option wasn't working ([9304f31](https://github.com/kreuzerk/import-conductor/commit/9304f316f9b9d7ec38b32be1d6b239275ca8cef3)) 104 | 105 | ### Features 106 | 107 | - 🎸 support multiple sources ([5e16de2](https://github.com/kreuzerk/import-conductor/commit/5e16de21d30ef7caf5c547aa6bc936f1d8f91f3e)) 108 | 109 | ## [2.0.3](https://github.com/kreuzerk/import-conductor/compare/v2.0.2...v2.0.3) (2020-09-05) 110 | 111 | ### Bug Fixes 112 | 113 | - 🐛 re-runing the conductor adds an extra new line ([ff8bc6d](https://github.com/kreuzerk/import-conductor/commit/ff8bc6d6c7eb8f558be1001b208d7d56fe18b420)), closes [#37](https://github.com/kreuzerk/import-conductor/issues/37) 114 | 115 | ## [2.0.2](https://github.com/kreuzerk/import-conductor/compare/v2.0.1...v2.0.2) (2020-09-01) 116 | 117 | ### Bug Fixes 118 | 119 | - 🐛 conductor removing comments from the imports section ([05f0215](https://github.com/kreuzerk/import-conductor/commit/05f02154a151f5c903111bd3607ca9916a056e7e)), closes [#25](https://github.com/kreuzerk/import-conductor/issues/25) 120 | 121 | ## [2.0.1](https://github.com/kreuzerk/import-conductor/compare/v2.0.0...v2.0.1) (2020-08-31) 122 | 123 | ### Bug Fixes 124 | 125 | - 🐛 merge statements breaks with trailing comma ([661d594](https://github.com/kreuzerk/import-conductor/commit/661d594cf2f28715b962a96160e94baf8da74387)) 126 | - 🐛 third party wrong classification ([7e33e47](https://github.com/kreuzerk/import-conductor/commit/7e33e47ca71776181714015ef88938f8d0fc8a57)) 127 | 128 | # [2.0.0](https://github.com/kreuzerk/import-conductor/compare/v1.5.1...v2.0.0) (2020-08-29) 129 | 130 | ### Features 131 | 132 | - 🎸 support ignore files and dry run ([4a28554](https://github.com/kreuzerk/import-conductor/commit/4a28554c25be4105664206bec7666878d46936c1)), closes [#20](https://github.com/kreuzerk/import-conductor/issues/20) 133 | 134 | ### BREAKING CHANGES 135 | 136 | - 🧨 config options 137 | 138 | ## [1.5.1](https://github.com/kreuzerk/import-conductor/compare/v1.5.0...v1.5.1) (2020-08-21) 139 | 140 | ### Bug Fixes 141 | 142 | - 🐛 custom imports not categorised correctly ([a735201](https://github.com/kreuzerk/import-conductor/commit/a735201fd55be5d16131bd43cb54876556acd47f)), closes [#21](https://github.com/kreuzerk/import-conductor/issues/21) 143 | 144 | # [1.5.0](https://github.com/kreuzerk/import-conductor/compare/v1.4.4...v1.5.0) (2020-08-07) 145 | 146 | ### Features 147 | 148 | - **options:** trigger new release ([1f4d380](https://github.com/kreuzerk/import-conductor/commit/1f4d3800c615007e57204fc7dfade5f671f6e499)) 149 | 150 | ## [1.4.4](https://github.com/kreuzerk/import-conductor/compare/v1.4.3...v1.4.4) (2020-07-27) 151 | 152 | ### Bug Fixes 153 | 154 | - **imports:** fix wrong import of gitChangedFiles ([f775f69](https://github.com/kreuzerk/import-conductor/commit/f775f69720349e8c27ee04b9b9685f661f3986fb)) 155 | 156 | ## [1.4.3](https://github.com/kreuzerk/import-conductor/compare/v1.4.2...v1.4.3) (2020-07-05) 157 | 158 | ### Bug Fixes 159 | 160 | - **imports:** adjust imports ([ce3607a](https://github.com/kreuzerk/import-conductor/commit/ce3607af93ddfc39a4853f75490604fc97283615)) 161 | 162 | ## [1.4.2](https://github.com/kreuzerk/import-conductor/compare/v1.4.1...v1.4.2) (2020-06-26) 163 | 164 | ### Bug Fixes 165 | 166 | - **gif:** revert GIF ([2d08627](https://github.com/kreuzerk/import-conductor/commit/2d0862717a4a7e3ca7b49f05624c1fe30bced1a3)) 167 | - **gif:** update demo GIF ([8bc5532](https://github.com/kreuzerk/import-conductor/commit/8bc55325e4568f90e5b92bab07bcf0b2985e70a8)) 168 | 169 | ## [1.4.1](https://github.com/kreuzerk/import-conductor/compare/v1.4.0...v1.4.1) (2020-06-26) 170 | 171 | ### Bug Fixes 172 | 173 | - **docs:** update README.md ([794bbd7](https://github.com/kreuzerk/import-conductor/commit/794bbd773410a520b0f8a93d8dac3a188e07011a)) 174 | - **docs:** update README.md ([e212184](https://github.com/kreuzerk/import-conductor/commit/e2121843dd313bd052ab9198f14e502d427e8775)) 175 | 176 | # [1.4.0](https://github.com/kreuzerk/import-conductor/compare/v1.3.0...v1.4.0) (2020-06-26) 177 | 178 | ### Features 179 | 180 | - **regex:** use regex instead of path ([607a062](https://github.com/kreuzerk/import-conductor/commit/607a06216ed9532dfefaa34177c89e8cf999a3af)) 181 | 182 | # [1.3.0](https://github.com/kreuzerk/import-conductor/compare/v1.2.0...v1.3.0) (2020-06-25) 183 | 184 | ### Features 185 | 186 | - **organization:** touch imports only ([6bb5545](https://github.com/kreuzerk/import-conductor/commit/6bb5545d6c5a462bc13671b45f25a0a3575b7685)) 187 | 188 | # [1.2.0](https://github.com/kreuzerk/import-conductor/compare/v1.1.0...v1.2.0) (2020-06-25) 189 | 190 | ### Features 191 | 192 | - **log:** log process ([8a833d1](https://github.com/kreuzerk/import-conductor/commit/8a833d18b6cc99f55d6bf513b3a630fa06c75675)) 193 | 194 | # [1.1.0](https://github.com/kreuzerk/import-conductor/compare/v1.0.1...v1.1.0) (2020-06-25) 195 | 196 | ### Features 197 | 198 | - **merge:** merge import statments from same lib ([ee6c247](https://github.com/kreuzerk/import-conductor/commit/ee6c247396a6928d613c6f52cd896190be0d7eb4)) 199 | 200 | ## [1.0.1](https://github.com/kreuzerk/import-conductor/compare/v1.0.0...v1.0.1) (2020-06-24) 201 | 202 | ### Bug Fixes 203 | 204 | - **deps:** add TypeScript as a dependency ([5765ffe](https://github.com/kreuzerk/import-conductor/commit/5765ffec8f60cd4e0dd7343466fe631428a1d0ca)) 205 | 206 | # 1.0.0 (2020-06-24) 207 | 208 | ### Bug Fixes 209 | 210 | - **newlines:** do not add newlines if previous category is empty ([33c82ea](https://github.com/kreuzerk/import-conductor/commit/33c82ea2452bfad673e46fee16951cfcd6377026)) 211 | - **newlines:** improve newline detection ([ef06b06](https://github.com/kreuzerk/import-conductor/commit/ef06b06b2ba21a041879b4455d7b91867dbbf625)) 212 | 213 | ### Features 214 | 215 | - **add:** automatically add files on staged option ([861e659](https://github.com/kreuzerk/import-conductor/commit/861e659cd5e7339b0ed0038f85f96ad54d4fc819)) 216 | - **add:** log processed files ([21a4fd0](https://github.com/kreuzerk/import-conductor/commit/21a4fd08ae98ff9afee01cb3cf806f91d0a8dcfa)) 217 | - **files:** run only against staged files ([6a7a6ac](https://github.com/kreuzerk/import-conductor/commit/6a7a6ac554c83b61bfdbf6906da34a16a99de078)) 218 | - **map:** create import statement map ([fa3b61f](https://github.com/kreuzerk/import-conductor/commit/fa3b61f714100e5c4def213606bd649092c5943c)) 219 | - **pots:** split import paths to pots ([ec26683](https://github.com/kreuzerk/import-conductor/commit/ec2668324d7f0d47427ccdb6d595ec0ccda1ec2f)) 220 | - **statements:** update content and sort statements ([83dad60](https://github.com/kreuzerk/import-conductor/commit/83dad60e207e81efc5f748d3cf667255a893b2ae)) 221 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | Our Pledge 3 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 4 | 5 | Our Standards 6 | Examples of behavior that contributes to creating a positive environment include: 7 | 8 | Using welcoming and inclusive language 9 | Being respectful of differing viewpoints and experiences 10 | Gracefully accepting constructive criticism 11 | Focusing on what is best for the community 12 | Showing empathy towards other community members 13 | Examples of unacceptable behavior by participants include: 14 | 15 | The use of sexualized language or imagery and unwelcome sexual attention or advances 16 | Trolling, insulting/derogatory comments, and personal or political attacks 17 | Public or private harassment 18 | Publishing others' private information, such as a physical or electronic address, without explicit permission 19 | Other conduct which could reasonably be considered inappropriate in a professional setting 20 | Our Responsibilities 21 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 24 | 25 | Scope 26 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 27 | 28 | Enforcement 29 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kevin.kreuzer90@icloud.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 30 | 31 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 32 | 33 | Attribution 34 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://raw.githubusercontent.com/kreuzerk/import-conductor/master/assets/logo.png) 2 | 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) 5 | 6 | 7 | # import-conductor 8 | 9 | > Automatically organize your TypeScript imports to keep them clean and readable. 10 | 11 | 12 | 13 | 14 | - [What it does](#what-it-does) 15 | - [Usage](#usage) 16 | - [Options](#options) 17 | 18 | 19 | 20 | ![Demo](https://raw.githubusercontent.com/kreuzerk/import-conductor/master/assets/demo.gif) 21 | 22 | ## What it does 23 | 24 | Import conductor will order all imports into the following blocks: 25 | 26 | ``` 27 | 1. Block - third party libraries 28 | 29 | 2. Block - user / company libraries 30 | 31 | 3. Block - imports from other modules or directories in your codebase 32 | 33 | 4. Block - imports for the same module 34 | ``` 35 | 36 | Take a look at the following source file. It's hard to distinguish 37 | between third-party imports, company wide imports and files from same module. 38 | 39 | ```typescript 40 | import { Component, OnInit } from '@angular/core'; 41 | import { CustomerService } from './customer.service'; 42 | import { Customer } from './customer.model'; 43 | import { Order } from '../order/order.model'; 44 | import { LoggerService } from '@myorg/logger'; 45 | import { Observable } from 'rxjs'; 46 | ``` 47 | 48 | A cleaner version that is easy scannable would look like this: 49 | 50 | ```typescript 51 | import { Component, OnInit } from '@angular/core'; 52 | import { Observable } from 'rxjs'; 53 | 54 | import { LoggerService } from '@myorg/logger'; 55 | 56 | import { Order } from '../order/order.model'; 57 | 58 | import { Customer } from './customer.model'; 59 | import { CustomerService } from './customer.service'; 60 | ``` 61 | 62 | Of course, it's a lot of work to order all import statements in existing code bases. 63 | Furthermore, in bigger development teams it's hard to enforce this syntax so that every 64 | developer orders their imports accordingly. Especially with AutoImports in IDEs. 65 | 66 | **That's where import-conductor comes into play**. 67 | Import-conductor can reorder all imports in your project, and combined with tools like [`husky`](https://github.com/typicode/husky#readme) you can automatically reorder 68 | imports of changed files in a pre commit hook. 69 | 70 | ## Usage 71 | 72 | - Run in the command line: 73 | 74 | ```shell script 75 | npx import-conductor -s customer.component.ts -p @myorg 76 | ``` 77 | 78 | - Run as a npm script: 79 | 80 | ```json 81 | "scripts": { 82 | "import-conductor": "import-conductor -p @myorg" 83 | }, 84 | ``` 85 | 86 | - Integrate with tools like [`husky`](https://github.com/typicode/husky#readme): 87 | 88 | ```json 89 | "lint-staged": { 90 | "*.{ts,tsx}": [ 91 | "import-conductor --staged -p @myorg", 92 | "prettier --write", 93 | "eslint --fix", 94 | "git add" 95 | ] 96 | }, 97 | ``` 98 | 99 | ## Options 100 | 101 | - `source` - Regex to that matches the source files: (defaults to `[./src/**/*.ts]`) 102 | 103 | ```shell script 104 | import-conductor --source mySrc/**/*.ts anotherSrc/**/*.ts 105 | import-conductor -s mySrc/**/*.ts anotherSrc/**/*.ts 106 | import-conductor mySrc/**/*.ts anotherSrc/**/*.ts 107 | ``` 108 | 109 | - `ignore`\* - Ignore files that match the pattern: (defaults to `[]`) 110 | 111 | ```shell script 112 | import-conductor --ignore 'mySrc/**/*some.ts' 'main.ts' 113 | import-conductor -i 'mySrc/**/*some.ts' 'main.ts' 114 | ``` 115 | 116 | **\*Note**: you can also skip a file by adding the following comment at the top: 117 | 118 | ```typescript 119 | // import-conductor-skip 120 | ... 121 | ``` 122 | 123 | - `userLibPrefixes` - The prefix of custom user libraries - the prefix used to distinguish between third party libraries and company libs: (defaults to `[]`) 124 | 125 | ```shell script 126 | import-conductor --userLibPrefixes @customA @customB 127 | import-conductor -p @customA @customB 128 | ``` 129 | 130 | - `separator` - The string separator between the imports sections: (defaults to `\n`) 131 | 132 | ```shell script 133 | import-conductor --separator '' ==> no separator 134 | ``` 135 | 136 | - `groupOrder` - The group order to follow: (defaults to `[thirdParty, userLibrary, differentModule, sameModule]`) 137 | 138 | ```shell script 139 | import-conductor --groupOrder 'userLibrary' 'differentModule' 'sameModule' 'thirdParty' 140 | import-conductor -g 'userLibrary' 'differentModule' 'sameModule' 'thirdParty' 141 | ``` 142 | 143 | - `staged` - Run against staged files: (defaults to `false`) 144 | 145 | ```shell script 146 | import-conductor --staged 147 | ``` 148 | 149 | - `noAutoMerge` - Disable automatically merging 2 import statements from the same source: (defaults to `false`) 150 | 151 | ```shell script 152 | import-conductor --noAutoMerge 153 | ``` 154 | 155 | - `autoAdd` - Automatically adding the committed files when using the staged option: (defaults to `false`) 156 | 157 | ```shell script 158 | import-conductor --autoAdd 159 | import-conductor -a 160 | ``` 161 | 162 | - `dryRun` - Run without applying any changes: (defaults to `false`) 163 | 164 | ```shell script 165 | import-conductor --dryRun 166 | import-conductor -d 167 | ``` 168 | 169 | - `verbose` - Run with detailed log output: (defaults to `false`) 170 | 171 | ```shell script 172 | import-conductor --verbose 173 | import-conductor -v 174 | ``` 175 | 176 | - `version`: 177 | 178 | ```shell script 179 | import-conductor --version 180 | ``` 181 | 182 | - `help`: 183 | 184 | ```shell script 185 | import-conductor --help 186 | import-conductor -h 187 | ``` 188 | 189 | ## Core Team 190 | 191 | 192 | 193 | 194 | 195 | 196 |

Kevin Kreuzer


Shahar Kazaz

197 | 198 | ## Contributors ✨ 199 | 200 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 |
Kevin Kreuzer
Kevin Kreuzer

💻 🎨 📖 🤔 🚇 🚧 ⚠️
Shahar Kazaz
Shahar Kazaz

💻 📖 🤔 🚇 🚧 ⚠️
Robert Laurenz
Robert Laurenz

📖
Lonli-Lokli
Lonli-Lokli

💻 ⚠️ 🐛
Yuri Santos
Yuri Santos

💻
markoberholzer-es
markoberholzer-es

💻 🤔
Michael Trefzer
Michael Trefzer

💻
218 | 219 | 220 | 221 | 222 | 223 | 224 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 225 | -------------------------------------------------------------------------------- /__tests__/get-package-file-deps.spec.ts: -------------------------------------------------------------------------------- 1 | import { getPackageFileDeps } from '@ic/helpers/get-package-file-deps'; 2 | 3 | describe(`get package.json file deps`, () => { 4 | const packageFileContent = { 5 | name: 'name', 6 | version: '1.2.3', 7 | dependencies: { 8 | 'dep-a': '0.0.1', 9 | 'dep-b': '^1.0.5', 10 | }, 11 | devDependencies: { 12 | '@dev/dep-a': '~2.0.0', 13 | '@dev/dep-b': '8.1.0', 14 | }, 15 | }; 16 | 17 | it('should return deps and devDeps', () => { 18 | const expectedDeps = ['@dev/dep-a', '@dev/dep-b', 'dep-a', 'dep-b']; 19 | expect(getPackageFileDeps(packageFileContent)).toEqual(expectedDeps); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/get-package-lock-file-deps.spec.ts: -------------------------------------------------------------------------------- 1 | import { getLockFileVersion } from '@ic/helpers/get-lock-file-version'; 2 | import { getPackageLockFileDeps } from '@ic/helpers/get-package-lock-file-deps'; 3 | 4 | describe(`get package-lock.json file deps`, () => { 5 | it('should return deps with lockfileVersion < 3', () => { 6 | const packageLockFileContentVersion2 = JSON.parse(`{ 7 | "name": "lockfile version 2", 8 | "version": "1.2.3", 9 | "lockfileVersion": 2, 10 | "dependencies": { 11 | "dep-a": "0.0.1", 12 | "dep-b": "^1.0.5" 13 | } 14 | }`); 15 | const expectedDeps = ['dep-a', 'dep-b']; 16 | expect(getLockFileVersion(packageLockFileContentVersion2)).toBe(2); 17 | expect(getPackageLockFileDeps(packageLockFileContentVersion2)).toEqual(expectedDeps); 18 | }); 19 | 20 | it('should return deps with lockfileVersion = 3', () => { 21 | const packageLockFileContentVersion3 = JSON.parse(`{ 22 | "name": "lockfile version 3", 23 | "version": "1.2.3", 24 | "lockfileVersion": 3, 25 | "packages": { 26 | "": { 27 | "dependencies": { 28 | "dep-a": "0.0.1", 29 | "dep-b": "^1.0.5" 30 | } 31 | } 32 | } 33 | }`); 34 | const expectedDeps = ['dep-a', 'dep-b']; 35 | expect(getLockFileVersion(packageLockFileContentVersion3)).toBe(3); 36 | expect(getPackageLockFileDeps(packageLockFileContentVersion3)).toEqual(expectedDeps); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/merge-import-statements.spec.ts: -------------------------------------------------------------------------------- 1 | import { mergeImportStatements } from '@ic/conductor/merge-import-statements'; 2 | 3 | describe('mergeImportStatements', () => { 4 | it('should merge two imports together', () => { 5 | const importStatementOne = "import {quux} from './quux/quux';"; 6 | const importStatementTwo = "import {quox} from './quux/quux';"; 7 | 8 | const expectedImportStatement = "import {quux,quox} from './quux/quux';"; 9 | const actualImportStatement = mergeImportStatements(importStatementOne, importStatementTwo); 10 | 11 | expect(actualImportStatement).toBe(expectedImportStatement); 12 | }); 13 | 14 | it('should merge two imports together (support multi line)', () => { 15 | const importStatementOne = "import {quux} from './quux/quux';"; 16 | const importStatementTwo = ` 17 | import { 18 | quox, 19 | quex 20 | } from './quux/quux';`; 21 | 22 | const expectedImportStatement = "import {quux,quox,quex} from './quux/quux';"; 23 | const actualImportStatement = mergeImportStatements(importStatementOne, importStatementTwo); 24 | 25 | expect(actualImportStatement).toBe(expectedImportStatement); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/optimize-imports-mocks.ts: -------------------------------------------------------------------------------- 1 | export type TestCase = { input: string; expected: string; noOfRuns?: number; groupOrder?: string[] }; 2 | 3 | export const readmeExample: TestCase = { 4 | input: `import fs from 'fs'; 5 | import { CustomerService } from './customer.service'; 6 | import { Customer } from './customer.model'; 7 | import { Order } from '../order/order.model'; 8 | import { Component, OnInit } from '@angular/core'; 9 | import { LoggerService } from '@myorg/logger'; 10 | import { Observable } from 'rxjs'; 11 | import { spawn } from 'child_process';`, 12 | expected: `import { Component, OnInit } from '@angular/core'; 13 | import { spawn } from 'child_process'; 14 | import fs from 'fs'; 15 | import { Observable } from 'rxjs'; 16 | 17 | import { LoggerService } from '@myorg/logger'; 18 | 19 | import { Order } from '../order/order.model'; 20 | 21 | import { Customer } from './customer.model'; 22 | import { CustomerService } from './customer.service';`, 23 | }; 24 | 25 | export const comments: TestCase = { 26 | input: `// file level comments shouldn't move 27 | import fs from 'fs'; 28 | import { CustomerService } from './customer.service'; 29 | import { Customer } from './customer.model'; 30 | // should be above this import 31 | import { Order } from '../order/order.model'; 32 | import { Component, OnInit } from '@angular/core'; 33 | /* I will follow LoggerService wherever he goes */ 34 | import { LoggerService } from '@myorg/logger'; 35 | /** 36 | * important comment about Observables 37 | */ 38 | import { Observable } from 'rxjs'; 39 | import { spawn } from 'child_process';`, 40 | expected: `// file level comments shouldn't move 41 | import { Component, OnInit } from '@angular/core'; 42 | import { spawn } from 'child_process'; 43 | import fs from 'fs'; 44 | /** 45 | * important comment about Observables 46 | */ 47 | import { Observable } from 'rxjs'; 48 | 49 | /* I will follow LoggerService wherever he goes */ 50 | import { LoggerService } from '@myorg/logger'; 51 | 52 | // should be above this import 53 | import { Order } from '../order/order.model'; 54 | 55 | import { Customer } from './customer.model'; 56 | import { CustomerService } from './customer.service';`, 57 | }; 58 | 59 | export const codeBetweenImports: TestCase = { 60 | input: `/* file level comments shouldn't move 61 | and another line */ 62 | import fs from 'fs'; 63 | /// 64 | 65 | declare var global: any** 66 | import { CustomerService } from './customer.service'; 67 | import { Customer } from './customer.model'; 68 | 69 | import { Order } from '../order/order.model'; 70 | 71 | if (environment.production) { 72 | enableProdMode(); 73 | } 74 | 75 | import { Component, OnInit } from '@angular/core'; 76 | import { LoggerService } from '@myorg/logger'; 77 | import { Observable } from 'rxjs'; 78 | import { spawn } from 'child_process';`, 79 | expected: `/* file level comments shouldn't move 80 | and another line */ 81 | import { Component, OnInit } from '@angular/core'; 82 | import { spawn } from 'child_process'; 83 | import fs from 'fs'; 84 | import { Observable } from 'rxjs'; 85 | 86 | import { LoggerService } from '@myorg/logger'; 87 | 88 | import { Order } from '../order/order.model'; 89 | 90 | import { Customer } from './customer.model'; 91 | import { CustomerService } from './customer.service'; 92 | /// 93 | 94 | declare var global: any** 95 | 96 | if (environment.production) { 97 | enableProdMode(); 98 | }`, 99 | }; 100 | 101 | export const emptyNewLineSeparator: TestCase = { 102 | noOfRuns: 2, 103 | input: `import { Component, HostListener } from '@angular/core'; 104 | import { Observable } from 'rxjs'; 105 | import { MatDialogRef } from '@angular/material/dialog'; 106 | 107 | 108 | import { AboutDialogBloc, AboutState } from './about-dialog.bloc';`, 109 | expected: `import { Component, HostListener } from '@angular/core'; 110 | import { MatDialogRef } from '@angular/material/dialog'; 111 | import { Observable } from 'rxjs'; 112 | import { AboutDialogBloc, AboutState } from './about-dialog.bloc';`, 113 | }; 114 | 115 | export const noImportStatement: TestCase = { 116 | input: `const x = 2;`, 117 | expected: `const x = 2;`, 118 | }; 119 | 120 | export const importsOnDifferentGroupOrder: TestCase = { 121 | groupOrder: ['userLibrary', 'sameModule', 'differentModule', 'thirdParty'], 122 | input: `import { Component } from '@angular/core'; 123 | import fs from 'fs'; 124 | import { LoggerService } from '@myorg/logger'; 125 | import { Order } from '../order/order.model'; 126 | import { CustomService } from './customer.service';`, 127 | expected: `import { LoggerService } from '@myorg/logger'; 128 | import { CustomService } from './customer.service'; 129 | import { Order } from '../order/order.model'; 130 | import { Component } from '@angular/core'; 131 | import fs from 'fs';`, 132 | }; 133 | 134 | export const importsWithGroupOrderIncorrect: TestCase = { 135 | groupOrder: ['userLibrary', 'differentModule', 'thirdParty'], 136 | input: `import fs from 'fs'; 137 | import { CustomerService } from './customer.service'; 138 | import { Order } from '../order/order.model'; 139 | import { Component, OnInit } from '@angular/core'; 140 | import { LoggerService } from '@myorg/logger'; 141 | import { Observable } from 'rxjs'; 142 | import { spawn } from 'child_process';`, 143 | expected: `import { Component, OnInit } from '@angular/core'; 144 | import { spawn } from 'child_process'; 145 | import fs from 'fs'; 146 | import { Observable } from 'rxjs'; 147 | 148 | import { LoggerService } from '@myorg/logger'; 149 | 150 | import { Order } from '../order/order.model'; 151 | 152 | import { CustomerService } from './customer.service';`, 153 | }; 154 | -------------------------------------------------------------------------------- /__tests__/optimize-imports.spec.ts: -------------------------------------------------------------------------------- 1 | import { actions, organizeImports, organizeImportsForFile } from '@ic/conductor/organize-imports'; 2 | import * as config from '@ic/config'; 3 | import fs from 'fs'; 4 | import { Config } from '@ic/types'; 5 | import { 6 | readmeExample, 7 | comments, 8 | TestCase, 9 | codeBetweenImports, 10 | emptyNewLineSeparator, 11 | noImportStatement, 12 | importsOnDifferentGroupOrder, 13 | importsWithGroupOrderIncorrect, 14 | } from './optimize-imports-mocks'; 15 | import { defaultConfig } from '@ic/defaultConfig'; 16 | import { getGroupOrder } from '@ic/conductor/get-group-order'; 17 | 18 | jest.mock('fs'); 19 | jest.mock('simple-git'); 20 | 21 | describe('optimizeImports', () => { 22 | const basicConfig: Config = { 23 | ...defaultConfig, 24 | source: ['test.ts'], 25 | userLibPrefixes: ['@myorg'], 26 | thirdPartyDependencies: new Set(['@angular/core', 'rxjs']), 27 | }; 28 | 29 | let spy: jasmine.Spy; 30 | beforeEach(() => { 31 | spy = spyOn(config, 'getConfig'); 32 | spy.and.returnValue(basicConfig); 33 | (fs.existsSync as any).mockReturnValue(true); 34 | (fs.writeFileSync as any).mockClear(); 35 | }); 36 | 37 | async function assertConductor({ expected, input, noOfRuns }: TestCase) { 38 | (fs.readFileSync as any).mockReturnValue(Buffer.from(input)); 39 | let noOfRun = noOfRuns ?? 1; 40 | const file = 'test.ts'; 41 | let result: string; 42 | do { 43 | result = await organizeImportsForFile(file); 44 | } while (--noOfRun > 0); 45 | 46 | expect(fs.writeFileSync).toHaveBeenCalledWith(file, expected); 47 | 48 | return result; 49 | } 50 | 51 | it('should work with a basic example', async () => { 52 | await assertConductor(readmeExample); 53 | }); 54 | 55 | it('should work with comments', async () => { 56 | await assertConductor(comments); 57 | }); 58 | 59 | it('should work with non import node between the import blocks', async () => { 60 | await assertConductor(codeBetweenImports); 61 | }); 62 | 63 | it('should give you an error if you import something that is not installed', async () => { 64 | spy.and.returnValue({ ...basicConfig, separator: '' }); 65 | await assertConductor(emptyNewLineSeparator); 66 | }); 67 | 68 | it('should not change conducted file', async () => { 69 | (fs.readFileSync as any).mockReturnValue(Buffer.from(readmeExample.expected)); 70 | const file = 'test.ts'; 71 | await organizeImports(file); 72 | expect(fs.writeFileSync).not.toHaveBeenCalled(); 73 | }); 74 | 75 | it('should skip the file when skip comment exists', async () => { 76 | const testCases = ['//import-conductor-skip', '// import-conductor-skip', '/* import-conductor-skip*/', '/*import-conductor-skip */']; 77 | for (const testCase of testCases) { 78 | (fs.readFileSync as any).mockReturnValue(Buffer.from(testCase)); 79 | const file = 'test.ts'; 80 | const result = await organizeImportsForFile(file); 81 | expect(result).toBe(actions.skipped); 82 | expect(fs.writeFileSync).not.toHaveBeenCalled(); 83 | } 84 | }); 85 | 86 | it('should do nothing if the file has no import', async () => { 87 | (fs.readFileSync as any).mockReturnValue(Buffer.from(noImportStatement.input)); 88 | const file = 'test.ts'; 89 | const result = await organizeImportsForFile(file); 90 | expect(result).toBe(actions.none); 91 | expect(fs.writeFileSync).not.toHaveBeenCalled(); 92 | }); 93 | 94 | it('should change group order', async () => { 95 | spy.and.returnValue({ ...basicConfig, groupOrder: importsOnDifferentGroupOrder.groupOrder, separator: '' }); 96 | await assertConductor(importsOnDifferentGroupOrder); 97 | }); 98 | 99 | it('should use default order because incorrect group order input', async () => { 100 | spy.and.returnValue({ ...basicConfig, groupOrder: getGroupOrder({ groupOrder: importsWithGroupOrderIncorrect.groupOrder }) }); 101 | await assertConductor(importsWithGroupOrderIncorrect); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/import-conductor/c90eac4229af787f1bf4aa5856afbe229afaaa40/assets/demo.gif -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nivekcode/import-conductor/c90eac4229af787f1bf4aa5856afbe229afaaa40/assets/logo.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig.spec'); 3 | 4 | module.exports = { 5 | roots: [''], 6 | transform: { 7 | '^.+\\.tsx?$': 'ts-jest', 8 | }, 9 | globals: { 10 | 'ts-jest': { 11 | tsConfig: 'tsconfig.spec.json', 12 | }, 13 | }, 14 | testRegex: '/__tests__/.*(test|spec)\\.tsx?$', 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 16 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 17 | testPathIgnorePatterns: ['/bin/help-menu.ts', '/bin/pretty-html-log.bin.ts'], 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "import-conductor", 3 | "version": "2.6.1", 4 | "description": "Automatically organize your Typescript import statements", 5 | "main": "index.js", 6 | "scripts": { 7 | "contributors:add": "all-contributors add", 8 | "prebuild": "rimraf dist && npm run copy:readme", 9 | "build": "tsc", 10 | "build:dev": "tsc --watch", 11 | "postbuild": "node ./scripts/post-build.js", 12 | "commit": "git-cz", 13 | "bump-version": "rjp package.json version $VERSION", 14 | "copy:readme": "copyfiles ./README.md ./dist", 15 | "format:write": "prettier --write 'index.ts'", 16 | "test": "jest", 17 | "start": "ts-node ./src/index.ts -s playground/sampleDir/main.ts -p '@myorg'", 18 | "start:staged": "ts-node ./src/index.ts --staged", 19 | "start:bar": "ts-node ./src/index.ts --dryRun -s playground/sampleDir/bar.ts", 20 | "release": "semantic-release && node ./scripts/release.js", 21 | "hooks:pre-commit": "npm run conduct:staged && node ./scripts/hooks/pre-commit.js", 22 | "conduct:staged": "ts-node ./src/index.ts --staged" 23 | }, 24 | "husky": { 25 | "hooks": { 26 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 27 | "pre-commit": "npm run hooks:pre-commit && pretty-quick --staged", 28 | "pre-push": "npm run test" 29 | } 30 | }, 31 | "bin": { 32 | "import-conductor": "index.js" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/kreuzerk/import-conductor.git" 37 | }, 38 | "keywords": [ 39 | "Clean", 40 | "Code", 41 | "Imports", 42 | "Automatization" 43 | ], 44 | "author": "Kevin Kreuzer", 45 | "license": "ISC", 46 | "bugs": { 47 | "url": "https://github.com/kreuzerk/import-conductor/issues" 48 | }, 49 | "homepage": "https://github.com/kreuzerk/import-conductor#readme", 50 | "dependencies": { 51 | "changed-git-files": "0.0.1", 52 | "command-line-args": "^5.1.1", 53 | "command-line-usage": "^6.1.0", 54 | "commander": "^5.1.0", 55 | "git-changed-files": "^1.0.0", 56 | "fast-glob": "^3.2.12", 57 | "ora": "^4.1.0", 58 | "pkg-up": "^3.1.0", 59 | "simple-git": "^3.3.0", 60 | "typescript": "^3.9.5" 61 | }, 62 | "devDependencies": { 63 | "@commitlint/cli": "10.0.0", 64 | "@commitlint/config-conventional": "10.0.0", 65 | "@semantic-release/changelog": "^3.0.6", 66 | "@semantic-release/exec": "^3.3.8", 67 | "@semantic-release/git": "^7.0.18", 68 | "@types/jest": "^26.0.3", 69 | "@types/node": "^14.0.12", 70 | "all-contributors-cli": "^6.17.0", 71 | "chalk": "^4.1.0", 72 | "copyfiles": "^2.3.0", 73 | "git-cz": "^4.7.0", 74 | "husky": "^4.2.5", 75 | "jest": "^26.1.0", 76 | "prettier": "^2.0.5", 77 | "pretty-quick": "^2.0.1", 78 | "replace-json-property": "^1.4.3", 79 | "rimraf": "^3.0.2", 80 | "semantic-release": "^15.9.0", 81 | "shelljs": "^0.8.4", 82 | "ts-jest": "^26.1.1", 83 | "ts-node": "^8.10.2" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /playground/sampleDir/bar/bar.ts: -------------------------------------------------------------------------------- 1 | import { foo } from '../foo/foo'; 2 | import { quux } from './quux/quux'; 3 | const foo = 'foo'; 4 | import { quox } from './quux/quux'; 5 | 6 | export const bar = 'bar'; 7 | 8 | class Test { 9 | private baz: string; 10 | 11 | constructor() { 12 | this.baz = 'Blub'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /playground/sampleDir/bar/quux/quux.ts: -------------------------------------------------------------------------------- 1 | import { sync } from 'glob'; 2 | import { isVariableDeclaration } from 'typescript'; 3 | 4 | import { FooModule } from '../someModule'; 5 | import { enableProdMode } from '@custom/something'; 6 | 7 | import { environment } from './environments/environment'; 8 | import { SomeModule } from './someModule'; 9 | 10 | export const quux = 'quux'; 11 | -------------------------------------------------------------------------------- /playground/sampleDir/baz/baz.ts: -------------------------------------------------------------------------------- 1 | import { isVariableDeclaration } from 'typescript'; 2 | 3 | import { bar } from '../bar/bar'; 4 | import { foo } from '../foo/foo'; 5 | import { Blub } from '@custom/something'; 6 | 7 | export const baz = 'baz'; 8 | console.log('Foo', foo); 9 | 10 | console.log(); 11 | -------------------------------------------------------------------------------- /playground/sampleDir/baz/qux/qux.ts: -------------------------------------------------------------------------------- 1 | import { isVariableDeclaration } from 'typescript'; 2 | 3 | import { foo } from '../../foo/foo'; 4 | import { baz } from '../baz'; 5 | 6 | console.log('Foo', foo); 7 | -------------------------------------------------------------------------------- /playground/sampleDir/foo/foo.ts: -------------------------------------------------------------------------------- 1 | export const foo = 'foo'; 2 | -------------------------------------------------------------------------------- /playground/sampleDir/main.ts: -------------------------------------------------------------------------------- 1 | // file level comments shouldn't move 2 | import fs from 'fs'; 3 | import { CustomerService } from './customer.service'; 4 | import { Customer } from './customer.model'; 5 | // should be above this import 6 | import { Order } from '../order/order.model'; 7 | import { Component, OnInit } from '@angular/core'; 8 | /* I will follow LoggerService wherever he goes */ 9 | import { LoggerService } from '@myorg/logger'; 10 | /** 11 | * important comment about Observables 12 | */ 13 | import { Observable } from 'rxjs'; 14 | import { spawn } from 'child_process'; 15 | 16 | if (environment.production) { 17 | enableProdMode(); 18 | } 19 | 20 | platformBrowserDynamic() 21 | .bootstrapModule(SomeModule) 22 | .catch((err) => console.error(err)); 23 | -------------------------------------------------------------------------------- /scripts/hooks/pre-commit.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | const gitChangedFiles = require('git-changed-files'); 5 | 6 | gitChangedFiles({ showCommitted: false }).then(({ unCommittedFiles }) => preCommit(unCommittedFiles)); 7 | 8 | function forbiddenTokens(stagedFiles) { 9 | const FILES_REGEX = { ts: /\.ts$/, spec: /\.spec\.ts$/ }; 10 | /** Map of forbidden tokens and their match regex */ 11 | const forbiddenTokens = { 12 | fit: { rgx: /fit\(/, fileRgx: FILES_REGEX.spec }, 13 | fdescribe: { rgx: /fdescribe\(/, fileRgx: FILES_REGEX.spec }, 14 | '.skip': { rgx: /(describe|context|it)\.skip/, fileRgx: FILES_REGEX.spec }, 15 | '.only': { rgx: /(describe|context|it)\.only/, fileRgx: FILES_REGEX.spec }, 16 | debugger: { rgx: /(debugger);?/, fileRgx: FILES_REGEX.ts }, 17 | }; 18 | 19 | let status = 0; 20 | 21 | for (let [term, value] of Object.entries(forbiddenTokens)) { 22 | const { rgx, fileRgx, message } = value; 23 | /* Filter relevant files using the files regex */ 24 | const relevantFiles = stagedFiles.filter((file) => fileRgx.test(file.trim())); 25 | const failedFiles = relevantFiles.reduce((acc, fileName) => { 26 | const filePath = path.resolve(process.cwd(), fileName); 27 | if (fs.existsSync(filePath)) { 28 | const content = fs.readFileSync(filePath).toString('utf-8'); 29 | if (rgx.test(content)) { 30 | status = 1; 31 | acc.push(fileName); 32 | } 33 | } 34 | return acc; 35 | }, []); 36 | 37 | /* Log all the failed files for this token with the matching message */ 38 | if (failedFiles.length > 0) { 39 | const msg = message || `The following files contains '${term}' in them:`; 40 | console.log(chalk.bgRed.black.bold(msg)); 41 | console.log(chalk.bgRed.black(failedFiles.join('\n'))); 42 | } 43 | } 44 | 45 | return status; 46 | } 47 | 48 | function preCommit(stagedFiles) { 49 | let status = 0; 50 | const checks = [forbiddenTokens]; 51 | for (const check of checks) { 52 | if (status !== 0) break; 53 | status = check(stagedFiles); 54 | } 55 | 56 | process.exit(status); 57 | } 58 | -------------------------------------------------------------------------------- /scripts/post-build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const { scripts, devDependencies, husky, ...cleanPackage } = JSON.parse(fs.readFileSync('package.json').toString()); 4 | fs.writeFileSync('dist/package.json', JSON.stringify(cleanPackage, null, 2)); 5 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { execSync } = require('child_process'); 3 | 4 | const { version } = JSON.parse(fs.readFileSync('package.json').toString()); 5 | fs.writeFileSync('src/version.ts', `export const packageVersion = '${version}';\n`); 6 | execSync('git add src/version.ts'); 7 | execSync('git commit --amend --no-edit --no-verify'); 8 | -------------------------------------------------------------------------------- /src/cliOptions.ts: -------------------------------------------------------------------------------- 1 | export const optionDefinitions = [ 2 | { 3 | name: 'verbose', 4 | alias: 'v', 5 | type: Boolean, 6 | description: 'Run with detailed log output', 7 | }, 8 | { 9 | name: 'separator', 10 | type: String, 11 | description: 'Separator between import groups', 12 | }, 13 | { 14 | name: 'source', 15 | alias: 's', 16 | type: String, 17 | multiple: true, 18 | defaultOption: true, 19 | description: 'Path to the source files', 20 | }, 21 | { 22 | name: 'userLibPrefixes', 23 | alias: 'p', 24 | type: String, 25 | multiple: true, 26 | description: 'The prefix of custom user libraries', 27 | }, 28 | { 29 | name: 'groupOrder', 30 | alias: 'g', 31 | type: String, 32 | multiple: true, 33 | description: 'The group order it should follow', 34 | }, 35 | { 36 | name: 'staged', 37 | type: Boolean, 38 | description: 'Run against staged files', 39 | }, 40 | { 41 | name: 'autoAdd', 42 | alias: 'a', 43 | type: Boolean, 44 | description: 'Automatically add the committed files when the staged option is used', 45 | }, 46 | { 47 | name: 'noAutoMerge', 48 | type: Boolean, 49 | description: `Disable automatically merge 2 import statements from the same source`, 50 | }, 51 | { 52 | name: 'ignore', 53 | alias: 'i', 54 | type: String, 55 | multiple: true, 56 | description: `Files to ignore`, 57 | }, 58 | { 59 | name: 'dryRun', 60 | alias: 'd', 61 | type: Boolean, 62 | description: 'Run in dry run mode', 63 | }, 64 | { 65 | name: 'help', 66 | alias: 'h', 67 | type: Boolean, 68 | description: 'Help me, please!', 69 | }, 70 | { 71 | name: 'version', 72 | type: Boolean, 73 | description: 'Get the import-conductor version', 74 | }, 75 | ]; 76 | 77 | export const sections = [ 78 | { 79 | header: 'Import conductor', 80 | content: 'Automatically organize your imports.', 81 | }, 82 | { 83 | header: 'Options', 84 | optionList: optionDefinitions, 85 | }, 86 | ]; 87 | -------------------------------------------------------------------------------- /src/conductor/categorize-imports.ts: -------------------------------------------------------------------------------- 1 | import { isCustomImport } from '../helpers/is-custom-import'; 2 | import { isThirdParty } from '../helpers/is-third-party'; 3 | import { ImportCategories } from '../types'; 4 | 5 | export function categorizeImportLiterals(importLiterals: Map): ImportCategories { 6 | const thirdParty = new Map(); 7 | const userLibrary = new Map(); 8 | const differentModule = new Map(); 9 | const sameModule = new Map(); 10 | 11 | importLiterals.forEach((fullImportStatement: string, importLiteral: string) => { 12 | const normalized = importLiteral.replace(/['"]/g, ''); 13 | 14 | if (normalized.startsWith('./')) { 15 | sameModule.set(importLiteral, fullImportStatement); 16 | return; 17 | } 18 | 19 | if (normalized.startsWith('..')) { 20 | differentModule.set(importLiteral, fullImportStatement); 21 | return; 22 | } 23 | 24 | if (isCustomImport(normalized)) { 25 | userLibrary.set(importLiteral, fullImportStatement); 26 | return; 27 | } 28 | 29 | if (isThirdParty(normalized)) { 30 | thirdParty.set(importLiteral, fullImportStatement); 31 | } else { 32 | differentModule.set(importLiteral, fullImportStatement); 33 | } 34 | }); 35 | 36 | return { thirdParty, differentModule, sameModule, userLibrary }; 37 | } 38 | -------------------------------------------------------------------------------- /src/conductor/collect-import-nodes.ts: -------------------------------------------------------------------------------- 1 | import { Node, isImportDeclaration } from 'typescript'; 2 | 3 | export function collectImportNodes(rootNode: Node): Node[] { 4 | const importNodes: Node[] = []; 5 | const traverse = (node: Node) => { 6 | if (isImportDeclaration(node)) { 7 | importNodes.push(node); 8 | } 9 | }; 10 | rootNode.forEachChild(traverse); 11 | 12 | return importNodes; 13 | } 14 | -------------------------------------------------------------------------------- /src/conductor/collect-non-import-nodes.ts: -------------------------------------------------------------------------------- 1 | import { isImportDeclaration, Node } from 'typescript'; 2 | 3 | export function collectNonImportNodes(rootNode: Node, lastImport: Node): Node[] { 4 | const nonImportNodes: Node[] = []; 5 | let importsEnded = false; 6 | const traverse = (node: Node) => { 7 | importsEnded = importsEnded || node === lastImport; 8 | if (!importsEnded) { 9 | if (!isImportDeclaration(node)) { 10 | nonImportNodes.push(node); 11 | } 12 | } 13 | }; 14 | rootNode.forEachChild(traverse); 15 | 16 | return nonImportNodes; 17 | } 18 | -------------------------------------------------------------------------------- /src/conductor/conduct.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import gitChangedFiles from 'git-changed-files'; 3 | import ora from 'ora'; 4 | 5 | import { resolveConfig, setConfig } from '../config'; 6 | import { log } from '../helpers/log'; 7 | import { Config } from '../types'; 8 | 9 | import { organizeImportsForFile } from './organize-imports'; 10 | import { getFilesPaths } from './get-files-paths'; 11 | import { actions } from './organize-imports'; 12 | 13 | export async function conduct(configuration: Partial): Promise { 14 | const config = resolveConfig(configuration); 15 | setConfig(config); 16 | const { staged, source, verbose, ignore, dryRun } = config; 17 | const filePaths = staged ? (await gitChangedFiles({ showCommitted: false })).unCommittedFiles : getFilesPaths(source); 18 | 19 | if (filePaths.length === 0) { 20 | const msg = staged ? 'No staged files found' : `No matching files for regex: "${source}"`; 21 | console.log(chalk.yellow(`⚠️ ${msg}`)); 22 | return []; 23 | } 24 | 25 | dryRun && console.log('🧪 Dry run 🧪'); 26 | let spinner = verbose ? null : ora('Conducting imports').start(); 27 | const results = { 28 | [actions.skipped]: 0, 29 | [actions.reordered]: 0, 30 | }; 31 | 32 | for await (const path of filePaths) { 33 | const ignoreFile = ignore.includes(path) || ignore.some((p) => path.includes(p)); 34 | if (ignoreFile) { 35 | results[actions.skipped]++; 36 | log('gray', 'skipped (via ignore pattern)', path); 37 | continue; 38 | } 39 | 40 | const actionDone = await organizeImportsForFile(path); 41 | if (actionDone in results) { 42 | results[actionDone]++; 43 | } 44 | } 45 | 46 | let messages = []; 47 | const reorderMessage = 48 | results.reordered === 0 ? '✨ No changes needed in all the files.' : `🔀 ${results.reordered} file imports were reordered.`; 49 | messages.push(reorderMessage); 50 | if (results.skipped > 0) { 51 | messages.push(`🦘 ${results.skipped} file${results.skipped > 1 ? 's were' : ' was'} skipped.`); 52 | } 53 | 54 | spinner?.succeed(`Conducting imports - done!`); 55 | 56 | return messages; 57 | } 58 | -------------------------------------------------------------------------------- /src/conductor/format-import-statements.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../config'; 2 | import { ImportCategories } from '../types'; 3 | 4 | type CategoryEntry = [string, Map]; 5 | 6 | export function formatImportStatements(importCategories: ImportCategories, lineEnding: string) { 7 | const { separator } = getConfig(); 8 | const [first, ...otherCategories] = Object.entries(importCategories) 9 | .filter(hasImports) 10 | .sort(byCategoriesOrder(getConfig().groupOrder)) 11 | .map((imports) => toImportBlock(imports, lineEnding)); 12 | 13 | let result = first || ''; 14 | 15 | for (const imports of otherCategories) { 16 | result += `${separator}${lineEnding}${imports}`; 17 | } 18 | 19 | return result; 20 | } 21 | 22 | function byCategoriesOrder(categoriesOrder: string[]) { 23 | return ([a]: CategoryEntry, [b]: CategoryEntry): number => categoriesOrder.indexOf(a) - categoriesOrder.indexOf(b); 24 | } 25 | 26 | function hasImports([, imports]: CategoryEntry) { 27 | return imports.size > 0; 28 | } 29 | 30 | function toImportBlock([, imports]: CategoryEntry, lineEnding: string) { 31 | return [...imports.values()].map((l) => trim(l, ` ${lineEnding}`)).join(lineEnding); 32 | } 33 | 34 | function escapeRegex(string: string) { 35 | return string.replace(/[\[\](){}?*+\^$\\.|\-]/g, '\\$&'); 36 | } 37 | 38 | function trim(input: string, characters: string) { 39 | characters = escapeRegex(characters); 40 | 41 | return input.replace(new RegExp('^[' + characters + ']+|[' + characters + ']+$', 'g'), ''); 42 | } 43 | -------------------------------------------------------------------------------- /src/conductor/get-files-paths.ts: -------------------------------------------------------------------------------- 1 | import { sync } from 'fast-glob'; 2 | 3 | export function getFilesPaths(source: string[]): string[] { 4 | return source.map((pattern) => sync(pattern, { onlyFiles: true })).flat(); 5 | } 6 | -------------------------------------------------------------------------------- /src/conductor/get-group-order.ts: -------------------------------------------------------------------------------- 1 | import { defaultConfig } from '../defaultConfig'; 2 | import { Config } from '../types'; 3 | 4 | export function getGroupOrder(config: Partial) { 5 | const groups = new Set(config?.groupOrder || []); 6 | const uniqueGroups = Array.from(groups); 7 | return isValidGroupArgument(uniqueGroups) ? uniqueGroups : defaultConfig.groupOrder; 8 | } 9 | 10 | function isValidGroupArgument(groups: string[]): boolean { 11 | return groups.length === defaultConfig.groupOrder.length && groups.every((group) => defaultConfig.groupOrder.includes(group)); 12 | } 13 | -------------------------------------------------------------------------------- /src/conductor/get-import-statement-map.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript'; 2 | 3 | import { getConfig } from '../config'; 4 | 5 | import { mergeImportStatements } from './merge-import-statements'; 6 | 7 | export function getImportStatementMap(importNodes: ts.Node[]): Map { 8 | const { autoMerge } = getConfig(); 9 | const importStatementMap = new Map(); 10 | 11 | importNodes.forEach((node: ts.Node) => { 12 | const importSegments = node.getChildren(); 13 | let importStatement = node.getFullText(); 14 | if (importStatement.startsWith('\n')) { 15 | importStatement = importStatement.replace(/^\n*/, ''); 16 | } 17 | const importLiteral = importSegments.find((segment) => segment.kind === ts.SyntaxKind.StringLiteral)?.getText(); 18 | 19 | if (!importLiteral) { 20 | return; 21 | } 22 | 23 | const existingImport = importStatementMap.get(importLiteral); 24 | const canMerge = autoMerge && existingImport && [existingImport, importStatement].every((i) => !i.includes('*')); 25 | 26 | if (canMerge) { 27 | importStatementMap.set(importLiteral, mergeImportStatements(existingImport, importStatement)); 28 | } else { 29 | const key = existingImport ? `${importLiteral}_${Math.random()}` : importLiteral; 30 | importStatementMap.set(key, importStatement); 31 | } 32 | }); 33 | 34 | return importStatementMap; 35 | } 36 | -------------------------------------------------------------------------------- /src/conductor/get-third-party.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import pkgUp from 'pkg-up'; 3 | 4 | import { getPackageFileDeps } from '../helpers/get-package-file-deps'; 5 | import { getPackageLockFileDeps } from '../helpers/get-package-lock-file-deps'; 6 | import { parseJsonFile } from '../helpers/parse-json-file'; 7 | 8 | export function getThirdParty(): Set { 9 | const packagePath = pkgUp.sync(); 10 | const lockPath = packagePath.replace('package.json', 'package-lock.json'); 11 | let deps: string[]; 12 | 13 | if (fs.existsSync(lockPath)) { 14 | deps = getPackageLockFileDeps(parseJsonFile(lockPath)); 15 | } else { 16 | deps = getPackageFileDeps(parseJsonFile(packagePath)); 17 | } 18 | 19 | return new Set(deps); 20 | } 21 | -------------------------------------------------------------------------------- /src/conductor/merge-import-statements.ts: -------------------------------------------------------------------------------- 1 | export function mergeImportStatements(importStatementOne, importStatementTwo): string { 2 | let importedValues = importStatementTwo.replace(/\n\s*/g, '').match('{(.*)}')[1]; 3 | const hasTrailingComma = /,\s*}/.test(importStatementOne); 4 | importedValues = hasTrailingComma ? `${importedValues},}` : `,${importedValues}}`; 5 | 6 | return importStatementOne.replace('}', importedValues); 7 | } 8 | -------------------------------------------------------------------------------- /src/conductor/organize-imports.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync, existsSync } from 'fs'; 2 | import simpleGit, { SimpleGit } from 'simple-git'; 3 | import ts from 'typescript'; 4 | 5 | import { getConfig } from '../config'; 6 | import { detectLineEnding } from '../helpers/line-ending-detector'; 7 | import { log } from '../helpers/log'; 8 | 9 | import { categorizeImportLiterals } from './categorize-imports'; 10 | import { collectImportNodes } from './collect-import-nodes'; 11 | import { collectNonImportNodes } from './collect-non-import-nodes'; 12 | import { formatImportStatements } from './format-import-statements'; 13 | import { getImportStatementMap } from './get-import-statement-map'; 14 | import { sortImportCategories } from './sort-import-categories'; 15 | 16 | const git: SimpleGit = simpleGit(); 17 | 18 | export const actions = { 19 | none: 'none', 20 | skipped: 'skipped', 21 | reordered: 'reordered', 22 | }; 23 | 24 | function getFileComment(fileContent: string): string { 25 | const fileComment = /^(?:(\/\/.*)|(\/\*[^]*?\*\/))/.exec(fileContent); 26 | if (fileComment) { 27 | const [singleLine, multiLine] = fileComment; 28 | return singleLine || multiLine; 29 | } 30 | 31 | return ''; 32 | } 33 | 34 | export async function organizeImportsForFile(filePath: string): Promise { 35 | // staged files might also include deleted files, we need to verify they exist. 36 | if (!/\.tsx?$/.test(filePath) || !existsSync(filePath)) { 37 | return actions.none; 38 | } 39 | 40 | let fileContent = readFileSync(filePath).toString(); 41 | if (/\/[/*]\s*import-conductor-skip/.test(fileContent)) { 42 | log('gray', 'skipped (via comment)', filePath); 43 | return actions.skipped; 44 | } 45 | const { staged, autoAdd, dryRun } = getConfig(); 46 | const fileWithOrganizedImports = await organizeImports(fileContent); 47 | const fileHasChanged = fileWithOrganizedImports !== fileContent; 48 | const isValidAction = [actions.none, actions.skipped].every((action) => action !== fileWithOrganizedImports); 49 | 50 | if (fileHasChanged && isValidAction) { 51 | !dryRun && writeFileSync(filePath, fileWithOrganizedImports); 52 | let msg = 'imports reordered'; 53 | if (staged && autoAdd) { 54 | await git.add(filePath); 55 | msg += ', added to git'; 56 | } 57 | log('green', msg, filePath); 58 | return actions.reordered; 59 | } 60 | 61 | log('gray', 'no change needed', filePath); 62 | return actions.none; 63 | } 64 | 65 | export async function organizeImports(fileContent: string): Promise { 66 | const lineEnding = detectLineEnding(fileContent); 67 | if (/\/[/*]\s*import-conductor-skip/.test(fileContent)) { 68 | log('gray', 'Format skipped (via comment)'); 69 | return actions.skipped; 70 | } 71 | 72 | let fileComment = getFileComment(fileContent); 73 | if (fileComment) { 74 | fileContent = fileContent.replace(fileComment, ''); 75 | } 76 | 77 | const rootNode = ts.createSourceFile('temp', fileContent, ts.ScriptTarget.Latest, true); 78 | const importNodes = collectImportNodes(rootNode); 79 | const importStatementMap = getImportStatementMap(importNodes); 80 | if (importStatementMap.size === 0) { 81 | return actions.none; 82 | } 83 | 84 | const categorizedImports = categorizeImportLiterals(importStatementMap); 85 | const sortedAndCategorizedImports = sortImportCategories(categorizedImports); 86 | let updatedContent = formatImportStatements(sortedAndCategorizedImports, lineEnding); 87 | 88 | const lastImport = importNodes.pop(); 89 | const contentWithoutImportStatements = fileContent.slice(lastImport.end); 90 | 91 | const nonImportNodes = collectNonImportNodes(rootNode, lastImport); 92 | if (nonImportNodes) { 93 | updatedContent += nonImportNodes.map((n) => n.getFullText()).join(''); 94 | } 95 | 96 | updatedContent += contentWithoutImportStatements; 97 | 98 | if (fileComment) { 99 | updatedContent = `${fileComment}\n` + updatedContent; 100 | } 101 | return updatedContent; 102 | } 103 | -------------------------------------------------------------------------------- /src/conductor/sort-import-categories.ts: -------------------------------------------------------------------------------- 1 | import { ImportCategories } from '../types'; 2 | 3 | export function sortImportCategories(importCategories: ImportCategories): ImportCategories { 4 | return Object.keys(importCategories).reduce((sorted, key) => { 5 | sorted[key] = new Map([...importCategories[key]].sort()); 6 | 7 | return sorted; 8 | }, {}) as ImportCategories; 9 | } 10 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { sync } from 'fast-glob'; 2 | 3 | import { getGroupOrder } from './conductor/get-group-order'; 4 | import { getThirdParty } from './conductor/get-third-party'; 5 | import { defaultConfig } from './defaultConfig'; 6 | import { CliConfig, Config } from './types'; 7 | 8 | let config: Config; 9 | 10 | export function resolveConfig(cliConfig: Partial): Config { 11 | const { noAutoAdd, ...rest } = cliConfig; 12 | const normalized = { 13 | ...rest, 14 | autoAdd: !noAutoAdd, 15 | }; 16 | 17 | const merged = { 18 | ...defaultConfig, 19 | ...normalized, 20 | thirdPartyDependencies: getThirdParty(), 21 | groupOrder: getGroupOrder(normalized), 22 | }; 23 | if (merged.ignore.length > 0) { 24 | merged.ignore = merged.ignore.map((pattern) => (pattern.includes('*') ? sync(pattern) : pattern)).flat(); 25 | } 26 | 27 | return merged; 28 | } 29 | 30 | export function getConfig(): Config { 31 | return config; 32 | } 33 | 34 | export function setConfig(cliConfig: Config) { 35 | config = cliConfig; 36 | } 37 | -------------------------------------------------------------------------------- /src/defaultConfig.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './types'; 2 | 3 | export const defaultConfig: Config = { 4 | source: ['./src/**/*.ts'], 5 | userLibPrefixes: [], 6 | separator: '\n', 7 | autoMerge: true, 8 | autoAdd: false, 9 | verbose: false, 10 | staged: false, 11 | dryRun: false, 12 | ignore: [], 13 | groupOrder: ['thirdParty', 'userLibrary', 'differentModule', 'sameModule'], 14 | }; 15 | -------------------------------------------------------------------------------- /src/helpers/get-lock-file-version.ts: -------------------------------------------------------------------------------- 1 | export function getLockFileVersion(packageLockContent: any) { 2 | return packageLockContent?.lockfileVersion ?? -1; 3 | } 4 | -------------------------------------------------------------------------------- /src/helpers/get-package-file-deps.ts: -------------------------------------------------------------------------------- 1 | export function getPackageFileDeps(packageContent: any): string[] { 2 | const { dependencies, devDependencies } = packageContent; 3 | return Object.keys(devDependencies).concat(Object.keys(dependencies)); 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/get-package-lock-file-deps.ts: -------------------------------------------------------------------------------- 1 | import { getLockFileVersion } from './get-lock-file-version'; 2 | 3 | export function getPackageLockFileDeps(packageLockContent: any): string[] { 4 | const lockFileVersion = getLockFileVersion(packageLockContent); 5 | if (lockFileVersion < 3) { 6 | return Object.keys(packageLockContent.dependencies); 7 | } 8 | return Object.keys(packageLockContent.packages[''].dependencies); 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/is-custom-import.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../config'; 2 | 3 | export function isCustomImport(literal: string): boolean { 4 | return getConfig().userLibPrefixes.some((prefix) => literal.startsWith(prefix)); 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/is-third-party.ts: -------------------------------------------------------------------------------- 1 | import { getConfig } from '../config'; 2 | 3 | function breakdownPath(path: string): string[] { 4 | return path.split('/').map((_, i, arr) => arr.slice(0, i + 1).join('/')); 5 | } 6 | 7 | export function isThirdParty(libName: string) { 8 | let isThirdPartyModule = false; 9 | const { thirdPartyDependencies, userLibPrefixes } = getConfig(); 10 | 11 | try { 12 | isThirdPartyModule = require.resolve(libName).indexOf('/') < 0; 13 | } catch { 14 | console.log(); 15 | console.warn(`⚡ You are importing ${libName} but it is not installed.`); 16 | console.warn(`⚡ Trying to figure out import category based on library name: ${libName}`); 17 | isThirdPartyModule = !libName.startsWith('.') && !userLibPrefixes.some((prefix) => libName.startsWith(prefix)); 18 | } 19 | return isThirdPartyModule || breakdownPath(libName).some((subPath) => thirdPartyDependencies.has(subPath)); 20 | } 21 | -------------------------------------------------------------------------------- /src/helpers/line-ending-detector.ts: -------------------------------------------------------------------------------- 1 | export const detectLineEnding = (value: string) => { 2 | if (typeof value !== 'string') { 3 | throw new TypeError('Expected a string for new line detection'); 4 | } 5 | const newlines = value.match(/(?:\r?\n)/g) || []; 6 | 7 | if (newlines.length === 0) { 8 | return; 9 | } 10 | 11 | const crlf = newlines.filter((newline) => newline === '\r\n').length; 12 | const lf = newlines.length - crlf; 13 | return crlf > lf ? '\r\n' : '\n'; 14 | }; 15 | -------------------------------------------------------------------------------- /src/helpers/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | import { getConfig } from '../config'; 4 | 5 | export function log(color: string, message: string, file?: string) { 6 | getConfig().verbose && file ? console.log(chalk[color](`${file} - ${message}`)) : console.log(chalk[color](`${message}`)); 7 | } 8 | -------------------------------------------------------------------------------- /src/helpers/parse-json-file.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export function parseJsonFile(path) { 4 | return JSON.parse(fs.readFileSync(path).toString()); 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // import-conductor-skip 3 | import './pollyfils'; 4 | import commandLineArgs from 'command-line-args'; 5 | import commandLineUsage from 'command-line-usage'; 6 | 7 | import { packageVersion } from './version'; 8 | import chalk from 'chalk'; 9 | 10 | import { organizeImports } from './conductor/organize-imports'; 11 | import { optionDefinitions, sections } from './cliOptions'; 12 | import { conduct } from './conductor/conduct'; 13 | 14 | export { conduct, organizeImports }; 15 | 16 | const cliConfig = commandLineArgs(optionDefinitions, { 17 | camelCase: true, 18 | stopAtFirstUnknown: true, 19 | }); 20 | const { help, version } = cliConfig; 21 | 22 | if (version) { 23 | console.log(packageVersion); 24 | process.exit(); 25 | } 26 | 27 | if (help) { 28 | const usage = commandLineUsage(sections); 29 | console.log(usage); 30 | process.exit(); 31 | } 32 | 33 | conduct(cliConfig).then((summary) => { 34 | if (summary.length) { 35 | const message = `${chalk.underline('🏁 Summary:')}\n${summary.join('\n')}\n`; 36 | console.log(message); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /src/pollyfils.ts: -------------------------------------------------------------------------------- 1 | if (!Array.prototype.flat) { 2 | // 1 lvl of nested arrays 3 | Array.prototype.flat = function () { 4 | return (this as any).reduce((acc, val) => { 5 | if (Array.isArray(val)) { 6 | return acc.concat(val); 7 | } 8 | acc.push(val); 9 | 10 | return acc; 11 | }, []); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ImportCategories { 2 | [key: string]: Map; 3 | } 4 | 5 | export interface Config { 6 | verbose: boolean; 7 | dryRun: boolean; 8 | staged: boolean; 9 | autoAdd: boolean; 10 | autoMerge: boolean; 11 | source: string[]; 12 | separator: string; 13 | ignore: string[]; 14 | userLibPrefixes: string[]; 15 | thirdPartyDependencies?: Set; 16 | groupOrder: string[]; 17 | } 18 | 19 | export type CliConfig = Omit & { noAutoAdd: boolean }; 20 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | export const packageVersion = '2.0.3'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "lib": ["es2017", "esnext", "es2015"], 11 | "resolveJsonModule": true, 12 | "types": ["node"] 13 | }, 14 | "include": ["src"], 15 | "exclude": ["node_modules", "**/*.spec.ts", "playground"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jest", "node"], 7 | "paths": { 8 | "@ic/*": ["src/*"] 9 | } 10 | }, 11 | "include": ["**/*.spec.ts"], 12 | "exclude": [] 13 | } 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": {}, 5 | "rules": {}, 6 | "rulesDirectory": [] 7 | } 8 | --------------------------------------------------------------------------------