Кодмод
codemod переводит тесты с jest-auto-spies и Jest или с jasmine-auto-spies и jasmine на Vitest и эту библиотеку. Он переписывает то, что может решить безопасно, всё остальное оставляет ровно как было и перечисляет каждое место, которое нужно переписать руками.
npx vitest-auto-spy codemod # предпросмотр: печатает дифф, ничего не пишет
npx vitest-auto-spy codemod --write # применить
npx vitest-auto-spy codemod --verify # проверить, что переводить больше нечегоПочему не поиск с заменой? Потому что несколько простых переименований компилируются и делают обратное задуманному:
jest.Mock<void, [Order]>→Mock<void, [Order]>компилируется, но значит обратное: у Jest первым идёт тип возврата, у Vitest — аргументы.jest.requireMock, переименованный вvi.requireMock, падает сTypeError: vi.requireMock is not a function.spyOn(o, 'm')из jasmine, переименованный вvi.spyOn(o, 'm'), начинает вызывать настоящий метод: jasmine его заглушает, а Vitest вызывает.- Если перенести все импорты
jest-auto-spiesв корневойvitest-auto-spy, сломаетсяprovideAutoSpy: корневая точка входа его не экспортирует.
Пошаговое руководство по переходу — Переход с jest-auto-spies; для jasmine — Переход с jasmine-auto-spies.
Перевести тесты
- Посмотрите. Запустите
npx vitest-auto-spy codemodи прочитайте дифф. Ничего не пишется. - Примените. Запустите ещё раз с
--write. - Доделайте оставленное. Каждая строка
errorиwarnв отчёте называетpath:lineи говорит, что делать. - Проверьте.
npx vitest-auto-spy codemod --verifyвыходит с кодом 0 и сообщениемNothing left to migrate., когда всё сделано. Оставьте его в CI. - Проверьте, что оставил переход.
npx vitest-auto-spy doctorсообщит об include-паттерне вtsconfig, который сломала миграция, оjest.config.tsдля раннера, которого уже нет, и о setup-файлах, которыми пользовался только этот конфиг.
По одной директории. Передайте директорию каждой команде:
npx vitest-auto-spy codemod src/app # предпросмотр
npx vitest-auto-spy codemod src/app --write # применить
npx vitest-auto-spy codemod src/app --verify # проверитьС путём кодмод обходит все исходники под ним, а не только спеки, так что jest. в коде приложения там тоже переписывается.
Как читать отчёт
$ npx vitest-auto-spy codemod
vitest-auto-spy codemod — /work/app
Dry run — nothing is written. Re-run with --write to apply.
src/app/service.spec.ts
auto-spies-import 1 edit
inject-cast 1 edit
jest-namespace 2 edits
jest-types 1 edit
mock-implementation-arity 1 edit
import { createSpyFromClass, Spy, asSpy } from 'vitest-auto-spy';
import { provideAutoSpy } from 'vitest-auto-spy/angular';
import type { Mock } from 'vitest';
--- a/src/app/service.spec.ts
+++ b/src/app/service.spec.ts
@@ -1,16 +1,18 @@
-import { createSpyFromClass, provideAutoSpy, Spy } from 'jest-auto-spies';
+import { createSpyFromClass, Spy, asSpy } from 'vitest-auto-spy';
+import { provideAutoSpy } from 'vitest-auto-spy/angular';
+import type { Mock } from 'vitest';
import { Service } from './service';
describe('Service', () => {
let service: Spy<Service>;
- let hook: jest.Mock<void, [Service]>;
+ let hook: Mock<(arg0: Service) => void>;
beforeEach(() => {
- service = TestBed.inject(Service) as Spy<Service>;
- jest.spyOn(service, 'load').mockImplementation();
+ service = asSpy<Service>(TestBed.inject(Service));
+ vi.spyOn(service, 'load').mockImplementation(() => undefined);
jest.requireActual('./service');
- hook = jest.fn();
+ hook = vi.fn();
});
});
1 file would change, 6 edits
error residue/jest-namespace src/app/service.spec.ts:12
Still matches after the run: "jest."
→ `jest-namespace` declined it: `jest.requireActual` was left alone. Rewrite it by hand.
Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/codemod#verifying-by-matching-not-by-diffing
warn no-vi-twin src/app/service.spec.ts:12
`jest.requireActual` was left alone.
→ `vi.importActual(id)` is asynchronous and only legal inside a `vi.mock` factory; rewriting
it changes control flow.
1 error, 1 warning, 0 notesДля каждого изменённого файла отчёт показывает:
- Правки по трансформациям — сколько раз сработала каждая.
- Итоговые инструкции импорта целиком. Смотрите их первыми: они показывают, из какой точки входа теперь приходит каждое имя.
- Дифф.
Дальше идут находки. Об одной строке может быть две: error говорит, что работа не закончена, а warn — почему кодмод её не сделал. Чинить нужно саму строку. Остаток — это текст, которого после перехода быть не должно: jest., from 'jest-auto-spies', as Spy<. Здесь одна строка, jest.requireActual, осталась как была, поэтому прогон выходит с кодом 1: работа для вас ещё есть. С шестью правками всё в порядке.
Остаток, в котором названа трансформация («jest-namespace declined it»), оставлен сознательно; дальше идёт причина. Остаток без такой пометки стоит там, куда не дотянулась ни одна трансформация: внутри шаблонного литерала или после несбалансированных скобок.
| Код | Когда |
|---|---|
0 | Всё переписано, остатков нет |
1 | Что-то оставлено вам на ручную правку или остаток всё ещё на месте — в том числе под --verify |
2 | Ничего не запускалось: неизвестный id трансформации в --only / --skip, неизвестный флаг, --format markdown или путь без единого файла |
Какие файлы он обходит
- Без пути — все
*.spec.*и*.test.*в репозитории, включая JavaScript.jest.вmain.ts— это код приложения, а не тест, и прогон без пути его не трогает. - С путём — все исходники под ним, кроме файлов деклараций.
- Файл, в котором выбранным трансформациям нечего менять, в отчёт не попадает, так что в выводе только файлы, которым есть что сказать.
- Путь, под который не подходит ни один файл, — ошибка, код 2, и ничего не читается. Опечатка в строке CI не должна выглядеть как чистый результат. Пути считаются от
--cwd. Подходят абсолютный путь, префикс./, завершающий слэш и разделитель Windows;.— корень репозитория.
Выберите диалект через --from
npx vitest-auto-spy codemod --from jasmine # или jasmine-auto-spies
npx vitest-auto-spy codemod --from jest-auto-spies
npx vitest-auto-spy codemod # --from auto, по умолчаниюauto читает каждый файл и решает сам. Файл с членом jasmine., с .and., с импортом jasmine-auto-spies или vitest-auto-spy/jasmine получает трансформации jasmine; остальные — нет. Четыре общие трансформации работают всегда.
Если ваши jasmine-тесты используют только spyOn, передайте --from jasmine. Голый spyOn( — не признак jasmine: оба диалекта пишут его одинаково, а поведение по умолчанию у них противоположное (jasmine заглушает, Vitest вызывает настоящий метод). Если угадать неверно, spyOn молча начнёт вызывать настоящий метод.
Неизвестное значение — ошибка со списком допустимых, а не молчаливый откат.
Тринадцать трансформаций
У каждой трансформации есть id, который можно передать в --only или --skip. Все они читают исходный файл до того, как применится хоть одна правка, поэтому --skip убирает правки одной трансформации и не влияет на остальные.
Четыре общие, потому что конструкция есть в обоих диалектах:
| Id | Переписывает | Оставляет как есть |
|---|---|---|
auto-spies-import | import … from 'jest-auto-spies' / 'jasmine-auto-spies' → точки входа, которые экспортируют каждое имя | Импорт по умолчанию или namespace-импорт; имя, которое не экспортирует ни одна точка входа |
inject-cast | TestBed.inject(X) as Spy<X> → asSpy<X>(TestBed.inject(X)) с добавлением импорта | as Spy<T> над чем-либо другим |
jasmine-aliases | xit / xdescribe / fit / fdescribe / xtest → it.skip / describe.skip / … | Имя, которое не вызывается, — shape.fit(box), function fit(…) { |
mock-implementation-arity | mockImplementation() без аргумента → mockImplementation(() => undefined) | Вызов, у которого функция уже есть |
Три — из Jest:
| Id | Переписывает | Оставляет как есть |
|---|---|---|
jest-globals-import | from '@jest/globals' → from 'vitest', переименовывая привязку jest в vi | Остальное в импорте — describe / it / expect называются так же |
jest-namespace | jest.<member> → vi.<member> для 26 методов, у которых есть аналог в vi | Метод без аналога и любой метод, которого она не знает |
jest-types | jest.Mock<R, [A]> → Mock<(a: A) => R> плюс четыре переименования, с импортом имени из Vitest | Список аргументов типа, который не удаётся разделить на верхнем уровне; jest из @jest/globals — его только переименовывают |
Шесть — из jasmine. От чего защищает каждая — в Переходе с jasmine-auto-spies:
| Id | Переписывает | Оставляет как есть |
|---|---|---|
jasmine-and-helpers | spy.load.and.nextWith(v) → spy.load.nextWith(v) — десять хелперов auto-spies теряют пространство имён | Ничего; список закрыт |
jasmine-strategies | .and.returnValue / callFake / stub / throwError / returnValues / resolveTo → двойник mock*, а .withArgs( → .calledWith( | .and.callThrough(), которому некуда вызывать; незнакомую стратегию; .withArgs( в цепочке spyOn |
jasmine-spy-on | spyOn(o, 'm') → vi.spyOn(o, 'm').mockImplementation(() => undefined), а spyOnProperty(o, 'p') получает 'get', который jasmine подставлял сам | Вызов, в цепочке которого уже есть стратегия — там заглушка лишняя, — и вызов, который продолжается в .withArgs( |
jasmine-globals | jasmine.createSpy / createSpyObj / any / clock() / addMatchers → их двойники в vi, expect и vitest-auto-spy | getEnv, truthy / falsy / empty / notEmpty, DEFAULT_TIMEOUT_INTERVAL, стратегии спаев |
jasmine-types | jasmine.Spy → Mock из vitest, jasmine.SpyObj<T> → Spy<T> из этого пакета | Оба имени, если таблицу точек входа не удалось построить |
jasmine-matchers | toBeTrue / toBeFalse / toHaveSize / toHaveBeenCalledOnceWith / withContext / fail → их написание в Vitest | fail, который не вызывается; withContext у пустого expect() |
auto-spies-import идёт первым: он создаёт импорт vitest-auto-spy, в который inject-cast потом добавляет asSpy. Порядок остальных не важен.
Несколько правок, которые стоит знать:
jest.dontMockстановитсяvi.doUnmock, аjest.SpyInstance—MockInstance.vi.mock()с относительным путём переименовывается, а потом о нём выдаётся предупреждение. Под тестовым билдером, который собирает код в бандл (например,@angular/build:unit-test), этого модуля отдельно уже нет, и мок молча ничего не делает. Вместо этого дайте настоящий шов: передавайте подмену явно, через DI или аргумент..and.throwError(x)из jasmine превращает любую строку вError. Поэтому переменная становитсяthrow typeof x === 'string' ? new Error(x) : x, а не голымthrow x: иначе соседнийtoThrow(Error)упал бы. Строковый литерал становится простымnew Error('…'). Аргумент, который проверке пришлось бы вычислить дважды, остаётся как был.jest.Mock<R, [A]>не переименовывается, а собирается заново: см. Ловушка: у Jest тип возврата стоит первым.
Предупреждения, которые может выдать переписывание
Кроме фрагментов, которые кодмод оставляет как есть, он предупреждает там, где уже сделанная правка требует второго взгляда:
| Проверка | Что значит и что делать |
|---|---|
overlapping-edit | Две правки претендуют на один фрагмент. Правка, которую называет предупреждение, не применена и не входит в число правок; другая есть в диффе. Проверьте строку руками |
name-declared-locally | Правке нужно имя — asSpy, Mock, … — которое файл объявляет сам (const, let, var, function, class, enum, interface, type, namespace), поэтому импорт не добавлен. Переименуйте локальную привязку и импортируйте имя из библиотеки. Деструктурированная привязка не распознаётся — такие места проверьте руками |
fake-timers-option | В vi.useFakeTimers передана опция только из Jest, и Vitest молча её игнорирует. По предупреждению на опцию, с аналогом в Vitest: advanceTimers → shouldAdvanceTime (с advanceTimeDelta для числа), doNotFake → toFake с обратным списком, legacyFakeTimers → ничего, timerLimit → loopLimit |
vi-without-globals | Файл использует vi без импорта, а конфиг Vitest не задаёт test.globals: true, так что тесты бросят ReferenceError: vi is not defined. Импортируйте vi, describe, it и expect из vitest или включите globals |
vi-without-globals — заметка, а не добавленный import { vi }: при выключенных globals нет и describe, it и expect, и импорт одного vi тесты не починит. globals читается из каждого vitest.config.*, vite.config.* и vitest.workspace.*, которые находит сканирование. angular.json с @angular/build:unit-test считается включённым (билдер включает globals), другие раннеры не оцениваются.
Что он сознательно оставляет как есть
У фрагментов ниже есть очевидная правка, которая компилируется, но неверна. Поэтому кодмод оставляет их как есть и печатает каждый с причиной и path:line, по которому редактор сразу откроет место.
| Оставлено как есть | Почему и что написать вместо |
|---|---|
jest.requireActual(id) | vi.importActual(id) асинхронный и допустим только внутри фабрики vi.mock. Перепишите руками, как показано под таблицей |
jest.requireMock(id), jest.setMock(…) | Двойника в vi нет. Передайте подмену через TestBed или DI-контейнер либо аргументом |
jest.replaceProperty(o, k, v) | Используйте mockValueProp(o, k, v) из этого пакета — другой хелпер со своим восстановлением |
jest.setTimeout(n) | Используйте опцию конфига testTimeout или vi.setConfig({ testTimeout: n }) |
enableAutomock, createMockFromModule, now, retryTimes, runAllTicks, … | Члена vi с таким именем нет, и каждому нужен свой подход. Перепишите руками |
Любой незнакомый jest.<member> | Не переименовывается: кодмод не предполагает, что в vi есть такой же метод. Из такого предположения и получается vi.requireMock is not a function |
as Spy<T> над чем угодно, кроме TestBed.inject(...) | Если значение и правда спай — asSpy(...); если это самодельная подмена, её построит createAutoMock<T>() |
| Импорт по умолчанию или namespace-импорт легаси-пакета | Хелперы живут за разными точками входа, и одно пространство имён не может покрыть их все |
| Всё внутри шаблонного литерала | Паттерны сопоставляются с маскированным видом, где шаблонный литерал пуст |
| Любой фрагмент с несбалансированными скобками | Непонятно, где кончается выражение; если угадать, правка может захватить только половину типа |
.withArgs(…) в цепочке spyOn(…) | У vi.spyOn нет calledWith, так что переименование падает с calledWith is not a function. Сообщается как jasmine-with-args-on-spy-on; spyOn под ним тоже остаётся как был |
.and.throwError(build()) | В переписанном коде аргумент стоит дважды, и выражение выполнилось бы дважды. Для err или state.err это нормально. Отклоняется с unknown-jasmine-strategy |
expect() без аргумента | Нет субъекта, рядом с которым можно поставить сообщение withContext |
fail, fit, xit и их родня там, где это не вызовы | Метод или функция с тем же именем, которую переименование превратило бы в синтаксическую ошибку. --verify объявления тоже не сообщает |
Как переписать jest.requireActual руками. Если он собирает частичный мок, перенесите его в фабрику vi.mock и дождитесь через await. Если спеке нужен просто настоящий модуль, импортируйте его обычным импортом.
// было: jest.mock('./x', () => ({ ...jest.requireActual('./x'), load: jest.fn() }));
vi.mock('./x', async () => ({ ...(await vi.importActual<typeof import('./x')>('./x')), load: vi.fn() }));Проверка сопоставлением, а не диффом
--verify ищет в файлах то, что переход должен был убрать, и сообщает о каждом совпадении с file:line. Если кодмод прислал вас сюда, откройте эту строку и перепишите руками то, что цитирует сообщение. Сообщение говорит, какая трансформация отказалась и почему.
$ npx vitest-auto-spy codemod --verify
vitest-auto-spy codemod — /work/app
1 files matched against 7 transform patterns.
error residue/auto-spies-import src/app/service.spec.ts:1
Still matches after the run: "from 'jest-auto-spies'"
→ `auto-spies-import` did not rewrite this. Rewrite it by hand.
Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/codemod#verifying-by-matching-not-by-diffing
error residue/inject-cast src/app/service.spec.ts:10
Still matches after the run: "as Spy<"
→ `inject-cast` did not rewrite this. Rewrite it by hand.
Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/codemod#verifying-by-matching-not-by-diffing
error residue/jest-types src/app/service.spec.ts:7
Still matches after the run: "jest.Mock"
→ `jest-types` did not rewrite this. Rewrite it by hand.
Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/codemod#verifying-by-matching-not-by-diffingКаждая трансформация объявляет паттерн остатка — то, что она должна убрать. --verify ничего не преобразует и только сопоставляет эти паттерны. Строка под заголовком вывода считает паттерны трансформаций, выбранных для этих тестов, — выше это четыре общие и три из Jest. Отсюда два свойства, которых нет у проверки по диффу:
- Она работает на файлах, которых кодмод не касался: спека, переведённая руками, спека, которую правил кто-то другой, фрагмент, исключённый через
--skip. На переведённом репозитории она выходит с кодом 0 иNothing left to migrate., а с кодом 1 — как только вернётсяjest., так что её можно держать в CI. - Она ловит дифф, который выглядит правильно, но работа не закончена. Трансформации не заходят внутрь шаблонного литерала и за несбалансированные скобки. Остальной дифф верен, а в файле всё ещё есть
jest..
Это та проверка, которую советует заключительная заметка руководства по переходу: проверка «изменился ли файл так, как я хотел?» бывает зелёной и на сломанном результате, поэтому сверяйте результат с тем, чего быть уже не должно. По той же причине отчёт сухого прогона печатает итоговые инструкции импорта целиком, а не их число.
Флаги
| Флаг | Что делает |
|---|---|
| (нет) | Сухой прогон. Печатает дифф, правки по трансформациям, итоговые импорты и отчёт. Ничего не пишет |
--write | Применить правки, каждый файл через временный файл и переименование. Оставленное остаётся как было и по-прежнему сообщается |
--verify | Ничего не преобразовывать; сопоставить файлы с паттернами остатков. Код 1, если что-то совпало |
--from <pkg> | Какой это диалект: jest-auto-spies, jasmine-auto-spies (псевдоним jasmine) или auto (по умолчанию) |
--only <ids> | Запустить только эти трансформации, через запятую |
--skip <ids> | Запустить все, кроме этих |
--list | Напечатать трансформации и сгенерированную таблицу точек входа и выйти с кодом 0 |
--cwd <dir> | Работать в другой директории |
--format <f> | text (по умолчанию) или json: один JSON-документ в stdout вместо текстового отчёта |
Id, которого не знают --only или --skip, даёт код 2 и список известных id, а не запуск всего подряд. Флаг, которого нет в таблице, тоже даёт код 2 — до того, как что-то прочитано, — и подсказывает, какой, скорее всего, имелся в виду: codemod --wirte не должен выглядеть как сухой прогон, который случайно ничего не записал.
--list показывает, что сделает прогон, ничего не делая. Трансформация, которую прогон пропустит, помечена - в левой колонке, так что --list --skip jest-types отвечает на вопрос «что именно я сейчас запущу». Ещё он печатает таблицу точек входа:
$ npx vitest-auto-spy codemod --list
Transforms
auto-spies-import import … from 'jest-auto-spies' → the vitest-auto-spy entry points that export each name.
inject-cast TestBed.inject(X) as Spy<X> → asSpy<X>(TestBed.inject(X)), adding the import.
…
Entry-point table (/work/app/node_modules/vitest-auto-spy)
createSpyFromClass vitest-auto-spy, vitest-auto-spy/bun, vitest-auto-spy/bun-angular, vitest-auto-spy/node, …
injectSpy vitest-auto-spy/bun-angular, vitest-auto-spy/angular, vitest-auto-spy/nestjs
provideAutoSpy vitest-auto-spy/bun-angular, vitest-auto-spy/angular, vitest-auto-spy/nestjs, vitest-auto-spy/vue
renderShallow vitest-auto-spy/bun-angular, vitest-auto-spy/angular--format json для CI
--format json печатает один JSON-документ вместо текстового отчёта — чтобы шаг CI его читал:
npx vitest-auto-spy codemod --verify --format json > codemod.json- Прогон:
schema,command,version,cwd,run(dry-run,writeилиverify),exitCode,tally,findingsиfiles— у каждогоfile,changed,edits,fired(правки по трансформациям), итоговыеimportsи unified-диффdiff. - С
--list: трансформации (id,family,summary,selected) и таблица точек входа (entries).
Код выхода тот же, что у текстового прогона. --format markdown, который принимают doctor и perf, отклоняется с кодом 2.
Когда сканирование упирается в предел
Сканирование файлов останавливается на 50 000 файлов. Дальше кодмод пишет об этом в stderr, а не называет репозиторий переведённым:
The repository scan stopped at its safety cap of 50000 files — part of the tree was never looked at,
so a clean result here is not a migrated repository. Raise the cap with VITEST_AUTO_SPY_SCAN_CAP, or
pass the directories to migrate as arguments: `npx vitest-auto-spy codemod src/app`.Обычно лучше передать директории для перевода аргументами: медленная часть — трансформации, а не сканирование. Поднять предел:
VITEST_AUTO_SPY_SCAN_CAP=200000 npx vitest-auto-spy codemod --verifyСканирование не заходит в директорию, которая сама является репозиторием, — git worktree (у которого .git — файл) или вложенный клон. Эти файлы принадлежат другой ветке, и --write не должен их переписывать.
В CI
- run: npx vitest-auto-spy codemod --verifyПока вы переводите по одной директории, проверяйте только готовые: npx vitest-auto-spy codemod src/app --verify.
Одна строка, без сети, конфига и токена. На переведённом репозитории она проходит и ничего не меняет. Она падает, когда возвращается jest., легаси-импорт или приведение as Spy<…>, — а в больших тестах такое возвращается.
Подробнее
Ловушка: у Jest тип возврата стоит первым
- let hook: jest.Mock<void, [Order]>;
+ let hook: Mock<(arg0: Order) => void>;Первый аргумент типа у Jest — тип возврата, второй — кортеж аргументов. Единственный аргумент типа у Vitest — сигнатура вызова. Если просто переименовать и оставить аргументы типа на месте, код скомпилируется, но будет означать обратное, а ошибка всплывёт позже — там, где мок вызывают.
Поэтому jest-types собирает тип заново, а не переименовывает:
=>при поиске скобок пропускается целиком. Иначеjest.Mock<() => void, []>«закрылся» бы на>стрелки, и правка забрала бы полтипа.- Элементы кортежа сохраняют метки, необязательность и rest.
[id: number, force?: boolean, ...rest: string[]]становится(id: number, force?: boolean, ...rest: string[]). Элемент без метки получает имяarg0,arg1: у функционального типа параметры должны быть названы. - Всё, что не литерал кортежа, разворачивается через spread.
jest.Mock<void, Args>становитсяMock<(...args: Args) => void>, и это верно для любогоArgs. - Три и больше аргументов типа или несбалансированные
<…>дают заметкуjest-mock-type-argumentsи никакой правки. Строка остаётся байт в байт такой же. Перепишите её руками в форме выше.
jest.Mock без аргументов типа становится просто Mock: на обеих сторонах он значит одно и то же.
Файл, который импортирует jest из @jest/globals, сохраняет аргументы типа. Этот jest — из jest-mock 29 и новее, где jest.Mock уже принимает тип функции (jest.Mock<() => string>), как и в Vitest. Поэтому меняется только имя. Перестановка дала бы Mock<() => () => string> — мок, который возвращает функцию.
Аргументы типа у вызова не переписываются, а сообщаются. jest.fn<R, [A]>() и jest.spyOn<…>() в написании @types/jest тоже ставят тип возврата первым. jest-namespace переименовывает функцию в vi.fn / vi.spyOn, где единственный аргумент типа — сигнатура вызова, поэтому jest-mock-type-arguments называет этот список и оставляет его вам — опять же только в файле, чей jest не из @jest/globals.
Таблица точек входа генерируется, а не записана руками
Из этого раздела кодмод выдаёт три заметки:
ambiguous-entry-point(предупреждение): имя экспортируют несколько точек входа, и ничто в файле не говорит, какая нужна. Кодмод выбрал самую частую и перечислил остальные. Если этой спеке нужна другая, поменяйте спецификатор руками: каждая точка входа регистрирует адаптер моков (связку сvi.fn()илиmock()вашего раннера) для своего раннера, так что не та точка входа ломает все спаи.unmapped-legacy-export(ошибка): имя не экспортирует ни одна точка входа установленного пакета. Импорт остаётся на легаси-пакете; замену ищите в таблице соответствия.no-entry-table(ошибка): таблицу не удалось построить, так что ничего не размещено. Установитеvitest-auto-spyв репозиторий (npm i -D vitest-auto-spy) и запустите снова.
Чтобы разложить import { createSpyFromClass, provideAutoSpy, Spy } from 'jest-auto-spies', кодмод должен знать, какая точка входа экспортирует какое имя. Таблица, вписанная в кодмод, была бы верна для одной версии и неверна для той, что установили вы. Поэтому он строит таблицу во время запуска:
- Находит установленный
vitest-auto-spy, поднимаясь поnode_modulesдо шести директорий от рабочей (этого хватает для поднятых зависимостей в workspace), а если не нашёл — берёт копию, из которой запущен сам CLI. - Читает карту
exportsэтого пакета и разрешает каждую точку входа в файл, предпочитая условиеtypes: файл деклараций перечисляет экспорты только типов, которых нет в бандле. Еслиdistнет, берутся исходники рядом — так монорепозиторий может пользоваться пакетом до первого релиза. - Собирает экспортированные имена — именованные списки, объявления и обе формы реэкспорта со звёздочкой,
export * from './…'иexport type * from './…', — проходя баррели на глубину до восьми, по виду с замаскированными комментариями и строками.
Результат — не догадка об API, а сам API, прочитанный с копии на диске.
Две формы export * не взаимозаменяемы
Пакет использует export type *, чтобы одной строкой реэкспортировать все свои публичные типы. Обходчик, который знает только форму для значений, теряет все типы разом — здесь это Spy<T> и всё, что объявлено рядом. Потеря ничем не выдаёт себя: таблица строится, а трансформация импорта просто не может разместить имя и оставляет его на jest-auto-spies с ошибкой остатка. Проявляется это только на исходниках, потому что .d.ts перечисляет экспорты явно, так что тесты против собранного dist зелёные, а те же тесты до сборки падают.
Когда имя экспортируют несколько точек входа, кодмод выбирает:
- Корень, если он экспортирует имя, — он регистрирует адаптер моков и есть у каждого раннера.
- Иначе точку входа вашего репозитория, определённую так же, как её определяет
init:provideAutoSpyуходит в/angularв Angular-репозитории и в/nestjsв NestJS. - Иначе единственную точку входа, где оно есть.
- Иначе спрашивает файл: точку входа, из которой файл уже импортирует, или точку для фреймворка, который называет сам текст файла — на Bun её Bun-вариант (
/bun-angular, а не/angular). - Иначе угадывает самую вероятную точку входа для вашей среды выполнения и сообщает
ambiguous-entry-pointсо списком альтернатив.
Имя, которое не экспортирует ни одна точка входа, остаётся на легаси-пакете и сообщается как unmapped-legacy-export.
Если установленную копию найти не удалось, таблицы нет. Четыре трансформации, которым она нужна, — auto-spies-import, inject-cast и две jasmine-трансформации, размещающие createSpyObj и Spy<T>, — отказываются работать и говорят об этом. Неверная точка входа, которая всё равно компилируется, — ровно то, от чего эта команда защищает.
--list печатает всю таблицу, чтобы её можно было прочитать, прежде чем ей верить.
Инструкция импорта правится на месте, а не собирается заново
Блок импортов — место, где файл хранит то, что правка терять не должна: алиас, комментарий к спецификатору, окончание строк, принятое в репозитории. Поэтому список правится по диапазонам, а не собирается заново из имён.
- Строчный комментарий внутри списка — не список имён. Список делится по запятым маскированного вида, так что слова внутри
// keep this oneникогда не читаются как импорт. Добавленное имя встаёт после последнего символа кода и перед комментарием; удалённый спецификатор забирает свою запятую и свой комментарий в конце строки и больше ничего. Алиасы (a as b) и переводы строк внутри списка сохраняются. - Новая инструкция встаёт на отдельную строку сразу после предыдущей инструкции, так что комментарий в конце той строки остаётся. Перевод строки — такой же, как в файле: CRLF-файл остаётся CRLF.
- Метка порядка байтов (BOM) в начале файла сохраняется.
Результат парсится до того, как он записан
Если кодмод сообщил codemod-broke-syntax, файл остался ровно таким, каким был. Переведите его руками и сообщите о конструкции в названной строке как о дефекте кодмода.
error codemod-broke-syntax src/app/service.spec.ts:7
The rewritten file would not parse at line 7 (',' expected.), so the file was left as it
was.
→ Migrate this file by hand, and report the construct on that line as a codemod defect.
Docs: https://asdalexey.github.io/vitest-auto-spy/utilities/codemod#the-result-is-parsed-before-it-is-writtenВсе трансформации работают с текстом, и опасная поломка — правдоподобный дифф в файле, который больше не компилируется. Поэтому каждый результат проходит через парсер: typescript переводимого репозитория, найденный от --cwd, чтобы глобальный npx vitest-auto-spy не читал ваши файлы чужим компилятором.
- Файл, в котором прогон добавил синтаксических ошибок, не записывается; его правки и новые строки импорта отбрасываются вместе с ним.
- Строка и сообщение — от самого парсера, по переписанному тексту.
- Ошибки до и после сравниваются между собой, а не с нулём, так что спеку с уже существовавшей синтаксической ошибкой в ней не обвинят.
- Если
typescriptв репозитории не находится, проверка молча пропускается.
Один сломанный файл не останавливает прогон
Каждый файл преобразуется отдельно, как и каждый проход --verify по нему. Файл, на котором кодмод бросил исключение, становится ошибкой codemod-file-failed: в нём ничего не меняется, а прогон идёт дальше. Переведите этот файл руками и сообщите, какая форма кода к этому привела.
--write пишет каждый изменённый файл через временный файл в той же директории и переименование. Прерванный прогон оставляет каждый файл либо как был, либо полностью переведённым, но никогда не наполовину записанным. Права файла сохраняются, а через симлинк пишется в целевой файл. Неудачная запись (права доступа, полный диск) становится ошибкой codemod-write-failed, и файл на диске не меняется. Записываются только файлы, текст которых изменился.
Как паттерн никогда не попадает в комментарий
Кодмод, который сопоставлял бы сырой исходник, переписал бы комментарий, объясняющий переход, и строку, которую проверяет утверждение. Ни одна из этих поломок не шумит: файл компилируется, а дифф выглядит правдоподобно.
Поэтому каждый паттерн сопоставляется с маской: тот же текст, та же длина, те же переводы строк, а содержимое комментариев, строк, шаблонных литералов и регулярных выражений заменено пробелами. Смещения в маске верны и в оригинале, так что «найти в маске, вырезать из исходника» безопасно. Кавычки сохраняются, так что from 'jest-auto-spies' по-прежнему находится: кавычка совпадает в маске, а спецификатор читается из исходника.
JSX — это код, а не регулярное выражение. Слэш в <Thing /> и </Thing> стоит там, где может начаться регулярное выражение. Кодмод знает, что / сразу после < не открывает регулярное выражение и что литерал с < внутри тоже им не является. Поэтому .tsx-тесты переводятся и проверяются целиком.
Для --verify маска устроена чуть иначе — в двух местах, потому что то, что он ищет, стоит внутри литерала. Спецификатор модуля остаётся видимым: from 'jest-auto-spies' и есть остаток. Шаблонный литерал остаётся видимым: трансформации внутрь него не заходят, так что jest. там настоящий. Содержимое обычной строки остаётся замаскированным: expect(text).toBe('jest.spyOn(…)') — это текст о переходе, а не код для перевода.