diff --git a/packages/docs/pages/plugins/plugin-json-schema.mdx b/packages/docs/pages/plugins/plugin-json-schema.mdx new file mode 100644 index 000000000..226c59adb --- /dev/null +++ b/packages/docs/pages/plugins/plugin-json-schema.mdx @@ -0,0 +1,47 @@ +import { Tab, Tabs } from 'nextra-theme-docs' + +# JSON Schema plugin + +[JSON Schema](https://json-schema.org/) is a declarative language that allows you to annotate and validate JSON documents. + +Orama provides its own official plugin to convert JSON Schema to Orama schema. + +## Installation + +You can install the plugin using any major Node.js package manager: + + + + ```bash copy + npm install @orama/plugin-json-schema + ``` + + + ```bash copy + yarn add @orama/plugin-json-schema + ``` + + + ```bash copy + pnpm add @orama/plugin-json-schema + ``` + + + + +## Usage + +When you configure Orama, you'll also import the `schemaFromJson` function from this plguin: + +```js +import { create } from '@orama/orama' +import { schemaFromJson } from '@orama/plugin-json-schema' + +const jsonSchema = { ... } + +const db = await create({ + schema: schemaFromJson(jsonSchema), +}) +``` + +And that's it! The Orama plugin will do the rest for you. \ No newline at end of file diff --git a/packages/plugin-json-schema/LICENSE.md b/packages/plugin-json-schema/LICENSE.md new file mode 100644 index 000000000..40fba3ce6 --- /dev/null +++ b/packages/plugin-json-schema/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 OramaSearch Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/plugin-json-schema/README.md b/packages/plugin-json-schema/README.md new file mode 100644 index 000000000..bf6c158b0 --- /dev/null +++ b/packages/plugin-json-schema/README.md @@ -0,0 +1,13 @@ +# JSON Schema plugin + +[![Tests](https://github.com/oramasearch/orama/actions/workflows/turbo.yml/badge.svg)](https://github.com/oramasearch/orama/actions/workflows/turbo.yml) + +Official plugin to convert JSON schema into Orama schema! + +# Usage + +For the complete usage guide, please refer to the [official plugin documentation](https://docs.oramasearch.com/plugins/plugin-json-schema). + +# License + +[MIT](/LICENSE.md) diff --git a/packages/plugin-json-schema/package.json b/packages/plugin-json-schema/package.json new file mode 100644 index 000000000..91ec70dda --- /dev/null +++ b/packages/plugin-json-schema/package.json @@ -0,0 +1,58 @@ +{ + "name": "@orama/plugin-json-schema", + "version": "0.0.1", + "description": "Schema utilities for Orama", + "keywords": [ + "orama", + "search", + "schema", + "json" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/oramasearch/orama" + }, + "bugs": { + "url": "https://github.com/oramasearch/orama" + }, + "type": "module", + "sideEffects": false, + "main": "./dist/commonjs.cjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/commonjs.cjs" + } + }, + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "swc --delete-dir-on-start -s -w --extensions .ts,.cts -d dist src", + "build": "swc --delete-dir-on-start --extensions .ts,.cts -d dist src", + "postbuild": "tsc -p . --emitDeclarationOnly", + "test": "c8 -c test/config/c8.json tap --rcfile=test/config/tap.yml test/*.test.ts" + }, + "dependencies": { + "@orama/orama": "workspace:*" + }, + "devDependencies": { + "@swc/cli": "^0.1.59", + "@swc/core": "^1.3.27", + "@types/json-schema": "^7.0.12", + "@types/node": "^18.11.18", + "@types/tap": "^15.0.7", + "c8": "^7.12.0", + "tap": "^16.3.4", + "tsx": "^3.12.2", + "typescript": "^4.9.4" + }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + } +} diff --git a/packages/plugin-json-schema/src/index.ts b/packages/plugin-json-schema/src/index.ts new file mode 100644 index 000000000..22ec20055 --- /dev/null +++ b/packages/plugin-json-schema/src/index.ts @@ -0,0 +1,3 @@ +import { schemaFromJson } from './jsonSchema.js' + +export { schemaFromJson } diff --git a/packages/plugin-json-schema/src/jsonSchema.ts b/packages/plugin-json-schema/src/jsonSchema.ts new file mode 100644 index 000000000..a70c9c093 --- /dev/null +++ b/packages/plugin-json-schema/src/jsonSchema.ts @@ -0,0 +1,45 @@ +import type { Schema, SearchableType } from '@orama/orama'; +import type { JSONSchema4 } from 'json-schema'; + +const isJsonObject = (jsonSchema: JSONSchema4) => jsonSchema.type === 'object' + +const assertTypeObject = (jsonSchema: JSONSchema4) => { + if (!isJsonObject(jsonSchema)) { + throw new Error('Provided JSON schema must be an object type'); + } +} + +const ORAMA_SUPPORTED_TYPES: Set = new Set(['string', 'number', 'boolean']) + +const isArraySupportedByOrama = (jsonSchema: JSONSchema4): boolean => { + if (jsonSchema.type === 'array' && jsonSchema.items && !Array.isArray(jsonSchema.items)) { + return ORAMA_SUPPORTED_TYPES.has(jsonSchema.items.type); + } + return false +} + +const isSupportedByOrama = (jsonSchema: JSONSchema4): boolean => { + return ORAMA_SUPPORTED_TYPES.has(jsonSchema.type) || isArraySupportedByOrama(jsonSchema); +} + +const extractOramaType = (jsonSchema: JSONSchema4): SearchableType => { + const oramaType = ORAMA_SUPPORTED_TYPES.has(jsonSchema.type) ? jsonSchema.type : `${(jsonSchema.items as JSONSchema4)!.type}[]` + + return oramaType as SearchableType +} + +export const schemaFromJson = async (jsonSchema: JSONSchema4): Promise => { + assertTypeObject(jsonSchema) + + const oramaSchema: Schema = {} + + for (const [propertyName, propertyDefinition] of Object.entries(jsonSchema.properties || {})) { + if (isSupportedByOrama(propertyDefinition)) { + oramaSchema[propertyName] = extractOramaType(propertyDefinition) + } else if (isJsonObject(propertyDefinition)) { + oramaSchema[propertyName] = await schemaFromJson(propertyDefinition) + } + } + + return oramaSchema; +} \ No newline at end of file diff --git a/packages/plugin-json-schema/test/config/c8.json b/packages/plugin-json-schema/test/config/c8.json new file mode 100644 index 000000000..14bc5e00e --- /dev/null +++ b/packages/plugin-json-schema/test/config/c8.json @@ -0,0 +1,8 @@ +{ + "check-coverage": true, + "reporter": ["text", "json"], + "branches": 80, + "functions": 80, + "lines": 80, + "statements": 80 +} diff --git a/packages/plugin-json-schema/test/config/tap.yml b/packages/plugin-json-schema/test/config/tap.yml new file mode 100644 index 000000000..ee28002fa --- /dev/null +++ b/packages/plugin-json-schema/test/config/tap.yml @@ -0,0 +1,8 @@ +--- +jobs: 5 +timeout: 120 +reporter: spec +coverage: false +node-arg: + - --loader=tsx + - --no-warnings=loader diff --git a/packages/plugin-json-schema/test/index.test.ts b/packages/plugin-json-schema/test/index.test.ts new file mode 100644 index 000000000..7d9484178 --- /dev/null +++ b/packages/plugin-json-schema/test/index.test.ts @@ -0,0 +1,288 @@ +import t from 'tap'; +import { schemaFromJson } from '../src/index.js'; + +t.test("it should throw for non-object json schema", async t => { + const jsonSchema = { + type: 'array', + items: { + type: 'string' + } + } as const + + t.rejects(() => schemaFromJson(jsonSchema), Error, 'Provided JSON schema must be an object type'); +}) + +t.test("it should return empty object for missing properties", async t => { + const jsonSchema = { + type: 'object', + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, {}) +}) + +t.test("it should convert type string", async t => { + const jsonSchema = { + type: 'object', + properties: { + myString: { + type: 'string' + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myString: 'string' + }) +}) + +t.test("it should convert type number", async t => { + const jsonSchema = { + type: 'object', + properties: { + myNumber: { + type: 'number' + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myNumber: 'number' + }) +}) + + +t.test("it should convert type boolean", async t => { + const jsonSchema = { + type: 'object', + properties: { + myBoolean: { + type: 'boolean' + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myBoolean: 'boolean' + }) +}) + +t.test("it should convert all types", async t => { + const jsonSchema = { + type: 'object', + properties: { + myString: { + type: 'string' + }, + myNumber: { + type: 'number' + }, + myBoolean: { + type: 'boolean' + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myString: 'string', + myNumber: 'number', + myBoolean: 'boolean' + }) +}) + +t.test("it should convert type string[]", async t => { + const jsonSchema = { + type: 'object', + properties: { + myStringArray: { + type: 'array', + items: { + type: 'string' + } + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myStringArray: 'string[]' + }) +}) + +t.test("it should convert type number[]", async t => { + const jsonSchema = { + type: 'object', + properties: { + myNumberArray: { + type: 'array', + items: { + type: 'number' + } + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myNumberArray: 'number[]' + }) +}) + +t.test("it should convert type boolean[]", async t => { + const jsonSchema = { + type: 'object', + properties: { + myBooleanArray: { + type: 'array', + items: { + type: 'boolean' + } + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myBooleanArray: 'boolean[]' + }) +}) + +t.test("it should convert type array", async t => { + const jsonSchema = { + type: 'object', + properties: { + myStringArray: { + type: 'array', + items: { + type: 'string' + } + }, + myNumberArray: { + type: 'array', + items: { + type: 'number' + } + }, + myBooleanArray: { + type: 'array', + items: { + type: 'boolean' + } + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myStringArray: 'string[]', + myNumberArray: 'number[]', + myBooleanArray: 'boolean[]' + }) +}) + +t.test("it should convert all types", async t => { + const jsonSchema = { + type: 'object', + properties: { + myString: { + type: 'string' + }, + myNumber: { + type: 'number' + }, + myBoolean: { + type: 'boolean' + }, + myStringArray: { + type: 'array', + items: { + type: 'string' + } + }, + myNumberArray: { + type: 'array', + items: { + type: 'number' + } + }, + myBooleanArray: { + type: 'array', + items: { + type: 'boolean' + } + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myString: 'string', + myNumber: 'number', + myBoolean: 'boolean', + myStringArray: 'string[]', + myNumberArray: 'number[]', + myBooleanArray: 'boolean[]' + }) +}) + +t.test("it should skip unknown types", async t => { + const jsonSchema = { + type: 'object', + properties: { + myBoolean: { + type: 'boolean' + }, + myAny: { + type: 'any' + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myBoolean: 'boolean' + }) +}) + +t.test("it should convert nested objects", async t => { + const jsonSchema = { + type: 'object', + properties: { + myBoolean: { + type: 'boolean' + }, + myObject: { + type: 'object', + properties: { + myString: { + type: 'string' + } + } + } + } + } as const + + const oramaSchema = await schemaFromJson(jsonSchema) + + t.same(oramaSchema, { + myBoolean: 'boolean', + myObject: { + myString: 'string' + } + }) +}) \ No newline at end of file diff --git a/packages/plugin-json-schema/tsconfig.json b/packages/plugin-json-schema/tsconfig.json new file mode 100644 index 000000000..012a0b3ee --- /dev/null +++ b/packages/plugin-json-schema/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowJs": true, + "target": "ESNext", + "module": "ESNext", + "outDir": "dist", + "lib": ["ESNext", "DOM"], + "esModuleInterop": true, + "declaration": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "sourceMap": true, + "moduleResolution": "nodenext" + }, + "include": ["src/*.ts", "src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 850c9ee6c..88f199099 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -114,7 +114,7 @@ importers: version: link:../plugin-nextra next: specifier: ^13.3.0 - version: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.3.0(react-dom@18.2.0)(react@18.2.0) nextra: specifier: ^2.4.0 version: 2.4.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) @@ -407,6 +407,40 @@ importers: specifier: ^5.75.0 version: 5.75.0(@swc/core@1.3.27) + packages/plugin-json-schema: + dependencies: + '@orama/orama': + specifier: workspace:* + version: link:../orama + devDependencies: + '@swc/cli': + specifier: ^0.1.59 + version: 0.1.59(@swc/core@1.3.27) + '@swc/core': + specifier: ^1.3.27 + version: 1.3.27 + '@types/json-schema': + specifier: ^7.0.12 + version: 7.0.12 + '@types/node': + specifier: ^18.11.18 + version: 18.11.18 + '@types/tap': + specifier: ^15.0.7 + version: 15.0.7 + c8: + specifier: ^7.12.0 + version: 7.12.0 + tap: + specifier: ^16.3.4 + version: 16.3.4(ts-node@10.9.1)(typescript@4.9.4) + tsx: + specifier: ^3.12.2 + version: 3.12.2 + typescript: + specifier: ^4.9.4 + version: 4.9.4 + packages/plugin-match-highlight: dependencies: '@orama/orama': @@ -451,7 +485,7 @@ importers: version: 2.3.2 next: specifier: ^13.2.4 - version: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + version: 13.3.0(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 @@ -2603,7 +2637,7 @@ packages: react-router-config: 5.1.1(react-router@5.3.4)(react@17.0.2) react-router-dom: 5.3.4(react@17.0.2) rtl-detect: 1.0.4 - semver: 7.3.8 + semver: 7.5.0 serve-handler: 6.1.5 shelljs: 0.8.5 terser-webpack-plugin: 5.3.9(@swc/core@1.3.27)(webpack@5.75.0) @@ -4102,7 +4136,7 @@ packages: bin-wrapper: 4.1.0 commander: 7.2.0 fast-glob: 3.2.12 - semver: 7.3.8 + semver: 7.5.0 slash: 3.0.0 source-map: 0.7.4 dev: true @@ -4746,7 +4780,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.5.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -4767,7 +4801,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.5.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -4788,7 +4822,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.8 + semver: 7.5.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 transitivePeerDependencies: @@ -4812,7 +4846,7 @@ packages: eslint: 8.32.0 eslint-scope: 5.1.1 eslint-utils: 3.0.0(eslint@8.32.0) - semver: 7.3.8 + semver: 7.5.0 transitivePeerDependencies: - supports-color - typescript @@ -4835,7 +4869,7 @@ packages: '@typescript-eslint/typescript-estree': 5.59.9(typescript@4.9.4) eslint: 8.32.0 eslint-scope: 5.1.1 - semver: 7.3.8 + semver: 7.5.0 transitivePeerDependencies: - supports-color - typescript @@ -5797,7 +5831,7 @@ packages: /builtins@5.0.1: resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} dependencies: - semver: 7.3.8 + semver: 7.5.0 dev: true /bundle-name@3.0.0: @@ -6585,7 +6619,7 @@ packages: postcss-modules-scope: 3.0.0(postcss@8.4.24) postcss-modules-values: 4.0.0(postcss@8.4.24) postcss-value-parser: 4.2.0 - semver: 7.3.8 + semver: 7.5.0 webpack: 5.75.0(@swc/core@1.3.27) dev: false @@ -8492,7 +8526,7 @@ packages: memfs: 3.5.3 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.8 + semver: 7.5.0 tapable: 1.1.3 typescript: 4.9.4 webpack: 5.75.0(@swc/core@1.3.27) @@ -11575,7 +11609,7 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - next: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.3.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -11587,12 +11621,12 @@ packages: react: '*' react-dom: '*' dependencies: - next: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.3.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /next@13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0): + /next@13.3.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-OVTw8MpIPa12+DCUkPqRGPS3thlJPcwae2ZL4xti3iBff27goH024xy4q2lhlsdoYiKOi8Kz6uJoLW/GXwgfOA==} engines: {node: '>=14.6.0'} hasBin: true @@ -11620,7 +11654,7 @@ packages: postcss: 8.4.14 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - styled-jsx: 5.1.1(@babel/core@7.22.5)(react@18.2.0) + styled-jsx: 5.1.1(react@18.2.0) optionalDependencies: '@next/swc-darwin-arm64': 13.3.0 '@next/swc-darwin-x64': 13.3.0 @@ -11648,7 +11682,7 @@ packages: react-cusdis: optional: true dependencies: - next: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.3.0(react-dom@18.2.0)(react@18.2.0) next-themes: 0.2.1(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) nextra: 2.4.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -11671,7 +11705,7 @@ packages: git-url-parse: 13.1.0 intersection-observer: 0.12.2 match-sorter: 6.3.1 - next: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.3.0(react-dom@18.2.0)(react@18.2.0) next-seo: 5.15.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) next-themes: 0.2.1(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) nextra: 2.4.0(next@13.3.0)(react-dom@18.2.0)(react@18.2.0) @@ -11697,7 +11731,7 @@ packages: gray-matter: 4.0.3 katex: 0.16.7 lodash.get: 4.4.2 - next: 13.3.0(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) + next: 13.3.0(react-dom@18.2.0)(react@18.2.0) next-mdx-remote: 4.4.1(react-dom@18.2.0)(react@18.2.0) p-limit: 3.1.0 react: 18.2.0 @@ -11781,7 +11815,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.12.1 - semver: 7.3.8 + semver: 7.5.0 validate-npm-package-license: 3.0.4 dev: true @@ -12546,7 +12580,7 @@ packages: jiti: 1.18.2 klona: 2.0.6 postcss: 8.4.24 - semver: 7.3.8 + semver: 7.5.0 webpack: 5.75.0(@swc/core@1.3.27) dev: false @@ -14219,7 +14253,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -14810,7 +14843,7 @@ packages: inline-style-parser: 0.1.1 dev: false - /styled-jsx@5.1.1(@babel/core@7.22.5)(react@18.2.0): + /styled-jsx@5.1.1(react@18.2.0): resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} peerDependencies: @@ -14823,7 +14856,6 @@ packages: babel-plugin-macros: optional: true dependencies: - '@babel/core': 7.22.5 client-only: 0.0.1 react: 18.2.0 dev: false @@ -15784,7 +15816,7 @@ packages: is-yarn-global: 0.3.0 latest-version: 5.1.0 pupa: 2.1.1 - semver: 7.3.8 + semver: 7.5.0 semver-diff: 3.1.1 xdg-basedir: 4.0.0 dev: false