Skip to content

Commit 3fd9bf7

Browse files
authored
Add screenshots field (#505)
This adds support for a screenshots field in the registry. Snaps may have three screenshots, with a size of 960x540. Screenshots should be placed in `src/images/<snapId>`, i.e., `src/images/@organisation/snap-name`, and should be named `1.png`, `2.png`, `3.png` (or `.jpe?g`). They can then be added to the registry by adding `screenshots` to the metadata, with the path to each file as array items. ```json { "verifiedSnaps": { "npm:@organisation/snap-name": { "metadata": { "screenshots": [ "./images/@organisation/snap-name/1.png", "./images/@organisation/snap-name/2.png", "./images/@organisation/snap-name/3.png" ] } } } } ```
1 parent 0d57dd9 commit 3fd9bf7

File tree

5 files changed

+151
-2
lines changed

5 files changed

+151
-2
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"eslint-plugin-prettier": "^4.2.1",
6060
"eslint-plugin-promise": "^6.1.1",
6161
"fast-deep-equal": "^3.1.3",
62+
"image-size": "^1.1.1",
6263
"jest": "^28.1.3",
6364
"jest-it-up": "^2.0.2",
6465
"prettier": "^2.7.1",

scripts/verify-snaps.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { detectSnapLocation, fetchSnap } from '@metamask/snaps-controllers';
22
import type { SnapId } from '@metamask/snaps-sdk';
33
import { getLocalizedSnapManifest } from '@metamask/snaps-utils';
4-
import { assertIsSemVerVersion } from '@metamask/utils';
4+
import { assertIsSemVerVersion, getErrorMessage } from '@metamask/utils';
55
import deepEqual from 'fast-deep-equal';
6+
import { imageSize as imageSizeSync } from 'image-size';
7+
import { resolve } from 'path';
68
import semver from 'semver/preload';
79
import type { Infer } from 'superstruct';
10+
import { promisify } from 'util';
811

912
import type { VerifiedSnapStruct } from '../src';
1013
import registry from '../src/registry.json';
1114

15+
const imageSize = promisify(imageSizeSync);
16+
1217
type VerifiedSnap = Infer<typeof VerifiedSnapStruct>;
1318

1419
/**
@@ -65,6 +70,51 @@ async function verifySnapVersion(
6570
}
6671
}
6772

73+
/**
74+
* Get the size of an image.
75+
*
76+
* @param path - The path to the image.
77+
* @param snapId - The snap ID.
78+
*/
79+
async function getImageSize(path: string, snapId: string) {
80+
try {
81+
return await imageSize(path);
82+
} catch (error) {
83+
throw new Error(
84+
`Could not determine the size of screenshot "${path}" for "${snapId}": ${getErrorMessage(
85+
error,
86+
)}.`,
87+
);
88+
}
89+
}
90+
91+
/**
92+
* Verify that the screenshots for a snap exist and have the correct dimensions.
93+
*
94+
* @param snapId - The snap ID.
95+
* @param screenshots - The screenshots.
96+
* @throws If a screenshot does not exist or has the wrong dimensions.
97+
*/
98+
async function verifyScreenshots(snapId: string, screenshots: string[]) {
99+
const basePath = resolve(__dirname, '..', 'src');
100+
101+
for (const screenshot of screenshots) {
102+
const path = resolve(basePath, screenshot);
103+
const size = await getImageSize(path, snapId);
104+
if (!size?.width || !size?.height) {
105+
throw new Error(
106+
`Could not determine the size of screenshot "${screenshot}" for "${snapId}".`,
107+
);
108+
}
109+
110+
if (size.width !== 960 || size.height !== 540) {
111+
throw new Error(
112+
`Screenshot "${screenshot}" for "${snapId}" does not have the correct dimensions. Expected 960x540, got ${size.width}x${size.height}.`,
113+
);
114+
}
115+
}
116+
}
117+
68118
/**
69119
* Verify a snap.
70120
*
@@ -91,6 +141,14 @@ async function verifySnap(snap: VerifiedSnap) {
91141
process.exitCode = 1;
92142
});
93143
}
144+
145+
const { screenshots } = snap.metadata;
146+
if (screenshots) {
147+
await verifyScreenshots(snap.id, screenshots).catch((error) => {
148+
console.error(error.message);
149+
process.exitCode = 1;
150+
});
151+
}
94152
}
95153

96154
/**

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
} from '@metamask/utils';
66
import type { Infer } from 'superstruct';
77
import {
8+
pattern,
9+
size,
810
object,
911
array,
1012
record,
@@ -53,6 +55,11 @@ export const AdditionalSourceCodeStruct = object({
5355
url: string(),
5456
});
5557

58+
export const ImagePathStruct = pattern(
59+
string(),
60+
/\.\/images\/.*\/\d+\.(?:png|jpe?g)$/u,
61+
);
62+
5663
export const VerifiedSnapStruct = object({
5764
id: NpmIdStruct,
5865
metadata: object({
@@ -80,6 +87,7 @@ export const VerifiedSnapStruct = object({
8087
privacyPolicy: optional(string()),
8188
termsOfUse: optional(string()),
8289
additionalSourceCode: optional(array(AdditionalSourceCodeStruct)),
90+
screenshots: optional(size(array(ImagePathStruct), 3, 3)),
8391
}),
8492
versions: record(VersionStruct, VerifiedSnapVersionStruct),
8593
});

src/registry.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ describe('Snaps Registry', () => {
5858
url: 'https://metamask.io/example/source-code3',
5959
},
6060
],
61+
screenshots: [
62+
'./images/example-snap/1.png',
63+
'./images/example-snap/2.jpg',
64+
'./images/example-snap/3.jpeg',
65+
],
6166
},
6267
versions: {
6368
['0.1.0' as SemVerVersion]: {
@@ -120,7 +125,7 @@ describe('Snaps Registry', () => {
120125
expect(() => assert(registryDb, SnapsRegistryDatabaseStruct)).not.toThrow();
121126
});
122127

123-
it('should throw when the metadata has an unexpected field', () => {
128+
it('throws when the metadata has an unexpected field', () => {
124129
const registryDb: SnapsRegistryDatabase = {
125130
verifiedSnaps: {
126131
'npm:example-snap': {
@@ -145,4 +150,60 @@ describe('Snaps Registry', () => {
145150
'At path: verifiedSnaps.npm:example-snap.metadata.unexpected -- Expected a value of type `never`, but received: `"field"`',
146151
);
147152
});
153+
154+
it('throws when the screenshots are invalid', () => {
155+
expect(() =>
156+
assert(
157+
{
158+
verifiedSnaps: {
159+
'npm:example-snap': {
160+
id: 'npm:example-snap',
161+
metadata: {
162+
name: 'Example Snap',
163+
screenshots: ['./images/example-snap/1.png'],
164+
},
165+
versions: {
166+
['0.1.0' as SemVerVersion]: {
167+
checksum: 'A83r5/ZIcKeKw3An13HBeV4CAofj7jGK5hOStmHY6A0=',
168+
},
169+
},
170+
},
171+
},
172+
blockedSnaps: [],
173+
},
174+
SnapsRegistryDatabaseStruct,
175+
),
176+
).toThrow(
177+
'At path: verifiedSnaps.npm:example-snap.metadata.screenshots -- Expected a array with a length of `3` but received one with a length of `1`',
178+
);
179+
180+
expect(() =>
181+
assert(
182+
{
183+
verifiedSnaps: {
184+
'npm:example-snap': {
185+
id: 'npm:example-snap',
186+
metadata: {
187+
name: 'Example Snap',
188+
screenshots: [
189+
'./images/example-snap/1.png',
190+
'./images/example-snap/2.png',
191+
'./images/example-snap/3.gif',
192+
],
193+
},
194+
versions: {
195+
['0.1.0' as SemVerVersion]: {
196+
checksum: 'A83r5/ZIcKeKw3An13HBeV4CAofj7jGK5hOStmHY6A0=',
197+
},
198+
},
199+
},
200+
},
201+
blockedSnaps: [],
202+
},
203+
SnapsRegistryDatabaseStruct,
204+
),
205+
).toThrow(
206+
'At path: verifiedSnaps.npm:example-snap.metadata.screenshots.2 -- Expected a string matching `/\\.\\/images\\/.*\\/\\d+\\.(?:png|jpe?g)$/` but received "./images/example-snap/3.gif"',
207+
);
208+
});
148209
});

yarn.lock

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,7 @@ __metadata:
12361236
eslint-plugin-prettier: ^4.2.1
12371237
eslint-plugin-promise: ^6.1.1
12381238
fast-deep-equal: ^3.1.3
1239+
image-size: ^1.1.1
12391240
jest: ^28.1.3
12401241
jest-it-up: ^2.0.2
12411242
prettier: ^2.7.1
@@ -4065,6 +4066,17 @@ __metadata:
40654066
languageName: node
40664067
linkType: hard
40674068

4069+
"image-size@npm:^1.1.1":
4070+
version: 1.1.1
4071+
resolution: "image-size@npm:1.1.1"
4072+
dependencies:
4073+
queue: 6.0.2
4074+
bin:
4075+
image-size: bin/image-size.js
4076+
checksum: 23b3a515dded89e7f967d52b885b430d6a5a903da954fce703130bfb6069d738d80e6588efd29acfaf5b6933424a56535aa7bf06867e4ebd0250c2ee51f19a4a
4077+
languageName: node
4078+
linkType: hard
4079+
40684080
"immer@npm:^9.0.6":
40694081
version: 9.0.21
40704082
resolution: "immer@npm:9.0.21"
@@ -5913,6 +5925,15 @@ __metadata:
59135925
languageName: node
59145926
linkType: hard
59155927

5928+
"queue@npm:6.0.2":
5929+
version: 6.0.2
5930+
resolution: "queue@npm:6.0.2"
5931+
dependencies:
5932+
inherits: ~2.0.3
5933+
checksum: ebc23639248e4fe40a789f713c20548e513e053b3dc4924b6cb0ad741e3f264dcff948225c8737834dd4f9ec286dbc06a1a7c13858ea382d9379f4303bcc0916
5934+
languageName: node
5935+
linkType: hard
5936+
59165937
"react-is@npm:^18.0.0":
59175938
version: 18.2.0
59185939
resolution: "react-is@npm:18.2.0"

0 commit comments

Comments
 (0)