diff --git a/package.json b/package.json index bf1bd324..b197577c 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "test:cover:wasm": "npx nyc --no-clean npm run test:wasm", "test:cover:td": "npx nyc --no-clean npm run test:td", "cover:clean": "rimraf .nyc_output coverage/", - "cover:report": "nyc report --reporter=text-summary --reporter=html --reporter=json", + "cover:report": "npx nyc report --reporter=text-summary --reporter=html --reporter=json", "test:browser": "karma start --single-run", "test:browser:firefox": "karma start --single-run --browsers FirefoxHeadless", "test:browser:chrome": "karma start --single-run --browsers ChromeHeadless", diff --git a/src/ExtensionCodec.ts b/src/ExtensionCodec.ts index 3f12cb3b..ece0e9c2 100644 --- a/src/ExtensionCodec.ts +++ b/src/ExtensionCodec.ts @@ -50,25 +50,25 @@ export class ExtensionCodec implements ExtensionCodecType { } public tryToEncode(object: unknown): ExtData | null { - // built-in extensions - for (let i = 0; i < this.builtInEncoders.length; i++) { - const encoder = this.builtInEncoders[i]; + // custom extensions + for (let i = 0; i < this.encoders.length; i++) { + const encoder = this.encoders[i]; if (encoder != null) { const data = encoder(object); if (data != null) { - const type = -1 - i; + const type = i; return new ExtData(type, data); } } } - // custom extensions - for (let i = 0; i < this.encoders.length; i++) { - const encoder = this.encoders[i]; + // built-in extensions + for (let i = 0; i < this.builtInEncoders.length; i++) { + const encoder = this.builtInEncoders[i]; if (encoder != null) { const data = encoder(object); if (data != null) { - const type = i; + const type = -1 - i; return new ExtData(type, data); } } diff --git a/src/JavaScriptCodec.ts b/src/JavaScriptCodec.ts new file mode 100644 index 00000000..04fbf5d1 --- /dev/null +++ b/src/JavaScriptCodec.ts @@ -0,0 +1,69 @@ +import { ExtensionCodec, ExtensionCodecType } from "./ExtensionCodec"; +import { encode } from "./encode"; +import { decode } from "./decode"; + +export const EXT_JAVASCRIPT = 0; + +const enum JSData { + Map, + Set, + Date, + RegExp, + BigInt, +} + +export function encodeJavaScriptData(input: unknown): Uint8Array | null { + if (input instanceof Map) { + return encode([JSData.Map, [...input]]); + } else if (input instanceof Set) { + return encode([JSData.Set, [...input]]); + } else if (input instanceof Date) { + // Not a MessagePack timestamp because + // it may be overrided by users + return encode([JSData.Date, input.getTime()]); + } else if (input instanceof RegExp) { + return encode([JSData.RegExp, [input.source, input.flags]]); + } else if (typeof input === "bigint") { + return encode([JSData.BigInt, input.toString()]); + } else { + return null; + } +} + +export function decodeJavaScriptData(data: Uint8Array) { + const [jsDataType, source] = decode(data) as [JSData, any]; + + switch (jsDataType) { + case JSData.Map: { + return new Map(source); + } + case JSData.Set: { + return new Set(source); + } + case JSData.Date: { + return new Date(source); + } + case JSData.RegExp: { + const [pattern, flags] = source; + return new RegExp(pattern, flags); + } + case JSData.BigInt: { + return BigInt(source); + } + default: { + throw new Error(`Unknown data type: ${jsDataType}`); + } + } +} + +export const JavaScriptCodec: ExtensionCodecType = (() => { + const ext = new ExtensionCodec(); + + ext.register({ + type: EXT_JAVASCRIPT, + encode: encodeJavaScriptData, + decode: decodeJavaScriptData, + }); + + return ext; +})(); diff --git a/src/index.ts b/src/index.ts index 50b50813..b1eb94a0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,4 +20,6 @@ export { decodeTimestampExtension, } from "./timestamp"; +export { JavaScriptCodec, EXT_JAVASCRIPT, encodeJavaScriptData, decodeJavaScriptData } from "./JavaScriptCodec"; + export { WASM_AVAILABLE as __WASM_AVAILABLE } from "./wasmFunctions"; diff --git a/test/javascript-codec.test.ts b/test/javascript-codec.test.ts new file mode 100644 index 00000000..2769ec80 --- /dev/null +++ b/test/javascript-codec.test.ts @@ -0,0 +1,28 @@ +import assert from "assert"; +import { encode, decode, JavaScriptCodec } from "@msgpack/msgpack"; + +describe("JavaScriptCodec", () => { + context("mixed", () => { + // this data comes from https://github.com/yahoo/serialize-javascript + + it("encodes and decodes the object", () => { + const object = { + str: "string", + num: 0, + obj: { foo: "foo", bar: "bar" }, + arr: [1, 2, 3], + bool: true, + nil: null, + // undef: undefined, // not supported + date: new Date("Thu, 28 Apr 2016 22:02:17 GMT"), + map: new Map([["foo", 10], ["bar", 20]]), + set: new Set([123, 456]), + regexp: /foo\n/i, + bigint: typeof(BigInt) !== "undefined" ? BigInt(Number.MAX_SAFE_INTEGER) + BigInt(1) : null, + }; + const encoded = encode(object, { extensionCodec: JavaScriptCodec }); + + assert.deepStrictEqual(decode(encoded, { extensionCodec: JavaScriptCodec }), object); + }); + }); +});