Ручное обновление импортов и паттернов в крупном проекте занимает много времени. Автоматизация через codemod, lint-правила и скрипты сокращает этот процесс до нескольких минут.
jscodeshift — инструмент для автоматического переписывания JavaScript/TypeScript кода на основе AST.
npm install --global jscodeshift
// transforms/timeago-named-imports.js
module.exports = function(fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Найти: import timeago from 'timeago.js'
root.find(j.ImportDeclaration, {
source: { value: 'timeago.js' },
}).forEach(path => {
const defaultSpecifier = path.node.specifiers.find(
s => s.type === 'ImportDefaultSpecifier'
);
if (!defaultSpecifier) return;
const localName = defaultSpecifier.local.name; // Имя переменной
// Найти все uses: timeago.format(...) → format(...)
const usedMethods = new Set();
root.find(j.MemberExpression, { object: { name: localName } })
.forEach(memberPath => {
usedMethods.add(memberPath.node.property.name);
j(memberPath.parent).replaceWith(
j.callEx * pression(
j.identifier(memberPath.node.property.name),
memberPath.parent.node.arguments
)
);
});
// Заменить дефолтный импорт на именованный
path.node.specifiers = Array.from(usedMethods).map(name =>
j.importSpecifier(j.identifier(name))
);
});
return root.toSource();
};
jscodeshift -t transforms/timeago-named-imports.js src/
// transforms/timeago-locale-paths.js
module.exports = function(fileInfo, api) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
root.find(j.ImportDeclaration).forEach(path => {
const src = path.node.source.value;
// Заменить старые пути на новые
if (src.startsWith('timeago.js/locales/')) {
const locale = src.replace('timeago.js/locales/', '');
path.node.source = j.stringLiteral(`timeago.js/esm/lang/${locale}`);
}
});
return root.toSource();
};
// .eslintrc.js
module.exports = {
rules: {
// Запретить дефолтный импорт timeago.js
'no-restricted-imports': ['error', {
patterns: [{
group: ['timeago.js'],
importNames: ['default'],
message: 'Используйте именованные импорты: import { format, render } from "timeago.js"',
}],
}],
},
};
npx eslint src/ --fix
#!/bin/bash
# find-legacy-timeago.sh
echo "=== Дефолтные импорты timeago.js ==="
grep -r "import timeago from 'timeago.js'" src/ --include="*.ts" --include="*.tsx"
echo "=== Старые пути локалей ==="
grep -r "timeago.js/locales/" src/ --include="*.ts" --include="*.tsx"
echo "=== render без cancel ==="
grep -rn "render(" src/ --include="*.tsx" | grep -v "cancel" | head -20
# Выявить все ошибки типов после обновления
npx tsc --noEmit 2>&1 | grep timeago
// scripts/test-migration.ts
import { execSync } from 'child_process';
const VERSIONS = ['3.0.2', '4.0.0', '4.0.2'];
for (const version of VERSIONS) {
console.log(`Testing timeago.js@${version}...`);
execSync(`npm install timeago.js@${version} --no-save`);
execSync('npm test -- --testPathPattern=contract');
console.log(`✓ ${version} passes`);
}
// Восстановить актуальную версию
execSync('npm install');
{
"scripts": {
"migrate:timeago": "jscodeshift -t transforms/timeago-named-imports.js src/ && jscodeshift -t transforms/timeago-locale-paths.js src/",
"check:timeago": "eslint src/ --rule 'no-restricted-imports: error' && tsc --noEmit",
"audit:timeago": "bash scripts/find-legacy-timeago.sh"
}
}
# .github/workflows/migration-check.yml
name: Migration Check
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- name: Check for deprecated timeago patterns
run: npm run audit:timeago
- name: TypeScript check
run: npx tsc --noEmit
- name: Tests
run: npm test
Если автоматическая трансформация дала неверный результат:
git diff --stat # Посмотреть изменения
git diff # Детальный diff
# Откатить конкретный файл
git checkout -- src/components/TimeAgo.tsx
# Откатить всё
git checkout -- .
Codemod трансформации рекомендуется применять маленькими порциями с промежуточными коммитами.