diff --git a/.prettierignore b/.prettierignore index 873881cef5..c4d49a767a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -78,5 +78,3 @@ packages/examples/examples/webpack/index.html packages/snaps-execution-environments/lavamoat/**/*.json packages/snaps-jest/public packages/snaps-simulator/vendor - -packages/examples/packages/*/snap.manifest.json diff --git a/packages/examples/packages/bip32/snap.manifest.json b/packages/examples/packages/bip32/snap.manifest.json index 4a817cdee4..4b9136702f 100644 --- a/packages/examples/packages/bip32/snap.manifest.json +++ b/packages/examples/packages/bip32/snap.manifest.json @@ -24,37 +24,21 @@ "snap_dialog": {}, "snap_getBip32Entropy": [ { - "path": [ - "m", - "44'", - "0'" - ], + "path": ["m", "44'", "0'"], "curve": "secp256k1" }, { - "path": [ - "m", - "44'", - "0'" - ], + "path": ["m", "44'", "0'"], "curve": "ed25519" }, { - "path": [ - "m", - "44'", - "0'" - ], + "path": ["m", "44'", "0'"], "curve": "ed25519Bip32" } ], "snap_getBip32PublicKey": [ { - "path": [ - "m", - "44'", - "0'" - ], + "path": ["m", "44'", "0'"], "curve": "secp256k1" } ] diff --git a/packages/examples/packages/bip44/snap.manifest.json b/packages/examples/packages/bip44/snap.manifest.json index 6b04160c4c..2c39cfc189 100644 --- a/packages/examples/packages/bip44/snap.manifest.json +++ b/packages/examples/packages/bip44/snap.manifest.json @@ -19,9 +19,7 @@ "initialPermissions": { "endowment:rpc": { "dapps": true, - "allowedOrigins": [ - "npm:@metamask/json-rpc-example-snap" - ] + "allowedOrigins": ["npm:@metamask/json-rpc-example-snap"] }, "snap_dialog": {}, "snap_getBip44Entropy": [ diff --git a/packages/examples/packages/get-file/snap.manifest.json b/packages/examples/packages/get-file/snap.manifest.json index 3c180eb8cc..771cf66e98 100644 --- a/packages/examples/packages/get-file/snap.manifest.json +++ b/packages/examples/packages/get-file/snap.manifest.json @@ -15,9 +15,7 @@ "registry": "https://registry.npmjs.org" } }, - "files": [ - "./files/foo.json" - ] + "files": ["./files/foo.json"] }, "initialPermissions": { "endowment:rpc": { diff --git a/packages/examples/packages/localization/snap.manifest.json b/packages/examples/packages/localization/snap.manifest.json index 06de6009f5..66585bec34 100644 --- a/packages/examples/packages/localization/snap.manifest.json +++ b/packages/examples/packages/localization/snap.manifest.json @@ -15,11 +15,7 @@ "registry": "https://registry.npmjs.org/" } }, - "locales": [ - "locales/da.json", - "locales/en.json", - "locales/nl.json" - ] + "locales": ["locales/da.json", "locales/en.json", "locales/nl.json"] }, "initialPermissions": { "endowment:rpc": { diff --git a/packages/examples/packages/name-lookup/snap.manifest.json b/packages/examples/packages/name-lookup/snap.manifest.json index a9b37e9c18..a32e40a940 100644 --- a/packages/examples/packages/name-lookup/snap.manifest.json +++ b/packages/examples/packages/name-lookup/snap.manifest.json @@ -18,9 +18,7 @@ }, "initialPermissions": { "endowment:name-lookup": { - "chains": [ - "eip155:1" - ] + "chains": ["eip155:1"] } }, "manifestVersion": "0.1" diff --git a/packages/snaps-cli/src/commands/manifest/implementation.test.ts b/packages/snaps-cli/src/commands/manifest/implementation.test.ts index abaeffbd69..792e100dc1 100644 --- a/packages/snaps-cli/src/commands/manifest/implementation.test.ts +++ b/packages/snaps-cli/src/commands/manifest/implementation.test.ts @@ -118,4 +118,63 @@ describe('manifest', () => { expect.stringMatching('The snap manifest file has been updated.'), ); }); + + it('formats a snap manifest with Prettier', async () => { + const error = jest.spyOn(console, 'error').mockImplementation(); + const log = jest.spyOn(console, 'log').mockImplementation(); + + await fs.writeFile( + '/snap/snap.manifest.json', + JSON.stringify( + getSnapManifest({ + shasum: 'G/W5b2JZVv+epgNX9pkN63X6Lye9EJVJ4NLSgAw/afd=', + initialPermissions: { + 'endowment:name-lookup': { + chains: ['eip155:1', 'eip155:2', 'eip155:3'], + }, + }, + }), + ), + ); + + const spinner = ora(); + const result = await manifest('/snap/snap.manifest.json', true, spinner); + expect(result).toBe(true); + + expect(error).not.toHaveBeenCalled(); + expect(log).toHaveBeenCalledWith( + expect.stringMatching('The snap manifest file has been updated.'), + ); + + expect(await fs.readFile('/snap/snap.manifest.json', 'utf8')) + .toMatchInlineSnapshot(` + "{ + "version": "1.0.0", + "description": "The test example snap!", + "proposedName": "@metamask/example-snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/example-snap.git" + }, + "source": { + "shasum": "d4W7f1lzpVGMj8jjCn1lYhhHmKc/9TSk5QLH5ldKQoI=", + "location": { + "npm": { + "filePath": "dist/bundle.js", + "packageName": "@metamask/example-snap", + "registry": "https://registry.npmjs.org", + "iconPath": "images/icon.svg" + } + } + }, + "initialPermissions": { + "endowment:name-lookup": { + "chains": ["eip155:1", "eip155:2", "eip155:3"] + } + }, + "manifestVersion": "0.1" + } + " + `); + }); }); diff --git a/packages/snaps-cli/src/commands/manifest/implementation.ts b/packages/snaps-cli/src/commands/manifest/implementation.ts index 90ab9728c3..33a4cc6c20 100644 --- a/packages/snaps-cli/src/commands/manifest/implementation.ts +++ b/packages/snaps-cli/src/commands/manifest/implementation.ts @@ -1,4 +1,5 @@ import { checkManifest, indent } from '@metamask/snaps-utils/node'; +import { writeManifest } from '@metamask/snaps-webpack-plugin'; import { assert } from '@metamask/utils'; import { red, yellow, green } from 'chalk'; import type { Ora } from 'ora'; @@ -24,6 +25,7 @@ export async function manifest( ): Promise { const { reports, updated } = await checkManifest(dirname(path), { updateAndWriteManifest: write, + writeFileFn: writeManifest, }); const errors = []; diff --git a/packages/snaps-webpack-plugin/package.json b/packages/snaps-webpack-plugin/package.json index b982d2e47b..c8dd04cd7e 100644 --- a/packages/snaps-webpack-plugin/package.json +++ b/packages/snaps-webpack-plugin/package.json @@ -60,6 +60,7 @@ "@metamask/snaps-sdk": "workspace:^", "@metamask/snaps-utils": "workspace:^", "@metamask/utils": "^9.2.1", + "prettier": "^2.8.8", "webpack-sources": "^3.2.3" }, "devDependencies": { @@ -90,7 +91,6 @@ "jest-it-up": "^2.0.0", "jest-silent-reporter": "^0.6.0", "memfs": "^3.4.13", - "prettier": "^2.8.8", "prettier-plugin-packagejson": "^2.5.2", "typescript": "~5.3.3", "webpack": "^5.88.0" diff --git a/packages/snaps-webpack-plugin/src/__fixtures__/.prettierrc.js b/packages/snaps-webpack-plugin/src/__fixtures__/.prettierrc.js new file mode 100644 index 0000000000..6dd857a707 --- /dev/null +++ b/packages/snaps-webpack-plugin/src/__fixtures__/.prettierrc.js @@ -0,0 +1,4 @@ +// Prettier config used for testing. +module.exports = { + tabWidth: 4, +}; diff --git a/packages/snaps-webpack-plugin/src/index.ts b/packages/snaps-webpack-plugin/src/index.ts index 77e5e41ef6..f32e602a5a 100644 --- a/packages/snaps-webpack-plugin/src/index.ts +++ b/packages/snaps-webpack-plugin/src/index.ts @@ -1,2 +1,3 @@ +export { writeManifest } from './manifest'; export { default } from './plugin'; export type { Options } from './plugin'; diff --git a/packages/snaps-webpack-plugin/src/manifest.test.ts b/packages/snaps-webpack-plugin/src/manifest.test.ts new file mode 100644 index 0000000000..163fb71551 --- /dev/null +++ b/packages/snaps-webpack-plugin/src/manifest.test.ts @@ -0,0 +1,121 @@ +import { getSnapManifest } from '@metamask/snaps-utils/test-utils'; +import { promises as fs } from 'fs'; +import { resolve } from 'path'; + +import { writeManifest } from './manifest'; + +jest.mock('fs', () => ({ + ...jest.requireActual('fs'), + promises: { + writeFile: jest.fn(), + }, +})); + +describe('writeManifest', () => { + it('formats the manifest with Prettier before writing to disk', async () => { + const manifest = JSON.stringify(getSnapManifest()); + await writeManifest('test.json', manifest); + + expect(jest.mocked(fs.writeFile).mock.calls[0][1]).toMatchInlineSnapshot(` + "{ + "version": "1.0.0", + "description": "The test example snap!", + "proposedName": "@metamask/example-snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/example-snap.git" + }, + "source": { + "shasum": "rNyfINgNh161cBmUop+F7xlE+GSEDZH53Y/HDpGLGGg=", + "location": { + "npm": { + "filePath": "dist/bundle.js", + "packageName": "@metamask/example-snap", + "registry": "https://registry.npmjs.org", + "iconPath": "images/icon.svg" + } + } + }, + "initialPermissions": { + "snap_dialog": {}, + "endowment:rpc": { "snaps": true, "dapps": false } + }, + "manifestVersion": "0.1" + } + " + `); + }); + + it('uses a custom Prettier config if found', async () => { + const manifest = JSON.stringify(getSnapManifest()); + await writeManifest( + resolve(__dirname, '__fixtures__', 'foo.json'), + manifest, + ); + + expect(jest.mocked(fs.writeFile).mock.calls[0][1]).toMatchInlineSnapshot(` + "{ + "version": "1.0.0", + "description": "The test example snap!", + "proposedName": "@metamask/example-snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/example-snap.git" + }, + "source": { + "shasum": "rNyfINgNh161cBmUop+F7xlE+GSEDZH53Y/HDpGLGGg=", + "location": { + "npm": { + "filePath": "dist/bundle.js", + "packageName": "@metamask/example-snap", + "registry": "https://registry.npmjs.org", + "iconPath": "images/icon.svg" + } + } + }, + "initialPermissions": { + "snap_dialog": {}, + "endowment:rpc": { "snaps": true, "dapps": false } + }, + "manifestVersion": "0.1" + } + " + `); + }); + + it('accepts a custom write function', async () => { + const fn = jest.fn(); + const manifest = JSON.stringify(getSnapManifest()); + await writeManifest('test.json', manifest, fn); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn.mock.calls[0][1]).toMatchInlineSnapshot(` + "{ + "version": "1.0.0", + "description": "The test example snap!", + "proposedName": "@metamask/example-snap", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/example-snap.git" + }, + "source": { + "shasum": "rNyfINgNh161cBmUop+F7xlE+GSEDZH53Y/HDpGLGGg=", + "location": { + "npm": { + "filePath": "dist/bundle.js", + "packageName": "@metamask/example-snap", + "registry": "https://registry.npmjs.org", + "iconPath": "images/icon.svg" + } + } + }, + "initialPermissions": { + "snap_dialog": {}, + "endowment:rpc": { "snaps": true, "dapps": false } + }, + "manifestVersion": "0.1" + } + " + `); + }); +}); diff --git a/packages/snaps-webpack-plugin/src/manifest.ts b/packages/snaps-webpack-plugin/src/manifest.ts new file mode 100644 index 0000000000..041828404b --- /dev/null +++ b/packages/snaps-webpack-plugin/src/manifest.ts @@ -0,0 +1,32 @@ +import type { WriteFileFunction } from '@metamask/snaps-utils/node'; +import { promises as fs } from 'fs'; +import { format, resolveConfig } from 'prettier'; + +/** + * Format the manifest data with Prettier and write it to disk. + * + * It uses the Prettier configuration found in the project directory (if any), + * or the default Prettier configuration if none is found. + * + * @param path - The path to write the manifest to. + * @param data - The manifest data. + * @param writeFileFn - The function to use to write the manifest. + * @returns A promise that resolves when the manifest has been written. + */ +export async function writeManifest( + path: string, + data: string, + writeFileFn: WriteFileFunction = fs.writeFile, +) { + const config = await resolveConfig(path, { + editorconfig: true, + }); + + const formattedManifest = format(data, { + ...config, + parser: 'json', + filepath: path, + }); + + await writeFileFn(path, formattedManifest); +} diff --git a/packages/snaps-webpack-plugin/src/plugin.test.ts b/packages/snaps-webpack-plugin/src/plugin.test.ts index d934c74900..0b80589a97 100644 --- a/packages/snaps-webpack-plugin/src/plugin.test.ts +++ b/packages/snaps-webpack-plugin/src/plugin.test.ts @@ -15,6 +15,7 @@ import * as pathUtils from 'path'; import type { Stats, Configuration } from 'webpack'; import webpack from 'webpack'; +import { writeManifest } from './manifest'; import type { Options } from './plugin'; import SnapsWebpackPlugin from './plugin'; @@ -24,6 +25,10 @@ jest.mock('@metamask/snaps-utils/node', () => ({ checkManifest: jest.fn(), })); +jest.mock('./manifest', () => ({ + writeManifest: jest.fn(), +})); + type BundleOptions = { code?: string; options?: Options; @@ -215,6 +220,17 @@ describe('SnapsWebpackPlugin', () => { sourceCode: expect.any(String), writeFileFn: expect.any(Function), }); + + const writeFileFn = mock.mock.calls[0][1]?.writeFileFn; + expect(writeFileFn).toBeDefined(); + await writeFileFn?.('/snap.manifest.json', 'foo'); + + expect(writeManifest).toHaveBeenCalledTimes(1); + expect(writeManifest).toHaveBeenCalledWith( + '/snap.manifest.json', + 'foo', + expect.any(Function), + ); }); it('does not fix the manifest if configured', async () => { diff --git a/packages/snaps-webpack-plugin/src/plugin.ts b/packages/snaps-webpack-plugin/src/plugin.ts index 6fadf5c679..89cf240b12 100644 --- a/packages/snaps-webpack-plugin/src/plugin.ts +++ b/packages/snaps-webpack-plugin/src/plugin.ts @@ -13,6 +13,8 @@ import type { Compiler } from 'webpack'; import { Compilation, WebpackError } from 'webpack'; import { RawSource, SourceMapSource } from 'webpack-sources'; +import { writeManifest } from './manifest'; + const PLUGIN_NAME = 'SnapsWebpackPlugin'; type PluginOptions = { @@ -145,11 +147,13 @@ export default class SnapsWebpackPlugin { { updateAndWriteManifest: this.options.writeManifest, sourceCode: bundleContent, - writeFileFn: promisify( - compiler.outputFileSystem.writeFile.bind( - compiler.outputFileSystem, - ), - ), + writeFileFn: async (path, data) => { + return writeManifest( + path, + data, + promisify(compiler.outputFileSystem.writeFile), + ); + }, }, );