diff --git a/babel-node-modules-opt-in.txt b/babel-node-modules-opt-in.txt index e75de638..7418c06a 100644 --- a/babel-node-modules-opt-in.txt +++ b/babel-node-modules-opt-in.txt @@ -1,6 +1,5 @@ # Patterns matching node_modules dependencies that Babel and Jest should transform approx-string-match -decircular @grrr/cookie-consent @grrr/utils is-absolute-url diff --git a/howdju-common/lib/general.test.ts b/howdju-common/lib/general.test.ts index 0749c9b5..e1a7f9f1 100644 --- a/howdju-common/lib/general.test.ts +++ b/howdju-common/lib/general.test.ts @@ -221,11 +221,15 @@ describe("toJson", () => { test("handles circular references", () => { const obj: any = { a: 1, b: "2" }; obj.c = { d: obj }; - expect(toJson(obj)).toBe('{"a":1,"b":"2","c":{"d":"[Circular *]"}}'); + expect(toJson(obj)).toBe('{"a":1,"b":"2","c":{"d":"[Circular]"}}'); }); test("does not consider all identical fields circular", () => { const child = { d: 3 }; const obj: any = { a: child, b: child }; expect(toJson(obj)).toBe('{"a":{"d":3},"b":{"d":3}}'); }); + test("serializes moments as ISO strings", () => { + const obj: any = { a: moment.utc("2022-11-08T21:44:00") }; + expect(toJson(obj)).toBe('{"a":"2022-11-08T21:44:00.000Z"}'); + }); }); diff --git a/howdju-common/lib/general.ts b/howdju-common/lib/general.ts index ddf87986..9990c382 100644 --- a/howdju-common/lib/general.ts +++ b/howdju-common/lib/general.ts @@ -24,7 +24,6 @@ import { } from "lodash"; import moment, { Moment, unitOfTime, Duration, TemplateFunction } from "moment"; import isAbsoluteUrlLib from "is-absolute-url"; -import decircular from "decircular"; import { newProgrammingError } from "./commonErrors"; import { CamelCasedPropertiesDeep, MergeDeep } from "type-fest"; @@ -455,12 +454,38 @@ export const keysTo = (obj: { [k: string]: any }, val: any) => {} as { [k: string]: typeof val } ); -export const toJson = function toJson( - val: any, - replacer?: (key: string, value: any) => any -) { - return JSON.stringify(decircular(val), replacer); -}; +export function toJson(val: any) { + const seen = new WeakSet(); + return JSON.stringify(val, handleCircularReferences(seen)); +} + +function handleCircularReferences(seen: WeakSet) { + return function (_key: string, value: any) { + if (value === null || typeof value !== "object") { + return value; + } + + if (seen.has(value)) { + return "[Circular]"; + } + + if (value.toJSON) { + return value.toJSON(); + } + + seen.add(value); + + const newValue: Record = Array.isArray(value) ? [] : {}; + + for (const [key, val] of Object.entries(value)) { + newValue[key] = handleCircularReferences(seen)(key, val); + } + + seen.delete(value); + + return newValue; + }; +} export const fromJson = function fromJson(json: string) { return JSON.parse(json); diff --git a/howdju-common/package.json b/howdju-common/package.json index f6c3e468..5a16d5ec 100644 --- a/howdju-common/package.json +++ b/howdju-common/package.json @@ -20,7 +20,6 @@ "ajv": "^8.1.0", "ajv-formats": "^2.0.2", "approx-string-match": "^2.0.0", - "decircular": "^1.0.0", "dom-anchor-text-position": "^5.0.0", "dom-anchor-text-quote": "^4.0.2", "is-absolute-url": "^4.0.1", diff --git a/howdju-mobile-app/jest.config.ts b/howdju-mobile-app/jest.config.ts index 485a42fc..144da40c 100644 --- a/howdju-mobile-app/jest.config.ts +++ b/howdju-mobile-app/jest.config.ts @@ -4,7 +4,6 @@ import { merge } from "lodash"; import baseConfig from "../jest.config.base"; const transformOptInPatterns = [ "approx-string-match", - "decircular", "is-absolute-url", "(@|jest-)?react-native", "normalize-url", diff --git a/howdju-service-common/lib/logging/AwsLogger.js b/howdju-service-common/lib/logging/AwsLogger.js index 658d5478..fc3e888a 100644 --- a/howdju-service-common/lib/logging/AwsLogger.js +++ b/howdju-service-common/lib/logging/AwsLogger.js @@ -5,7 +5,7 @@ const join = require("lodash/join"); const map = require("lodash/map"); const mapValues = require("lodash/mapValues"); -const { utcTimestamp, toJson } = require("howdju-common"); +const { utcTimestamp } = require("howdju-common"); const { processArgs } = require("./processArgs"); @@ -91,7 +91,7 @@ const makeJsonLogArguments = function (logLevel, logLevelNumber, ...args) { logRecord["data"] = data; } - const logRecordJson = toJson(logRecord, jsonStringifyReplacer); + const logRecordJson = JSON.stringify(logRecord, jsonStringifyReplacer); return [logRecordJson]; }; diff --git a/howdju-text-fragments/dist/rangeToFragment.js b/howdju-text-fragments/dist/rangeToFragment.js index bf3a1f68..4f3d5620 100644 --- a/howdju-text-fragments/dist/rangeToFragment.js +++ b/howdju-text-fragments/dist/rangeToFragment.js @@ -1349,10 +1349,10 @@ }(); var ctxClearTimeout = context.clearTimeout !== root.clearTimeout && context.clearTimeout, ctxNow = Date2 && Date2.now !== root.Date.now && Date2.now, ctxSetTimeout = context.setTimeout !== root.setTimeout && context.setTimeout; var nativeCeil = Math2.ceil, nativeFloor = Math2.floor, nativeGetSymbols = Object2.getOwnPropertySymbols, nativeIsBuffer = Buffer2 ? Buffer2.isBuffer : undefined2, nativeIsFinite = context.isFinite, nativeJoin = arrayProto.join, nativeKeys = overArg(Object2.keys, Object2), nativeMax = Math2.max, nativeMin = Math2.min, nativeNow = Date2.now, nativeParseInt = context.parseInt, nativeRandom = Math2.random, nativeReverse = arrayProto.reverse; - var DataView = getNative(context, "DataView"), Map2 = getNative(context, "Map"), Promise2 = getNative(context, "Promise"), Set2 = getNative(context, "Set"), WeakMap2 = getNative(context, "WeakMap"), nativeCreate = getNative(Object2, "create"); - var metaMap = WeakMap2 && new WeakMap2(); + var DataView = getNative(context, "DataView"), Map2 = getNative(context, "Map"), Promise2 = getNative(context, "Promise"), Set2 = getNative(context, "Set"), WeakMap = getNative(context, "WeakMap"), nativeCreate = getNative(Object2, "create"); + var metaMap = WeakMap && new WeakMap(); var realNames = {}; - var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map2), promiseCtorString = toSource(Promise2), setCtorString = toSource(Set2), weakMapCtorString = toSource(WeakMap2); + var dataViewCtorString = toSource(DataView), mapCtorString = toSource(Map2), promiseCtorString = toSource(Promise2), setCtorString = toSource(Set2), weakMapCtorString = toSource(WeakMap); var symbolProto = Symbol2 ? Symbol2.prototype : undefined2, symbolValueOf = symbolProto ? symbolProto.valueOf : undefined2, symbolToString = symbolProto ? symbolProto.toString : undefined2; function lodash(value) { if (isObjectLike(value) && !isArray3(value) && !(value instanceof LazyWrapper)) { @@ -3406,7 +3406,7 @@ return result2; }; var getTag = baseGetTag; - if (DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag || Map2 && getTag(new Map2()) != mapTag || Promise2 && getTag(Promise2.resolve()) != promiseTag || Set2 && getTag(new Set2()) != setTag || WeakMap2 && getTag(new WeakMap2()) != weakMapTag) { + if (DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag || Map2 && getTag(new Map2()) != mapTag || Promise2 && getTag(Promise2.resolve()) != promiseTag || Set2 && getTag(new Set2()) != setTag || WeakMap && getTag(new WeakMap()) != weakMapTag) { getTag = function(value) { var result2 = baseGetTag(value), Ctor = result2 == objectTag ? value.constructor : undefined2, ctorString = Ctor ? toSource(Ctor) : ""; if (ctorString) { @@ -10103,29 +10103,30 @@ } }); - // ../node_modules/decircular/index.js - function decircular(object) { - const seenObjects = /* @__PURE__ */ new WeakMap(); - function internalDecircular(value, path = []) { - if (!(value !== null && typeof value === "object")) { - return value; - } - const existingPath = seenObjects.get(value); - if (existingPath) { - return `[Circular *${existingPath.join(".")}]`; - } - seenObjects.set(value, path); - const newValue = Array.isArray(value) ? [] : {}; - for (const [key2, value2] of Object.entries(value)) { - newValue[key2] = internalDecircular(value2, [...path, key2]); + // ../node_modules/safe-stringify/index.js + function safeStringifyReplacer(seen) { + return function(key, value) { + if (value !== null && typeof value === "object") { + if (seen.has(value)) { + return "[Circular]"; + } + seen.add(value); + const newValue = Array.isArray(value) ? [] : {}; + for (const [key2, value2] of Object.entries(value)) { + newValue[key2] = safeStringifyReplacer(seen)(key2, value2); + } + seen.delete(value); + return newValue; } - seenObjects.delete(value); - return newValue; - } - return internalDecircular(object); + return value; + }; + } + function safeStringify(object, { indentation } = {}) { + const seen = /* @__PURE__ */ new WeakSet(); + return JSON.stringify(object, safeStringifyReplacer(seen), indentation); } - var init_decircular = __esm({ - "../node_modules/decircular/index.js"() { + var init_safe_stringify = __esm({ + "../node_modules/safe-stringify/index.js"() { } }); @@ -10204,6 +10205,9 @@ return void 0; }; } + function toJson(val) { + return safeStringify(val); + } function toSlug(text) { if (!text) { return text; @@ -10241,14 +10245,14 @@ function isAbsoluteUrl2(val) { return isAbsoluteUrl(val); } - var import_lodash2, import_moment, mapKeysDeep, minDate, zeroDate, isTruthy, isFalsey, assert, isDefined, utcNow, momentAdd, momentSubtract, differenceDuration, formatDuration, timestampFormatString, utcTimestamp, arrayToObject, pushAll, insertAt, insertAllAt, removeAt, encodeQueryStringObject, encodeSorts, decodeSorts, toSingleLine, omitDeep, keysTo, toJson, fromJson, cleanWhitespace, normalizeText, toEntries; + var import_lodash2, import_moment, mapKeysDeep, minDate, zeroDate, isTruthy, isFalsey, assert, isDefined, utcNow, momentAdd, momentSubtract, differenceDuration, formatDuration, timestampFormatString, utcTimestamp, arrayToObject, pushAll, insertAt, insertAllAt, removeAt, encodeQueryStringObject, encodeSorts, decodeSorts, toSingleLine, omitDeep, keysTo, fromJson, cleanWhitespace, normalizeText, toEntries; var init_general = __esm({ "../howdju-common/lib/general.ts"() { "use strict"; import_lodash2 = __toESM(require_lodash()); import_moment = __toESM(require_moment()); init_is_absolute_url(); - init_decircular(); + init_safe_stringify(); init_commonErrors(); mapKeysDeep = (obj, fn2, parentKey = void 0) => { if ((0, import_lodash2.isArray)(obj)) { @@ -10413,9 +10417,6 @@ }, {} ); - toJson = function toJson2(val, replacer) { - return JSON.stringify(decircular(val), replacer); - }; fromJson = function fromJson2(json) { return JSON.parse(json); }; @@ -19797,12 +19798,12 @@ exports._ = _3; var plus = new _Code("+"); function str2(strs, ...args) { - const expr = [safeStringify(strs[0])]; + const expr = [safeStringify2(strs[0])]; let i3 = 0; while (i3 < args.length) { expr.push(plus); addCodeArg(expr, args[i3]); - expr.push(plus, safeStringify(strs[++i3])); + expr.push(plus, safeStringify2(strs[++i3])); } optimize(expr); return new _Code(expr); @@ -19854,16 +19855,16 @@ } exports.strConcat = strConcat; function interpolate(x3) { - return typeof x3 == "number" || typeof x3 == "boolean" || x3 === null ? x3 : safeStringify(Array.isArray(x3) ? x3.join(",") : x3); + return typeof x3 == "number" || typeof x3 == "boolean" || x3 === null ? x3 : safeStringify2(Array.isArray(x3) ? x3.join(",") : x3); } function stringify(x3) { - return new _Code(safeStringify(x3)); + return new _Code(safeStringify2(x3)); } exports.stringify = stringify; - function safeStringify(x3) { + function safeStringify2(x3) { return JSON.stringify(x3).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029"); } - exports.safeStringify = safeStringify; + exports.safeStringify = safeStringify2; function getProperty(key) { return typeof key == "string" && exports.IDENTIFIER.test(key) ? new _Code(`.${key}`) : _3`[${key}]`; } @@ -27744,8 +27745,8 @@ "../node_modules/lodash/_WeakMap.js"(exports, module) { var getNative = require_getNative(); var root = require_root(); - var WeakMap2 = getNative(root, "WeakMap"); - module.exports = WeakMap2; + var WeakMap = getNative(root, "WeakMap"); + module.exports = WeakMap; } }); @@ -27756,7 +27757,7 @@ var Map2 = require_Map(); var Promise2 = require_Promise(); var Set2 = require_Set(); - var WeakMap2 = require_WeakMap(); + var WeakMap = require_WeakMap(); var baseGetTag = require_baseGetTag(); var toSource = require_toSource(); var mapTag = "[object Map]"; @@ -27769,9 +27770,9 @@ var mapCtorString = toSource(Map2); var promiseCtorString = toSource(Promise2); var setCtorString = toSource(Set2); - var weakMapCtorString = toSource(WeakMap2); + var weakMapCtorString = toSource(WeakMap); var getTag = baseGetTag; - if (DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag || Map2 && getTag(new Map2()) != mapTag || Promise2 && getTag(Promise2.resolve()) != promiseTag || Set2 && getTag(new Set2()) != setTag || WeakMap2 && getTag(new WeakMap2()) != weakMapTag) { + if (DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag || Map2 && getTag(new Map2()) != mapTag || Promise2 && getTag(Promise2.resolve()) != promiseTag || Set2 && getTag(new Set2()) != setTag || WeakMap && getTag(new WeakMap()) != weakMapTag) { getTag = function(value) { var result = baseGetTag(value), Ctor = result == objectTag ? value.constructor : void 0, ctorString = Ctor ? toSource(Ctor) : ""; if (ctorString) { diff --git a/yarn.lock b/yarn.lock index 33f36eeb..d1df26a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16586,13 +16586,6 @@ __metadata: languageName: node linkType: hard -"decircular@npm:^1.0.0": - version: 1.0.0 - resolution: "decircular@npm:1.0.0" - checksum: 8710dec50e2e643e38f12866537ff3aeac60c035e662474a9ec6641050d600b68bd18bc9e46c60d8b3d23a7f989815d646ff6aa818d446451143207b898fe4ae - languageName: node - linkType: hard - "decode-named-character-reference@npm:^1.0.0": version: 1.0.2 resolution: "decode-named-character-reference@npm:1.0.2" @@ -21797,7 +21790,6 @@ __metadata: ajv: "npm:^8.1.0" ajv-formats: "npm:^2.0.2" approx-string-match: "npm:^2.0.0" - decircular: "npm:^1.0.0" dom-anchor-text-position: "npm:^5.0.0" dom-anchor-text-quote: "npm:^4.0.2" esbuild: "npm:^0.18.17"