diff --git a/AGENTS.md b/AGENTS.md index e9941d065..71ded5f3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -225,8 +225,9 @@ This repo is a Yarn 4 monorepo of MetaMask Snaps. The two products live in `pack Standard commands are documented above (see "Running tests", "Linting and formatting", "Building packages"). Non-obvious caveats for running things here: -- **Build before testing.** A Snap's Jest suite (`@metamask/snaps-jest`) expects the built bundle. CI always runs `yarn workspace build` before `yarn workspace run test`. If tests behave unexpectedly, run `yarn build` (or the per-package build) first. -- **`yarn lint` deletes `dist/`.** `lint:eslint` runs `build:only-clean` (`rimraf -g 'packages/*/dist'`) before linting. After running `yarn lint`, re-run `yarn build` before serving a snap or running snap tests. +- **Install builds libraries.** `yarn` / `yarn allow-scripts` runs `yarn build:libs` (`ts-bridge` packages only). Snap bundles are not built on install; run `yarn build` or `yarn build:snaps` when you need `dist/bundle.js`, `installSnap` tests, or `serve`. Solana unit tests use `snaps-jest` and will lazy-build the Snap via `jest.globalSetup` if the bundle is missing. +- **Unit tests vs integration tests.** Most Snap unit Jest configs use the Node environment and do not require a Snap bundle. Tests that call `installSnap` live under `integration-test/` or `*.integration.test.*` and need a prior snap build (`yarn build` / `yarn build:snaps`). +- **`yarn lint` cleans then restores library `dist/`.** `lint:eslint` still deletes `packages/*/dist` before eslint, then runs `yarn build:libs`. Snap bundles still need `yarn build` / `yarn build:snaps` afterward. - **Running a snap:** `yarn workspace run serve` serves the pre-built bundle at `http://localhost:8080` (`/snap.manifest.json` and `/dist/bundle.js`); `yarn workspace run start` (`mm-snap watch`) rebuilds on change. Both snaps use port 8080, so only run one at a time. -- **No headless end-to-end.** Fully exercising a snap normally requires the MetaMask extension in a browser, which isn't available headless. Use the `snaps-jest` test suites (they install the snap and invoke its JSON-RPC methods, e.g. sample-snap's `hello`) to exercise core functionality without a browser. -- **`.env` is optional** for `bitcoin-wallet-snap`; `snap.config.ts` reads it via dotenv but all values have sane defaults (see `.env.example`), so the snap builds and serves without one. +- **No headless end-to-end.** Fully exercising a snap normally requires the MetaMask extension in a browser, which isn't available headless. Use the `snaps-jest` integration suites (they install the snap and invoke its JSON-RPC methods, e.g. sample-snap's `hello`) to exercise core functionality without a browser. +- **Configure `.env` before building or testing wallet snaps.** Copy each package's `.env.example` / `.env.sample` and set values locally; `snap.config.ts` loads them via dotenv but does not provide runtime fallbacks. diff --git a/docs/README.md b/docs/README.md index f177787c0..842a7796b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,7 +19,6 @@ Hi! Welcome to the contributor documentation for the `internal-snaps` monorepo. - [Testing changes to packages in other projects](./processes/testing-changes-in-other-projects.md) - [Building packages](./processes/building.md) - [Adding new packages to the monorepo](./processes/adding-new-packages.md) -- [Migrating external snaps to the monorepo](./processes/snap-migration-process-guide.md) ## Code guidelines diff --git a/docs/getting-started/setting-up-your-environment.md b/docs/getting-started/setting-up-your-environment.md index 28fd99d18..b66574f9d 100644 --- a/docs/getting-started/setting-up-your-environment.md +++ b/docs/getting-started/setting-up-your-environment.md @@ -4,4 +4,13 @@ - If you are using [NVM](https://github.com/creationix/nvm#installation) (recommended), running `nvm install` will install the latest version, and running `nvm use` will automatically choose the right Node version for you. 2. Run `corepack enable` to install [Yarn](https://yarnpkg.com) via [Corepack](https://github.com/nodejs/corepack?tab=readme-ov-file#how-to-install). - If you have Yarn installed globally via Homebrew or NPM, you'll need to uninstall it before running this command. -3. Run `yarn install` to install dependencies and run any required post-install scripts. +3. Run `yarn install` to install dependencies and run post-install hooks. + - After install, library packages are built automatically via `yarn build:libs` (chained from `allow-scripts`). + - Snap bundles are **not** built during install. Run `yarn build` / `yarn build:snaps` when you need `dist/bundle.js`, `serve`, or `installSnap` integration tests. +4. Copy each wallet snap's `.env.example` / `.env.sample` to `.env` and configure it before building or testing snaps. +5. Verify the tree: + +```bash +yarn typecheck +yarn test +``` diff --git a/docs/processes/building.md b/docs/processes/building.md index 6618b6fe1..54b77ba13 100644 --- a/docs/processes/building.md +++ b/docs/processes/building.md @@ -2,5 +2,7 @@ Built files show up in the `dist/` directory in each package. These are the files which will ultimately be published to NPM. -- Run `yarn build` to build all packages in the monorepo. +- Run `yarn build` to build all packages in the monorepo (libraries and snaps, topological). +- Run `yarn build:libs` to build only non-snap (library) packages. This also runs automatically after `yarn install`. +- Run `yarn build:snaps` to build only Snap packages (`mm-snap`). - Run `yarn workspace run build` to build a single package. diff --git a/docs/processes/testing.md b/docs/processes/testing.md index 0454c009a..0e1d49609 100644 --- a/docs/processes/testing.md +++ b/docs/processes/testing.md @@ -4,12 +4,15 @@ Please follow the [MetaMask unit testing guidelines](https://github.com/MetaMask/contributor-docs/blob/main/docs/testing/unit-testing.md) when writing tests. -If you need to customize the behavior of Jest for a package, see `jest.config.js` within that package. +If you need to customize the behavior of Jest for a package, see `jest.config.js` / `jest.config.mjs` within that package. -- Run `yarn workspace run test` to run all tests for a package. +- Run `yarn workspace run test` to run **unit** tests for a package. - Run `yarn workspace run jest --no-coverage ` to run a test file within the context of a package. -- Run `yarn test` to run tests for all packages. +- Run `yarn test` to run unit tests for packages in the monorepo. +- For Snaps that define them, run `yarn workspace run test:integration` for `installSnap` / SES suites. Those require a prior Snap build (`yarn build` or `yarn build:snaps`). > **Note** > > `workspaceName` in these commands is the `name` field within a package's `package.json`, e.g., `@metamask/bitcoin-wallet-snap`. +> +> Snap unit configs use the Node environment (or, for Solana, `snaps-jest` with a lazy build via `jest.globalSetup`). Do not put `installSnap` tests in the default unit suite — use `integration-test/` or `*.integration.test.*` instead. diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 5b13d4f15..be72dc796 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -1181,7 +1181,7 @@ "count": 2 } }, - "packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/render.test.tsx": { + "packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/render.integration.test.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 }, @@ -1204,7 +1204,7 @@ "count": 2 } }, - "packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/render.test.tsx": { + "packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/render.integration.test.tsx": { "@typescript-eslint/no-explicit-any": { "count": 1 }, @@ -1227,7 +1227,7 @@ "count": 3 } }, - "packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx": { + "packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/render.integration.test.tsx": { "@typescript-eslint/explicit-function-return-type": { "count": 2 }, @@ -1258,9 +1258,6 @@ }, "@typescript-eslint/no-shadow": { "count": 1 - }, - "jest/unbound-method": { - "count": 2 } }, "packages/solana-wallet-snap/src/index.ts": { diff --git a/jest.config.packages.js b/jest.config.packages.js index a7c6bc1e1..eb266ad33 100644 --- a/jest.config.packages.js +++ b/jest.config.packages.js @@ -78,7 +78,8 @@ module.exports = { // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // Here we ensure that Jest resolves `@metamask/*` imports to the uncompiled source code for packages that live in this repo. - // NOTE: This must be synchronized with the `paths` option in `tsconfig.base.json`. + // NOTE: This must be synchronized with the `paths` option in `tsconfig.packages.json` + // (`@metamask/*` → `packages/*/src` from the monorepo root). moduleNameMapper: { '^@metamask/json-rpc-engine/v2$': [ '/../json-rpc-engine/src/v2/index.ts', diff --git a/jest.config.snaps.unit.cjs b/jest.config.snaps.unit.cjs new file mode 100644 index 000000000..72492161f --- /dev/null +++ b/jest.config.snaps.unit.cjs @@ -0,0 +1,40 @@ +const path = require('path'); + +/** + * Shared Jest config for Snap unit tests. + * + * Uses the Node environment (not @metamask/snaps-jest) so suites run without + * a pre-built dist/bundle.js. SES / installSnap coverage belongs in each + * package's integration config instead. + * + * Workspace @metamask/* mapping must stay synchronized with + * tsconfig.packages.json paths (./packages//src). + */ +module.exports = { + testEnvironment: 'node', + preset: 'ts-jest', + transform: { + '^.+\\.(t|j)sx?$': 'ts-jest', + }, + collectCoverage: true, + coverageDirectory: 'coverage', + coveragePathIgnorePatterns: ['.*/index\\.ts'], + coverageProvider: 'babel', + coverageReporters: ['text', 'html', 'json-summary', 'lcov'], + resetMocks: true, + testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'], + testPathIgnorePatterns: [ + '/node_modules/', + '/integration-test/', + '\\.integration\\.test\\.[tj]sx?$', + ], + moduleNameMapper: { + '^@metamask/utils/node$': require.resolve('@metamask/utils/node'), + // Only rewrite bare workspace package names. Subpath imports such as + // `@metamask/snaps-controllers/node` must fall through to node_modules. + '^@metamask/([^/]+)$': [ + path.join(__dirname, 'packages/$1/src'), + path.join(__dirname, 'node_modules/@metamask/$1'), + ], + }, +}; diff --git a/package.json b/package.json index 9cd08b4af..89529c815 100644 --- a/package.json +++ b/package.json @@ -8,15 +8,17 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "workspaces": [ - "examples/*", "packages/*" ], "files": [], "scripts": { + "allow-scripts": "yarn exec allow-scripts && yarn build:libs", "build": "yarn workspaces foreach --all --no-private --topological-dev --parallel --interlaced --verbose run build", "build:clean": "yarn build:only-clean && yarn build", "build:docs": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run build:docs", + "build:libs": "node ./scripts/build-workspace-kind.mjs", "build:only-clean": "rimraf -g 'packages/*/dist'", + "build:snaps": "node ./scripts/build-workspace-kind.mjs --snaps", "build:types": "tsc --build tsconfig.build.json --verbose", "changelog:update": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run changelog:update", "changelog:validate": "yarn workspaces foreach --all --no-private --parallel --interlaced --verbose run changelog:validate", @@ -25,7 +27,7 @@ "lint": "yarn lint:eslint && echo && yarn lint:misc --check && yarn constraints && yarn lint:dependencies && yarn readme-content:check", "lint:dependencies": "depcheck && yarn dedupe --check", "lint:dependencies:fix": "depcheck && yarn dedupe", - "lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=6144' yarn eslint", + "lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=6144' yarn eslint && yarn build:libs", "lint:fix": "yarn lint:eslint --fix --prune-suppressions && echo && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies:fix && yarn readme-content:update", "lint:misc": "oxfmt --ignore-path .gitignore", "lint:misc:check": "yarn lint:misc --check", diff --git a/packages/bitcoin-wallet-snap/jest.config.mjs b/packages/bitcoin-wallet-snap/jest.config.mjs index e8bd87859..ee642e653 100644 --- a/packages/bitcoin-wallet-snap/jest.config.mjs +++ b/packages/bitcoin-wallet-snap/jest.config.mjs @@ -1,27 +1,14 @@ // @ts-check +import { createRequire } from 'node:module'; + +const requireWithCreateRequire = createRequire(import.meta.url); +const shared = requireWithCreateRequire('../../jest.config.snaps.unit.cjs'); + /** * @type {import('ts-jest').JestConfigWithTsJest} */ const config = { - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['./src/**/*.ts', './src/**/*.tsx'], - - // The directory where Jest should output its coverage files - coverageDirectory: 'coverage', - - // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['.*/index\\.ts'], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'babel', - - // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ['text', 'html', 'json-summary', 'lcov'], - - // An object that configures minimum threshold enforcement for coverage results + ...shared, coverageThreshold: { global: { branches: 65.5, @@ -30,13 +17,6 @@ const config = { statements: 74.57, }, }, - - preset: '@metamask/snaps-jest', - transform: { - '^.+\\.(t|j)sx?$': 'ts-jest', - }, - resetMocks: true, - testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'], }; export default config; diff --git a/packages/bitcoin-wallet-snap/tsconfig.json b/packages/bitcoin-wallet-snap/tsconfig.json index a8a6abb0f..8dfec9077 100644 --- a/packages/bitcoin-wallet-snap/tsconfig.json +++ b/packages/bitcoin-wallet-snap/tsconfig.json @@ -1,19 +1,11 @@ { - "extends": "../../tsconfig.packages.json", + "extends": "../../tsconfig.snaps.json", "compilerOptions": { - "baseUrl": "./", "lib": ["ES2021", "DOM"], - "resolveJsonModule": true /* lets us import JSON modules from within TypeScript modules. */, - "jsx": "react-jsx", - "jsxImportSource": "@metamask/snaps-sdk", "exactOptionalPropertyTypes": false, "forceConsistentCasingInFileNames": true, "noErrorTruncation": true, - "noUncheckedIndexedAccess": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "types": ["jest"] + "noUncheckedIndexedAccess": true }, "include": ["**/*.ts", "**/*.tsx", "locales/*.json"] } diff --git a/packages/sample-snap/src/index.test.tsx b/packages/sample-snap/integration-test/index.test.tsx similarity index 100% rename from packages/sample-snap/src/index.test.tsx rename to packages/sample-snap/integration-test/index.test.tsx diff --git a/packages/sample-snap/jest.config.js b/packages/sample-snap/jest.config.js index f0a22c3ea..2dc03ba04 100644 --- a/packages/sample-snap/jest.config.js +++ b/packages/sample-snap/jest.config.js @@ -1,6 +1,7 @@ +const shared = require('../../jest.config.snaps.unit.cjs'); + module.exports = { - preset: '@metamask/snaps-jest', - transform: { - '^.+\\.(t|j)sx?$': 'ts-jest', - }, + ...shared, + // Sample snap coverage is exercised via integration tests (`installSnap`). + passWithNoTests: true, }; diff --git a/packages/sample-snap/jest.integration.config.js b/packages/sample-snap/jest.integration.config.js new file mode 100644 index 000000000..49fa27035 --- /dev/null +++ b/packages/sample-snap/jest.integration.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: '@metamask/snaps-jest', + transform: { + '^.+\\.(t|j)sx?$': 'ts-jest', + }, + testMatch: ['**/integration-test/**/*.[jt]s?(x)'], +}; diff --git a/packages/sample-snap/package.json b/packages/sample-snap/package.json index 4a98e4a4a..f246068ab 100644 --- a/packages/sample-snap/package.json +++ b/packages/sample-snap/package.json @@ -35,6 +35,7 @@ "start": "mm-snap watch", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --reporters=jest-silent-reporter", "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", "since-latest-release": "../../scripts/since-latest-release.sh" diff --git a/packages/sample-snap/tsconfig.json b/packages/sample-snap/tsconfig.json index 6db6f2381..036b93ac5 100644 --- a/packages/sample-snap/tsconfig.json +++ b/packages/sample-snap/tsconfig.json @@ -1,11 +1,4 @@ { - "extends": "../../tsconfig.packages.json", - "compilerOptions": { - "baseUrl": "./", - "jsx": "react-jsx", - "skipLibCheck": true, - "jsxImportSource": "@metamask/snaps-sdk", - "types": ["jest"] - }, + "extends": "../../tsconfig.snaps.json", "include": ["**/*.ts", "**/*.tsx", "locales/*.json"] } diff --git a/packages/solana-wallet-snap/jest.config.js b/packages/solana-wallet-snap/jest.config.js index 462cb7ed4..ffb3a1948 100644 --- a/packages/solana-wallet-snap/jest.config.js +++ b/packages/solana-wallet-snap/jest.config.js @@ -10,4 +10,11 @@ module.exports = { collectCoverage: true, setupFilesAfterEnv: ['./jest.setup.ts'], coverageReporters: ['html', 'json-summary', 'text', 'lcov'], + // Lazy-build the Snap bundle when missing so `yarn test` works after install. + globalSetup: '/jest.globalSetup.cjs', + testPathIgnorePatterns: [ + '/node_modules/', + '/integration-test/', + '\\.integration\\.test\\.[tj]sx?$', + ], }; diff --git a/packages/solana-wallet-snap/jest.globalSetup.cjs b/packages/solana-wallet-snap/jest.globalSetup.cjs new file mode 100644 index 000000000..eff0bb16d --- /dev/null +++ b/packages/solana-wallet-snap/jest.globalSetup.cjs @@ -0,0 +1,42 @@ +const { access } = require('node:fs/promises'); +const { spawn } = require('node:child_process'); +const { join } = require('node:path'); + +/** + * Ensure `dist/bundle.js` exists before snaps-jest starts its HTTP server. + * Library packages are already built by `yarn build:libs` during install. + * + * @returns {Promise} + */ +module.exports = async function globalSetup() { + const bundlePath = join(__dirname, 'dist', 'bundle.js'); + try { + await access(bundlePath); + return; + } catch { + // Bundle missing — build below. + } + + console.log( + '[solana-wallet-snap] dist/bundle.js missing; running yarn build before tests…', + ); + + const code = await new Promise((resolve, reject) => { + const child = spawn('yarn', ['build'], { + cwd: __dirname, + stdio: 'inherit', + shell: true, + env: process.env, + }); + child.on('error', reject); + child.on('close', (exitCode) => { + resolve(exitCode ?? 1); + }); + }); + + if (code !== 0) { + throw new Error( + '[solana-wallet-snap] Failed to build Snap bundle required by snaps-jest', + ); + } +}; diff --git a/packages/solana-wallet-snap/jest.integration.config.js b/packages/solana-wallet-snap/jest.integration.config.js new file mode 100644 index 000000000..6eb2ba510 --- /dev/null +++ b/packages/solana-wallet-snap/jest.integration.config.js @@ -0,0 +1,13 @@ +module.exports = { + preset: '@metamask/snaps-jest', + transform: { + '^.+\\.(t|j)sx?$': 'ts-jest', + '^.+\\.svg$': '/svg-transformer.js', + }, + testMatch: [ + '**/integration-test/**/*.[jt]s?(x)', + '**/src/**/*.integration.test.[jt]s?(x)', + ], + maxWorkers: 1, + setupFilesAfterEnv: ['./jest.setup.ts'], +}; diff --git a/packages/solana-wallet-snap/package.json b/packages/solana-wallet-snap/package.json index ac8eea907..cee0327f0 100644 --- a/packages/solana-wallet-snap/package.json +++ b/packages/solana-wallet-snap/package.json @@ -46,6 +46,7 @@ "test:core:watch": "yarn test:core --watch", "test:features": "jest src/features --passWithNoTests --runInBand", "test:features:watch": "yarn test:features --watch", + "test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --reporters=jest-silent-reporter", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose" diff --git a/packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/render.test.tsx b/packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/render.integration.test.tsx similarity index 100% rename from packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/render.test.tsx rename to packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/render.integration.test.tsx diff --git a/packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/render.test.tsx b/packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/render.integration.test.tsx similarity index 100% rename from packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/render.test.tsx rename to packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/render.integration.test.tsx diff --git a/packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx b/packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/render.integration.test.tsx similarity index 100% rename from packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/render.test.tsx rename to packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/render.integration.test.tsx diff --git a/packages/solana-wallet-snap/src/index.integration.test.ts b/packages/solana-wallet-snap/src/index.integration.test.ts new file mode 100644 index 000000000..82d7f33d2 --- /dev/null +++ b/packages/solana-wallet-snap/src/index.integration.test.ts @@ -0,0 +1,39 @@ +import { installSnap } from '@metamask/snaps-jest'; + +describe('onRpcRequest', () => { + it('throws an error if the requested method does not exist', async () => { + const snap = await installSnap(); + + const response = await snap.request({ + method: 'foo', + }); + + expect(response).toRespondWithError({ + code: 4100, + message: 'Permission denied', + stack: expect.any(String), + }); + }); +}); + +describe('onKeyringRequest', () => { + it('throws an error if the requested method does not exist', async () => { + const snap = await installSnap(); + + const response = await snap.request({ + method: 'wallet_invokeSnap', + params: { + snapId: 'npm:@metamask/solana-wallet-snap', + request: { + method: 'foo', + }, + }, + }); + + expect(response).toRespondWithError({ + code: 4100, + message: 'Permission denied', + stack: expect.any(String), + }); + }); +}); diff --git a/packages/solana-wallet-snap/src/index.test.ts b/packages/solana-wallet-snap/src/index.test.ts index 11814354e..3fddfb1b2 100644 --- a/packages/solana-wallet-snap/src/index.test.ts +++ b/packages/solana-wallet-snap/src/index.test.ts @@ -1,5 +1,4 @@ import { expect } from '@jest/globals'; -import { installSnap } from '@metamask/snaps-jest'; import { onCronjob } from '.'; import { handlers } from './core/handlers/onCronjob'; @@ -25,44 +24,6 @@ jest.mock('./snapContext', () => ({ }, })); -describe('onRpcRequest', () => { - it('throws an error if the requested method does not exist', async () => { - const { request } = await installSnap(); - - const response = await request({ - method: 'foo', - }); - - expect(response).toRespondWithError({ - code: 4100, - message: 'Permission denied', - stack: expect.any(String), - }); - }); -}); - -describe('onKeyringRequest', () => { - it('throws an error if the requested method does not exist', async () => { - const { request } = await installSnap(); - - const response = await request({ - method: 'wallet_invokeSnap', - params: { - snapId: 'npm:@metamask/solana-wallet-snap', - request: { - method: 'foo', - }, - }, - }); - - expect(response).toRespondWithError({ - code: 4100, - message: 'Permission denied', - stack: expect.any(String), - }); - }); -}); - describe('onCronjob', () => { it('throws an error if the requested method is invalid', async () => { await expect( diff --git a/packages/solana-wallet-snap/tsconfig.json b/packages/solana-wallet-snap/tsconfig.json index 3eb63f359..9d4d37eb1 100644 --- a/packages/solana-wallet-snap/tsconfig.json +++ b/packages/solana-wallet-snap/tsconfig.json @@ -1,20 +1,13 @@ { - "extends": "../../tsconfig.packages.json", + "extends": "../../tsconfig.snaps.json", "compilerOptions": { "baseUrl": "./", - "jsx": "react-jsx", - "jsxImportSource": "@metamask/snaps-sdk", - "resolveJsonModule": true, + "lib": ["ES2023", "DOM"], + "target": "es2023", "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, "noErrorTruncation": true, - "noUncheckedIndexedAccess": true, - "skipLibCheck": true, - "lib": ["ES2023", "DOM"], - "target": "es2023", - "module": "preserve", - "moduleResolution": "bundler", - "types": ["jest"] + "noUncheckedIndexedAccess": true }, "include": ["**/*.ts", "**/*.tsx", "locales/*.json"] } diff --git a/packages/tron-wallet-snap/jest.config.mjs b/packages/tron-wallet-snap/jest.config.mjs index 299fdc07d..ba5232b44 100644 --- a/packages/tron-wallet-snap/jest.config.mjs +++ b/packages/tron-wallet-snap/jest.config.mjs @@ -1,27 +1,15 @@ // @ts-check +import { createRequire } from 'node:module'; + +const requireWithCreateRequire = createRequire(import.meta.url); +const shared = requireWithCreateRequire('../../jest.config.snaps.unit.cjs'); + /** * @type {import('ts-jest').JestConfigWithTsJest} */ const config = { - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: true, - - // An array of glob patterns indicating a set of files for which coverage information should be collected + ...shared, collectCoverageFrom: ['./src/**/*.ts', './src/**/*.tsx'], - - // The directory where Jest should output its coverage files - coverageDirectory: 'coverage', - - // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['.*/index\\.ts'], - - // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'babel', - - // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: ['text', 'html', 'json-summary', 'lcov'], - - // An object that configures minimum threshold enforcement for coverage results coverageThreshold: { global: { branches: 69.96, @@ -30,16 +18,10 @@ const config = { statements: 82.62, }, }, - - preset: '@metamask/snaps-jest', - transform: { - '^.+\\.(t|j)sx?$': 'ts-jest', - }, moduleNameMapper: { + ...shared.moduleNameMapper, '\\.svg$': 'jest-transform-stub', }, - resetMocks: true, - testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'], setupFilesAfterEnv: ['/jest.setup.ts'], }; diff --git a/packages/tron-wallet-snap/jest.setup.ts b/packages/tron-wallet-snap/jest.setup.ts index 7adb2fb26..8902e2361 100644 --- a/packages/tron-wallet-snap/jest.setup.ts +++ b/packages/tron-wallet-snap/jest.setup.ts @@ -1,7 +1,3 @@ import { config } from 'dotenv'; config(); - -// Set default environment for tests if not already set -// eslint-disable-next-line no-restricted-globals -process.env.ENVIRONMENT ??= 'test'; diff --git a/packages/tron-wallet-snap/package.json b/packages/tron-wallet-snap/package.json index cd0706154..67b8e4036 100644 --- a/packages/tron-wallet-snap/package.json +++ b/packages/tron-wallet-snap/package.json @@ -44,6 +44,7 @@ "start": "node scripts/update-manifest-local.js && concurrently \"mm-snap watch\" \"yarn build:locale:watch\"", "test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter", "test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache", + "test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.mjs --reporters=jest-silent-reporter", "test:verbose": "NODE_OPTIONS=--experimental-vm-modules jest --verbose", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch" }, diff --git a/packages/tron-wallet-snap/tsconfig.json b/packages/tron-wallet-snap/tsconfig.json index df5a8f6b6..797db4496 100644 --- a/packages/tron-wallet-snap/tsconfig.json +++ b/packages/tron-wallet-snap/tsconfig.json @@ -1,19 +1,12 @@ { - "extends": "../../tsconfig.packages.json", + "extends": "../../tsconfig.snaps.json", "compilerOptions": { - "resolveJsonModule": true /* lets us import JSON modules from within TypeScript modules. */, - "jsx": "react-jsx", - "jsxImportSource": "@metamask/snaps-sdk", + "lib": ["ES2023", "DOM"], + "target": "es2023", "exactOptionalPropertyTypes": false, "forceConsistentCasingInFileNames": true, "noErrorTruncation": true, - "noUncheckedIndexedAccess": true, - "skipLibCheck": true, - "lib": ["ES2023", "DOM"], - "target": "es2023", - "module": "preserve", - "moduleResolution": "bundler", - "types": ["jest"] + "noUncheckedIndexedAccess": true }, "include": ["**/*.ts", "**/*.tsx", "locales/*.json"] } diff --git a/scripts/build-workspace-kind.mjs b/scripts/build-workspace-kind.mjs new file mode 100644 index 000000000..5d410dacf --- /dev/null +++ b/scripts/build-workspace-kind.mjs @@ -0,0 +1,120 @@ +import { spawn } from 'node:child_process'; +import { access } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + +/** + * Run a command and resolve with its exit code. + * + * @param {string} command - Executable to run. + * @param {string[]} args - Arguments. + * @param {{ cwd?: string, stdio?: 'inherit' | 'pipe' }} [options] - Spawn options. + * @returns {Promise} Exit code. + */ +async function run(command, args, options = {}) { + return await new Promise((resolvePromise, reject) => { + const child = spawn(command, args, { + cwd: options.cwd ?? root, + stdio: options.stdio ?? 'inherit', + shell: true, + }); + child.on('error', reject); + child.on('close', (code) => { + resolvePromise(code ?? 1); + }); + }); +} + +/** + * Returns true when a file exists. + * + * @param {string} filePath - Absolute path to check. + * @returns {Promise} Whether the file exists. + */ +async function fileExists(filePath) { + try { + await access(filePath); + return true; + } catch { + return false; + } +} + +/** + * List workspace package names filtered to snaps or libraries. + * + * @param {boolean} snapsOnly - When true, select snap workspaces; otherwise libraries. + * @returns {Promise} Workspace package names. + */ +async function listWorkspaceNames(snapsOnly) { + const list = spawn('yarn', ['workspaces', 'list', '--json'], { + cwd: root, + shell: true, + }); + + let stdout = ''; + let stderr = ''; + list.stdout?.on('data', (chunk) => { + stdout += String(chunk); + }); + list.stderr?.on('data', (chunk) => { + stderr += String(chunk); + }); + + const status = await new Promise((resolvePromise, reject) => { + list.on('error', reject); + list.on('close', (code) => { + resolvePromise(code ?? 1); + }); + }); + + if (status !== 0) { + throw new Error(stderr || stdout || 'yarn workspaces list failed'); + } + + const workspaces = stdout + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line)) + .filter((workspace) => workspace.location !== '.'); + + const selected = []; + for (const workspace of workspaces) { + const isSnap = await fileExists( + join(root, workspace.location, 'snap.manifest.json'), + ); + if (snapsOnly ? isSnap : !isSnap) { + selected.push(workspace.name); + } + } + return selected; +} + +const snapsOnly = process.argv.includes('--snaps'); +const label = snapsOnly ? 'snaps' : 'libraries'; + +const names = await listWorkspaceNames(snapsOnly); + +if (names.length === 0) { + console.log(`No ${label} workspaces to build.`); +} else { + console.log(`Building ${label}: ${names.join(', ')}`); + const code = await run('yarn', [ + 'workspaces', + 'foreach', + '--all', + '--topological-dev', + '--parallel', + '--interlaced', + '--verbose', + ...names.flatMap((name) => ['--include', name]), + 'run', + 'build', + ]); + if (code !== 0) { + throw new Error(`Failed to build ${label} (exit ${code})`); + } +} diff --git a/tsconfig.packages.json b/tsconfig.packages.json index a655abc1f..5a86c2a91 100644 --- a/tsconfig.packages.json +++ b/tsconfig.packages.json @@ -5,14 +5,15 @@ "extends": "./tsconfig.base.json", "compilerOptions": { /** - * Here we ensure that TypeScript resolves `@metamask/*` imports to the - * uncompiled source code for packages that live in this repo. + * Resolve `@metamask/*` imports to uncompiled source for packages in this + * repo. Paths are relative to this file (the monorepo root), not to each + * package that extends it. * * NOTE: This must be synchronized with the `moduleNameMapper` option in - * `jest.config.packages.js`. + * `jest.config.packages.js` and `jest.config.snaps.unit.js`. */ "paths": { - "@metamask/*": ["../*/src"] + "@metamask/*": ["./packages/*/src"] } } } diff --git a/tsconfig.snaps.json b/tsconfig.snaps.json new file mode 100644 index 000000000..470a6ac8e --- /dev/null +++ b/tsconfig.snaps.json @@ -0,0 +1,21 @@ +{ + /** + * Shared TypeScript settings for Snap packages. + * + * Snaps are bundled with `mm-snap` and are not part of the root + * `tsc --build` graph, so `composite` is disabled here. That lets + * workspace `@metamask/*` path mappings resolve to sibling `src/` + * without requiring project-reference build artifacts. + */ + "extends": "./tsconfig.packages.json", + "compilerOptions": { + "composite": false, + "jsx": "react-jsx", + "jsxImportSource": "@metamask/snaps-sdk", + "module": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "skipLibCheck": true, + "types": ["jest"] + } +}