From e9cdf0c4269c9ead1a35706491586a61b0204200 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Fri, 7 Feb 2025 11:56:30 +0100 Subject: [PATCH] chore(snippet-manager): migrate from joi to zod MONGOSH-2010 (#2360) This aligns us with the validators we've standardized on in Compass, improves TypeScript integration, and reduces executable size by 564 kB and startup time by 1.5% (locally on an M3). --- package-lock.json | 50 +++------- packages/snippet-manager/package.json | 2 +- .../src/snippet-manager.spec.ts | 5 +- .../snippet-manager/src/snippet-manager.ts | 92 ++++++++----------- 4 files changed, 52 insertions(+), 97 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d4bca0a8..49c7421a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3118,17 +3118,6 @@ "version": "1.1.3", "license": "MIT" }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.5.0", "license": "Apache-2.0", @@ -8455,21 +8444,6 @@ "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", "license": "0BSD" }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "license": "BSD-3-Clause" - }, "node_modules/@sigstore/bundle": { "version": "2.3.2", "license": "Apache-2.0", @@ -18545,17 +18519,6 @@ "node": ">= 0.6.0" } }, - "node_modules/joi": { - "version": "17.8.3", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/jose": { "version": "4.15.5", "license": "MIT", @@ -28833,6 +28796,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/arg-parser": { "name": "@mongosh/arg-parser", "version": "3.2.0", @@ -29868,8 +29840,8 @@ "bson": "^6.10.1", "cross-spawn": "^7.0.5", "escape-string-regexp": "^4.0.0", - "joi": "^17.4.0", - "tar": "^6.1.15" + "tar": "^6.1.15", + "zod": "^3.24.1" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", diff --git a/packages/snippet-manager/package.json b/packages/snippet-manager/package.json index 22aa3049d..f50cd0efe 100644 --- a/packages/snippet-manager/package.json +++ b/packages/snippet-manager/package.json @@ -42,7 +42,7 @@ "bson": "^6.10.1", "cross-spawn": "^7.0.5", "escape-string-regexp": "^4.0.0", - "joi": "^17.4.0", + "zod": "^3.24.1", "tar": "^6.1.15" }, "devDependencies": { diff --git a/packages/snippet-manager/src/snippet-manager.spec.ts b/packages/snippet-manager/src/snippet-manager.spec.ts index df1ae238e..c844c92ad 100644 --- a/packages/snippet-manager/src/snippet-manager.spec.ts +++ b/packages/snippet-manager/src/snippet-manager.spec.ts @@ -440,9 +440,10 @@ describe('SnippetManager', function () { await snippetManager.runSnippetCommand(['refresh']); expect.fail('missed exception'); } catch (err: any) { - expect(err.message).to.equal( - `The specified index file ${indexURL} is not a valid index file: "indexFileVersion" must be less than or equal to 1` + expect(err.message).to.include( + `The specified index file ${indexURL} is not a valid index file:` ); + expect(err.message).to.include(`Number must be less than or equal to 1`); } }); diff --git a/packages/snippet-manager/src/snippet-manager.ts b/packages/snippet-manager/src/snippet-manager.ts index 4582e1045..83a2d8a28 100644 --- a/packages/snippet-manager/src/snippet-manager.ts +++ b/packages/snippet-manager/src/snippet-manager.ts @@ -16,7 +16,7 @@ import { once } from 'events'; import tar from 'tar'; import zlib from 'zlib'; import bson from 'bson'; -import joi from 'joi'; +import { z } from 'zod'; import type { AgentWithInitialize, DevtoolsProxyOptions, @@ -34,63 +34,46 @@ export interface SnippetOptions { proxyOptions?: DevtoolsProxyOptions | AgentWithInitialize; } -export interface ErrorMatcher { - matches: RegExp[]; - message: string; -} - -export interface SnippetDescription { - name: string; - snippetName: string; - installSpec?: string; - version: string; - description: string; - license: string; - readme: string; - errorMatchers?: ErrorMatcher[]; -} - -export interface SnippetIndexFile { - indexFileVersion: 1; - index: SnippetDescription[]; - metadata: { homepage: string }; - sourceURL: string; -} - interface NpmMetaDataResponse { dist?: { tarball?: string; }; } -const indexFileSchema = joi.object({ - indexFileVersion: joi.number().integer().max(1).required(), - - metadata: joi.object({ - homepage: joi.string(), - }), - - index: joi - .array() - .required() - .items( - joi.object({ - name: joi.string().required(), - snippetName: joi.string().required(), - installSpec: joi.string(), - version: joi.string().required(), - description: joi.string().required().allow(''), - license: joi.string().required(), - readme: joi.string().required().allow(''), - errorMatchers: joi.array().items( - joi.object({ - message: joi.string().required(), - matches: joi.array().required().items(joi.object().regex()), - }) - ), - }) - ), +const regExpTag = Object.prototype.toString.call(/foo/); +const errorMatcherSchema = z.object({ + message: z.string(), + matches: z.array( + z.custom((val) => Object.prototype.toString.call(val) === regExpTag) + ), +}); +const indexDescriptionSchema = z.object({ + name: z.string(), + snippetName: z.string(), + installSpec: z.string().optional(), + version: z.string(), + description: z.string(), + license: z.string(), + readme: z.string(), + errorMatchers: z.array(errorMatcherSchema).optional(), }); +const indexFileSchema = z.object({ + indexFileVersion: z.number().int().max(1), + + metadata: z + .object({ + homepage: z.string(), + }) + .passthrough(), + + index: z.array(indexDescriptionSchema), +}); + +export type ErrorMatcher = z.infer; +export type SnippetIndexFile = z.infer & { + sourceURL: string; +}; +export type SnippetDescription = z.infer; async function unpackBSON(data: Buffer): Promise { return bson.deserialize(await brotliDecompress(data)) as T; @@ -361,9 +344,8 @@ export class SnippetManager implements ShellPlugin { `The specified index file ${url} could not be parsed: ${err.message}` ); } - const { error } = indexFileSchema.validate(data, { - allowUnknown: true, - }); + const { error, data: parsedData } = + indexFileSchema.safeParse(data); if (error) { this.messageBus.emit('mongosh-snippets:fetch-index-error', { action: 'validate-fetched', @@ -374,7 +356,7 @@ export class SnippetManager implements ShellPlugin { `The specified index file ${url} is not a valid index file: ${error.message}` ); } - return { ...data, sourceURL: url }; + return { ...parsedData, sourceURL: url }; }) ); // If possible, write the result to disk for caching.