Skip to content

Commit

Permalink
Merge branch 'master' into feature/support-bigint
Browse files Browse the repository at this point in the history
  • Loading branch information
gaetanmaisse committed May 13, 2021
2 parents 763c843 + 3f1ec32 commit b3b18d4
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 21 deletions.
22 changes: 16 additions & 6 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
version: 2
version: 2.1

orbs:
node: circleci/[email protected]

jobs:
test:
test-with-node:
parameters:
node-version:
type: string
docker:
- image: circleci/node:10
- image: cimg/base:stable
steps:
- checkout
- node/install:
node-version: << parameters.node-version >>
install-yarn: true
- run: yarn install
- run: yarn build
- run: yarn lint
- run: yarn test

workflows:
version: 2

build_and_test:
jobs:
- test
- test-with-node:
matrix:
parameters:
node-version: ["12", "14", "16"]
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ Functions are stripped of comments and whitespace.

**Dates** are parsed back into actual Date objects.

**DOM Events** are processed to extract the internal (hidden) properties, resulting in an object containing the same properties but not being an instance of the original class.

## API

You have 2 choices:
Expand Down
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "telejson",
"version": "5.1.1",
"version": "5.2.0",
"description": "",
"keywords": [
"JSON",
Expand Down Expand Up @@ -30,16 +30,19 @@
"build": "babel src --out-dir dist --extensions \".ts\" && tsc --emitDeclarationOnly",
"lint": "eslint src --ext .js,.ts",
"prepublish": "yarn build",
"test": "TZ=UTC jest"
"test": "yarn test-node && yarn test-browser",
"test-browser": "TZ=UTC jest --env=jsdom ./common ./browser",
"test-node": "TZ=UTC jest --env=node ./common ./node"
},
"eslintConfig": {
"extends": [
"@storybook/eslint-config-storybook"
]
},
"jest": {
"testEnvironment": "node"
],
"env": {
"browser": true
}
},
"jest": {},
"dependencies": {
"@types/is-function": "^1.0.0",
"global": "^4.4.0",
Expand Down
48 changes: 48 additions & 0 deletions src/dom-event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const eventProperties: Array<keyof Event> = [
'bubbles',
'cancelBubble',
'cancelable',
'composed',
'currentTarget',
'defaultPrevented',
'eventPhase',
'isTrusted',
'returnValue',
'srcElement',
'target',
'timeStamp',
'type',
];

const customEventSpecificProperties: Array<Exclude<keyof CustomEvent, keyof Event>> = ['detail'];

/**
* Dom Event (and all its subclasses) is built in a way its internal properties
* are accessible when querying them directly but "hidden" when iterating its
* keys.
*
* With a code example it means: `Object.keys(new Event('click')) = ["isTrusted"]`
*
* So to be able to stringify/parse more than just `isTrusted` info we need to
* create a new object and set the properties by hand. As there is no way to
* iterate the properties we rely on a list of hardcoded properties.
*
* @param event The event we want to extract properties
*/
export function extractEventHiddenProperties(event: Event): unknown {
const rebuildEvent = eventProperties
.filter((value) => event[value] !== undefined)
.reduce((acc, value) => {
return { ...acc, [value]: event[value] };
}, {} as any);

if (event instanceof CustomEvent) {
customEventSpecificProperties
.filter((value) => event[value] !== undefined)
.forEach((value) => {
rebuildEvent[value] = event[value];
});
}

return rebuildEvent;
}
20 changes: 17 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import isSymbol from 'is-symbol';
import isObjectAny from 'isobject';
import get from 'lodash/get';
import memoize from 'memoizerific';
import { extractEventHiddenProperties } from './dom-event';

const isRunningInBrowser = typeof window !== 'undefined' && typeof window.document !== 'undefined';

// eslint-disable-next-line @typescript-eslint/ban-types, no-use-before-define
const isObject = isObjectAny as <T = object>(val: any) => val is T;
Expand Down Expand Up @@ -100,6 +103,17 @@ export interface Options {
// eslint-disable-next-line no-useless-escape
export const isJSON = (input: string) => input.match(/^[\[\{\"\}].*[\]\}\"]$/);

function convertUnconventionalData(data: unknown) {
// `Event` has a weird structure, for details see `extractEventHiddenProperties` doc
// Plus we need to check if running in a browser to ensure `Event` exist and
// is really the dom Event class.
if (isRunningInBrowser && data instanceof Event) {
return extractEventHiddenProperties(data);
}

return data;
}

export const replacer = function replacer(options: Options) {
let objects: Map<any, string>;
let stack: any[];
Expand Down Expand Up @@ -225,7 +239,7 @@ export const replacer = function replacer(options: Options) {
keys.push(key);
stack.unshift(value);
objects.set(value, JSON.stringify(keys));
return value;
return convertUnconventionalData(value);
}

// actually, here's the only place where the keys keeping is useful
Expand Down Expand Up @@ -358,9 +372,9 @@ const defaultOptions: Options = {
lazyEval: true,
};

export const stringify = (data: any, options: Partial<Options> = {}) => {
export const stringify = (data: unknown, options: Partial<Options> = {}) => {
const mergedOptions: Options = { ...defaultOptions, ...options };
return JSON.stringify(data, replacer(mergedOptions), options.space);
return JSON.stringify(convertUnconventionalData(data), replacer(mergedOptions), options.space);
};

const mutator = () => {
Expand Down
81 changes: 81 additions & 0 deletions test/browser/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as src from '../../src';
import * as dist from '../../dist';

const tests = ({ stringify, parse }) => {
test('HTML Event', () => {
const event = new MouseEvent('click', { bubbles: true, composed: true, cancelable: true });

const stringified = stringify(event);

const parsed = parse(stringified);

expect(parsed).toMatchObject({
isTrusted: expect.any(Boolean),
type: 'click',
bubbles: true,
composed: true,
cancelable: true,
returnValue: expect.any(Boolean),
timeStamp: expect.any(Number),
});
});

test('HTML Custom Event', () => {
const event = new CustomEvent('custom:click', {
bubbles: true,
composed: true,
cancelable: true,
detail: { aKey: 'a Value' },
});

const stringified = stringify(event, { allowClass: true });

const parsed = parse(stringified);

expect(parsed).toMatchObject({
isTrusted: expect.any(Boolean),
type: 'custom:click',
bubbles: true,
composed: true,
cancelable: true,
returnValue: expect.any(Boolean),
timeStamp: expect.any(Number),
detail: { aKey: 'a Value' },
});
});

test('Nested HTML Custom Event', () => {
const event = new CustomEvent('custom:click', {
bubbles: true,
composed: true,
cancelable: true,
detail: { aKey: 'a Value' },
});

const stringified = stringify({ key: 'value', args: event }, { allowClass: true });

const parsed = parse(stringified);

expect(parsed).toMatchObject({
key: 'value',
args: {
isTrusted: expect.any(Boolean),
type: 'custom:click',
bubbles: true,
composed: true,
cancelable: true,
returnValue: expect.any(Boolean),
timeStamp: expect.any(Number),
detail: { aKey: 'a Value' },
},
});
});
};

describe('Source', () => {
tests(src);
});

describe('Dist', () => {
tests(dist);
});
File renamed without changes.
8 changes: 2 additions & 6 deletions test/index.test.js → test/common/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as src from '../src/index';
import * as dist from '../dist/index';
import * as src from '../../src';
import * as dist from '../../dist';

const regex1 = /foo/;
const regex2 = /foo/g;
Expand Down Expand Up @@ -124,10 +124,6 @@ const tests = ({ stringify, parse }) => {
expect(stringifiedSpaced).toMatchSnapshot();
});

test('stringify the global object', () => {
expect(() => stringify(global, { maxDepth: 10000 })).not.toThrow();
});

test('check duplicate value', () => {
const Fruit = {
apple: true,
Expand Down
16 changes: 16 additions & 0 deletions test/node/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as src from '../../src';
import * as dist from '../../dist';

const tests = ({ stringify }) => {
test('stringify the global object', () => {
expect(() => stringify(global, { maxDepth: 10000 })).not.toThrow();
});
};

describe('Source', () => {
tests(src);
});

describe('Dist', () => {
tests(dist);
});

0 comments on commit b3b18d4

Please sign in to comment.