From 3aa887ce54ae7a00e3beaf2ee1f5fd71b6578f6e Mon Sep 17 00:00:00 2001 From: sharevb Date: Sat, 28 Sep 2024 11:20:01 +0200 Subject: [PATCH] feat(new tool): JSON Size Analyzer Fix #450 --- components.d.ts | 3 + package.json | 1 + pnpm-lock.yaml | 67 +++++++++-- src/tools/index.ts | 2 + src/tools/json-size-analyzer/index.ts | 12 ++ .../json-size-analyzer/json-analyzer.d.ts | 13 ++ .../json-size-analyzer.service.test.ts | 113 ++++++++++++++++++ .../json-size-analyzer.service.ts | 51 ++++++++ .../json-size-analyzer/json-size-analyzer.vue | 68 +++++++++++ 9 files changed, 321 insertions(+), 9 deletions(-) create mode 100644 src/tools/json-size-analyzer/index.ts create mode 100644 src/tools/json-size-analyzer/json-analyzer.d.ts create mode 100644 src/tools/json-size-analyzer/json-size-analyzer.service.test.ts create mode 100644 src/tools/json-size-analyzer/json-size-analyzer.service.ts create mode 100644 src/tools/json-size-analyzer/json-size-analyzer.vue diff --git a/components.d.ts b/components.d.ts index 89f41f80..df12a99e 100644 --- a/components.d.ts +++ b/components.d.ts @@ -109,6 +109,7 @@ declare module '@vue/runtime-core' { Ipv6UlaGenerator: typeof import('./src/tools/ipv6-ula-generator/ipv6-ula-generator.vue')['default'] JsonDiff: typeof import('./src/tools/json-diff/json-diff.vue')['default'] JsonMinify: typeof import('./src/tools/json-minify/json-minify.vue')['default'] + JsonSizeAnalyzer: typeof import('./src/tools/json-size-analyzer/json-size-analyzer.vue')['default'] JsonToCsv: typeof import('./src/tools/json-to-csv/json-to-csv.vue')['default'] JsonToToml: typeof import('./src/tools/json-to-toml/json-to-toml.vue')['default'] JsonToXml: typeof import('./src/tools/json-to-xml/json-to-xml.vue')['default'] @@ -138,6 +139,7 @@ declare module '@vue/runtime-core' { NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] + NInput: typeof import('naive-ui')['NInput'] NInputNumber: typeof import('naive-ui')['NInputNumber'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] @@ -145,6 +147,7 @@ declare module '@vue/runtime-core' { NScrollbar: typeof import('naive-ui')['NScrollbar'] NSlider: typeof import('naive-ui')['NSlider'] NSwitch: typeof import('naive-ui')['NSwitch'] + NTree: typeof import('naive-ui')['NTree'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] diff --git a/package.json b/package.json index 6191f702..4125f4f1 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", "js-base64": "^3.7.6", + "json-analyzer": "^1.2.2", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3044541a..6a8d3dd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ dependencies: js-base64: specifier: ^3.7.6 version: 3.7.7 + json-analyzer: + specifier: ^1.2.2 + version: 1.2.2 json5: specifier: ^2.2.3 version: 2.2.3 @@ -3360,7 +3363,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.11.1(vue@3.3.4) + '@vueuse/shared': 11.1.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -3993,19 +3996,19 @@ packages: - vue dev: false - /@vueuse/shared@10.11.1(vue@3.3.4): - resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + /@vueuse/shared@10.3.0(vue@3.3.4): + resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} dependencies: - vue-demi: 0.14.10(vue@3.3.4) + vue-demi: 0.14.5(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue dev: false - /@vueuse/shared@10.3.0(vue@3.3.4): - resolution: {integrity: sha512-kGqCTEuFPMK4+fNWy6dUOiYmxGcUbtznMwBZLC1PubidF4VZY05B+Oht7Jh7/6x4VOWGpvu3R37WHi81cKpiqg==} + /@vueuse/shared@11.1.0(vue@3.3.4): + resolution: {integrity: sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==} dependencies: - vue-demi: 0.14.5(vue@3.3.4) + vue-demi: 0.14.10(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -4529,6 +4532,11 @@ packages: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} dev: true + /colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + dev: false + /combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -6580,6 +6588,21 @@ packages: hasBin: true dev: true + /json-analyzer@1.2.2: + resolution: {integrity: sha512-3xWTxyIggxOYIPT9NkucQPxlOBPJY14/ifely3eCtifE5pAxJXl/jUmSDwq+fLVhHIIj9MIJ6Bv7u3ItChG8vQ==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + commander: 2.20.3 + lodash.flowright: 3.5.0 + lodash.get: 4.4.2 + lodash.isobject: 3.0.2 + pako: 1.0.11 + pretty-bytes: 5.6.0 + prettyjson: 1.2.5 + utf8-length: 0.0.1 + dev: false + /json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} dev: true @@ -6713,6 +6736,18 @@ packages: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} dev: true + /lodash.flowright@3.5.0: + resolution: {integrity: sha512-YxTYuodkvyINbDInmFcGGvkQwoAuoGUYosqstRTr5eq63GQt7WQ2xFU0wG1UfdbKYPwevd3zWDd6ybEE2g6qvA==} + dev: false + + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: false + + /lodash.isobject@3.0.2: + resolution: {integrity: sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true @@ -6944,7 +6979,6 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - dev: true /mlly@1.4.0: resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} @@ -7263,6 +7297,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: false + /param-case@2.1.1: resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} dependencies: @@ -7495,7 +7533,6 @@ packages: /pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} - dev: true /pretty-bytes@6.1.0: resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==} @@ -7511,6 +7548,14 @@ packages: react-is: 18.2.0 dev: true + /prettyjson@1.2.5: + resolution: {integrity: sha512-rksPWtoZb2ZpT5OVgtmy0KHVM+Dca3iVwWY9ifwhcexfjebtgjg3wmrUt9PvJ59XIYBcknQeYHD8IAnVlh9lAw==} + hasBin: true + dependencies: + colors: 1.4.0 + minimist: 1.2.8 + dev: false + /prosemirror-changeset@2.2.1: resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} dependencies: @@ -8945,6 +8990,10 @@ packages: requires-port: 1.0.0 dev: true + /utf8-length@0.0.1: + resolution: {integrity: sha512-j/XH2ftofBiobnyApxlN/J6j/ixwT89WEjDcjT66d2i0+GIn9RZfzt8lpEXXE4jUe4NsjBSUq70kS2euQ4nnMw==} + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true diff --git a/src/tools/index.ts b/src/tools/index.ts index b4c161ef..5f5d1fb2 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -2,6 +2,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; import { tool as emailNormalizer } from './email-normalizer'; +import { tool as jsonSizeAnalyzer } from './json-size-analyzer'; import { tool as asciiTextDrawer } from './ascii-text-drawer'; @@ -147,6 +148,7 @@ export const toolsByCategory: ToolCategory[] = [ crontabGenerator, jsonViewer, jsonMinify, + jsonSizeAnalyzer, jsonToCsv, sqlPrettify, chmodCalculator, diff --git a/src/tools/json-size-analyzer/index.ts b/src/tools/json-size-analyzer/index.ts new file mode 100644 index 00000000..2e11977d --- /dev/null +++ b/src/tools/json-size-analyzer/index.ts @@ -0,0 +1,12 @@ +import { FileAnalytics } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Json Size Analyzer', + path: '/json-size-analyzer', + description: 'Measure JSON nodes relative weights', + keywords: ['json', 'size', 'analyzer'], + component: () => import('./json-size-analyzer.vue'), + icon: FileAnalytics, + createdAt: new Date('2024-07-14'), +}); diff --git a/src/tools/json-size-analyzer/json-analyzer.d.ts b/src/tools/json-size-analyzer/json-analyzer.d.ts new file mode 100644 index 00000000..09a9d8b2 --- /dev/null +++ b/src/tools/json-size-analyzer/json-analyzer.d.ts @@ -0,0 +1,13 @@ +declare module 'json-analyzer' { + export default function analyze({ + json, + verbose, + maxDepth, + target, + }: { + json: any, + verbose: boolean, + maxDepth: number, + target: string, + }); +} \ No newline at end of file diff --git a/src/tools/json-size-analyzer/json-size-analyzer.service.test.ts b/src/tools/json-size-analyzer/json-size-analyzer.service.test.ts new file mode 100644 index 00000000..dd2a9e2d --- /dev/null +++ b/src/tools/json-size-analyzer/json-size-analyzer.service.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from 'vitest'; +import { getJsonUsageTreeNodes } from './json-size-analyzer.service'; + +describe('json-size-analyzer', () => { + describe('getJsonUsageTreeNodes', () => { + it('return correct tree nodes structures', () => { + expect(getJsonUsageTreeNodes([{ a: [1, 2, 3] }, { b: 'a' }])).to.deep.eq({ + children: [ + { + children: [ + { + children: [ + { + children: [], + key: '$.[0].a.[0]', + label: '$.[0].a.[0]: 1 B(26 B gzip)', + }, + { + children: [], + key: '$.[0].a.[1]', + label: '$.[0].a.[1]: 1 B(24 B gzip)', + }, + { + children: [], + key: '$.[0].a.[2]', + label: '$.[0].a.[2]: 1 B(25 B gzip)', + }, + ], + key: '$.[0].a', + label: '$.[0].a: 7 B(35 B gzip) ; 28.000% of parent ; biggest child node: \'0\'', + }, + ], + key: '$.[0]', + label: '$.[0]: 13 B(43 B gzip) ; 52.000% of parent ; biggest child node: \'a\'', + }, + { + children: [ + { + children: [], + key: '$.[1].b', + label: '$.[1].b: 1 B(25 B gzip)', + }, + ], + key: '$.[1]', + label: '$.[1]: 9 B(34 B gzip) ; 36.000% of parent ; biggest child node: \'b\'', + }, + ], + key: '$', + label: '$: 25 B(61 B gzip) ; 100.00% of parent ; biggest child node: \'0\'', + }); + expect(getJsonUsageTreeNodes({ a: { b: [1, 2, 3], c: 12 } })).to.deep.eq({ + children: [ + { + children: [ + { + children: [ + { + children: [], + key: '$.a.b.[0]', + label: '$.a.b.[0]: 1 B(26 B gzip)', + }, + { + children: [], + key: '$.a.b.[1]', + label: '$.a.b.[1]: 1 B(24 B gzip)', + }, + { + children: [], + key: '$.a.b.[2]', + label: '$.a.b.[2]: 1 B(25 B gzip)', + }, + ], + key: '$.a.b', + label: '$.a.b: 7 B(35 B gzip) ; 26.923% of parent ; biggest child node: \'0\'', + }, + { + children: [], + key: '$.a.c', + label: '$.a.c: 2 B(24 B gzip)', + }, + ], + key: '$.a', + label: '$.a: 20 B(50 B gzip) ; 76.923% of parent ; biggest child node: \'b\'', + }, + ], + key: '$', + label: '$: 26 B(63 B gzip) ; 100.00% of parent ; biggest child node: \'a\'', + }); + expect(getJsonUsageTreeNodes({ a: { b: 'azerty', c: 'ueop' } })).to.deep.eq({ + children: [ + { + children: [ + { + children: [], + key: '$.a.b', + label: '$.a.b: 6 B(30 B gzip)', + }, + { + children: [], + key: '$.a.c', + label: '$.a.c: 4 B(29 B gzip)', + }, + ], + key: '$.a', + label: '$.a: 25 B(51 B gzip) ; 80.645% of parent ; biggest child node: \'b\'', + }, + ], + key: '$', + label: '$: 31 B(61 B gzip) ; 100.00% of parent ; biggest child node: \'a\'', + }); + }); + }); +}); diff --git a/src/tools/json-size-analyzer/json-size-analyzer.service.ts b/src/tools/json-size-analyzer/json-size-analyzer.service.ts new file mode 100644 index 00000000..cff422ca --- /dev/null +++ b/src/tools/json-size-analyzer/json-size-analyzer.service.ts @@ -0,0 +1,51 @@ +import jsonAnalyzer from 'json-analyzer'; + +export interface Meta { + __meta__: { + size?: { + value: number + raw: string + gzip: string + } + number_of_childs?: number + parent_relative_percentage?: string + biggest_node_child: string + } +} +export type AnalysisNode = { + [key: string]: object & Meta +} & Meta; + +export type TreeNode = { + key: string + label: string + children: Array +} & Record; + +function getTreeNodes(obj: AnalysisNode, parentName: string): TreeNode { + const childNodes = Object.entries(obj) + .filter(([key, v]) => key !== '__meta__' && typeof v === 'object') + .map(([k, v]) => ({ + key: (Number.isNaN(Number.parseInt(k, 10)) ? `.${k}` : `.[${k}]`), + value: v as AnalysisNode, + })); + const biggest_child_node = obj.__meta__.biggest_node_child ? ` ; biggest child node: '${obj.__meta__.biggest_node_child}'` : ''; + const parent_relative_percentage = obj.__meta__.parent_relative_percentage ? ` ; ${obj.__meta__.parent_relative_percentage} of parent` : ''; + return { + key: parentName, + label: obj.__meta__ + ? `${parentName}: ${obj.__meta__.size?.raw}(${obj.__meta__.size?.gzip} gzip)${parent_relative_percentage}${biggest_child_node}` + : parentName, + children: childNodes.map(childNode => getTreeNodes(childNode.value, parentName + childNode.key)), + }; +} + +export function getJsonUsageTreeNodes(jsonObj: any, maxDepth: number = 100, targetNode: string = ''): TreeNode { + const analysis = jsonAnalyzer({ + json: jsonObj, + verbose: true, + maxDepth, + target: targetNode, + }); + return getTreeNodes(analysis, '$'); +} diff --git a/src/tools/json-size-analyzer/json-size-analyzer.vue b/src/tools/json-size-analyzer/json-size-analyzer.vue new file mode 100644 index 00000000..b142e2bb --- /dev/null +++ b/src/tools/json-size-analyzer/json-size-analyzer.vue @@ -0,0 +1,68 @@ + + +