├── .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 | 
2 |
3 |
4 | [](#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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------