diff --git a/package-lock.json b/package-lock.json index 74529619..f11227c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "decimal.js": "^10.3.1", "mongodb": "^3.6.6", "tsdx": "^0.14.1", - "typescript": "^5.9.3", + "typescript": "^6.0.2", "vitest": "^0.34.6" }, "engines": { @@ -57,7 +57,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2611,18 +2610,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -3091,7 +3078,6 @@ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -3146,8 +3132,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/chai-subset": { "version": "1.3.6", @@ -3335,7 +3320,6 @@ "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/experimental-utils": "2.34.0", "functional-red-black-tree": "^1.0.1", @@ -3388,7 +3372,6 @@ "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@types/eslint-visitor-keys": "^1.0.0", "@typescript-eslint/experimental-utils": "2.34.0", @@ -3632,7 +3615,6 @@ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4161,7 +4143,6 @@ "deprecated": "babel-eslint is now @babel/eslint-parser. This package will no longer receive updates.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/parser": "^7.7.0", @@ -4583,7 +4564,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5857,7 +5837,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.0.0", "ajv": "^6.10.0", @@ -6000,7 +5979,6 @@ "integrity": "sha512-bhewp36P+t7cEV0b6OdmoRWJCBYRiHFlqPZAG1oS3SF+Y0LQkeDvFSM4oxoxvczD1OdONCXMlJfQFiWLcV9urw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "lodash": "^4.17.15" }, @@ -6017,7 +5995,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6085,7 +6062,6 @@ "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", @@ -6138,7 +6114,6 @@ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", @@ -6172,7 +6147,6 @@ "integrity": "sha512-Y2c4b55R+6ZzwtTppKwSmK/Kar8AdLiC2f9NADCuxbcTgPPg41Gyqa6b9GppgXSvCtkRw43ZE86CT5sejKC6/g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=7" }, @@ -8506,7 +8480,6 @@ "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^25.5.4", "import-local": "^3.0.2", @@ -11121,7 +11094,6 @@ "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -11844,7 +11816,6 @@ "integrity": "sha512-/2HA0Ec70TvQnXdzynFffkjA6XN+1e2pEv/uKS5Ulca40g2L7KuOE3riasHoNVHOsFD5KKZgDsMk1CP3Tw9s+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/node": "*", @@ -13954,7 +13925,6 @@ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14131,12 +14101,11 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14490,20 +14459,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vite-node/node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/vite-node/node_modules/rollup": { "version": "4.53.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", @@ -14546,27 +14501,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/vite-node/node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vite-node/node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -14790,27 +14724,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/vitest/node_modules/terser": { - "version": "5.44.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", - "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/vitest/node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index e6210940..b4164e73 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "scripts": { "build": "tsc", "test": "vitest run", + "test:temporal": "node --harmony-temporal ./node_modules/vitest/vitest.mjs run", "prepack": "npm run build" }, "prettier": { @@ -54,7 +55,7 @@ "decimal.js": "^10.3.1", "mongodb": "^3.6.6", "tsdx": "^0.14.1", - "typescript": "^5.9.3", + "typescript": "^6.0.2", "vitest": "^0.34.6" }, "dependencies": { diff --git a/src/index.test.ts b/src/index.test.ts index 00917c97..b535657d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -20,6 +20,7 @@ import { Decimal } from 'decimal.js'; import { describe, it, expect, test } from 'vitest'; const isNode10 = process.version.indexOf('v10') === 0; +const hasTemporal = typeof Temporal !== 'undefined'; describe('stringify & parse', () => { const cases: Record< @@ -29,7 +30,7 @@ describe('stringify & parse', () => { output: JSONValue | ((v: JSONValue) => void); outputAnnotations?: SuperJSONResult['meta']; customExpectations?: (value: any) => void; - skipOnNode10?: boolean; + skip?: boolean; dontExpectEquality?: boolean; only?: boolean; } @@ -496,7 +497,7 @@ describe('stringify & parse', () => { }, 'works for symbols': { - skipOnNode10: true, + skip: isNode10, input: () => { const parent = Symbol('Parent'); const child = Symbol('Child'); @@ -571,7 +572,7 @@ describe('stringify & parse', () => { }, 'issue #58': { - skipOnNode10: true, + skip: isNode10, input: () => { const cool = Symbol('cool'); SuperJSON.registerSymbol(cool); @@ -757,6 +758,268 @@ describe('stringify & parse', () => { }, }, }, + + 'works with Temporal.Instant': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.Instant.from('2024-01-01T10:00:00Z'), + }), + output: { + a: '2024-01-01T10:00:00Z', + }, + outputAnnotations: { + values: { + a: [['temporal', 'Instant']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.Instant); + expect( + value.a.equals(Temporal.Instant.from('2024-01-01T10:00:00Z')) + ).toBe(true); + }, + }, + + 'works with Temporal.Duration': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.Duration.from({ hours: 2, minutes: 30, seconds: 15 }), + }), + output: { + a: 'PT2H30M15S', + }, + outputAnnotations: { + values: { + a: [['temporal', 'Duration']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.Duration); + expect(value.a.hours).toBe(2); + expect(value.a.minutes).toBe(30); + expect(value.a.seconds).toBe(15); + }, + }, + + 'works with Temporal.PlainDate': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.PlainDate.from('2024-03-15'), + }), + output: { + a: '2024-03-15', + }, + outputAnnotations: { + values: { + a: [['temporal', 'PlainDate']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.PlainDate); + expect(value.a.equals(Temporal.PlainDate.from('2024-03-15'))).toBe( + true + ); + }, + }, + + 'works with Temporal.PlainDateTime': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.PlainDateTime.from('2024-03-15T14:30:45.123'), + }), + output: { + a: '2024-03-15T14:30:45.123', + }, + outputAnnotations: { + values: { + a: [['temporal', 'PlainDateTime']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.PlainDateTime); + expect( + value.a.equals(Temporal.PlainDateTime.from('2024-03-15T14:30:45.123')) + ).toBe(true); + }, + }, + + 'works with Temporal.PlainTime': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.PlainTime.from('14:30:45'), + }), + output: { + a: '14:30:45', + }, + outputAnnotations: { + values: { + a: [['temporal', 'PlainTime']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.PlainTime); + expect(value.a.equals(Temporal.PlainTime.from('14:30:45'))).toBe(true); + }, + }, + + 'works with Temporal.PlainYearMonth': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.PlainYearMonth.from('2024-03'), + }), + output: { + a: '2024-03', + }, + outputAnnotations: { + values: { + a: [['temporal', 'PlainYearMonth']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.PlainYearMonth); + expect(value.a.equals(Temporal.PlainYearMonth.from('2024-03'))).toBe( + true + ); + }, + }, + + 'works with Temporal.PlainMonthDay': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.PlainMonthDay.from('03-15'), + }), + output: { + a: '03-15', + }, + outputAnnotations: { + values: { + a: [['temporal', 'PlainMonthDay']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.PlainMonthDay); + expect(value.a.equals(Temporal.PlainMonthDay.from('03-15'))).toBe(true); + }, + }, + + 'works with Temporal.ZonedDateTime': { + skip: !hasTemporal, + input: () => ({ + a: Temporal.ZonedDateTime.from( + '2024-03-15T14:30:45-04:00[America/New_York]' + ), + }), + output: { + a: '2024-03-15T14:30:45-04:00[America/New_York]', + }, + outputAnnotations: { + values: { + a: [['temporal', 'ZonedDateTime']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.a).toBeInstanceOf(Temporal.ZonedDateTime); + expect( + value.a.equals( + Temporal.ZonedDateTime.from( + '2024-03-15T14:30:45-04:00[America/New_York]' + ) + ) + ).toBe(true); + }, + }, + + 'works with multiple Temporal types in one object': { + skip: !hasTemporal, + input: () => ({ + instant: Temporal.Instant.from('2024-01-01T00:00:00Z'), + duration: Temporal.Duration.from({ days: 5 }), + date: Temporal.PlainDate.from('2024-06-01'), + nested: { + time: Temporal.PlainTime.from('09:15:00'), + }, + }), + output: { + instant: '2024-01-01T00:00:00Z', + duration: 'P5D', + date: '2024-06-01', + nested: { + time: '09:15:00', + }, + }, + outputAnnotations: { + values: { + instant: [['temporal', 'Instant']], + duration: [['temporal', 'Duration']], + date: [['temporal', 'PlainDate']], + 'nested.time': [['temporal', 'PlainTime']], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.instant).toBeInstanceOf(Temporal.Instant); + expect(value.duration).toBeInstanceOf(Temporal.Duration); + expect(value.date).toBeInstanceOf(Temporal.PlainDate); + expect(value.nested.time).toBeInstanceOf(Temporal.PlainTime); + }, + }, + + 'works with top-level Temporal value': { + skip: !hasTemporal, + input: () => Temporal.PlainDate.from('2024-12-25'), + output: '2024-12-25', + outputAnnotations: { + values: [['temporal', 'PlainDate']], + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value).toBeInstanceOf(Temporal.PlainDate); + expect(value.equals(Temporal.PlainDate.from('2024-12-25'))).toBe(true); + }, + }, + + 'works with Temporal inside arrays, sets, and maps': { + skip: !hasTemporal, + input: () => ({ + arr: [ + Temporal.PlainDate.from('2024-01-01'), + Temporal.PlainDate.from('2024-02-01'), + ], + set: new Set([Temporal.Duration.from({ hours: 1 })]), + map: new Map([ + ['start', Temporal.Instant.from('2024-01-01T00:00:00Z')], + ]), + }), + output: { + arr: ['2024-01-01', '2024-02-01'], + set: ['PT1H'], + map: [['start', '2024-01-01T00:00:00Z']], + }, + outputAnnotations: { + values: { + 'arr.0': [['temporal', 'PlainDate']], + 'arr.1': [['temporal', 'PlainDate']], + set: ['set', { 0: [['temporal', 'Duration']] }], + map: ['map', { '0.1': [['temporal', 'Instant']] }], + }, + }, + dontExpectEquality: true, + customExpectations: value => { + expect(value.arr[0]).toBeInstanceOf(Temporal.PlainDate); + expect(value.arr[1]).toBeInstanceOf(Temporal.PlainDate); + const setValues = [...value.set]; + expect(setValues[0]).toBeInstanceOf(Temporal.Duration); + expect(value.map.get('start')).toBeInstanceOf(Temporal.Instant); + }, + }, }; function deepFreeze(object: any, alreadySeenObjects = new Set()) { @@ -803,14 +1066,14 @@ describe('stringify & parse', () => { output: expectedOutput, outputAnnotations: expectedOutputAnnotations, customExpectations, - skipOnNode10, + skip, dontExpectEquality, only, }, ] of Object.entries(cases)) { let testFunc = test; - if (skipOnNode10 && isNode10) { + if (skip) { testFunc = test.skip; } @@ -860,7 +1123,7 @@ describe('stringify & parse', () => { private topSpeed: number, private color: 'red' | 'blue' | 'yellow', private brand: string, - public carriages: Set, + public carriages: Set ) {} public brag() { @@ -871,25 +1134,35 @@ describe('stringify & parse', () => { SuperJSON.registerClass(Train); const { json, meta } = SuperJSON.serialize({ - s7: new Train(100, 'yellow', 'Bombardier', new Set([new Carriage('front'), new Carriage('back')])) as any, + s7: new Train( + 100, + 'yellow', + 'Bombardier', + new Set([new Carriage('front'), new Carriage('back')]) + ) as any, }); - + expect(json).toEqual({ s7: { topSpeed: 100, color: 'yellow', brand: 'Bombardier', - carriages: [ - { name: 'front' }, - { name: 'back' }, - ], + carriages: [{ name: 'front' }, { name: 'back' }], }, }); expect(meta).toEqual({ v: 1, values: { - s7: [['class', 'Train'], { carriages: ["set", { 0: [['class', 'Carriage']], 1: [['class', 'Carriage']] }] }], + s7: [ + ['class', 'Train'], + { + carriages: [ + 'set', + { 0: [['class', 'Carriage']], 1: [['class', 'Carriage']] }, + ], + }, + ], }, }); @@ -1339,7 +1612,9 @@ test('doesnt iterate to keys that dont exist', () => { test('deserialize in place', () => { const serialized = SuperJSON.serialize({ a: new Date() }); const deserializedCopy = SuperJSON.deserialize(serialized); - const deserializedInPlace = SuperJSON.deserialize(serialized, { inPlace: true }); + const deserializedInPlace = SuperJSON.deserialize(serialized, { + inPlace: true, + }); expect(deserializedInPlace).toBe(serialized.json); expect(deserializedCopy).not.toBe(serialized.json); expect(deserializedCopy).toEqual(deserializedInPlace); diff --git a/src/is.test.ts b/src/is.test.ts index 5f8e5e41..297cb95a 100644 --- a/src/is.test.ts +++ b/src/is.test.ts @@ -12,6 +12,7 @@ import { isPlainObject, isTypedArray, isURL, + isTemporal, } from './is.js'; import { test, expect } from 'vitest'; @@ -35,6 +36,9 @@ test('Basic true tests', () => { expect(isSymbol(Symbol())).toBe(true); expect(isTypedArray(new Uint8Array())).toBe(true); expect(isURL(new URL('https://example.com'))).toBe(true); + if (typeof Temporal !== 'undefined') { + expect(isTemporal(new Temporal.Duration())).toBe(true); + } expect(isPlainObject({})).toBe(true); // eslint-disable-next-line no-new-object expect(isPlainObject(new Object())).toBe(true); @@ -58,6 +62,10 @@ test('Basic false tests', () => { expect(isURL('https://example.com')).toBe(false); + expect(isTemporal(new Date())).toBe(false); + expect(isTemporal({})).toBe(false); + expect(isTemporal(NaN)).toBe(false); + expect(isPlainObject(null)).toBe(false); expect(isPlainObject([])).toBe(false); expect(isPlainObject(Object.prototype)).toBe(false); @@ -92,3 +100,18 @@ test('Regression: null-prototype object', () => { expect(isPlainObject(Object.create(null))).toBe(true); expect(isPrimitive(Object.create(null))).toBe(false); }); + +test.skipIf(typeof Temporal === 'undefined')('Temporal', () => { + expect(isTemporal(new Temporal.Duration())).toBe(true); + expect(isTemporal(Temporal.Now.instant())).toBe(true); + expect(isTemporal(new Temporal.PlainDate(2007, 10, 11))).toBe(true); + expect(isTemporal(new Temporal.PlainDateTime(2007, 10, 11))).toBe(true); + expect(isTemporal(new Temporal.PlainMonthDay(10, 11))).toBe(true); + expect(isTemporal(new Temporal.PlainTime())).toBe(true); + expect(isTemporal(new Temporal.PlainYearMonth(2007, 10))).toBe(true); + expect( + isTemporal( + new Temporal.ZonedDateTime(1704103200000000000n, 'America/New_York') + ) + ).toBe(true); +}); diff --git a/src/is.ts b/src/is.ts index 438ea498..c767962e 100644 --- a/src/is.ts +++ b/src/is.ts @@ -85,3 +85,57 @@ export const isTypedArray = (payload: any): payload is TypedArray => ArrayBuffer.isView(payload) && !(payload instanceof DataView); export const isURL = (payload: any): payload is URL => payload instanceof URL; + +export type TemporalConstructor = + | Temporal.DurationConstructor + | Temporal.PlainDateConstructor + | Temporal.PlainDateTimeConstructor + | Temporal.PlainMonthDayConstructor + | Temporal.PlainTimeConstructor + | Temporal.PlainYearMonthConstructor + | Temporal.ZonedDateTimeConstructor + | Temporal.InstantConstructor; + +export type TemporalTypes = InstanceType; + +export const isInstant = (payload: any): payload is Temporal.Instant => + getType(payload) === 'Temporal.Instant'; + +export const isDuration = (payload: any): payload is Temporal.Duration => + getType(payload) === 'Temporal.Duration'; + +export const isPlainDate = (payload: any): payload is Temporal.PlainDate => + getType(payload) === 'Temporal.PlainDate'; + +export const isPlainDateTime = ( + payload: any +): payload is Temporal.PlainDateTime => + getType(payload) === 'Temporal.PlainDateTime'; + +export const isPlainMonthDay = ( + payload: any +): payload is Temporal.PlainMonthDay => + getType(payload) === 'Temporal.PlainMonthDay'; + +export const isPlainTime = (payload: any): payload is Temporal.PlainTime => + getType(payload) === 'Temporal.PlainTime'; + +export const isPlainYearMonth = ( + payload: any +): payload is Temporal.PlainYearMonth => + getType(payload) === 'Temporal.PlainYearMonth'; + +export const isZonedDateTime = ( + payload: any +): payload is Temporal.ZonedDateTime => + getType(payload) === 'Temporal.ZonedDateTime'; + +export const isTemporal = (payload: any): payload is TemporalTypes => + isDuration(payload) || + isPlainDate(payload) || + isPlainDateTime(payload) || + isPlainMonthDay(payload) || + isPlainTime(payload) || + isPlainYearMonth(payload) || + isZonedDateTime(payload) || + isInstant(payload); diff --git a/src/transformer.ts b/src/transformer.ts index 69dedef1..ddf740f7 100644 --- a/src/transformer.ts +++ b/src/transformer.ts @@ -13,6 +13,8 @@ import { isTypedArray, TypedArrayConstructor, isURL, + isTemporal, + TemporalConstructor, } from './is.js'; import { findArr } from './util.js'; import SuperJSON from './index.js'; @@ -22,6 +24,7 @@ export type PrimitiveTypeAnnotation = 'number' | 'undefined' | 'bigint'; type LeafTypeAnnotation = PrimitiveTypeAnnotation | 'regexp' | 'Date' | 'URL'; type TypedArrayAnnotation = ['typed-array', string]; +type TemporalTypeAnnotation = ['temporal', string]; type ClassTypeAnnotation = ['class', string]; type SymbolTypeAnnotation = ['symbol', string]; type CustomTypeAnnotation = ['custom', string]; @@ -30,6 +33,7 @@ type SimpleTypeAnnotation = LeafTypeAnnotation | 'map' | 'set' | 'Error'; type CompositeTypeAnnotation = | TypedArrayAnnotation + | TemporalTypeAnnotation | ClassTypeAnnotation | SymbolTypeAnnotation | CustomTypeAnnotation; @@ -224,15 +228,16 @@ const constructorToName = [ const typedArrayRule = compositeTransformation( isTypedArray, v => ['typed-array', v.constructor.name], - v => [...v].map(n => { - // Handle special float values that JSON.stringify converts to null - if (typeof n === 'number') { - if (Number.isNaN(n)) return 'NaN'; - if (n === Infinity) return 'Infinity'; - if (n === -Infinity) return '-Infinity'; - } - return n; - }), + v => + [...v].map(n => { + // Handle special float values that JSON.stringify converts to null + if (typeof n === 'number') { + if (Number.isNaN(n)) return 'NaN'; + if (n === Infinity) return 'Infinity'; + if (n === -Infinity) return '-Infinity'; + } + return n; + }), (v, a) => { const ctor = constructorToName[a[1]]; @@ -252,6 +257,46 @@ const typedArrayRule = compositeTransformation( } ); +// Can not define temporalConstructorToName within global scope +// as Temporal may be undefined (don't introduce breaking changes). +// So lazily initialize array +let temporalNameToConstructor: Record | undefined; +const getTemporalConstructors = () => { + if (temporalNameToConstructor) return temporalNameToConstructor; + if (typeof Temporal === 'undefined') { + throw new Error('Temporal is not available in this runtime'); + } + temporalNameToConstructor = [ + Temporal.Duration, + Temporal.PlainDate, + Temporal.PlainDateTime, + Temporal.PlainMonthDay, + Temporal.PlainTime, + Temporal.PlainYearMonth, + Temporal.ZonedDateTime, + Temporal.Instant, + ].reduce>((obj, ctor) => { + obj[ctor.name] = ctor; + return obj; + }, {}); + return temporalNameToConstructor; +}; + +const temporalRule = compositeTransformation( + isTemporal, + t => ['temporal', t.constructor.name], + t => t.toString(), + (t, a) => { + const ctor = getTemporalConstructors()[a[1]]; + + if (!ctor) { + throw new Error('Trying to deserialize unknown temporal constructor'); + } + + return ctor.from(t); + } +); + export function isInstanceOfRegisteredClass( potentialClass: any, superJson: SuperJSON @@ -323,7 +368,13 @@ const customRule = compositeTransformation( } ); -const compositeRules = [classRule, symbolRule, customRule, typedArrayRule]; +const compositeRules = [ + classRule, + symbolRule, + customRule, + typedArrayRule, + temporalRule, +]; export const transformValue = ( value: any, @@ -373,6 +424,8 @@ export const untransformValue = ( return customRule.untransform(json, type, superJson); case 'typed-array': return typedArrayRule.untransform(json, type, superJson); + case 'temporal': + return temporalRule.untransform(json, type, superJson); default: throw new Error('Unknown transformation: ' + type); } diff --git a/tsconfig.json b/tsconfig.json index b0adc927..3a92ddae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,8 @@ "noFallthroughCasesInSwitch": true, "esModuleInterop": true, "downlevelIteration": true, - "skipLibCheck": true + "skipLibCheck": true, + "ignoreDeprecations": "6.0", + "types": ["node"] } }