Skip to content

Commit e9cdf0c

Browse files
authored
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).
1 parent 6d7e2e8 commit e9cdf0c

File tree

4 files changed

+52
-97
lines changed

4 files changed

+52
-97
lines changed

package-lock.json

Lines changed: 11 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/snippet-manager/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"bson": "^6.10.1",
4343
"cross-spawn": "^7.0.5",
4444
"escape-string-regexp": "^4.0.0",
45-
"joi": "^17.4.0",
45+
"zod": "^3.24.1",
4646
"tar": "^6.1.15"
4747
},
4848
"devDependencies": {

packages/snippet-manager/src/snippet-manager.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,10 @@ describe('SnippetManager', function () {
440440
await snippetManager.runSnippetCommand(['refresh']);
441441
expect.fail('missed exception');
442442
} catch (err: any) {
443-
expect(err.message).to.equal(
444-
`The specified index file ${indexURL} is not a valid index file: "indexFileVersion" must be less than or equal to 1`
443+
expect(err.message).to.include(
444+
`The specified index file ${indexURL} is not a valid index file:`
445445
);
446+
expect(err.message).to.include(`Number must be less than or equal to 1`);
446447
}
447448
});
448449

packages/snippet-manager/src/snippet-manager.ts

Lines changed: 37 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { once } from 'events';
1616
import tar from 'tar';
1717
import zlib from 'zlib';
1818
import bson from 'bson';
19-
import joi from 'joi';
19+
import { z } from 'zod';
2020
import type {
2121
AgentWithInitialize,
2222
DevtoolsProxyOptions,
@@ -34,63 +34,46 @@ export interface SnippetOptions {
3434
proxyOptions?: DevtoolsProxyOptions | AgentWithInitialize;
3535
}
3636

37-
export interface ErrorMatcher {
38-
matches: RegExp[];
39-
message: string;
40-
}
41-
42-
export interface SnippetDescription {
43-
name: string;
44-
snippetName: string;
45-
installSpec?: string;
46-
version: string;
47-
description: string;
48-
license: string;
49-
readme: string;
50-
errorMatchers?: ErrorMatcher[];
51-
}
52-
53-
export interface SnippetIndexFile {
54-
indexFileVersion: 1;
55-
index: SnippetDescription[];
56-
metadata: { homepage: string };
57-
sourceURL: string;
58-
}
59-
6037
interface NpmMetaDataResponse {
6138
dist?: {
6239
tarball?: string;
6340
};
6441
}
6542

66-
const indexFileSchema = joi.object({
67-
indexFileVersion: joi.number().integer().max(1).required(),
68-
69-
metadata: joi.object({
70-
homepage: joi.string(),
71-
}),
72-
73-
index: joi
74-
.array()
75-
.required()
76-
.items(
77-
joi.object({
78-
name: joi.string().required(),
79-
snippetName: joi.string().required(),
80-
installSpec: joi.string(),
81-
version: joi.string().required(),
82-
description: joi.string().required().allow(''),
83-
license: joi.string().required(),
84-
readme: joi.string().required().allow(''),
85-
errorMatchers: joi.array().items(
86-
joi.object({
87-
message: joi.string().required(),
88-
matches: joi.array().required().items(joi.object().regex()),
89-
})
90-
),
91-
})
92-
),
43+
const regExpTag = Object.prototype.toString.call(/foo/);
44+
const errorMatcherSchema = z.object({
45+
message: z.string(),
46+
matches: z.array(
47+
z.custom<RegExp>((val) => Object.prototype.toString.call(val) === regExpTag)
48+
),
49+
});
50+
const indexDescriptionSchema = z.object({
51+
name: z.string(),
52+
snippetName: z.string(),
53+
installSpec: z.string().optional(),
54+
version: z.string(),
55+
description: z.string(),
56+
license: z.string(),
57+
readme: z.string(),
58+
errorMatchers: z.array(errorMatcherSchema).optional(),
9359
});
60+
const indexFileSchema = z.object({
61+
indexFileVersion: z.number().int().max(1),
62+
63+
metadata: z
64+
.object({
65+
homepage: z.string(),
66+
})
67+
.passthrough(),
68+
69+
index: z.array(indexDescriptionSchema),
70+
});
71+
72+
export type ErrorMatcher = z.infer<typeof errorMatcherSchema>;
73+
export type SnippetIndexFile = z.infer<typeof indexFileSchema> & {
74+
sourceURL: string;
75+
};
76+
export type SnippetDescription = z.infer<typeof indexDescriptionSchema>;
9477

9578
async function unpackBSON<T = any>(data: Buffer): Promise<T> {
9679
return bson.deserialize(await brotliDecompress(data)) as T;
@@ -361,9 +344,8 @@ export class SnippetManager implements ShellPlugin {
361344
`The specified index file ${url} could not be parsed: ${err.message}`
362345
);
363346
}
364-
const { error } = indexFileSchema.validate(data, {
365-
allowUnknown: true,
366-
});
347+
const { error, data: parsedData } =
348+
indexFileSchema.safeParse(data);
367349
if (error) {
368350
this.messageBus.emit('mongosh-snippets:fetch-index-error', {
369351
action: 'validate-fetched',
@@ -374,7 +356,7 @@ export class SnippetManager implements ShellPlugin {
374356
`The specified index file ${url} is not a valid index file: ${error.message}`
375357
);
376358
}
377-
return { ...data, sourceURL: url };
359+
return { ...parsedData, sourceURL: url };
378360
})
379361
);
380362
// If possible, write the result to disk for caching.

0 commit comments

Comments
 (0)