From 48f3b4f45476acac6736ab24654574de081773dd Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 6 Feb 2025 19:35:37 +0100 Subject: [PATCH 1/3] chore(snippet-manager): migrate from joi to zod MONGOSH-2010 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 +- .../snippet-manager/src/snippet-manager.ts | 86 +++++++------------ 3 files changed, 45 insertions(+), 93 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d4bca0a8a..49c7421a70 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 22aa3049d6..f50cd0efe4 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.ts b/packages/snippet-manager/src/snippet-manager.ts index 4582e1045c..64f871b4b9 100644 --- a/packages/snippet-manager/src/snippet-manager.ts +++ b/packages/snippet-manager/src/snippet-manager.ts @@ -9,14 +9,14 @@ import type { SnippetShellUserConfig, MongoshBus } from '@mongosh/types'; import escapeRegexp from 'escape-string-regexp'; import path from 'path'; import { promisify, isDeepStrictEqual } from 'util'; -import { Console } from 'console'; +import { Console, error } from 'console'; import { promises as fs } from 'fs'; import stream, { PassThrough } from 'stream'; 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,64 +34,45 @@ 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(), +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), +}); +const indexFileSchema = z.object({ + indexFileVersion: z.number().int().max(1), - metadata: joi.object({ - homepage: joi.string(), + metadata: z.object({ + homepage: z.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()), - }) - ), - }) - ), + 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 +342,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 +354,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. From fecd14a089955ab7cc5a859858b568c6847cfc34 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 6 Feb 2025 20:07:13 +0100 Subject: [PATCH 2/3] fixup! chore(snippet-manager): migrate from joi to zod MONGOSH-2010 --- packages/snippet-manager/src/snippet-manager.spec.ts | 5 +++-- packages/snippet-manager/src/snippet-manager.ts | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/snippet-manager/src/snippet-manager.spec.ts b/packages/snippet-manager/src/snippet-manager.spec.ts index df1ae238e1..c844c92ade 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 64f871b4b9..bcf85fa6c3 100644 --- a/packages/snippet-manager/src/snippet-manager.ts +++ b/packages/snippet-manager/src/snippet-manager.ts @@ -55,14 +55,16 @@ const indexDescriptionSchema = z.object({ description: z.string(), license: z.string(), readme: z.string(), - errorMatchers: z.array(errorMatcherSchema), + errorMatchers: z.array(errorMatcherSchema).optional(), }); const indexFileSchema = z.object({ indexFileVersion: z.number().int().max(1), - metadata: z.object({ - homepage: z.string(), - }), + metadata: z + .object({ + homepage: z.string(), + }) + .passthrough(), index: z.array(indexDescriptionSchema), }); From c992fa11128c8b1205faa11c2fda325197b02ff4 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 6 Feb 2025 20:50:13 +0100 Subject: [PATCH 3/3] fixup: lint --- packages/snippet-manager/src/snippet-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/snippet-manager/src/snippet-manager.ts b/packages/snippet-manager/src/snippet-manager.ts index bcf85fa6c3..83a2d8a286 100644 --- a/packages/snippet-manager/src/snippet-manager.ts +++ b/packages/snippet-manager/src/snippet-manager.ts @@ -9,7 +9,7 @@ import type { SnippetShellUserConfig, MongoshBus } from '@mongosh/types'; import escapeRegexp from 'escape-string-regexp'; import path from 'path'; import { promisify, isDeepStrictEqual } from 'util'; -import { Console, error } from 'console'; +import { Console } from 'console'; import { promises as fs } from 'fs'; import stream, { PassThrough } from 'stream'; import { once } from 'events';