Стратегия постепенной миграции

Переход на Day.js в существующем проекте редко бывает одномоментным процессом: в реальных кодовых базах дата-время обычно распределено между форматированием, парсингом, бизнес-логикой, сериализацией API и утилитами. Поэтому наиболее устойчивой стратегией становится постепенная миграция с контролем совместимости и минимизацией регрессионных рисков.

Первый этап — полное картирование источников работы с датами. В типичном проекте встречаются:

  • нативный Date
  • сторонние библиотеки (Moment.js, Luxon, date-fns)
  • кастомные утилиты форматирования
  • ручные преобразования строк ISO

Критически важно разделить использование по категориям:

  • UI-форматирование (отображение дат пользователю)
  • Бизнес-логика (сравнение, дедлайны, интервалы)
  • API-слой (парсинг и сериализация)
  • внутренние расчёты (таймстемпы, календарные операции)

Такое разделение позволяет внедрять Day.js точечно, не затрагивая всю систему сразу.

Введение абстракционного слоя

Одним из ключевых инструментов миграции становится создание промежуточного слоя работы с датами.

Пример базового интерфейса:

export const dateAdapter = {
  now: () => Date.now(),

  parse: (value) => new Date(value),

  format: (date, fmt) => {
    return formatDateLegacy(date, fmt);
  },

  addDays: (date, days) => {
    const d = new Date(date);
    d.setDate(d.getDate() + days);
    return d;
  }
};

После внедрения Day.js этот слой постепенно переписывается:

import dayjs from "dayjs";

export const dateAdapter = {
  now: () => dayjs(),

  parse: (value) => dayjs(value),

  format: (date, fmt) => dayjs(date).format(fmt),

  addDays: (date, days) => dayjs(date).add(days, "day")
};

Такой подход позволяет изолировать изменения от остального кода.

Параллельное использование двух реализаций

На промежуточном этапе часто требуется поддерживать двойную реализацию — старую и новую.

import dayjs from "dayjs";

export function formatDate(date, fmt, useDayjs = false) {
  if (useDayjs) {
    return dayjs(date).format(fmt);
  }

  return legacyFormat(date, fmt);
}

Цель этого этапа — обеспечить идентичность результатов.

Особое внимание уделяется:

  • одинаковому часовому поясу
  • единым правилам округления
  • обработке null/undefined
  • некорректным датам

Синхронизация поведения и нормализация входных данных

Одна из самых частых проблем миграции — различие в парсинге строк.

Нативный Date и Day.js по-разному трактуют некорректные или неполные строки.

Поэтому вводится слой нормализации:

import dayjs from "dayjs";

const normalize = (input) => {
  if (!input) return null;

  const d = dayjs(input);
  return d.isValid() ? d : null;
};

В рамках миграции важно унифицировать:

  • формат ISO 8601 как основной
  • запрет неявного парсинга локализованных строк
  • явное указание форматов при разборе

Постепенная замена утилит

Миграция должна идти по функциональным зонам.

Форматирование дат

// было
formatDateLegacy(date)

// стало
dayjs(date).format("DD.MM.YYYY")

Операции с интервалами

// было
const nextWeek = new Date();
nextWeek.setDate(nextWeek.getDate() + 7);

// стало
const nextWeek = dayjs().add(7, "day");

Сравнение дат

// было
if (a > b)

// стало
if (dayjs(a).isAfter(dayjs(b)))

Управление плагинами

Day.js использует модульную систему расширений. На этапе миграции важно централизовать подключение плагинов:

import dayjs from "dayjs";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import utc from "dayjs/plugin/utc";

dayjs.extend(isSameOrAfter);
dayjs.extend(utc);

Рекомендуется формировать единый модуль и запрещать локальные расширения, чтобы избежать расхождений поведения.

Обработка временных зон

Если проект затрагивает несколько регионов, миграция должна учитывать отсутствие встроенной полноценной timezone-модели в Day.js без плагинов.

import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";

dayjs.extend(utc);
dayjs.extend(timezone);

const msk = dayjs().tz("Europe/Moscow");

На этапе миграции важно зафиксировать:

  • базовую временную зону хранения (обычно UTC)
  • правила конвертации на UI-слое
  • запрет смешивания локальных и UTC значений

Тестирование эквивалентности поведения

Ключевая часть миграции — сравнение старой и новой реализации.

test("format consistency", () => {
  const date = new Date("2024-01-01T10:00:00Z");

  expect(dayjs(date).format("YYYY-MM-DD"))
    .toBe(legacyFormat(date, "YYYY-MM-DD"));
});

Особенно важно тестировать:

  • переходы через границы месяца
  • високосные годы
  • DST (летнее время)
  • некорректные входные значения

Ограничение поверхности использования

Для предотвращения регрессий вводятся архитектурные ограничения:

  • запрет прямого использования Date в бизнес-логике
  • ESLint-правила на импорт Day.js только через адаптер
  • централизованные утилиты даты

Пример правила:

{
  "no-restricted-imports": [
    "error",
    {
      "paths": [
        {
          "name": "dayjs",
          "message": "Использовать dateAdapter"
        }
      ]
    }
  ]
}

Codemod-миграция

Для крупных проектов используется автоматическая замена:

// jscodeshift transform
module.exports = function transformer(file, api) {
  const j = api.jscodeshift;

  return j(file.source)
    .find(j.CallExpression)
    .replaceWith(path => {
      // преобразование moment() -> dayjs()
      return path.node;
    })
    .toSource();
};

Codemod снижает количество ручных изменений и обеспечивает единообразие.

Контроль размера бандла

Одним из преимуществ Day.js является малый размер, но при неправильном использовании он может увеличиваться из-за плагинов.

Контрольные меры:

  • импорт только нужных плагинов
  • запрет import *
  • анализ bundle analyzer
npx webpack-bundle-analyzer dist/stats.json

Двойная запись в критических системах

В системах с высокой критичностью применяется временная параллельная запись:

const legacyValue = legacyFormat(date);
const newValue = dayjs(date).format("YYYY-MM-DD");

logCompare(legacyValue, newValue);

Это позволяет выявить скрытые расхождения до полного отключения старой логики.

Поэтапное отключение старого кода

Финальный этап — удаление старых утилит:

  • удаление legacy-formatters
  • отключение feature flags
  • удаление polyfill-слоев
  • унификация API даты

При этом сохраняется только адаптер, работающий на Day.js, как единственная точка взаимодействия с датами.

Поддержка единообразия в команде

После завершения миграции критично закрепить правила:

  • все новые функции используют только адаптер
  • прямое создание Date считается техническим долгом
  • любые изменения форматов проходят через централизованный слой
  • плагины Day.js добавляются только через core-модуль

Такой подход стабилизирует поведение системы и предотвращает повторное расслоение логики работы с датами.