Sort Members
Инструмент, который физически приводит TS/JS-классы и Angular-шаблоны
к единому стилю — не подсвечивает нарушение (как ESLint-правило
@typescript-eslint/member-ordering), а сразу переставляет и правит код.
Существует в двух формах на одной и той же логике сортировки:
- VS Code-расширение — сортировка прямо в редакторе: горячая клавиша,
кнопка, контекстное меню,
Format Document.
- npm CLI (
sort-members) — массовая сортировка всего проекта одной
командой в терминале, без открытия файлов в редакторе (например, в CI
или разовой миграцией).
Обе формы собираются из одного кода в этом репозитории и дают идентичный
результат. Ниже — установка и запуск для каждой.
Установка и запуск: VS Code-расширение
Установка — соберите .vsix и поставьте вручную (расширение не
публикуется в VS Code Marketplace):
npm install
npm run compile
npx vsce package # получится sort-members-<версия>.vsix
Затем в VS Code: Extensions → ... (три точки вверху панели) →
Install from VSIX... → выбрать собранный файл.
Настройка — ключ memberOrdering.order в settings.json (подробнее
в разделе «Как настроить свой порядок» ниже).
Запуск — в открытом .ts/.tsx/.js/.jsx-файле любым способом:
- Палитра команд:
Ctrl+Shift+P → Sort Members: Sort Class
- Горячая клавиша:
Ctrl+Alt+M (macOS — Cmd+Alt+M)
- Правый клик в редакторе → Sort Members: Sort Class
- Кнопка-иконка в тулбаре открытого файла (правый верхний угол редактора)
Format Document (Shift+Alt+F), если назначить расширение форматтером
по умолчанию:
"[typescript]": { "editor.defaultFormatter": "sort-members.sort-members" },
"[typescriptreact]": { "editor.defaultFormatter": "sort-members.sort-members" },
"[javascript]": { "editor.defaultFormatter": "sort-members.sort-members" }
Для .html-файлов — команда Sort Members: Format Template (та же
горячая клавиша, кнопка, правый клик — работает так же, но только в HTML).
Команда сортирует все классы в файле за один вызов; повторный вызов на уже
отсортированном файле ничего не меняет.
Установка и запуск: npm CLI (sort-members)
Установка — в целевом Angular/TS-проекте (том, который будете
сортировать):
npm install --save-dev sort-members
Настройка — файл .sortmembersrc.json в корне проекта (см. раздел
«Свой порядок для CLI» ниже); без файла используется
порядок по умолчанию.
Запуск — добавьте в scripts package.json вашего проекта:
"scripts": {
"sort": "sort-members src",
"sort:ts": "sort-members --ts-only src",
"sort:html": "sort-members --html-only src"
}
и вызывайте как любой npm-скрипт:
npm run sort # весь src целиком: .ts/.tsx и .html
npm run sort:ts # только .ts/.tsx
npm run sort:html # только .html
Подробности CLI (флаги, обход директорий, локальная разработка без
публикации, публикация новой версии) — в разделе
«Массовая сортировка без VS Code (CLI)».
Порядок по умолчанию
Категории идут сверху вниз в этом порядке; в скобках — имя категории, как оно
записывается в memberOrdering.order (см. следующий раздел).
- DI-инъекции — поле вида
= inject(...) (injected-field), всегда
первое в классе.
- Input-поля — и через декоратор
@Input(), и через сигнальный API
(input(), model()) — трактуются одинаково, сразу после инжектов:
public-input-field → protected-input-field → private-input-field.
- Output-поля —
@Output() и output() тоже равнозначны:
public-output-field → protected-output-field → private-output-field.
- Static readonly поля:
public-static-readonly-field →
protected-static-readonly-field → private-static-readonly-field.
- Static mutable поля:
public-static-field → protected-static-field →
private-static-field.
- Сигналы состояния —
signal(), computed(), effect():
public-state-signal-field → protected-state-signal-field →
private-state-signal-field.
- Instance readonly поля — всё остальное
readonly: обычные поля,
@HostBinding, @ViewChild/@ContentChild (и декоратором, и как
viewChild()/contentChild()), любые другие декорированные поля —
сортируются наравне, только по видимости:
public-instance-readonly-field → protected-instance-readonly-field →
private-instance-readonly-field.
- Instance mutable поля — то же самое, но без
readonly:
public-instance-field → protected-instance-field →
private-instance-field.
- Конструктор (
constructor).
- Lifecycle hooks — по хронологии вызова Angular:
ngOnChanges →
ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked →
ngAfterViewInit → ngAfterViewChecked → ngOnDestroy. Только public
(Angular не вызывает protected/private версии).
- Accessors:
get → set.
- Static-методы — идут раньше обычных методов:
public-static-method → protected-static-method →
private-static-method.
- Instance-методы — включая
@HostListener и любые другие
декорированные методы, сортируются наравне с обычными:
public-instance-method → protected-instance-method →
private-instance-method.
Ключевые идеи: input/output-поля идут единым блоком сразу после инжектов
независимо от стиля объявления (декоратор или сигнал); @HostBinding,
@HostListener, @ViewChild, @ContentChild не выделены в свои категории —
подчиняются тем же правилам видимости/readonly, что и обычные поля/методы;
static-методы всегда раньше instance-методов.
Полный список категорий как код — в src/ranking.ts (KNOWN_CATEGORIES).
Гарантия поверх порядка: если поле в инициализаторе читает this.other
(например, readonly topics = this._topics.asReadonly()), other всегда
окажется выше — даже вопреки категории. Иначе в рантайме this._topics было
бы undefined в момент инициализации topics. Это применяется на уровне
целых категорий, поэтому вместо чередования пар (_x, x, _y, y, ...)
получаются два чистых блока — все private, потом все public.
Index signature и static-блоки никогда не переставляются — остаются на
исходных местах.
Что ещё правит команда (кроме порядка)
Помимо перестановки членов, та же команда попутно чинит ещё несколько пунктов
из свода правил проекта:
- Явный модификатор доступа (
public/protected/private) — если у поля,
метода, конструктора или accessor-а модификатор не указан, добавляется
public (это и есть поведение по умолчанию в TS, просто делается явным).
Исключение — любой декорированный член (@HostBinding, @HostListener,
@Input, @ViewChild и т.п.): модификатор не добавляется, декоратор уже
определяет роль члена.
- Пустые строки между членами класса расставляются заново, независимо от
исходного форматирования: между двумя
inject()-полями — никогда нет
пустой строки (слипаются в один блок вверху класса), между любыми другими
соседними членами (в т.ч. внутри одной категории, например двумя
@Input()) — всегда ровно одна пустая строка.
- Группировка импортов — импорты в начале файла разбиваются на два блока
через пустую строку: сначала внешние пакеты (
@angular/core, rxjs и т.п.),
затем локальные (относительные пути, .//../). Порядок внутри каждого
блока не меняется (сортировки по алфавиту нет).
- Пустая строка между
case — в switch перед каждой новой необъединённой
группой case (не являющейся fallthrough-продолжением предыдущей) добавляется
пустая строка, если её ещё нет.
- JSDoc и обычные комментарии переезжают вместе со своим членом класса при
перестановке — ничего не теряется и не рассинхронизируется.
// #region / // #endregion — делят тело класса на сегменты:
сортировка идёт внутри каждого региона отдельно, член никогда не
пересекает его границу. Сама пара region/endregion и текст региона
(например, // #region Public API) сохраняются на своих местах.
Все правки идемпотентны — повторный запуск на уже поправленном файле ничего
не меняет.
Как настроить свой порядок
Ключ memberOrdering.order в settings.json (глобально или в
.vscode/settings.json конкретного проекта):
{
"memberOrdering.order": [
"injected-field",
"constructor",
"public-instance-method",
"private-instance-method"
]
}
- Порядок в массиве = порядок в файле. VS Code подсказывает допустимые имена
автокомплитом.
- Категории, которых нет в списке, не исчезают — попадают в конец файла, в
исходном относительном порядке между собой.
- Пустой массив/без настройки → порядок по умолчанию (см. выше).
- Опечатка в имени категории тихо игнорируется, остальные категории
применяются как есть.
- Изменения подхватываются сразу, без перезагрузки окна.
Свой порядок для CLI
У npm CLI нет settings.json, поэтому порядок задаётся файлом
.sortmembersrc.json в корне целевого проекта (там же, откуда запускаете
npm run sort) — те же имена категорий, что и в memberOrdering.order:
{
"order": [
"injected-field",
"constructor",
"public-instance-method",
"private-instance-method"
]
}
Действуют те же правила, что и для VS Code (см. выше): категории, которых
нет в списке, уходят в конец файла в исходном порядке; пустой/отсутствующий
order → порядок по умолчанию; невалидный JSON или order не массивом
строк — CLI печатает предупреждение и использует порядок по умолчанию,
ничего не ломая. На HTML-форматирование (sort-members --html-only) этот
файл не влияет — порядок атрибутов в шаблонах не настраивается.
Как исключить файлы из сортировки
По умолчанию не трогаются **/*.spec.ts и **/*.spec.tsx — ничего
настраивать для этого не нужно.
VS Code — ключ memberOrdering.ignore в settings.json (тот же формат
glob-паттернов, что и ниже для CLI):
{
"memberOrdering.ignore": ["**/*.spec.ts", "**/*.e2e.ts"]
}
CLI — постоянно, полем ignore в .sortmembersrc.json:
{
"ignore": ["**/*.spec.ts", "**/*.e2e.ts"]
}
или разово флагом (можно указать несколько раз):
node out/cli.js --ignore "**/*.e2e.ts" --ignore "legacy/**" src
Если задать хотя бы один паттерн (в конфиге и/или флагом), он заменяет
список по умолчанию, а не дополняет его. Чтобы вернуть сортировку .spec.ts
обратно — задайте пустой список: "ignore": [] (или "memberOrdering.ignore": []
в VS Code).
Справочник имён категорий
Полный список того, что можно писать в memberOrdering.order, с расшифровкой:
| Имя категории |
Что это |
Пример |
injected-field |
Поле-инъекция = inject(...) |
private readonly svc = inject(Svc); |
public-input-field |
@Input() / input() / model() без модификатора или с public |
@Input() name!: string; |
protected-input-field |
То же, но protected |
protected readonly name = input(''); |
private-input-field |
То же, но private |
private readonly name = input(''); |
public-output-field |
@Output() / output(), public |
@Output() closed = new EventEmitter(); |
protected-output-field |
То же, protected |
— |
private-output-field |
То же, private |
— |
public-static-readonly-field |
static readonly, public |
static readonly MAX = 5; |
protected-static-readonly-field |
То же, protected |
— |
private-static-readonly-field |
То же, private |
— |
public-static-field |
static без readonly, public |
static counter = 0; |
protected-static-field |
То же, protected |
— |
private-static-field |
То же, private |
— |
public-state-signal-field |
signal() / computed() / effect(), public |
readonly total = computed(...); |
protected-state-signal-field |
То же, protected |
— |
private-state-signal-field |
То же, private |
— |
public-instance-readonly-field |
Любое другое readonly-поле экземпляра, public: включает @HostBinding, @ViewChild/@ContentChild, viewChild()/contentChild(), любые декорированные поля |
@HostBinding('class.x') readonly isX = true; |
protected-instance-readonly-field |
То же, protected |
— |
private-instance-readonly-field |
То же, private |
private readonly sub?: Subscription; |
public-instance-field |
То же, но без readonly, public |
isActive = false; |
protected-instance-field |
То же, protected |
— |
private-instance-field |
То же, private |
— |
constructor |
Конструктор класса |
constructor() {} |
public-lifecycle-ngOnChanges … public-lifecycle-ngOnDestroy |
8 отдельных категорий — по одной на каждый Angular lifecycle hook, в порядке их вызова |
ngOnInit(): void {} |
get |
Все get-accessor'ы |
get value() { ... } |
set |
Все set-accessor'ы |
set value(v) { ... } |
public-static-method |
static-метод, public |
static create(): Foo { ... } |
protected-static-method |
То же, protected |
— |
private-static-method |
То же, private |
— |
public-instance-method |
Обычный метод экземпляра, public: включает @HostListener и любые другие декорированные методы |
@HostListener('click') onClick() {} |
protected-instance-method |
То же, protected |
— |
private-instance-method |
То же, private |
private loadUser(): void {} |
Полный и всегда актуальный список — KNOWN_CATEGORY_NAMES в src/ranking.ts;
эта же таблица выше синхронизирована с ним вручную при каждом изменении логики.
Форматирование Angular-шаблонов (.html)
Отдельная команда — Sort Members: Format Template (та же горячая клавиша
Ctrl+Alt+M, но действует только в .html-файлах; правый клик в редакторе
или Format Document работают так же, как для TS).
Приводит открывающие теги к единому виду:
- Порядок атрибутов:
*ngIf/*ngFor (структурная директива) → #ref →
id → class → [class.xxx] → остальные bindings/атрибуты → (event).
- Если атрибутов 2 и больше — каждый переносится на свою строку, с отступом
на один уровень (+2 пробела) от отступа самого тега.
Не трогает: текстовое содержимое, комментарии, тело <script>/<style>.
Осознанно не автоматизировано (риск сломать разметку/CSS выше пользы):
БЭМ-нейминг классов (потребовало бы переименования в CSS/SCSS по всему
проекту), комментарии к логическим блокам (нечего генерировать осмысленного).
Массовая сортировка без VS Code (CLI)
Чтобы прогнать всю кодовую базу одной командой (без открытия каждого файла
вручную), есть CLI-скрипт src/cli.ts (собирается в out/cli.js):
node out/cli.js <путь-к-папке-или-файлу> [ещё пути...] # .ts/.tsx и .html вместе
node out/cli.js --ts-only <путь> # только .ts/.tsx
node out/cli.js --html-only <путь> # только .html
Запуск возможен из любой директории — просто укажите полный (абсолютный)
путь к cli.js и к папке/файлу проекта, который нужно отсортировать.
Пример на Windows (PowerShell), где cli.js лежит в папке этого расширения,
а сортируется другой (внешний) Angular-проект:
node "C:\путь\до\этого\расширения\out\cli.js" "C:\путь\до\вашего\проекта\src"
- Путь может указывать и на папку (рекурсивный обход,
node_modules/.git/
dist/out/.angular пропускаются), и на один конкретный файл.
- Использует ту же логику сортировки, что и команды расширения в
VS Code, — результат идентичен ручному запуску.
- Изменяет файлы на месте и печатает список изменённых.
- Идемпотентен: повторный запуск на уже отсортированных файлах ничего
не меняет и ничего не печатает про них.
- Перед массовым запуском на реальном проекте убедитесь, что изменения
можно отследить/откатить (например, через
git status и git diff —
правки применяются сразу к файлам на диске, без подтверждения).
Удобный запуск из целевого проекта (npm run sort)
Чтобы вызывать команду так же коротко, как ng serve (без путей к cli.js),
у пакета есть bin-запись sort-members.
Вариант A — пакет опубликован в npm (обычная установка):
npm install --save-dev sort-members
Вариант B — локальная разработка без публикации (например, вы правите
логику сортировки в этом репозитории и сразу хотите видеть эффект в другом
проекте): ставится как file-зависимость, путь после file: — до папки этого
расширения на вашем диске:
npm install --save-dev sort-members@file:C:\путь\до\этого\расширения
npm сам создаст симлинк node_modules/.bin/sort-members. При обновлении
логики сортировки здесь (npm run compile) достаточно перезапустить
npm run sort в целевом проекте — file-зависимость подхватывает изменения
без переустановки, т.к. npm создаёт symlink, а не копирует файлы.
В обоих случаях — добавьте в scripts package.json вашего проекта:
"scripts": {
"sort": "sort-members src",
"sort:ts": "sort-members --ts-only src",
"sort:html": "sort-members --html-only src"
}
И запускайте как любой другой npm-скрипт:
npm run sort # весь src целиком: .ts/.tsx и .html
npm run sort:ts # только .ts/.tsx
npm run sort:html # только .html
Без установки зависимости — можно вызвать cli.js и напрямую, указав
полный путь к нему при каждом запуске:
node "C:\путь\до\этого\расширения\out\cli.js" src
Публикация пакета в npm
npm run compile
npm login # один раз на машине
npm publish # публикует под именем "sort-members" (см. package.json)
Перед публикацией новой версии не забудьте поднять "version" в
package.json (npm не даёт дважды опубликовать одну и ту же версию).
.npmignore уже настроен так, чтобы в tarball попадали только out/,
resources/icon.png, LICENSE, README.md — исходники (src/), карты
сборки и тестовые скрипты исключены.
Разработка
npm install
npm run compile # или F5 — запустит Extension Development Host
npx vsce package # собрать .vsix