diff --git a/.changeset/lazy-knives-write.md b/.changeset/lazy-knives-write.md new file mode 100644 index 00000000000..b58cda68cee --- /dev/null +++ b/.changeset/lazy-knives-write.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': minor +--- + +feat: add `Temporal` serialization support diff --git a/packages/qwik/src/core/shared/serdes/allocate.ts b/packages/qwik/src/core/shared/serdes/allocate.ts index ffbec76c1eb..9495ad51549 100644 --- a/packages/qwik/src/core/shared/serdes/allocate.ts +++ b/packages/qwik/src/core/shared/serdes/allocate.ts @@ -78,6 +78,22 @@ export const allocate = (container: DeserializeContainer, typeId: number, value: return new URL(value as string); case TypeIds.Date: return new Date(value as number); + case TypeIds.TemporalDuration: + return Temporal.Duration.from(value as string); + case TypeIds.TemporalInstant: + return Temporal.Instant.from(value as string); + case TypeIds.TemporalPlainDate: + return Temporal.PlainDate.from(value as string); + case TypeIds.TemporalPlainDateTime: + return Temporal.PlainDateTime.from(value as string); + case TypeIds.TemporalPlainMonthDay: + return Temporal.PlainMonthDay.from(value as string); + case TypeIds.TemporalPlainTime: + return Temporal.PlainTime.from(value as string); + case TypeIds.TemporalPlainYearMonth: + return Temporal.PlainYearMonth.from(value as string); + case TypeIds.TemporalZonedDateTime: + return Temporal.ZonedDateTime.from(value as string); case TypeIds.Regex: const idx = (value as string).lastIndexOf('/'); return new RegExp((value as string).slice(1, idx), (value as string).slice(idx + 1)); diff --git a/packages/qwik/src/core/shared/serdes/can-serialize.ts b/packages/qwik/src/core/shared/serdes/can-serialize.ts index e1c1b925e80..140dbeab42e 100644 --- a/packages/qwik/src/core/shared/serdes/can-serialize.ts +++ b/packages/qwik/src/core/shared/serdes/can-serialize.ts @@ -64,6 +64,22 @@ export const canSerialize = (value: unknown, seen: WeakSet = new WeakSet()) return true; } else if (value instanceof Date) { return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.Duration) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.Instant) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.PlainDate) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.PlainDateTime) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.PlainMonthDay) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.PlainTime) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.PlainYearMonth) { + return true; + } else if (typeof Temporal !== 'undefined' && value instanceof Temporal.ZonedDateTime) { + return true; } else if (value instanceof RegExp) { return true; } else if (value instanceof URLSearchParams) { diff --git a/packages/qwik/src/core/shared/serdes/constants.ts b/packages/qwik/src/core/shared/serdes/constants.ts index 3b9851a3fdc..cf7fb6771fd 100644 --- a/packages/qwik/src/core/shared/serdes/constants.ts +++ b/packages/qwik/src/core/shared/serdes/constants.ts @@ -89,6 +89,14 @@ export const enum TypeIds { BigInt, URLSearchParams, ForwardRefs, + TemporalDuration, + TemporalInstant, + TemporalPlainDate, + TemporalPlainDateTime, + TemporalPlainMonthDay, + TemporalPlainTime, + TemporalPlainYearMonth, + TemporalZonedDateTime, /// All types below will be inflate()d Error, Promise, @@ -127,6 +135,14 @@ export const _typeIdNames = [ 'BigInt', 'URLSearchParams', 'ForwardRefs', + 'TemporalDuration', + 'TemporalInstant', + 'TemporalPlainDate', + 'TemporalPlainDateTime', + 'TemporalPlainMonthDay', + 'TemporalPlainTime', + 'TemporalPlainYearMonth', + 'TemporalZonedDateTime', 'Error', 'Promise', 'Set', diff --git a/packages/qwik/src/core/shared/serdes/serdes.unit.ts b/packages/qwik/src/core/shared/serdes/serdes.unit.ts index 8ec490ba53c..ad434213f43 100644 --- a/packages/qwik/src/core/shared/serdes/serdes.unit.ts +++ b/packages/qwik/src/core/shared/serdes/serdes.unit.ts @@ -165,6 +165,63 @@ describe('shared-serialization', () => { (6 chars)" `); }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalDuration), async () => { + expect(await dump(Temporal.Duration.from('PT194972H22M2.783S'))).toMatchInlineSnapshot(` + " + 0 TemporalDuration "PT194972H22M2.783S" + (25 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalInstant), async () => { + expect(await dump(Temporal.Instant.from('2003-12-29T00:00:00Z'))).toMatchInlineSnapshot(` + " + 0 TemporalInstant "2003-12-29T00:00:00Z" + (27 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalPlainDate), async () => { + expect(await dump(Temporal.PlainDate.from('2003-12-29'))).toMatchInlineSnapshot(` + " + 0 TemporalPlainDate "2003-12-29" + (17 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalPlainDateTime), async () => { + expect(await dump(Temporal.PlainDateTime.from('2003-12-29T04:20:00'))).toMatchInlineSnapshot(` + " + 0 TemporalPlainDateTime "2003-12-29T04:20:00" + (26 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalPlainMonthDay), async () => { + expect(await dump(Temporal.PlainMonthDay.from('12-29'))).toMatchInlineSnapshot(` + " + 0 TemporalPlainMonthDay "12-29" + (12 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalPlainTime), async () => { + expect(await dump(Temporal.PlainTime.from('04:20:00'))).toMatchInlineSnapshot(` + " + 0 TemporalPlainTime "04:20:00" + (15 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalPlainYearMonth), async () => { + expect(await dump(Temporal.PlainYearMonth.from('2003-12'))).toMatchInlineSnapshot(` + " + 0 TemporalPlainYearMonth "2003-12" + (14 chars)" + `); + }); + it.skipIf(typeof Temporal === 'undefined')(title(TypeIds.TemporalZonedDateTime), async () => { + expect(await dump(Temporal.ZonedDateTime.from('2003-12-29T04:20:00+01:00[Europe/Berlin]'))) + .toMatchInlineSnapshot(` + " + 0 TemporalZonedDateTime "2003-12-29T04:20:00+01:00[Europe/Berlin]" + (47 chars)" + `); + }); it(title(TypeIds.Regex), async () => { expect(await dump(/abc/gm)).toMatchInlineSnapshot(` " @@ -953,6 +1010,32 @@ describe('shared-serialization', () => { expect(date).toBeInstanceOf(Date); expect(date.toISOString()).toBe('2009-02-13T23:31:30.000Z'); }); + const testTemporal = (id: TypeIds, T_: () => typeof __TemporalStub, value: string) => { + it.skipIf(typeof Temporal === 'undefined')(title(id), async () => { + const T = T_(); + const original = T.from(value); + const objs = await serialize(original); + const deserialized = deserialize(objs)[0]; + expect(deserialized).toBeInstanceOf(T); + expect(original).toEqual(deserialized); + }); + }; + testTemporal(TypeIds.TemporalDuration, () => Temporal.Duration, 'PT194972H22M2.783S'); + testTemporal(TypeIds.TemporalInstant, () => Temporal.Instant, '2003-12-29T00:00:00Z'); + testTemporal(TypeIds.TemporalPlainDate, () => Temporal.PlainDate, '2003-12-29'); + testTemporal( + TypeIds.TemporalPlainDateTime, + () => Temporal.PlainDateTime, + '2003-12-29T04:20:00' + ); + testTemporal(TypeIds.TemporalPlainMonthDay, () => Temporal.PlainMonthDay, '12-29'); + testTemporal(TypeIds.TemporalPlainTime, () => Temporal.PlainTime, '04:20:00'); + testTemporal(TypeIds.TemporalPlainYearMonth, () => Temporal.PlainYearMonth, '2003-12'); + testTemporal( + TypeIds.TemporalZonedDateTime, + () => Temporal.ZonedDateTime, + '2003-12-29T04:20:00+01:00[Europe/Berlin]' + ); it(title(TypeIds.Regex), async () => { const objs = await serialize(/abc/gm); const regex = deserialize(objs)[0] as RegExp; diff --git a/packages/qwik/src/core/shared/serdes/serialize.ts b/packages/qwik/src/core/shared/serdes/serialize.ts index 28a14890368..f11c795b730 100644 --- a/packages/qwik/src/core/shared/serdes/serialize.ts +++ b/packages/qwik/src/core/shared/serdes/serialize.ts @@ -68,6 +68,7 @@ export class Serializer { private $parent$: SeenRef | undefined; private $qrlMap$ = new Map(); private $writer$: StreamWriter; + private $temporalDefined$: boolean = typeof Temporal !== 'undefined'; constructor(public $serializationContext$: SerializationContext) { this.$writer$ = $serializationContext$.$writer$; @@ -486,6 +487,22 @@ export class Serializer { this.output(TypeIds.URL, value.href); } else if (value instanceof Date) { this.output(TypeIds.Date, Number.isNaN(value.valueOf()) ? '' : value.valueOf()); + } else if (this.$temporalDefined$ && value instanceof Temporal.Duration) { + this.output(TypeIds.TemporalDuration, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.Instant) { + this.output(TypeIds.TemporalInstant, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.PlainDate) { + this.output(TypeIds.TemporalPlainDate, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.PlainDateTime) { + this.output(TypeIds.TemporalPlainDateTime, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.PlainMonthDay) { + this.output(TypeIds.TemporalPlainMonthDay, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.PlainTime) { + this.output(TypeIds.TemporalPlainTime, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.PlainYearMonth) { + this.output(TypeIds.TemporalPlainYearMonth, value.toJSON()); + } else if (this.$temporalDefined$ && value instanceof Temporal.ZonedDateTime) { + this.output(TypeIds.TemporalZonedDateTime, value.toJSON()); } else if (value instanceof RegExp) { this.output(TypeIds.Regex, value.toString()); } else if (value instanceof Error) { diff --git a/packages/qwik/src/core/shared/serdes/type-stub.d.ts b/packages/qwik/src/core/shared/serdes/type-stub.d.ts new file mode 100644 index 00000000000..6c0cc9d5e9f --- /dev/null +++ b/packages/qwik/src/core/shared/serdes/type-stub.d.ts @@ -0,0 +1,17 @@ +/** Minimal type-stub for the types in `Temporal`. Contains methods required for (de-)serializing. */ +declare class __TemporalStub { + static from(item: string): T; + toJSON(): string; +} + +/** Type-Stub for the types in `Temporal` */ +declare namespace Temporal { + class Duration extends __TemporalStub {} + class Instant extends __TemporalStub {} + class PlainDate extends __TemporalStub {} + class PlainDateTime extends __TemporalStub {} + class PlainMonthDay extends __TemporalStub {} + class PlainTime extends __TemporalStub {} + class PlainYearMonth extends __TemporalStub {} + class ZonedDateTime extends __TemporalStub {} +}