Skip to content

Commit

Permalink
Add screenshots field (#505)
Browse files Browse the repository at this point in the history
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"
        ]
      }
    }
  }
}
```
  • Loading branch information
Mrtenz authored Mar 20, 2024
1 parent 0d57dd9 commit 3fd9bf7
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 2 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"fast-deep-equal": "^3.1.3",
"image-size": "^1.1.1",
"jest": "^28.1.3",
"jest-it-up": "^2.0.2",
"prettier": "^2.7.1",
Expand Down
60 changes: 59 additions & 1 deletion scripts/verify-snaps.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { detectSnapLocation, fetchSnap } from '@metamask/snaps-controllers';
import type { SnapId } from '@metamask/snaps-sdk';
import { getLocalizedSnapManifest } from '@metamask/snaps-utils';
import { assertIsSemVerVersion } from '@metamask/utils';
import { assertIsSemVerVersion, getErrorMessage } from '@metamask/utils';
import deepEqual from 'fast-deep-equal';
import { imageSize as imageSizeSync } from 'image-size';
import { resolve } from 'path';
import semver from 'semver/preload';
import type { Infer } from 'superstruct';
import { promisify } from 'util';

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

const imageSize = promisify(imageSizeSync);

type VerifiedSnap = Infer<typeof VerifiedSnapStruct>;

/**
Expand Down Expand Up @@ -65,6 +70,51 @@ async function verifySnapVersion(
}
}

/**
* Get the size of an image.
*
* @param path - The path to the image.
* @param snapId - The snap ID.
*/
async function getImageSize(path: string, snapId: string) {
try {
return await imageSize(path);
} catch (error) {
throw new Error(
`Could not determine the size of screenshot "${path}" for "${snapId}": ${getErrorMessage(
error,
)}.`,
);
}
}

/**
* Verify that the screenshots for a snap exist and have the correct dimensions.
*
* @param snapId - The snap ID.
* @param screenshots - The screenshots.
* @throws If a screenshot does not exist or has the wrong dimensions.
*/
async function verifyScreenshots(snapId: string, screenshots: string[]) {
const basePath = resolve(__dirname, '..', 'src');

for (const screenshot of screenshots) {
const path = resolve(basePath, screenshot);
const size = await getImageSize(path, snapId);
if (!size?.width || !size?.height) {
throw new Error(
`Could not determine the size of screenshot "${screenshot}" for "${snapId}".`,
);
}

if (size.width !== 960 || size.height !== 540) {
throw new Error(
`Screenshot "${screenshot}" for "${snapId}" does not have the correct dimensions. Expected 960x540, got ${size.width}x${size.height}.`,
);
}
}
}

/**
* Verify a snap.
*
Expand All @@ -91,6 +141,14 @@ async function verifySnap(snap: VerifiedSnap) {
process.exitCode = 1;
});
}

const { screenshots } = snap.metadata;
if (screenshots) {
await verifyScreenshots(snap.id, screenshots).catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
}

/**
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
} from '@metamask/utils';
import type { Infer } from 'superstruct';
import {
pattern,
size,
object,
array,
record,
Expand Down Expand Up @@ -53,6 +55,11 @@ export const AdditionalSourceCodeStruct = object({
url: string(),
});

export const ImagePathStruct = pattern(
string(),
/\.\/images\/.*\/\d+\.(?:png|jpe?g)$/u,
);

export const VerifiedSnapStruct = object({
id: NpmIdStruct,
metadata: object({
Expand Down Expand Up @@ -80,6 +87,7 @@ export const VerifiedSnapStruct = object({
privacyPolicy: optional(string()),
termsOfUse: optional(string()),
additionalSourceCode: optional(array(AdditionalSourceCodeStruct)),
screenshots: optional(size(array(ImagePathStruct), 3, 3)),
}),
versions: record(VersionStruct, VerifiedSnapVersionStruct),
});
Expand Down
63 changes: 62 additions & 1 deletion src/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ describe('Snaps Registry', () => {
url: 'https://metamask.io/example/source-code3',
},
],
screenshots: [
'./images/example-snap/1.png',
'./images/example-snap/2.jpg',
'./images/example-snap/3.jpeg',
],
},
versions: {
['0.1.0' as SemVerVersion]: {
Expand Down Expand Up @@ -120,7 +125,7 @@ describe('Snaps Registry', () => {
expect(() => assert(registryDb, SnapsRegistryDatabaseStruct)).not.toThrow();
});

it('should throw when the metadata has an unexpected field', () => {
it('throws when the metadata has an unexpected field', () => {
const registryDb: SnapsRegistryDatabase = {
verifiedSnaps: {
'npm:example-snap': {
Expand All @@ -145,4 +150,60 @@ describe('Snaps Registry', () => {
'At path: verifiedSnaps.npm:example-snap.metadata.unexpected -- Expected a value of type `never`, but received: `"field"`',
);
});

it('throws when the screenshots are invalid', () => {
expect(() =>
assert(
{
verifiedSnaps: {
'npm:example-snap': {
id: 'npm:example-snap',
metadata: {
name: 'Example Snap',
screenshots: ['./images/example-snap/1.png'],
},
versions: {
['0.1.0' as SemVerVersion]: {
checksum: 'A83r5/ZIcKeKw3An13HBeV4CAofj7jGK5hOStmHY6A0=',
},
},
},
},
blockedSnaps: [],
},
SnapsRegistryDatabaseStruct,
),
).toThrow(
'At path: verifiedSnaps.npm:example-snap.metadata.screenshots -- Expected a array with a length of `3` but received one with a length of `1`',
);

expect(() =>
assert(
{
verifiedSnaps: {
'npm:example-snap': {
id: 'npm:example-snap',
metadata: {
name: 'Example Snap',
screenshots: [
'./images/example-snap/1.png',
'./images/example-snap/2.png',
'./images/example-snap/3.gif',
],
},
versions: {
['0.1.0' as SemVerVersion]: {
checksum: 'A83r5/ZIcKeKw3An13HBeV4CAofj7jGK5hOStmHY6A0=',
},
},
},
},
blockedSnaps: [],
},
SnapsRegistryDatabaseStruct,
),
).toThrow(
'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"',
);
});
});
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,7 @@ __metadata:
eslint-plugin-prettier: ^4.2.1
eslint-plugin-promise: ^6.1.1
fast-deep-equal: ^3.1.3
image-size: ^1.1.1
jest: ^28.1.3
jest-it-up: ^2.0.2
prettier: ^2.7.1
Expand Down Expand Up @@ -4065,6 +4066,17 @@ __metadata:
languageName: node
linkType: hard

"image-size@npm:^1.1.1":
version: 1.1.1
resolution: "image-size@npm:1.1.1"
dependencies:
queue: 6.0.2
bin:
image-size: bin/image-size.js
checksum: 23b3a515dded89e7f967d52b885b430d6a5a903da954fce703130bfb6069d738d80e6588efd29acfaf5b6933424a56535aa7bf06867e4ebd0250c2ee51f19a4a
languageName: node
linkType: hard

"immer@npm:^9.0.6":
version: 9.0.21
resolution: "immer@npm:9.0.21"
Expand Down Expand Up @@ -5913,6 +5925,15 @@ __metadata:
languageName: node
linkType: hard

"queue@npm:6.0.2":
version: 6.0.2
resolution: "queue@npm:6.0.2"
dependencies:
inherits: ~2.0.3
checksum: ebc23639248e4fe40a789f713c20548e513e053b3dc4924b6cb0ad741e3f264dcff948225c8737834dd4f9ec286dbc06a1a7c13858ea382d9379f4303bcc0916
languageName: node
linkType: hard

"react-is@npm:^18.0.0":
version: 18.2.0
resolution: "react-is@npm:18.2.0"
Expand Down

0 comments on commit 3fd9bf7

Please sign in to comment.