dayjs icon indicating copy to clipboard operation
dayjs copied to clipboard

dayjs(null) crashes when using it with objectSupport plugin

Open juliushuck opened this issue 2 years ago • 4 comments
trafficstars

Describe the bug See this codesandbox: dayjs(null) crashes when used with objectSupport plugin

There is a test that runs dayjs(null) (without the plugin)

The parse override of the plugin doesn't handle nullable values at all, it just returns the value as is:

https://github.com/iamkun/dayjs/blob/e70bee7f840c89ec523b9ac997e5ac621a522726/src/plugin/objectSupport/index.js#L12

Whereas the default parser returns new Date(NaN) (to have an invalid date):

https://github.com/iamkun/dayjs/blob/e70bee7f840c89ec523b9ac997e5ac621a522726/src/index.js#L62

I detected this problem using dayjs in Mui (React components library): https://github.com/mui/mui-x/issues/7571

Expected behavior Should not crash

Information

  • Day.js Version ^1.11.7

juliushuck avatar Jan 16 '23 12:01 juliushuck

Thank you for isolating this issue! One of our dependencies (@commandbar/foobar) started using the objectSupport plugin in v0.3.31, and I couldn't figure out why our app was suddenly breaking!

I'm using this temporary fix, as we were using our own function for this check anyway:

export function isValidDate(date: Parameters<typeof dayjs>[0]) {
  /**
   * `dayjs(null)` crashes when the objectSupport plugin is enabled.
   * @see https://github.com/iamkun/dayjs/issues/2208
   */
  if (date === null) return false;

  return dayjs(date).isValid();
}

Hope this helps!

kaelig avatar Jan 18 '23 00:01 kaelig

For me, I temporally solved it by copying the code of the object support plugin and then add a small null check.

See this:

const objectSupportDayPlugin = (o, c, dayjs) => {
    const proto = c.prototype;
    const isObject = (obj) =>
        obj !== null && // HACK <-- I added this line
        !(obj instanceof Date) &&
        !(obj instanceof Array) &&
        !proto.$utils().u(obj) &&
        obj.constructor.name === "Object";
    const prettyUnit = (u) => {
        const unit = proto.$utils().p(u);
        return unit === "date" ? "day" : unit;
    };
    const parseDate = (cfg) => {
        const { date, utc } = cfg;
        const $d = {};
        if (isObject(date)) {
            if (!Object.keys(date).length) {
                return new Date();
            }
            const now = utc ? dayjs.utc() : dayjs();
            Object.keys(date).forEach((k) => {
                $d[prettyUnit(k)] = date[k];
            });
            const d = $d.day || (!$d.year && !($d.month >= 0) ? now.date() : 1);
            const y = $d.year || now.year();
            const M = $d.month >= 0 ? $d.month : !$d.year && !$d.day ? now.month() : 0; // eslint-disable-line no-nested-ternary,max-len
            const h = $d.hour || 0;
            const m = $d.minute || 0;
            const s = $d.second || 0;
            const ms = $d.millisecond || 0;
            if (utc) {
                return new Date(Date.UTC(y, M, d, h, m, s, ms));
            }
            return new Date(y, M, d, h, m, s, ms);
        }
        return date;
    };

    const oldParse = proto.parse;
    proto.parse = function (cfg) {
        cfg.date = parseDate.bind(this)(cfg);
        oldParse.bind(this)(cfg);
    };

    const oldSet = proto.set;
    const oldAdd = proto.add;
    const oldSubtract = proto.subtract;

    const callObject = function (call, argument, string, offset = 1) {
        const keys = Object.keys(argument);
        let chain = this;
        keys.forEach((key) => {
            chain = call.bind(chain)(argument[key] * offset, key);
        });
        return chain;
    };

    proto.set = function (unit, value) {
        value = value === undefined ? unit : value;
        if (unit.constructor.name === "Object") {
            return callObject.bind(this)(
                function (i, s) {
                    return oldSet.bind(this)(s, i);
                },
                value,
                unit
            );
        }
        return oldSet.bind(this)(unit, value);
    };
    proto.add = function (value, unit) {
        if (value.constructor.name === "Object") {
            return callObject.bind(this)(oldAdd, value, unit);
        }
        return oldAdd.bind(this)(value, unit);
    };
    proto.subtract = function (value, unit) {
        if (value.constructor.name === "Object") {
            return callObject.bind(this)(oldAdd, value, unit, -1);
        }
        return oldSubtract.bind(this)(value, unit);
    };
};
day.extend(objectSupportDayPlugin);

juliushuck avatar Feb 19 '23 16:02 juliushuck

The workaround I came with is writing an additional small plugin:

// dayjsNullSupport.ts
export default function (options: any, dayjsClass: any): void {
	const oldParse = dayjsClass.prototype.parse;
	dayjsClass.prototype.parse = function (config: any) {
		if (config.date === null) {
			config.date = NaN;
		}
		oldParse.bind(this)(config);
	};
}
const objectSupport = require('dayjs/plugin/objectSupport');
const nullSupport = require('./dayjsNullSupport').default;

export const dayjs = require('dayjs');
dayjs.extend(objectSupport);
dayjs.extend(nullSupport);

antonabramovich avatar Apr 25 '23 12:04 antonabramovich

Can't believe I have to deal with this.

lynnporu avatar Jul 25 '24 11:07 lynnporu