├── .npmrc ├── .gitignore ├── .github ├── funding.yml └── workflows │ └── test.yml ├── tsconfig.json ├── lib ├── find-tsconfig.js ├── apply-text-changes.js ├── memoize.js ├── get-compiler-options.js ├── organize.js ├── service-host.js └── get-language-service.js ├── .editorconfig ├── prettier.d.ts ├── test ├── _utils.js ├── main.js └── vue.js ├── license ├── package.json ├── index.js ├── changelog.md └── readme.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: simonhaenisch 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "checkJs": true, 6 | "strict": true, 7 | "skipLibCheck": true, 8 | "noEmit": true 9 | }, 10 | "vueCompilerOptions": { "plugins": ["@vue/language-plugin-pug"] } 11 | } 12 | -------------------------------------------------------------------------------- /lib/find-tsconfig.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | const { memoize } = require('./memoize'); 3 | 4 | /** 5 | * Find the path of the project's tsconfig from a path to a file in the project. 6 | * 7 | * @type {(path: string) => string | undefined} 8 | */ 9 | module.exports.findTsconfig = memoize((path) => ts.findConfigFile(path, ts.sys.fileExists)); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [package.json] 13 | indent_style = space 14 | 15 | [*.yml] 16 | indent_style = space 17 | 18 | [*.md] 19 | indent_style = space 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /prettier.d.ts: -------------------------------------------------------------------------------- 1 | import ts = require('typescript'); 2 | 3 | declare module 'prettier' { 4 | interface Options { 5 | organizeImportsSkipDestructiveCodeActions?: boolean; 6 | organizeImportsTypeOrder?: ts.OrganizeImportsTypeOrder; 7 | } 8 | interface ParserOptions { 9 | organizeImportsSkipDestructiveCodeActions?: boolean; 10 | organizeImportsTypeOrder?: ts.OrganizeImportsTypeOrder; 11 | } 12 | } 13 | 14 | export {}; 15 | -------------------------------------------------------------------------------- /lib/apply-text-changes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply the given set of text changes to the input. 3 | * 4 | * @param {string} input 5 | * @param {readonly import('typescript').TextChange[]} changes 6 | */ 7 | module.exports.applyTextChanges = (input, changes) => 8 | changes.reduceRight((text, change) => { 9 | const head = text.slice(0, change.span.start); 10 | const tail = text.slice(change.span.start + change.span.length); 11 | 12 | return `${head}${change.newText}${tail}`; 13 | }, input); 14 | -------------------------------------------------------------------------------- /lib/memoize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple memoization utility that only uses the first argument as cache key and has no memory limit. 3 | * 4 | * @template {(...args: any[]) => any} F 5 | * @param {F} f 6 | * @returns {F} 7 | */ 8 | module.exports.memoize = (f) => { 9 | const cache = new Map(); 10 | 11 | // @ts-ignore 12 | return function (cacheKey, ...rest) { 13 | if (cache.has(cacheKey)) { 14 | return cache.get(cacheKey); 15 | } 16 | 17 | const result = f(cacheKey, ...rest); 18 | 19 | cache.set(cacheKey, result); 20 | 21 | return result; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | matrix: 9 | os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 10 | node: [16, 18, 20, 22] 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | 19 | - name: Use Node.js ${{ matrix.node }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node }} 23 | 24 | - run: npm install 25 | 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /lib/get-compiler-options.js: -------------------------------------------------------------------------------- 1 | const { dirname } = require('path'); 2 | const ts = require('typescript'); 3 | const { memoize } = require('./memoize'); 4 | 5 | /** 6 | * Get the compiler options from the path to a tsconfig. 7 | * 8 | * @param {string | undefined} tsconfig path to tsconfig 9 | */ 10 | function getCompilerOptions(tsconfig) { 11 | const compilerOptions = tsconfig 12 | ? ts.parseJsonConfigFileContent(ts.readConfigFile(tsconfig, ts.sys.readFile).config, ts.sys, dirname(tsconfig)) 13 | .options 14 | : ts.getDefaultCompilerOptions(); 15 | 16 | compilerOptions.allowJs = true; // for automatic JS support 17 | compilerOptions.allowNonTsExtensions = true // for Vue support 18 | 19 | return compilerOptions; 20 | } 21 | 22 | module.exports.getCompilerOptions = memoize(getCompilerOptions); 23 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | const prettier = require('prettier'); 2 | const { macro } = require('ava').default; 3 | 4 | /** 5 | * @param {string} code 6 | * @param {prettier.Options} [options] 7 | */ 8 | module.exports.prettify = async (code, options) => 9 | prettier.format(code, { plugins: ['./index.js'], filepath: 'file.ts', ...options }); 10 | 11 | /** 12 | * @param {prettier.Options['parser']} parser 13 | */ 14 | module.exports.getMacro = (parser) => 15 | macro({ 16 | /** 17 | * @param {import('ava').ExecutionContext} t 18 | * @param {string} input 19 | * @param {string} expected 20 | * @param {{ options?: import('prettier').Options; transformer?: (res: string) => string }} [options] 21 | */ 22 | async exec(t, input, expected, { options = {}, transformer = (res) => res.split('\n')[0] } = {}) { 23 | const formattedCode = await module.exports.prettify(input, { parser, ...options }); 24 | 25 | t.is(transformer(formattedCode), expected); 26 | }, 27 | title(providedTitle = '') { 28 | return `[${parser}] ${providedTitle}`; 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Simon Hänisch 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 | -------------------------------------------------------------------------------- /lib/organize.js: -------------------------------------------------------------------------------- 1 | const { sep, posix } = require('path'); 2 | const { applyTextChanges } = require('./apply-text-changes'); 3 | const { getLanguageService } = require('./get-language-service'); 4 | 5 | /** 6 | * Organize the given code's imports. 7 | * 8 | * @param {string} code 9 | * @param {import('prettier').ParserOptions} options 10 | */ 11 | module.exports.organize = ( 12 | code, 13 | { filepath = 'file.ts', organizeImportsSkipDestructiveCodeActions, parentParser, parser, organizeImportsTypeOrder }, 14 | ) => { 15 | if (parentParser === 'vue') { 16 | // we already did the preprocessing in the parent parser, so we skip the child parsers 17 | return code; 18 | } 19 | 20 | if (sep !== posix.sep) { 21 | filepath = filepath.split(sep).join(posix.sep); 22 | } 23 | 24 | const languageService = getLanguageService(parser, filepath, code); 25 | 26 | const fileChanges = languageService.organizeImports( 27 | { type: 'file', fileName: filepath, skipDestructiveCodeActions: organizeImportsSkipDestructiveCodeActions }, 28 | {}, 29 | { organizeImportsTypeOrder }, 30 | )[0]; 31 | 32 | return fileChanges ? applyTextChanges(code, fileChanges.textChanges) : code; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/service-host.js: -------------------------------------------------------------------------------- 1 | const { dirname } = require('path'); 2 | const ts = require('typescript'); 3 | const { findTsconfig } = require('./find-tsconfig'); 4 | 5 | const { getCompilerOptions } = require('./get-compiler-options'); 6 | 7 | /** 8 | * Create the most basic TS language service host for the given file to make import sorting work. 9 | * 10 | * @param {string} path path to file 11 | * @param {string} content file's content 12 | * 13 | * @returns {ts.LanguageServiceHost} 14 | */ 15 | function getTypeScriptLanguageServiceHost(path, content) { 16 | const tsconfig = findTsconfig(path); 17 | const compilerOptions = getCompilerOptions(tsconfig); 18 | const snapshot = ts.ScriptSnapshot.fromString(content); 19 | 20 | return { 21 | directoryExists: ts.sys.directoryExists, 22 | fileExists: ts.sys.fileExists, 23 | getDefaultLibFileName: ts.getDefaultLibFileName, 24 | getDirectories: ts.sys.getDirectories, 25 | readDirectory: ts.sys.readDirectory, 26 | readFile: ts.sys.readFile, 27 | getCurrentDirectory: () => (tsconfig ? dirname(tsconfig) : ts.sys.getCurrentDirectory()), 28 | getCompilationSettings: () => compilerOptions, 29 | getNewLine: () => ts.sys.newLine, 30 | getScriptFileNames: () => [path], 31 | getScriptVersion: () => '0', 32 | getScriptSnapshot: (filePath) => { 33 | if (filePath === path) { 34 | return snapshot; 35 | } 36 | }, 37 | }; 38 | } 39 | 40 | module.exports = { getTypeScriptLanguageServiceHost }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier-plugin-organize-imports", 3 | "version": "4.3.0", 4 | "description": "Make prettier organize your imports using the TypeScript language service API.", 5 | "keywords": [ 6 | "prettier", 7 | "prettier-plugin", 8 | "typescript", 9 | "imports", 10 | "organize-imports" 11 | ], 12 | "main": "index.js", 13 | "scripts": { 14 | "test": "tsc && ava --verbose", 15 | "preversion": "npm test" 16 | }, 17 | "files": [ 18 | "index.js", 19 | "lib", 20 | "prettier.d.ts" 21 | ], 22 | "author": "Simon Haenisch (https://github.com/simonhaenisch)", 23 | "license": "MIT", 24 | "repository": "simonhaenisch/prettier-plugin-organize-imports", 25 | "homepage": "https://github.com/simonhaenisch/prettier-plugin-organize-imports#readme", 26 | "peerDependencies": { 27 | "prettier": ">=2.0", 28 | "typescript": ">=2.9", 29 | "vue-tsc": "^2.1.0 || 3" 30 | }, 31 | "peerDependenciesMeta": { 32 | "vue-tsc": { 33 | "optional": true 34 | } 35 | }, 36 | "devDependencies": { 37 | "@types/node": "24.5.2", 38 | "@vue/language-plugin-pug": "3.0.7", 39 | "ava": "6.4.1", 40 | "prettier": "3.6.2", 41 | "typescript": "5.9.2", 42 | "vue-tsc": "3.0.7" 43 | }, 44 | "prettier": { 45 | "printWidth": 120, 46 | "singleQuote": true, 47 | "trailingComma": "all", 48 | "useTabs": true, 49 | "overrides": [ 50 | { 51 | "files": [ 52 | "package.json", 53 | "*.md" 54 | ], 55 | "options": { 56 | "useTabs": false 57 | } 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @todo change these to their new locations (`prettier/plugins/`) with 3 | * the next major release. (requires dropping Prettier 2.x support) 4 | */ 5 | // @ts-expect-error 6 | const { parsers: babelParsers } = require('prettier/parser-babel'); 7 | // @ts-expect-error 8 | const { parsers: htmlParsers } = require('prettier/parser-html'); 9 | // @ts-expect-error 10 | const { parsers: typescriptParsers } = require('prettier/parser-typescript'); 11 | 12 | const { organize } = require('./lib/organize'); 13 | 14 | /** 15 | * Organize the code's imports using the `organizeImports` feature of the TypeScript language service API. 16 | * 17 | * @param {string} code 18 | * @param {import('prettier').ParserOptions} options 19 | */ 20 | const organizeImports = (code, options) => { 21 | if (code.includes('// organize-imports-ignore') || code.includes('// tslint:disable:ordered-imports')) { 22 | return code; 23 | } 24 | 25 | const isRange = 26 | Boolean(options.originalText) || 27 | options.rangeStart !== 0 || 28 | (options.rangeEnd !== Infinity && options.rangeEnd !== code.length); 29 | 30 | if (isRange) { 31 | return code; // processing a range doesn't make sense 32 | } 33 | 34 | try { 35 | return organize(code, options); 36 | } catch (error) { 37 | if (process.env.DEBUG) { 38 | console.error(error); 39 | } 40 | 41 | return code; 42 | } 43 | }; 44 | 45 | /** 46 | * Set `organizeImports` as the given parser's `preprocess` hook, or merge it with the existing one. 47 | * 48 | * @param {import('prettier').Parser} parser prettier parser 49 | */ 50 | const withOrganizeImportsPreprocess = (parser) => { 51 | return { 52 | ...parser, 53 | /** 54 | * @param {string} code 55 | * @param {import('prettier').ParserOptions} options 56 | */ 57 | preprocess: (code, options) => 58 | organizeImports(parser.preprocess ? parser.preprocess(code, options) : code, options), 59 | }; 60 | }; 61 | 62 | /** 63 | * @type {import('prettier').Plugin} 64 | */ 65 | const plugin = { 66 | options: { 67 | organizeImportsSkipDestructiveCodeActions: { 68 | type: 'boolean', 69 | default: false, 70 | category: 'OrganizeImports', 71 | description: 'Skip destructive code actions like removing unused imports.', 72 | }, 73 | organizeImportsTypeOrder: { 74 | type: 'choice', 75 | choices: [ 76 | { 77 | value: 'last', 78 | description: 'Places type imports last.', 79 | }, 80 | { 81 | value: 'first', 82 | description: 'Places type imports first.', 83 | }, 84 | { 85 | value: 'inline', 86 | description: 'Keeps type imports in place.', 87 | }, 88 | ], 89 | category: 'OrganizeImports', 90 | description: 'How to sort type imports when mixed in an import statement.', 91 | }, 92 | }, 93 | parsers: { 94 | babel: withOrganizeImportsPreprocess(babelParsers.babel), 95 | 'babel-ts': withOrganizeImportsPreprocess(babelParsers['babel-ts']), 96 | typescript: withOrganizeImportsPreprocess(typescriptParsers.typescript), 97 | vue: withOrganizeImportsPreprocess(htmlParsers.vue), 98 | }, 99 | }; 100 | 101 | module.exports = plugin; 102 | -------------------------------------------------------------------------------- /lib/get-language-service.js: -------------------------------------------------------------------------------- 1 | const ts = require('typescript'); 2 | 3 | const { findTsconfig } = require('./find-tsconfig'); 4 | const { getTypeScriptLanguageServiceHost } = require('./service-host'); 5 | 6 | /** 7 | * Get the correct language service for the given parser. 8 | * 9 | * @param {import('prettier').ParserOptions['parser']} parser 10 | * @param {string} filepath 11 | * @param {string} code 12 | * 13 | * @returns {import('typescript').LanguageService} 14 | */ 15 | const getLanguageService = (parser, filepath, code) => { 16 | const langaugeServiceHost = getTypeScriptLanguageServiceHost(filepath, code); 17 | 18 | const languageService = ts.createLanguageService(langaugeServiceHost); 19 | 20 | switch (parser) { 21 | case 'vue': 22 | return getVueDecoratedProxyLanguageService(langaugeServiceHost, languageService, filepath); 23 | 24 | /** @todo add svelte support */ 25 | 26 | default: 27 | return languageService; 28 | } 29 | }; 30 | 31 | /** 32 | * Decorate a language service so it can handle Vue files. 33 | * 34 | * @param {import('typescript').LanguageServiceHost} langaugeServiceHost 35 | * @param {import('typescript').LanguageService} languageService 36 | * @param {string} filepath 37 | * 38 | * @returns {import('typescript').LanguageService} 39 | */ 40 | function getVueDecoratedProxyLanguageService(langaugeServiceHost, languageService, filepath) { 41 | const vueTscDir = tryCatch(() => require('path').dirname(require.resolve('vue-tsc/package.json'))); 42 | 43 | if (!vueTscDir) { 44 | console.error('Please install vue-tsc to organize imports in Vue files.'); 45 | return languageService; 46 | } 47 | 48 | const { createProxyLanguageService, decorateLanguageServiceHost } = require( 49 | require.resolve('@volar/typescript', { paths: [vueTscDir] }), 50 | ); 51 | 52 | /** @type {import('@vue/language-core')} */ 53 | const { 54 | createLanguage, 55 | createVueLanguagePlugin, 56 | FileMap, 57 | createParsedCommandLine, 58 | getDefaultCompilerOptions, 59 | } = require(require.resolve('@vue/language-core', { paths: [vueTscDir] })); 60 | 61 | const tsconfig = findTsconfig(filepath); 62 | 63 | const vueLanguagePlugin = createVueLanguagePlugin( 64 | ts, 65 | langaugeServiceHost.getCompilationSettings(), 66 | tsconfig ? createParsedCommandLine(ts, ts.sys, tsconfig).vueOptions : getDefaultCompilerOptions(), 67 | (s) => s, 68 | ); 69 | 70 | const language = createLanguage([vueLanguagePlugin], new FileMap(ts.sys.useCaseSensitiveFileNames), () => {}); 71 | 72 | const snapshot = langaugeServiceHost.getScriptSnapshot(filepath); 73 | 74 | if (snapshot) { 75 | language.scripts.set(filepath, snapshot); 76 | } 77 | 78 | const { initialize, proxy } = createProxyLanguageService(languageService); 79 | 80 | initialize(language); 81 | 82 | decorateLanguageServiceHost(ts, language, langaugeServiceHost); 83 | 84 | return proxy; 85 | } 86 | 87 | /** 88 | * @template T 89 | * @param {() => T} fn 90 | * @returns {T | undefined} 91 | */ 92 | function tryCatch(fn) { 93 | try { 94 | return fn(); 95 | } catch { 96 | return undefined; 97 | } 98 | } 99 | 100 | module.exports = { getLanguageService }; 101 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | Version `4.2.0` switches from using `vue-tsc`'s deprecated `resolveVueCompilerOptions` to the new `getDefaultCompilerOptions` instead, which then also allowed bumping the `vue-tsc` peer dependency range to include version 3. 2 | 3 | Version `4.1.0` bumps the peer dependency range for `vue-tsc` to `^2.1.0` because there was a breaking change in its API. If you're using Vue support, upgrade both packages simultaneously, e.g. `npm i -D prettier-plugin-organize-imports vue-tsc`. 4 | 5 | Version `4.0.0` upgrades/replaces the Volar packages used for Vue support, to use the latest `vue-tsc` package that's part of Volar 2. To migrate, you just have to remove `@volar/vue-typescript` and if you're using it, also `@volar/vue-language-plugin-pug`, and replace it with `vue-tsc` and `@vue/language-plugin-pug` respectively. There are no breaking changes other than this. 6 | 7 | Version `3.2.4` implements a fix to skip when formatting ranges (i.e. if the plugin is requested to format a range, it doesn't do anything because it would lack the full file context). 8 | 9 | Version `3.2.3` updates the readme with instructions for Prettier 3. 10 | 11 | Version `3.2.2` fixes a performance regression introduced in `3.2.0`. 12 | 13 | Version `3.2.1` fixes the implementation of the language service host's `getCurrentDirectory` method to return the directory containing the tsconfig, rather than using `ts.sys.getCurrentDirectory` (which returns `process.cwd()`). This should prevent issues with resolving compiler plugins with Volar (which is used for Vue support). 14 | 15 | Version `3.2.0` adds and fixes support for pug templates in Vue files (via `@volar/vue-language-plugin-pug`). Please be aware that you'll need to update your version of the `@volar/vue-typescript` peer dependency from `0.x` to `1.x`. 16 | 17 | Version `3.1.0` adds an option to skip destructive code actions like removing unused imports. 18 | 19 | Version `3.0.3` fixes a performance regression introduced in `3.0.2`. 20 | 21 | Version `3.0.2` fixes a regression introduced by adding some file-system related methods to the language service host (to fix a bug), which revealed that another method's implementation was incorrect. 22 | 23 | Version `3.0.1` bumps the `@volar/vue-typescript` version to fix more edge cases, e. g. not removing imports when a component is used via kebab-case naming. `@volar/vue-typescript` is now defined as an optional peer dependency and you'll need to install version `0.39` or later. Furthermore a fix has been added that should help support more module resolution algorithms. 24 | 25 | Version `3.0.0` switches to a different package for Vue support, which fixes some more issues, e. g. support for setup scripts. No breaking changes otherwise. 26 | 27 | Version `2.3.4` fixes an issue with Vue v2 files. 28 | 29 | Version `2.3.3` fixes a bug where default imports were removed erroneously. 30 | 31 | Version `2.3.1` adds debug logs and fixes Vue.js support. 32 | 33 | Version `2.2.0` adds a compiler options cache to improve performance. 34 | 35 | Version `2.1.0` adds support for Vue.js (`.vue` files). 36 | 37 | Version `2.0.0` adds support for the parsers `babel` (i. e. JavaScript) and `babel-ts` which are only available since Prettier v2 (and thus the peer dependency has received a major bump). 38 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | const { getMacro, prettify } = require('./_utils'); 2 | const test = require('ava').default; 3 | 4 | const macros = ['typescript', 'babel', 'babel-ts'].map((parser) => getMacro(parser)); 5 | 6 | for (const macro of macros) { 7 | test( 8 | 'sorts imports', 9 | macro, 10 | ` 11 | import { foo, bar } from "foobar" 12 | 13 | export const foobar = foo + bar 14 | `, 15 | 'import { bar, foo } from "foobar";', 16 | ); 17 | 18 | test( 19 | 'removes partially unused imports', 20 | macro, 21 | ` 22 | import { foo, bar, baz } from "foobar"; 23 | 24 | const foobar = foo + baz 25 | `, 26 | 'import { baz, foo } from "foobar";', 27 | ); 28 | 29 | test('removes completely unused imports', macro, 'import { foo } from "foobar"', ''); 30 | 31 | test( 32 | 'works with multi-line imports', 33 | macro, 34 | ` 35 | import { 36 | foo, 37 | bar, 38 | baz, 39 | } from "foobar"; 40 | 41 | console.log({ foo, bar, baz }); 42 | `, 43 | 'import { bar, baz, foo } from "foobar";', 44 | ); 45 | 46 | test( 47 | 'works without a filepath', 48 | macro, 49 | ` 50 | import { foo, bar } from "foobar" 51 | 52 | export const foobar = foo + bar 53 | `, 54 | 'import { bar, foo } from "foobar";', 55 | { options: { filepath: undefined } }, 56 | ); 57 | 58 | test( 59 | 'files with `// organize-imports-ignore` are skipped', 60 | macro, 61 | ` 62 | // organize-imports-ignore 63 | import { foo, bar } from "foobar" 64 | 65 | export const foobar = foo + bar 66 | `, 67 | 'import { foo, bar } from "foobar";', 68 | { transformer: (res) => res.split('\n')[1] }, 69 | ); 70 | } 71 | 72 | test('skips when formatting a range', async (t) => { 73 | const code = 'import { foo } from "./bar";'; 74 | 75 | const formattedCode1 = await prettify(code, { rangeEnd: 10 }); 76 | const formattedCode2 = await prettify(code, { rangeStart: 10 }); 77 | 78 | t.is(formattedCode1, code); 79 | t.is(formattedCode2, code); 80 | }); 81 | 82 | test('does not remove unused imports with `organizeImportsSkipDestructiveCodeActions` enabled', async (t) => { 83 | const code = `import { foo } from "./bar"; 84 | `; 85 | 86 | const formattedCode = await prettify(code, { organizeImportsSkipDestructiveCodeActions: true }); 87 | 88 | t.is(formattedCode, code); 89 | }); 90 | 91 | test('sorts type imports according to the `organizeImportsTypeOrder` option', async (t) => { 92 | const code = `import { foo, type baz, bar } from "./foobarbaz";\n\nexport const foobar: baz = foo + bar;\n`; 93 | 94 | const formattedCode1 = await prettify(code, { parser: 'typescript', organizeImportsTypeOrder: 'last' }); 95 | const formattedCode2 = await prettify(code, { parser: 'typescript', organizeImportsTypeOrder: 'first' }); 96 | const formattedCode3 = await prettify(code, { parser: 'typescript', organizeImportsTypeOrder: 'inline' }); 97 | 98 | t.is(formattedCode1, `import { bar, foo, type baz } from "./foobarbaz";\n\nexport const foobar: baz = foo + bar;\n`); 99 | t.is(formattedCode2, `import { type baz, bar, foo } from "./foobarbaz";\n\nexport const foobar: baz = foo + bar;\n`); 100 | t.is(formattedCode3, `import { bar, type baz, foo } from "./foobarbaz";\n\nexport const foobar: baz = foo + bar;\n`); 101 | }); 102 | -------------------------------------------------------------------------------- /test/vue.js: -------------------------------------------------------------------------------- 1 | const test = require('ava').default; 2 | const ts = require('typescript'); 3 | const { prettify } = require('./_utils'); 4 | 5 | test('works with TypeScript code inside Vue files', async (t) => { 6 | const code = ` 7 | 12 | `; 13 | 14 | const formattedCode = await prettify(code, { filepath: 'file.vue' }); 15 | 16 | t.is(formattedCode.split('\n')[1], `import { compile, defineComponent } from "vue";`); 17 | }); 18 | 19 | test('works with Vue setup scripts', async (t) => { 20 | const code = ` 21 | 25 | `; 26 | 27 | const formattedCode = await prettify(code, { filepath: 'file.vue' }); 28 | 29 | t.is(formattedCode.split('\n')[1], `import { defineComponent } from "vue";`); 30 | }); 31 | 32 | test('preserves new lines and comments in Vue files', async (t) => { 33 | const code = ` 44 | 45 | 46 | `; 47 | 48 | const formattedCode = await prettify(code, { filepath: 'file.vue' }); 49 | 50 | t.is(formattedCode, code); 51 | }); 52 | 53 | test('does not remove imports when Vue components use kebab case', async (t) => { 54 | const code = ` 59 | 60 | 63 | `; 64 | 65 | const formattedCode = await prettify(code, { filepath: 'file.vue' }); 66 | 67 | t.is(formattedCode, code); 68 | }); 69 | 70 | test('works with pug templates in Vue files', async (t) => { 71 | const code = ` 74 | 75 | 78 | `; 79 | 80 | const expected = ` 83 | 84 | 87 | `; 88 | 89 | const formattedCode = await prettify(code, { filepath: 'file.vue' }); 90 | 91 | t.is(formattedCode, expected); 92 | }); 93 | 94 | test.serial('works with Volar language plugins when not running from the project root', async (t) => { 95 | const originalGetCurrentDir = ts.sys.getCurrentDirectory; 96 | 97 | ts.sys.getCurrentDirectory = () => '/'; 98 | 99 | const code = ` 102 | 103 | 106 | `; 107 | 108 | const expected = ` 111 | 112 | 115 | `; 116 | 117 | const formattedCode = await prettify(code, { filepath: 'file.vue' }); 118 | 119 | t.is(formattedCode, expected); 120 | 121 | ts.sys.getCurrentDirectory = originalGetCurrentDir; 122 | }); 123 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/simonhaenisch/prettier-plugin-organize-imports/test.yml?label=CI)](https://github.com/simonhaenisch/prettier-plugin-organize-imports/actions?query=branch%3Amaster) 2 | 3 | # Prettier Plugin: Organize Imports 4 | 5 | > Make sure that your import statements stay consistent no matter who writes them and what their preferences are. 6 | 7 | A plugin that makes Prettier organize your imports (i. e. sorts, combines and removes unused ones) using the `organizeImports` feature of the TypeScript language service API. This is the same as using the "Organize Imports" action in VS Code. 8 | 9 | **Features** 10 | 11 | - 👌 Dependency-free (just peer-dependencies you probably already have). 12 | - 💪 Supports `.js`, `.jsx`, `.ts`, `.tsx` and `.vue` files. 13 | - 🚀 Zero config. 14 | - 🤓 No more weird diffs or annoying merge conflicts in PRs caused by import statements. 15 | - 🤯 If your editor supports auto-imports, you'll stop thinking about your imports so much that you won't even care about their order anymore. 16 | 17 | **Caveat** 18 | 19 | This plugin inherits, extends, and then overrides the built-in Prettier parsers for `babel`, `babel-ts`, `typescript` and `vue`. This means that it is incompatible with other plugins that do the same; only the last loaded plugin that exports one of those parsers will function. 20 | 21 | ## Installation 22 | 23 | ```sh 24 | npm install --save-dev prettier-plugin-organize-imports 25 | ``` 26 | 27 | _Note that `prettier` and `typescript` are peer dependencies, so make sure you have those installed in your project._ 28 | 29 | ## Usage 30 | 31 | ### Prettier 3 32 | 33 | Automatic plugin discovery [has been removed](https://prettier.io/blog/2023/07/05/3.0.0.html#plugin-search-feature-has-been-removed-14759httpsgithubcomprettierprettierpull14759-by-fiskerhttpsgithubcomfisker). Thus you need to configure Prettier to use the plugin according to the [Plugins docs](https://prettier.io/docs/en/plugins.html), for example by adding it to the `plugins` config option: 34 | 35 | ```json 36 | { 37 | "plugins": ["prettier-plugin-organize-imports"] 38 | } 39 | ``` 40 | 41 | ### Prettier 2 42 | 43 | The plugin will be loaded by Prettier automatically. No configuration needed. 44 | 45 | Note that automatic plugin discovery is not supported with some package managers, e. g. Yarn PnP (see https://github.com/prettier/prettier/issues/8474). In that case follow the instructions for Prettier 3 above. 46 | 47 | ## Configuration 48 | 49 | ### Skip Files 50 | 51 | Files containing the substring `// organize-imports-ignore` or `// tslint:disable:ordered-imports` are skipped. 52 | 53 | ### Skip Destructive Code Actions 54 | 55 | If you don't want destructive code actions (like removing unused imports), you can enable the option `organizeImportsSkipDestructiveCodeActions` via your Prettier config. 56 | 57 | ```json 58 | { 59 | "organizeImportsSkipDestructiveCodeActions": true 60 | } 61 | ``` 62 | 63 | ## Compatibility 64 | 65 | ### ESLint 66 | 67 | For compatibility with [ESLint](https://eslint.org/) or other linters, see ["Integrating with Linters"](https://prettier.io/docs/en/integrating-with-linters.html) in the Prettier docs. You should have any import order rules/plugins disabled. 68 | 69 | ### React 70 | 71 | Depending on your configuration, if you need the `React` import to stay even if it's "unused" (i.e. only needed for the JSX factory), make sure to have the `jsx` option set to `react` in your `tsconfig.json`. For more details [click here](https://www.typescriptlang.org/docs/handbook/jsx.html#basic-usage). 72 | 73 | ### Vue.js 74 | 75 | Make sure that you have the optional peer dependency `vue-tsc` installed. 76 | 77 | ``` 78 | npm install --save-dev vue-tsc 79 | ``` 80 | 81 | If you're using Vue.js with Pug templates, you'll also need to install `@vue/language-plugin-pug` as a dev dependency, and configure it in `vueCompilerOptions` (see [usage](https://www.npmjs.com/package/@vue/language-plugin-pug)). 82 | 83 | ## Debug Logs 84 | 85 | If something doesn't work, you can try to prefix your `prettier` command with `DEBUG=true` which will enable this plugin to print some logs. 86 | 87 | ## Rationale/Disclaimer 88 | 89 | This plugin acts outside of [Prettier's scope](https://prettier.io/docs/en/rationale#what-prettier-is-_not_-concerned-about) because _"Prettier only prints code. It does not transform it."_, and technically sorting is a code transformation because it changes the AST (this plugin even removes code, i. e. unused imports). In my opinion however, the import statements are not _really_ part of the code, they are merely directives that instruct the module system where to find the code (only true as long as your imports are side-effects free regarding the global scope, i. e. import order doesn't matter), comparable with `using` directives in C# or `#include` preprocessing directives in C. Therefore the practical benefits outweigh sticking with the philosophy in this case. 90 | 91 | ## Changelog 92 | 93 | See [changelog.md](/changelog.md). 94 | 95 | ## License 96 | 97 | [MIT](/license). 98 | --------------------------------------------------------------------------------