Почему date-fns падала на пустом значении после обновления и как мы проверили правку
Воспроизводим исключение в date-fns 4.1.0 и проверяем локальную правку в CommonJS и ESM на девяти сценариях.
При переходе с date-fns 2.8 на 4.0 разработчик обнаружил, что `parseISO(undefined)` выбрасывает исключение. Пустая строка при этом возвращала объект Invalid Date. Мы воспроизвели разницу на версиях 2.8.1 и 4.1.0, внесли локальную правку в исходник и проверили её на обеих поставляемых сборках. До правки проходило шесть проверок из девяти, после — все девять.
Одна и та же проверка после обновления перестала получать результат
В приложении значение даты может отсутствовать, если поле необязательное или ответ API пришёл без нужного ключа. При чтении такого поля JavaScript возвращает undefined. Если передать его в парсер, поведение зависит от версии библиотеки. В нашем прогоне 2.8.1 возвращала объект Date со значением NaN. В 4.1.0 функция прерывалась с сообщением Cannot read properties of undefined (reading 'split'). Для null результат изменился аналогично.
Это два разных исхода для вызывающего кода. Проверка через Number.isNaN(date.getTime()) позволяет обработать некорректную дату, только если вызов успел вернуть объект. Если парсер выбросил исключение, до проверки дело не дойдёт. Из-за этого после обновления может упасть код, который раньше превращал некорректное значение в пустое поле формы или пропускал строку импорта.
У parseISO в исходнике 4.1.0 указан параметр типа `string`. С точки зрения TypeScript с включённой проверкой nullish-значений такой аргумент нужно обработать заранее. История из issue касается поведения во время выполнения. Значения из JSON, обычного JavaScript или слишком широкой типизации всё равно могут дойти до функции. Цель эксперимента — проверить небольшой патч совместимости для двух конкретных входов. Решение о контракте всей библиотеки остаётся за её сопровождающими.
Как мы нашли точку сбоя и ограничили задачу для агента
Я взял публичный тег 4.1.0 и сравнил его с опубликованным npm-пакетом той же версии. Функция сначала готовит помощник invalidDate(), затем передаёт аргумент в splitDateString. Внутри вызывается .split(). У пустой строки метод есть, поэтому парсер доходит до проверки результата. У undefined и null вызов метода невозможен.
Задача для Codex сводилась к одному изменению: перехватить два отсутствующих значения до разбора строки, вернуть результат существующего помощника и сохранить поведение допустимых строк. Это ограничение важно, потому что у парсера есть логика для временных зон, дополнительных цифр года и пользовательского типа даты. Исправление случая из тикета не требует переписывать ни одну из этих частей.
Локальная правка в src/parseISO/index.ts выглядит так:
const invalidDate = () => constructFrom(options?.in, NaN); +if (argument === undefined || argument === null) return invalidDate(); + const additionalDigits = options?.additionalDigits ?? 2; const dateStrings = splitDateString(argument);
Здесь есть короткий путь, который выглядит правдоподобно: вернуть new Date(NaN). Но исходный помощник учитывает options.in. Если вызывающая сторона задала свой тип даты, прямой вызов конструктора Date обошёл бы это правило. Поэтому в тесты добавили отдельно некорректное значение с пользовательским типом. Такой тест проверяет свойство, которое легко пропустить при чтении одного сообщения об ошибке.
Что показали девять проверок до и после
Тесты запускались на Node.js 24.22.1. Исторический пакет 2.8.1 проверили на семи входах. Версию 4.1.0 проверили в CommonJS и ESM. К семи обычным входам добавили две проверки с пользовательским типом даты: для корректной строки и для undefined. В обе сборки 4.1.0 внесли ту же проверку, что и в TypeScript-исходник.
| Проверка | date-fns 2.8.1 | 4.1.0 до правки | 4.1.0 после правки |
undefined и null |
Некорректная дата | Исключение | Некорректная дата |
| Пустая строка, мусор и 30 февраля | Некорректная дата | Некорректная дата | Некорректная дата |
| ISO-строка в UTC и со смещением | Ожидаемое время UTC | Ожидаемое время UTC | Ожидаемое время UTC |
undefined с собственным типом даты |
В этом прогоне не проверяли | Исключение | Некорректная дата нужного типа |
| Валидная строка с собственным типом даты | В этом прогоне не проверяли | Дата нужного типа | Дата нужного типа |
Два положительных теста проверяли конкретные значения. Строка 2024-02-29T12:00:00Z должна дать полдень UTC, а 2024-02-29T12:00:00+03:00 — девять утра UTC. Вместе они помогают заметить побочный эффект, если правка заденет обработку смещений. У отрицательных тестов другой критерий: функция возвращает объект даты, а его getTime() даёт NaN. Отдельная проверка убеждается, что объект создан заданным типом.
Исходный diff проверили на применимость к коду тега. Для динамических тестов правку повторили в готовых файлах parseISO.cjs и parseISO.js. Полную сборку date-fns и её полный набор тестов в этот эксперимент не включали. Поэтому результат относится к девяти указанным сценариям на конкретном релизе. Локальный патч не отправлялся в репозиторий проекта.
Как повторить ошибку без сборки всего проекта
Для первого прогона достаточно Node.js и двух опубликованных пакетов. Создайте пустую папку, установите версии под разными именами и сравните результат. Пакеты устанавливаются рядом, так что один запуск покажет обе версии на одинаковом входе.
npm install --no-save --package-lock=false date-fns-old@npm:date-fns@2.8.1 date-fns-new@npm:date-fns@4.1.0
const oldParseISO = require('date-fns-old/parseISO');
const { parseISO: newParseISO } = require('date-fns-new');
for (const [version, parse] of [
['2.8.1', oldParseISO],
['4.1.0', newParseISO],
]) {
for (const value of [undefined, null, '']) {
try {
console.log(version, value, parse(value).getTime());
} catch (error) {
console.log(version, value, error.message);
}
}
}
Для корректной проверки не преобразуйте аргумент в строку перед вызовом. String(undefined) возвращает строку "undefined", у которой есть метод .split(). Такой тест минует точку сбоя, даже если вызов с настоящим undefined падает. По той же причине важно проверить null отдельно: некоторые формы и API явно передают его, а отсутствующее поле даёт другое значение.
Почему тест на пользовательский тип важнее размера diff
В 4.1.0 функция может создать дату через переданный контекст. Для проверки мы задали фабрику in: value => new CustomDate(value), где CustomDate наследует Date. В одном тесте она получает валидную ISO-строку, во втором — отсутствующее значение. После правки первый тест сохранил и момент времени, и класс результата. Во втором класс сохранился, а значение даты стало некорректным, как и для других ошибочных входов.
Обычная проверка Number.isNaN(result.getTime()) пропустила бы ошибку с прямым new Date(NaN): у обоих объектов время некорректно. Поэтому в тесте есть дополнительная проверка result instanceof CustomDate. Она превращает требование «сохрани контекст» в конкретное наблюдение. Это хороший вопрос к агенту перед правкой: какой тест упадёт, если выбрать самое короткое, но несовместимое решение?
Что из этого полезно при обновлении собственной зависимости
Если вы обновляете date-fns в приложении, самый короткий путь — проверить значение на границе своего кода. Для необязательного поля решите, что означает отсутствие даты: пропуск поля, значение по умолчанию или ошибка валидации. Вызывайте parseISO после этой проверки. Так результат понятен по вашему коду, даже если библиотека снова изменит поведение за пределами заявленного типа.
Сам сценарий можно повторить без привязки к date-fns. Возьмите публичное описание бага, зафиксируйте версии, сохраните проверку, которая падает до правки, и добавьте соседние случаи. Здесь соседним случаем оказался пользовательский тип даты. Он повлиял на выбор реализации сильнее, чем объём diff. Если агент уже нашёл место падения, следующая задача для него — показать, какой контракт сохраняет предлагаемая строка и где этот контракт проверяется.
Проглиб уже разбирал критерии приёмки для ИИ-агента на примере разработки сервиса. В багфиксе к этому добавляется проверка старой версии. Она показывает, какое поведение реально было до обновления и есть ли смысл его возвращать. В случае date-fns именно сравнение релизов превратило общий совет «проверь вход» в конкретное решение с проверяемыми границами.