From 95ee9a36812d02a961a83315d37efb4fdf6f4119 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 20:15:03 +0000 Subject: [PATCH 1/9] docs: add layered monorepo readiness design and plan Capture Approach C: post-install library builds, fixed TS paths, snap unit vs integration Jest split, and lint/build DX fixes. Co-authored-by: Ulisses Ferreira --- .../2026-08-08-layered-monorepo-readiness.md | 744 ++++++++++++++++++ ...08-08-layered-monorepo-readiness-design.md | 105 +++ 2 files changed, 849 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md create mode 100644 docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md diff --git a/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md b/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md new file mode 100644 index 000000000..2b5085405 --- /dev/null +++ b/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md @@ -0,0 +1,744 @@ +# Layered Monorepo Readiness Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** After a normal `yarn`, library packages are built, TypeScript works, and Snap **unit** Jest suites run; Snap bundles / `installSnap` tests still require `yarn build`. + +**Architecture:** Keep the dual package model (libraries via `ts-bridge`, snaps via `mm-snap`). Fix root TypeScript `paths`, post-install `build:libs` only, split Snap Jest into node unit vs `snaps-jest` integration, and stop lint from leaving library `dist/` wiped. + +**Tech Stack:** Yarn 4 workspaces, TypeScript 5.8 project refs/`paths`, Jest 30, `ts-bridge`, `@metamask/snaps-cli` / `@metamask/snaps-jest`, LavaMoat `allow-scripts` yarn plugin. + +**Spec:** `docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md` + +## Global Constraints + +- Do not build Snap bundles in the install hook (libraries only). +- Keep Yarn constraint: Snap `scripts.build` must start with `mm-snap build`; library `scripts.build` must remain the `ts-bridge … --no-references` command. +- Keep package `scripts.test` string required by `yarn.config.cjs` unless that constraint is updated in the same task that changes it. +- Prefer editing shared configs (`tsconfig.packages.json`, `tsconfig.snaps.json`, `jest.config.packages.js`, root `package.json`) over one-off per-snap drift. +- `installSnap` tests must not run under the default unit Jest environment. +- Every consumer-facing behavior change to a published package needs a changelog entry under Unreleased; pure tooling/docs-only root changes do not. + +--- + +## File map + +| File | Responsibility | +|---|---| +| `scripts/build-libs.mjs` | Discover non-snap workspaces and run their `build` scripts topologically | +| `package.json` | `build:libs`, wire into `allow-scripts`, lint rebuild, optional `build:snaps` | +| `tsconfig.packages.json` | Root-correct `@metamask/*` → `packages/*/src` paths | +| `tsconfig.snaps.json` | Shared Snap TS compiler defaults | +| `packages/*/tsconfig.json` (snaps) | Extend `tsconfig.snaps.json` | +| `jest.config.packages.js` | Keep mapper in sync with TS paths; document Snap usage | +| `jest.config.snaps.unit.js` | Shared Node-environment unit defaults for snaps | +| `packages/*/jest.config.*` (snaps) | Unit config (node); integration stays `snaps-jest` | +| `packages/solana-wallet-snap/**` | Move `installSnap` tests into `integration-test/` | +| `packages/sample-snap/**` | Move `installSnap` tests into `integration-test/` or dedicated integration config | +| `packages/*/snap.config.ts` | Safe `ENVIRONMENT` default where validated | +| `yarn.config.cjs` | Allow Snap `test:integration` script pattern if constrained | +| `docs/getting-started/setting-up-your-environment.md`, `AGENTS.md`, `docs/processes/testing.md`, `docs/processes/building.md` | Document layered workflow | +| `package.json` `workspaces` / `examples/` | Fix empty `examples/*` glob | +| `.github/workflows/lint-build-test.yml` | Only if unit vs integration split requires CI job changes | + +--- + +### Task 1: Fix TypeScript workspace path mapping + +**Files:** +- Modify: `tsconfig.packages.json` +- Modify: `jest.config.packages.js` (comment + mapper sanity check only if needed) +- Test: `packages/tron-wallet-snap` typecheck against `@metamask/snap-networks-utils` **without** that package’s `dist/` + +**Interfaces:** +- Consumes: existing `@metamask/*` imports in snaps/libs +- Produces: `compilerOptions.paths` of `@metamask/*` → `packages/*/src` resolved from repo root config + +- [ ] **Step 1: Confirm the failure without library dist** + +```bash +rm -rf packages/snap-networks-utils/dist +yarn workspace @metamask/tron-wallet-snap exec tsc --noEmit 2>&1 | head -20 +``` + +Expected: `TS2307: Cannot find module '@metamask/snap-networks-utils'` + +- [ ] **Step 2: Fix paths in `tsconfig.packages.json`** + +Replace the paths block with root-correct mappings (paths are resolved relative to this file’s directory — the repo root): + +```json +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "paths": { + "@metamask/*": ["packages/*/src"] + } + } +} +``` + +Keep the existing comment, but update it to say paths are rooted at the monorepo root (this file), and must stay synchronized with `jest.config.packages.js`. + +- [ ] **Step 3: Re-run typecheck without library dist** + +```bash +rm -rf packages/snap-networks-utils/dist +yarn workspace @metamask/tron-wallet-snap exec tsc --noEmit +``` + +Expected: exit 0 (or only pre-existing unrelated errors — none for `snap-networks-utils`). + +If it still fails: run `tsc --traceResolution` on one file and verify the candidate is `/workspace/packages/snap-networks-utils/src`, not `/snap-networks-utils/src`. Do **not** “fix” by restoring `dist` in this task. + +- [ ] **Step 4: Verify library package still typechecks** + +```bash +yarn workspace @metamask/snap-networks-utils exec tsc --noEmit +``` + +Expected: exit 0 + +- [ ] **Step 5: Commit** + +```bash +git add tsconfig.packages.json +git commit -m "fix: resolve TypeScript workspace paths from monorepo root" +``` + +--- + +### Task 2: Add shared Snap TypeScript config + +**Files:** +- Create: `tsconfig.snaps.json` +- Modify: `packages/bitcoin-wallet-snap/tsconfig.json` +- Modify: `packages/solana-wallet-snap/tsconfig.json` +- Modify: `packages/tron-wallet-snap/tsconfig.json` +- Modify: `packages/sample-snap/tsconfig.json` + +**Interfaces:** +- Consumes: `tsconfig.packages.json` (including fixed paths) +- Produces: shared Snap compiler options — JSX from snaps-sdk, `moduleResolution: bundler`, `module: preserve`, `skipLibCheck`, `resolveJsonModule`, `types: ["jest"]` + +- [ ] **Step 1: Create `tsconfig.snaps.json`** + +```json +{ + "extends": "./tsconfig.packages.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@metamask/snaps-sdk", + "module": "preserve", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "skipLibCheck": true, + "types": ["jest"] + } +} +``` + +- [ ] **Step 2: Slim each snap `tsconfig.json` to extend the shared config** + +Example for Tron (keep package-specific strictness flags that already differ only if required; prefer moving common flags into `tsconfig.snaps.json`): + +```json +{ + "extends": "../../tsconfig.snaps.json", + "compilerOptions": { + "lib": ["ES2023", "DOM"], + "target": "es2023", + "exactOptionalPropertyTypes": false, + "forceConsistentCasingInFileNames": true, + "noErrorTruncation": true, + "noUncheckedIndexedAccess": true + }, + "include": ["**/*.ts", "**/*.tsx", "locales/*.json"] +} +``` + +Repeat for bitcoin / solana / sample, preserving each package’s intentional `lib`/`target`/`exactOptionalPropertyTypes` differences. Remove redundant duplicates of jsx / moduleResolution / skipLibCheck / types. + +- [ ] **Step 3: Typecheck all snaps** + +```bash +yarn typecheck +``` + +Expected: exit 0 + +- [ ] **Step 4: Commit** + +```bash +git add tsconfig.snaps.json packages/*/tsconfig.json +git commit -m "chore: add shared tsconfig for snap packages" +``` + +--- + +### Task 3: Add `build:libs` and run it after install + +**Files:** +- Create: `scripts/build-libs.mjs` +- Modify: `package.json` (scripts) +- Optional test helper: none (verify with shell) + +**Interfaces:** +- Consumes: Yarn workspaces list; `snap.manifest.json` presence; each lib’s `scripts.build` +- Produces: + - `yarn build:libs` — builds every non-private workspace without `snap.manifest.json` + - `yarn build:snaps` — builds every workspace **with** `snap.manifest.json` (topological-dev via foreach) + - `scripts.allow-scripts` — runs LavaMoat allow-scripts then `yarn build:libs` + +- [ ] **Step 1: Add `scripts/build-libs.mjs`** + +```js +import { existsSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + +const list = spawnSync( + 'yarn', + ['workspaces', 'list', '--json'], + { cwd: root, encoding: 'utf8', shell: true }, +); + +if (list.status !== 0) { + console.error(list.stderr || list.stdout); + process.exit(list.status ?? 1); +} + +const workspaces = list.stdout + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line)) + .filter((workspace) => workspace.location !== '.'); + +const libraries = workspaces.filter( + (workspace) => + !existsSync(join(root, workspace.location, 'snap.manifest.json')), +); + +if (libraries.length === 0) { + console.log('No library workspaces to build.'); + process.exit(0); +} + +const names = libraries.map((workspace) => workspace.name); +console.log(`Building libraries: ${names.join(', ')}`); + +const result = spawnSync( + 'yarn', + [ + 'workspaces', + 'foreach', + '--all', + '--no-private', + '--topological-dev', + '--parallel', + '--interlaced', + '--verbose', + ...names.flatMap((name) => ['--include', name]), + 'run', + 'build', + ], + { cwd: root, stdio: 'inherit', shell: true }, +); + +process.exit(result.status ?? 1); +``` + +If Yarn’s foreach `--include` UX differs in 4.17.1, adjust to an equivalent filter (verify with `yarn workspaces foreach --help`). Fallback: loop `yarn workspace run build` in dependency order from `yarn workspaces list --json` plus each package’s `package.json` deps. + +- [ ] **Step 2: Wire root scripts in `package.json`** + +Add/adjust: + +```json +{ + "scripts": { + "allow-scripts": "yarn exec allow-scripts && yarn build:libs", + "build:libs": "node ./scripts/build-libs.mjs", + "build:snaps": "yarn workspaces foreach --all --no-private --topological-dev --parallel --interlaced --verbose --exclude @metamask/snap-networks-utils run build", + "setup": "yarn install" + } +} +``` + +Notes: +- Prefer excluding by “is snap” in a `build-snaps.mjs` mirror if `--exclude` of only today’s library is too brittle; when a second library appears, update to a snap-detecting script. +- `setup` can remain `yarn install` because `allow-scripts` already chains `build:libs` after install via the Yarn plugin. + +- [ ] **Step 3: Verify install hook builds libraries** + +```bash +rm -rf packages/snap-networks-utils/dist +yarn allow-scripts +test -f packages/snap-networks-utils/dist/index.d.mts && echo 'libs built' +``` + +Expected: `libs built`. Snaps’ `dist/bundle.js` should still be absent unless previously built. + +- [ ] **Step 4: Verify `mm-snap` can resolve the library** + +```bash +ENVIRONMENT=local yarn workspace @metamask/tron-wallet-snap run build +``` + +Expected: Snap bundle succeeds (no “Package path . is exported … no valid target”). + +- [ ] **Step 5: Commit** + +```bash +git add scripts/build-libs.mjs package.json +git commit -m "feat: build library packages after yarn install" +``` + +--- + +### Task 4: Rebuild libraries after eslint cleans dist + +**Files:** +- Modify: `package.json` (`lint:eslint`) +- Modify: `AGENTS.md` (Cursor Cloud caveats) + +**Interfaces:** +- Consumes: `build:only-clean`, `build:libs` +- Produces: `lint:eslint` ends with library `dist/` restored + +- [ ] **Step 1: Update `lint:eslint`** + +Change: + +```json +"lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=6144' yarn eslint" +``` + +to: + +```json +"lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=6144' yarn eslint && yarn build:libs" +``` + +- [ ] **Step 2: Smoke-test** + +```bash +yarn lint:eslint +test -f packages/snap-networks-utils/dist/index.d.mts && echo 'libs restored' +``` + +Expected: eslint completes; `libs restored`. + +- [ ] **Step 3: Update `AGENTS.md` caveat** + +Replace the “`yarn lint` deletes `dist/`” bullet with: eslint still cleans `packages/*/dist` before linting, then `build:libs` restores library artifacts; Snap bundles still need `yarn build` / `yarn build:snaps` afterward. + +- [ ] **Step 4: Commit** + +```bash +git add package.json AGENTS.md +git commit -m "fix: restore library builds after eslint dist clean" +``` + +--- + +### Task 5: Shared Snap unit Jest config (Node environment) + +**Files:** +- Create: `jest.config.snaps.unit.js` +- Modify: `packages/bitcoin-wallet-snap/jest.config.mjs` +- Modify: `packages/tron-wallet-snap/jest.config.mjs` +- Modify: `packages/solana-wallet-snap/jest.config.js` +- Modify: `packages/sample-snap/jest.config.js` (temporary: may still need integration-only until Task 6) +- Modify: `jest.config.packages.js` (keep mapper; ensure Snap unit configs reuse the same `@metamask/(.*)` workspace→source pattern) + +**Interfaces:** +- Consumes: `ts-jest`, workspace source mapper pattern from `jest.config.packages.js` +- Produces: default Snap `test` runs in `testEnvironment: 'node'` and does **not** load `@metamask/snaps-jest` preset + +- [ ] **Step 1: Create `jest.config.snaps.unit.js`** + +```js +const path = require('path'); + +module.exports = { + testEnvironment: 'node', + preset: 'ts-jest', + transform: { + '^.+\\.(t|j)sx?$': 'ts-jest', + }, + collectCoverage: true, + collectCoverageFrom: ['./src/**/*.ts', './src/**/*.tsx'], + coverageDirectory: 'coverage', + coveragePathIgnorePatterns: ['.*/index\\.ts'], + coverageProvider: 'babel', + coverageReporters: ['text', 'html', 'json-summary', 'lcov'], + resetMocks: true, + restoreMocks: true, + testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'], + testPathIgnorePatterns: [ + '/node_modules/', + '/integration-test/', + ], + moduleNameMapper: { + '\\.svg$': 'jest-transform-stub', + '^@metamask/utils/node$': require.resolve('@metamask/utils/node'), + '^@metamask/(.+)$': [ + path.join(__dirname, 'packages/$1/src'), + path.join(__dirname, 'node_modules/@metamask/$1'), + ], + }, +}; +``` + +- [ ] **Step 2: Point bitcoin/tron/solana unit configs at the shared config** + +Example ESM wrapper for bitcoin/tron (`jest.config.mjs`): + +```js +// @ts-check +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const shared = require('../../jest.config.snaps.unit.js'); + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +const config = { + ...shared, + coverageThreshold: { + global: { + branches: 65.5, + functions: 62.64, + lines: 75.29, + statements: 74.57, + }, + }, +}; + +export default config; +``` + +Preserve each package’s existing `coverageThreshold`, `setupFilesAfterEnv`, `maxWorkers`, and SVG transformer overrides. + +**Critical:** remove `preset: '@metamask/snaps-jest'` from unit configs. + +- [ ] **Step 3: Prove a former failure now passes without Snap bundle** + +```bash +rm -rf packages/tron-wallet-snap/dist +yarn workspace @metamask/tron-wallet-snap run jest --no-coverage src/services/assets/AssetsService.test.ts +``` + +Expected: tests run (PASS or real assertion failures) — **not** `dist/bundle.js does not exist`. + +- [ ] **Step 4: Commit** + +```bash +git add jest.config.snaps.unit.js packages/*/jest.config.* +git commit -m "test: run snap unit tests in node without snaps-jest" +``` + +--- + +### Task 6: Move `installSnap` tests to integration configs + +**Files:** +- Create/Modify: `packages/solana-wallet-snap/jest.integration.config.js` (or `.mjs`) +- Create: `packages/solana-wallet-snap/integration-test/` (move installSnap specs here) +- Modify: `packages/solana-wallet-snap/package.json` (add `test:integration`) +- Modify: `packages/sample-snap/jest.config.js`, `package.json`, move `src/index.test.tsx` → `integration-test/` +- Create: `packages/sample-snap/jest.integration.config.js` +- Modify: `packages/bitcoin-wallet-snap` / `tron-wallet-snap` only if needed for script naming consistency +- Modify: `yarn.config.cjs` if a new required script pattern is enforced for snaps + +**Interfaces:** +- Consumes: `@metamask/snaps-jest` `installSnap` +- Produces: + - `yarn workspace run test` → unit only (no bundle) + - `yarn workspace run test:integration` → `snaps-jest` after `yarn build` + +- [ ] **Step 1: Inventory current `installSnap` files** + +Known today: +- `packages/sample-snap/src/index.test.tsx` +- `packages/solana-wallet-snap/src/index.test.ts` (partially; also has non-installSnap cases — split the file) +- `packages/solana-wallet-snap/src/features/confirmation/views/**/render.test.tsx` +- `packages/bitcoin-wallet-snap/integration-test/*.test.ts` (already correct) + +- [ ] **Step 2: Move Solana `installSnap` tests** + +1. Create `packages/solana-wallet-snap/integration-test/`. +2. Move render tests that call `installSnap` into that folder (keep imports working; adjust relative paths). +3. Split `src/index.test.ts`: keep pure unit cases (e.g. mocked cronjob handler tests) under `src/`; move `installSnap` cases to `integration-test/on-rpc-request.test.ts` (name freely, but under `integration-test/`). +4. Add: + +```js +// packages/solana-wallet-snap/jest.integration.config.js +module.exports = { + preset: '@metamask/snaps-jest', + testMatch: ['**/integration-test/**/*.[jt]s?(x)'], +}; +``` + +5. Add script: + +```json +"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --reporters=jest-silent-reporter" +``` + +- [ ] **Step 3: Handle sample-snap** + +All current tests use `installSnap`. Move them to `integration-test/` and either: + +- Make unit `jest.config.js` use `passWithNoTests: true` and `testMatch` under `src/`, or +- Keep a trivial unit smoke test that does not use `installSnap` + +Add `test:integration` like Solana. Root `test:verbose` already excludes sample-snap; keep that unless you intentionally include unit smoke tests later. + +- [ ] **Step 4: Verify unit vs integration gate** + +```bash +rm -rf packages/solana-wallet-snap/dist +yarn workspace @metamask/solana-wallet-snap run test +``` + +Expected: unit suites pass without bundle. + +```bash +ENVIRONMENT=local yarn workspace @metamask/solana-wallet-snap run build +yarn workspace @metamask/solana-wallet-snap run test:integration +``` + +Expected: integration suites discover and run under `snaps-jest` (pass/fail based on assertions, but must find the bundle). + +- [ ] **Step 5: Update yarn constraints if they reject new scripts** + +Only if `yarn constraints` complains. Do not invent constraints for `test:integration` unless useful; if you add one, require snaps to define `test:integration` with a `jest --config` integration config. + +- [ ] **Step 6: Commit** + +```bash +git add packages/solana-wallet-snap packages/sample-snap yarn.config.cjs +git commit -m "test: move installSnap coverage into snap integration suites" +``` + +--- + +### Task 7: Safe Snap build environment defaults + +**Files:** +- Modify: `packages/solana-wallet-snap/snap.config.ts` +- Modify: other snap configs only if they validate `ENVIRONMENT` the same way +- Modify: `.env.example` files if present + +**Interfaces:** +- Consumes: `process.env.ENVIRONMENT` +- Produces: default `'local'` (or `'test'`) when unset so SES eval accepts the value + +- [ ] **Step 1: Reproduce** + +```bash +env -u ENVIRONMENT yarn workspace @metamask/solana-wallet-snap run build 2>&1 | tail -30 +``` + +Expected (today): SES error about `ENVIRONMENT` received `""`. + +- [ ] **Step 2: Default the env in `snap.config.ts`** + +```ts +const environment = { + ENVIRONMENT: process.env.ENVIRONMENT || 'local', + // ...unchanged keys +}; +``` + +Use `||` (not `??`) so empty string also falls back. + +- [ ] **Step 3: Re-run build without ENVIRONMENT** + +```bash +env -u ENVIRONMENT yarn build +``` + +Expected: all library + snap builds succeed (CI secrets still override when present). + +- [ ] **Step 4: Commit** + +```bash +git add packages/solana-wallet-snap/snap.config.ts +git commit -m "fix: default solana snap ENVIRONMENT to local for builds" +``` + +--- + +### Task 8: Workspace and docs hygiene + +**Files:** +- Modify: `package.json` (`workspaces`) **or** create `examples/.gitkeep` + placeholder — prefer removing `examples/*` until an example exists +- Modify: `docs/README.md` (remove or fix dead migration-guide link) +- Modify: `docs/getting-started/setting-up-your-environment.md` +- Modify: `docs/processes/building.md` +- Modify: `docs/processes/testing.md` +- Modify: `AGENTS.md` + +**Interfaces:** +- Produces: accurate contributor workflow for layered readiness + +- [ ] **Step 1: Fix workspaces glob** + +In root `package.json`, change: + +```json +"workspaces": [ + "packages/*" +] +``` + +(unless you intentionally add an `examples` package in this same change). + +- [ ] **Step 2: Update getting-started** + +After `yarn install`, document that library packages are built automatically via `allow-scripts` → `build:libs`. Then: + +```bash +yarn typecheck +yarn test +yarn build # when you need snap bundles / integration tests / serve +``` + +- [ ] **Step 3: Update building + testing process docs** + +State clearly: +- `yarn build:libs` — shared packages (`ts-bridge`) +- `yarn build` / `yarn build:snaps` — snap bundles +- `yarn test` — unit tests (node for snaps) +- `yarn workspace run test:integration` — requires built snap + +- [ ] **Step 4: Fix docs index** + +Remove the link to missing `./processes/snap-migration-process-guide.md` or add a stub page that says the guide is not in-tree yet (prefer remove until content exists). + +- [ ] **Step 5: Commit** + +```bash +git add package.json docs AGENTS.md +git commit -m "docs: describe layered install, typecheck, and test workflow" +``` + +--- + +### Task 9: CI alignment + +**Files:** +- Modify: `.github/workflows/lint-build-test.yml` only as needed + +**Interfaces:** +- Consumes: build artifacts upload/download +- Produces: CI unit tests match local unit semantics; integration remains optional/separate + +- [ ] **Step 1: Decide CI matrix behavior** + +Keep current flow (build all → download dist → `yarn workspace … run test`) — this remains valid and still exercises packages after a full build. + +Optional improvement in this task (recommended if low-risk): +- Unit job can run **without** snap artifacts once Task 5/6 landed +- Add a separate `test-integration` job for snaps that define `test:integration`, needing build artifacts (+ secrets/services as today) + +Minimum for this plan: ensure CI still passes with the new unit configs. Do not delete the build job. + +- [ ] **Step 2: Run the same commands CI runs** + +```bash +yarn build +yarn test:scripts +yarn workspaces foreach --all --exclude @metamask/sample-snap --parallel --verbose run test +``` + +Expected: pass (sample excluded as today, or included if it now has unit smoke tests). + +- [ ] **Step 3: Commit only if workflow files changed** + +```bash +git add .github/workflows/lint-build-test.yml +git commit -m "ci: align snap unit and integration test jobs" +``` + +--- + +### Task 10: End-to-end verification on a clean tree + +**Files:** +- None (verification only); fix regressions found in earlier tasks + +- [ ] **Step 1: Clean artifacts and reinstall** + +```bash +rm -rf packages/*/dist node_modules +yarn install +``` + +Expected: install ends with `build:libs` success; `packages/snap-networks-utils/dist` exists; snap `dist/bundle.js` files absent. + +- [ ] **Step 2: TypeScript + unit tests** + +```bash +yarn typecheck +yarn test +``` + +Expected: both pass. + +- [ ] **Step 3: Full build + one integration suite** + +```bash +yarn build +yarn workspace @metamask/bitcoin-wallet-snap run test:integration +``` + +Expected: build pass; bitcoin integration either runs (if Docker available) or fails only on missing Docker — not on missing bundle. If Docker is unavailable in the agent environment, substitute: + +```bash +yarn workspace @metamask/sample-snap run test:integration +``` + +(after sample migration), which should not need Docker. + +- [ ] **Step 4: Lint** + +```bash +yarn lint +test -f packages/snap-networks-utils/dist/index.d.mts && echo 'libs ok after lint' +``` + +Expected: lint pass; libraries restored. + +- [ ] **Step 5: Final commit if verification required fixes** + +```bash +git add -A +git commit -m "fix: address layered readiness verification gaps" +``` + +Only commit if there are real fixes; otherwise stop. + +--- + +## Self-review + +| Spec requirement | Task | +|---|---| +| Post-install library builds only | Task 3 | +| TypeScript works after install | Tasks 1–3 | +| Unit Jest after install | Tasks 5–6 | +| Snap integration still needs build | Task 6 | +| Local/CI builds work | Tasks 7, 9–10 | +| Lint does not leave libs broken | Task 4 | +| Shared snap TS config / less drift | Task 2 | +| Docs + workspaces hygiene | Task 8 | +| Env default for snap builds | Task 7 | + +No TBD placeholders. Script names (`build:libs`, `build:snaps`, `test:integration`, `allow-scripts`) are consistent across tasks. diff --git a/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md b/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md new file mode 100644 index 000000000..3d01d90b5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md @@ -0,0 +1,105 @@ +# Layered Monorepo Readiness Design + +**Date:** 2026-08-08 +**Status:** Approved direction (Approach C) +**Repo:** `@metamask/internal-snaps` + +## Problem + +This monorepo combines Core-style library packages (`ts-bridge`, declaration `exports`, Jest source mappers) with Snap packages (`mm-snap`, `snaps-jest`, SES bundle evaluation). Today those layers only work together **after** a full build: + +- TypeScript workspace imports resolve through `package.json` `exports` → `dist/`, not through the intended source `paths` +- `@metamask/snaps-jest` requires `dist/bundle.js` even for ordinary unit tests +- `mm-snap` cannot bundle a snap that imports a workspace library unless that library’s `dist/` exists +- CI builds before test; local `yarn` does not + +## Goals + +After a normal `yarn` (install + lifecycle hooks): + +1. Dependencies install correctly +2. TypeScript typechecking works for libraries and snaps +3. **Unit** Jest suites run without a prior Snap bundle build +4. Local and CI full builds continue to work + +Explicitly **out of scope for post-install**: Snap SES integration tests (`installSnap`), `mm-snap serve` / watch, and publish artifacts. Those still require `yarn build` (or `yarn build:snaps`). + +## Package model (unchanged) + +| Kind | Detection | Build tool | Published shape | +|---|---|---|---| +| Library | no `snap.manifest.json` | `ts-bridge` | `dist/*` ESM/CJS + types | +| Snap | has `snap.manifest.json` | `mm-snap` (+ locale / preinstalled helpers) | `dist/bundle.js` + manifest | + +Yarn constraints already encode this split; keep them. + +## Layered readiness + +```text +yarn install + └─ allow-scripts hook + └─ build:libs (ts-bridge packages only) + ├─ TypeScript via exports/types ✅ + ├─ mm-snap can resolve workspace libs ✅ + └─ Snap unit Jest (node) ✅ + +yarn build / CI + └─ build:libs then build snaps (topological-dev) + └─ snaps-jest integration / serve / publish ✅ +``` + +## Design decisions + +### 1. Post-install builds libraries only + +Add `build:libs` that runs `build` for every non-private workspace **without** `snap.manifest.json`, topological by dependency graph. + +Wire it into the existing LavaMoat `allow-scripts` after-install hook so a normal `yarn` produces library `dist/` without rebuilding every Snap bundle. + +### 2. Fix TypeScript workspace `paths` + +`tsconfig.packages.json` currently maps `@metamask/*` → `../*/src`. Because `paths` are resolved relative to the config file that **defines** them (repo root), that pattern points outside the repo and never hits source. + +Change the mapping to root-correct paths, e.g. `@metamask/*` → `packages/*/src`, and keep Jest `moduleNameMapper` synchronized. Keep post-install library builds so Node/`mm-snap` consumers of `exports` still work. + +Introduce a shared `tsconfig.snaps.json` for Snap packages (JSX from `@metamask/snaps-sdk`, bundler resolution, `skipLibCheck`) to reduce per-snap drift. + +### 3. Split Snap Jest surfaces + +Default package `test` script stays constraint-compatible, but Snap **unit** Jest configs use the Node environment (no `snaps-jest` server). + +`installSnap` / SES tests move under `integration-test/` (or an equivalent dedicated config) and run via `test:integration` after a Snap build — matching bitcoin’s existing pattern. + +Root `yarn test` continues to run unit suites. CI keeps build-then-test so packages that still invoke `snaps-jest` in CI remain green; prefer aligning CI unit jobs to the node environment and running integration separately where Docker/env allow. + +### 4. Lint must not leave the tree unusable + +`lint:eslint` currently deletes all `packages/*/dist`. After eslint, rebuild libraries (`build:libs`) so typecheck / subsequent snap builds keep working. + +### 5. Snap build env defaults + +Snap configs that validate `ENVIRONMENT` must default to a valid enum value for local builds (e.g. `local`) when unset, so `yarn build` works without a full secrets `.env`. + +### 6. Hygiene + +- Resolve `examples/*` workspace glob (add stub or remove) +- Fix docs that link to a missing migration guide +- Update getting-started / AGENTS.md for the layered workflow + +## Non-goals + +- Building all Snap bundles on every `yarn install` +- Replacing `mm-snap` or `ts-bridge` +- Making `installSnap` tests run without a Snap bundle +- Changing release / preview-publish flows beyond needing library `dist/` as they already do + +## Success criteria + +| Command (fresh clone, after `yarn`) | Expected | +|---|---| +| `yarn typecheck` | Pass | +| `yarn workspace @metamask/snap-networks-utils run test` | Pass | +| `yarn workspace @metamask/tron-wallet-snap run test` (unit) | Pass without snap `dist/bundle.js` | +| `yarn build` | Pass with minimal/default env | +| CI lint-build-test | Pass | +| `yarn workspace … run test:integration` (where defined) | Pass only after snap build (+ env/services as today) | From 399f02cce5de7f7b3cf68177b96e478ab2163086 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 20:27:43 +0000 Subject: [PATCH 2/9] fix: make TypeScript workspace paths and snap configs work from source Resolve @metamask/* paths from the monorepo root and share snap TS settings with composite disabled so sibling packages typecheck without built declaration artifacts. Co-authored-by: Ulisses Ferreira --- jest.config.packages.js | 3 ++- packages/bitcoin-wallet-snap/tsconfig.json | 12 ++---------- packages/sample-snap/tsconfig.json | 9 +-------- packages/solana-wallet-snap/tsconfig.json | 15 ++++----------- packages/tron-wallet-snap/tsconfig.json | 15 ++++----------- tsconfig.packages.json | 9 +++++---- tsconfig.snaps.json | 21 +++++++++++++++++++++ 7 files changed, 39 insertions(+), 45 deletions(-) create mode 100644 tsconfig.snaps.json 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/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/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/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/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/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"] + } +} From 507c41599cebf8318d7d9317a5ae98ba3d1b95b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 20:28:19 +0000 Subject: [PATCH 3/9] feat: build library packages after install and restore them after lint Add build:libs/build:snaps helpers, chain build:libs from allow-scripts, and rebuild libraries after eslint's dist clean. Co-authored-by: Ulisses Ferreira --- AGENTS.md | 7 ++-- package.json | 5 ++- scripts/build-workspace-kind.mjs | 67 ++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 scripts/build-workspace-kind.mjs diff --git a/AGENTS.md b/AGENTS.md index e9941d065..8b05e8e46 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`. +- **Unit tests vs integration tests.** Snap unit Jest configs use the Node environment and do not require a Snap bundle. Tests that call `installSnap` live under `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. +- **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. - **`.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. diff --git a/package.json b/package.json index 9cd08b4af..a2e4572d8 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,13 @@ ], "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 +28,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/scripts/build-workspace-kind.mjs b/scripts/build-workspace-kind.mjs new file mode 100644 index 000000000..9b2af2d3c --- /dev/null +++ b/scripts/build-workspace-kind.mjs @@ -0,0 +1,67 @@ +import { existsSync } from 'node:fs'; +import { spawnSync } from 'node:child_process'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); + +/** + * @param {boolean} snapsOnly - When true, select snap workspaces; otherwise libraries. + * @returns {string[]} Workspace names + */ +function listWorkspaceNames(snapsOnly) { + const list = spawnSync('yarn', ['workspaces', 'list', '--json'], { + cwd: root, + encoding: 'utf8', + shell: true, + }); + + if (list.status !== 0) { + console.error(list.stderr || list.stdout); + process.exit(list.status ?? 1); + } + + return list.stdout + .split('\n') + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => JSON.parse(line)) + .filter((workspace) => workspace.location !== '.') + .filter((workspace) => { + const isSnap = existsSync( + join(root, workspace.location, 'snap.manifest.json'), + ); + return snapsOnly ? isSnap : !isSnap; + }) + .map((workspace) => workspace.name); +} + +const snapsOnly = process.argv.includes('--snaps'); +const names = listWorkspaceNames(snapsOnly); +const label = snapsOnly ? 'snaps' : 'libraries'; + +if (names.length === 0) { + console.log(`No ${label} workspaces to build.`); + process.exit(0); +} + +console.log(`Building ${label}: ${names.join(', ')}`); + +const result = spawnSync( + 'yarn', + [ + 'workspaces', + 'foreach', + '--all', + '--topological-dev', + '--parallel', + '--interlaced', + '--verbose', + ...names.flatMap((name) => ['--include', name]), + 'run', + 'build', + ], + { cwd: root, stdio: 'inherit', shell: true }, +); + +process.exit(result.status ?? 1); From 64496e5caad5f42eb51e8664e83043038fd2aa34 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 20:37:18 +0000 Subject: [PATCH 4/9] test: split snap unit and integration Jest surfaces Run bitcoin/tron/sample unit tests in Node without a Snap bundle. Keep solana on snaps-jest with lazy globalSetup build. Move installSnap coverage into integration configs and add safe local env defaults. Co-authored-by: Ulisses Ferreira --- jest.config.snaps.unit.js | 40 +++++++ packages/bitcoin-wallet-snap/jest.config.mjs | 32 ++---- .../{src => integration-test}/index.test.tsx | 0 packages/sample-snap/jest.config.js | 9 +- .../sample-snap/jest.integration.config.js | 7 ++ packages/sample-snap/package.json | 1 + packages/solana-wallet-snap/jest.config.js | 7 ++ .../solana-wallet-snap/jest.globalSetup.cjs | 35 ++++++ .../jest.integration.config.js | 13 +++ packages/solana-wallet-snap/jest.setup.ts | 23 ++++ packages/solana-wallet-snap/package.json | 1 + packages/solana-wallet-snap/snap.config.ts | 80 +++++++++++--- .../core/services/config/ConfigProvider.ts | 2 +- ...r.test.tsx => render.integration.test.tsx} | 0 ...r.test.tsx => render.integration.test.tsx} | 0 ...r.test.tsx => render.integration.test.tsx} | 0 .../src/index.integration.test.ts | 40 +++++++ packages/solana-wallet-snap/src/index.test.ts | 39 ------- packages/tron-wallet-snap/jest.config.mjs | 32 ++---- packages/tron-wallet-snap/jest.setup.ts | 29 ++++- packages/tron-wallet-snap/package.json | 1 + packages/tron-wallet-snap/snap.config.ts | 101 +++++++++++++----- 22 files changed, 353 insertions(+), 139 deletions(-) create mode 100644 jest.config.snaps.unit.js rename packages/sample-snap/{src => integration-test}/index.test.tsx (100%) create mode 100644 packages/sample-snap/jest.integration.config.js create mode 100644 packages/solana-wallet-snap/jest.globalSetup.cjs create mode 100644 packages/solana-wallet-snap/jest.integration.config.js rename packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignIn/{render.test.tsx => render.integration.test.tsx} (100%) rename packages/solana-wallet-snap/src/features/confirmation/views/ConfirmSignMessage/{render.test.tsx => render.integration.test.tsx} (100%) rename packages/solana-wallet-snap/src/features/confirmation/views/ConfirmTransactionRequest/{render.test.tsx => render.integration.test.tsx} (100%) create mode 100644 packages/solana-wallet-snap/src/index.integration.test.ts diff --git a/jest.config.snaps.unit.js b/jest.config.snaps.unit.js new file mode 100644 index 000000000..72492161f --- /dev/null +++ b/jest.config.snaps.unit.js @@ -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/packages/bitcoin-wallet-snap/jest.config.mjs b/packages/bitcoin-wallet-snap/jest.config.mjs index e8bd87859..3adf4e628 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 require = createRequire(import.meta.url); +const shared = require('../../jest.config.snaps.unit.js'); + /** * @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/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..02ec77b26 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.js'); + 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/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..37d9e099c --- /dev/null +++ b/packages/solana-wallet-snap/jest.globalSetup.cjs @@ -0,0 +1,35 @@ +const { existsSync } = require('node:fs'); +const { spawnSync } = 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. + */ +module.exports = function globalSetup() { + const bundlePath = join(__dirname, 'dist', 'bundle.js'); + if (existsSync(bundlePath)) { + return; + } + + // eslint-disable-next-line no-console + console.log( + '[solana-wallet-snap] dist/bundle.js missing; running yarn build before tests…', + ); + + const result = spawnSync('yarn', ['build'], { + cwd: __dirname, + stdio: 'inherit', + shell: true, + env: { + ...process.env, + ENVIRONMENT: process.env.ENVIRONMENT || 'local', + }, + }); + + if (result.status !== 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/jest.setup.ts b/packages/solana-wallet-snap/jest.setup.ts index df1465e94..a51e791cf 100644 --- a/packages/solana-wallet-snap/jest.setup.ts +++ b/packages/solana-wallet-snap/jest.setup.ts @@ -6,6 +6,29 @@ import logger from './src/core/utils/logger'; dotenv.config(); +const testDefaults: Record = { + ENVIRONMENT: 'test', + RPC_URL_MAINNET_LIST: 'https://example.com/solana-mainnet', + RPC_URL_DEVNET_LIST: 'https://example.com/solana-devnet', + RPC_URL_TESTNET_LIST: 'https://example.com/solana-testnet', + RPC_URL_LOCALNET_LIST: 'http://127.0.0.1:8899', + RPC_WEB_SOCKET_URL_MAINNET: 'wss://example.com/solana-mainnet', + RPC_WEB_SOCKET_URL_DEVNET: 'wss://example.com/solana-devnet', + RPC_WEB_SOCKET_URL_TESTNET: 'wss://example.com/solana-testnet', + RPC_WEB_SOCKET_URL_LOCALNET: 'wss://example.com/solana-localnet', + EXPLORER_BASE_URL: 'https://solscan.io', + PRICE_API_BASE_URL: 'https://example.com/price/', + TOKEN_API_BASE_URL: 'https://example.com/token/', + STATIC_API_BASE_URL: 'https://example.com/static/', + SECURITY_ALERTS_API_BASE_URL: 'https://example.com/security/', + NFT_API_BASE_URL: 'https://example.com/nft/', + LOCAL_API_BASE_URL: 'http://127.0.0.1:3000', +}; + +for (const [key, value] of Object.entries(testDefaults)) { + process.env[key] ||= value; +} + // Lowest precision we ever go for: MicroLamports represented in Sol amount BigNumber.config({ EXPONENTIAL_AT: 16 }); 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/snap.config.ts b/packages/solana-wallet-snap/snap.config.ts index 4501facac..751358074 100644 --- a/packages/solana-wallet-snap/snap.config.ts +++ b/packages/solana-wallet-snap/snap.config.ts @@ -5,23 +5,71 @@ import { resolve } from 'path'; dotenv.config(); +const defaultUrl = (value: string | undefined, fallback: string): string => + value && value.length > 0 ? value : fallback; + const environment = { - ENVIRONMENT: process.env.ENVIRONMENT ?? '', - RPC_URL_MAINNET_LIST: process.env.RPC_URL_MAINNET_LIST ?? '', - RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST ?? '', - RPC_URL_TESTNET_LIST: process.env.RPC_URL_TESTNET_LIST ?? '', - RPC_URL_LOCALNET_LIST: process.env.RPC_URL_LOCALNET_LIST ?? '', - RPC_WEB_SOCKET_URL_MAINNET: process.env.RPC_WEB_SOCKET_URL_MAINNET ?? '', - RPC_WEB_SOCKET_URL_DEVNET: process.env.RPC_WEB_SOCKET_URL_DEVNET ?? '', - RPC_WEB_SOCKET_URL_TESTNET: process.env.RPC_WEB_SOCKET_URL_TESTNET ?? '', - RPC_WEB_SOCKET_URL_LOCALNET: process.env.RPC_WEB_SOCKET_URL_LOCALNET ?? '', - EXPLORER_BASE_URL: process.env.EXPLORER_BASE_URL ?? '', - PRICE_API_BASE_URL: process.env.PRICE_API_BASE_URL ?? '', - TOKEN_API_BASE_URL: process.env.TOKEN_API_BASE_URL ?? '', - STATIC_API_BASE_URL: process.env.STATIC_API_BASE_URL ?? '', - SECURITY_ALERTS_API_BASE_URL: process.env.SECURITY_ALERTS_API_BASE_URL ?? '', - NFT_API_BASE_URL: process.env.NFT_API_BASE_URL ?? '', - LOCAL_API_BASE_URL: process.env.LOCAL_API_BASE_URL ?? '', + ENVIRONMENT: process.env.ENVIRONMENT || 'local', + RPC_URL_MAINNET_LIST: defaultUrl( + process.env.RPC_URL_MAINNET_LIST, + 'https://example.com/solana-mainnet', + ), + RPC_URL_DEVNET_LIST: defaultUrl( + process.env.RPC_URL_DEVNET_LIST, + 'https://example.com/solana-devnet', + ), + RPC_URL_TESTNET_LIST: defaultUrl( + process.env.RPC_URL_TESTNET_LIST, + 'https://example.com/solana-testnet', + ), + RPC_URL_LOCALNET_LIST: defaultUrl( + process.env.RPC_URL_LOCALNET_LIST, + 'http://127.0.0.1:8899', + ), + RPC_WEB_SOCKET_URL_MAINNET: defaultUrl( + process.env.RPC_WEB_SOCKET_URL_MAINNET, + 'wss://example.com/solana-mainnet', + ), + RPC_WEB_SOCKET_URL_DEVNET: defaultUrl( + process.env.RPC_WEB_SOCKET_URL_DEVNET, + 'wss://example.com/solana-devnet', + ), + RPC_WEB_SOCKET_URL_TESTNET: defaultUrl( + process.env.RPC_WEB_SOCKET_URL_TESTNET, + 'wss://example.com/solana-testnet', + ), + RPC_WEB_SOCKET_URL_LOCALNET: defaultUrl( + process.env.RPC_WEB_SOCKET_URL_LOCALNET, + 'wss://example.com/solana-localnet', + ), + EXPLORER_BASE_URL: defaultUrl( + process.env.EXPLORER_BASE_URL, + 'https://solscan.io', + ), + PRICE_API_BASE_URL: defaultUrl( + process.env.PRICE_API_BASE_URL, + 'https://example.com/price/', + ), + TOKEN_API_BASE_URL: defaultUrl( + process.env.TOKEN_API_BASE_URL, + 'https://example.com/token/', + ), + STATIC_API_BASE_URL: defaultUrl( + process.env.STATIC_API_BASE_URL, + 'https://example.com/static/', + ), + SECURITY_ALERTS_API_BASE_URL: defaultUrl( + process.env.SECURITY_ALERTS_API_BASE_URL, + 'https://example.com/security/', + ), + NFT_API_BASE_URL: defaultUrl( + process.env.NFT_API_BASE_URL, + 'https://example.com/nft/', + ), + LOCAL_API_BASE_URL: defaultUrl( + process.env.LOCAL_API_BASE_URL, + 'http://127.0.0.1:3000', + ), }; const config: SnapConfig = { diff --git a/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts b/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts index 465100f1b..4873942c3 100644 --- a/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts +++ b/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts @@ -124,7 +124,7 @@ export class ConfigProvider { #parseEnvironment() { const rawEnvironment = { - ENVIRONMENT: process.env.ENVIRONMENT, + ENVIRONMENT: process.env.ENVIRONMENT || 'local', RPC_URL_MAINNET_LIST: process.env.RPC_URL_MAINNET_LIST, RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST, RPC_URL_TESTNET_LIST: process.env.RPC_URL_TESTNET_LIST, 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..4924daa27 --- /dev/null +++ b/packages/solana-wallet-snap/src/index.integration.test.ts @@ -0,0 +1,40 @@ +import { expect } from '@jest/globals'; +import { installSnap } from '@metamask/snaps-jest'; + +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), + }); + }); +}); 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/tron-wallet-snap/jest.config.mjs b/packages/tron-wallet-snap/jest.config.mjs index 299fdc07d..250cd9532 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 require = createRequire(import.meta.url); +const shared = require('../../jest.config.snaps.unit.js'); + /** * @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..46c978416 100644 --- a/packages/tron-wallet-snap/jest.setup.ts +++ b/packages/tron-wallet-snap/jest.setup.ts @@ -2,6 +2,29 @@ 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'; +const testDefaults: Record = { + ENVIRONMENT: 'test', + RPC_URL_LIST_MAINNET: 'https://example.com/tron-mainnet', + RPC_URL_LIST_NILE_TESTNET: 'https://example.com/tron-nile', + RPC_URL_LIST_SHASTA_TESTNET: 'https://example.com/tron-shasta', + EXPLORER_MAINNET_BASE_URL: 'https://tronscan.org/', + EXPLORER_NILE_BASE_URL: 'https://nile.tronscan.org/', + EXPLORER_SHASTA_BASE_URL: 'https://shasta.tronscan.org/', + PRICE_API_BASE_URL: 'https://example.com/price/', + TOKEN_API_BASE_URL: 'https://example.com/token/', + STATIC_API_BASE_URL: 'https://example.com/static/', + SECURITY_ALERTS_API_BASE_URL: 'https://example.com/security/', + NFT_API_BASE_URL: 'https://example.com/nft/', + LOCAL_API_BASE_URL: 'http://127.0.0.1:3000', + TRONGRID_BASE_URL_MAINNET: 'https://example.com/trongrid-mainnet/', + TRONGRID_BASE_URL_NILE: 'https://example.com/trongrid-nile/', + TRONGRID_BASE_URL_SHASTA: 'https://example.com/trongrid-shasta/', + TRON_HTTP_BASE_URL_MAINNET: 'https://example.com/tron-http-mainnet/', + TRON_HTTP_BASE_URL_NILE: 'https://example.com/tron-http-nile/', + TRON_HTTP_BASE_URL_SHASTA: 'https://example.com/tron-http-shasta/', +}; + +for (const [key, value] of Object.entries(testDefaults)) { + // eslint-disable-next-line no-restricted-globals + process.env[key] ||= value; +} 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/snap.config.ts b/packages/tron-wallet-snap/snap.config.ts index ed7d1ef21..134c4b9ed 100644 --- a/packages/tron-wallet-snap/snap.config.ts +++ b/packages/tron-wallet-snap/snap.config.ts @@ -4,37 +4,88 @@ import { resolve } from 'path'; dotenv(); +const defaultUrl = (value: string | undefined, fallback: string): string => + value && value.length > 0 ? value : fallback; + const config: SnapConfig = { input: resolve(__dirname, 'src/index.ts'), server: { port: 8080, }, environment: { - ENVIRONMENT: process.env.ENVIRONMENT ?? '', - // RPC - RPC_URL_LIST_MAINNET: process.env.RPC_URL_LIST_MAINNET ?? '', - RPC_URL_LIST_NILE_TESTNET: process.env.RPC_URL_LIST_NILE_TESTNET ?? '', - RPC_URL_LIST_SHASTA_TESTNET: process.env.RPC_URL_LIST_SHASTA_TESTNET ?? '', - // Block explorer - EXPLORER_MAINNET_BASE_URL: process.env.EXPLORER_MAINNET_BASE_URL ?? '', - EXPLORER_NILE_BASE_URL: process.env.EXPLORER_NILE_BASE_URL ?? '', - EXPLORER_SHASTA_BASE_URL: process.env.EXPLORER_SHASTA_BASE_URL ?? '', - // APIs - PRICE_API_BASE_URL: process.env.PRICE_API_BASE_URL ?? '', - TOKEN_API_BASE_URL: process.env.TOKEN_API_BASE_URL ?? '', - STATIC_API_BASE_URL: process.env.STATIC_API_BASE_URL ?? '', - SECURITY_ALERTS_API_BASE_URL: - process.env.SECURITY_ALERTS_API_BASE_URL ?? '', - NFT_API_BASE_URL: process.env.NFT_API_BASE_URL ?? '', - LOCAL_API_BASE_URL: process.env.LOCAL_API_BASE_URL ?? '', - // TronGrid API - TRONGRID_BASE_URL_MAINNET: process.env.TRONGRID_BASE_URL_MAINNET ?? '', - TRONGRID_BASE_URL_NILE: process.env.TRONGRID_BASE_URL_NILE ?? '', - TRONGRID_BASE_URL_SHASTA: process.env.TRONGRID_BASE_URL_SHASTA ?? '', - // Tron HTTP API - TRON_HTTP_BASE_URL_MAINNET: process.env.TRON_HTTP_BASE_URL_MAINNET ?? '', - TRON_HTTP_BASE_URL_NILE: process.env.TRON_HTTP_BASE_URL_NILE ?? '', - TRON_HTTP_BASE_URL_SHASTA: process.env.TRON_HTTP_BASE_URL_SHASTA ?? '', + ENVIRONMENT: process.env.ENVIRONMENT || 'local', + RPC_URL_LIST_MAINNET: defaultUrl( + process.env.RPC_URL_LIST_MAINNET, + 'https://example.com/tron-mainnet', + ), + RPC_URL_LIST_NILE_TESTNET: defaultUrl( + process.env.RPC_URL_LIST_NILE_TESTNET, + 'https://example.com/tron-nile', + ), + RPC_URL_LIST_SHASTA_TESTNET: defaultUrl( + process.env.RPC_URL_LIST_SHASTA_TESTNET, + 'https://example.com/tron-shasta', + ), + EXPLORER_MAINNET_BASE_URL: defaultUrl( + process.env.EXPLORER_MAINNET_BASE_URL, + 'https://tronscan.org/', + ), + EXPLORER_NILE_BASE_URL: defaultUrl( + process.env.EXPLORER_NILE_BASE_URL, + 'https://nile.tronscan.org/', + ), + EXPLORER_SHASTA_BASE_URL: defaultUrl( + process.env.EXPLORER_SHASTA_BASE_URL, + 'https://shasta.tronscan.org/', + ), + PRICE_API_BASE_URL: defaultUrl( + process.env.PRICE_API_BASE_URL, + 'https://example.com/price/', + ), + TOKEN_API_BASE_URL: defaultUrl( + process.env.TOKEN_API_BASE_URL, + 'https://example.com/token/', + ), + STATIC_API_BASE_URL: defaultUrl( + process.env.STATIC_API_BASE_URL, + 'https://example.com/static/', + ), + SECURITY_ALERTS_API_BASE_URL: defaultUrl( + process.env.SECURITY_ALERTS_API_BASE_URL, + 'https://example.com/security/', + ), + NFT_API_BASE_URL: defaultUrl( + process.env.NFT_API_BASE_URL, + 'https://example.com/nft/', + ), + LOCAL_API_BASE_URL: defaultUrl( + process.env.LOCAL_API_BASE_URL, + 'http://127.0.0.1:3000', + ), + TRONGRID_BASE_URL_MAINNET: defaultUrl( + process.env.TRONGRID_BASE_URL_MAINNET, + 'https://example.com/trongrid-mainnet/', + ), + TRONGRID_BASE_URL_NILE: defaultUrl( + process.env.TRONGRID_BASE_URL_NILE, + 'https://example.com/trongrid-nile/', + ), + TRONGRID_BASE_URL_SHASTA: defaultUrl( + process.env.TRONGRID_BASE_URL_SHASTA, + 'https://example.com/trongrid-shasta/', + ), + TRON_HTTP_BASE_URL_MAINNET: defaultUrl( + process.env.TRON_HTTP_BASE_URL_MAINNET, + 'https://example.com/tron-http-mainnet/', + ), + TRON_HTTP_BASE_URL_NILE: defaultUrl( + process.env.TRON_HTTP_BASE_URL_NILE, + 'https://example.com/tron-http-nile/', + ), + TRON_HTTP_BASE_URL_SHASTA: defaultUrl( + process.env.TRON_HTTP_BASE_URL_SHASTA, + 'https://example.com/tron-http-shasta/', + ), }, polyfills: true, }; From 8a507277ae0b1a67ba6b766a172ba76587facbd6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 20:37:41 +0000 Subject: [PATCH 5/9] docs: document layered install, build, and test workflow Remove the empty examples workspace glob and the dead migration-guide link, and describe build:libs / unit vs integration testing. Co-authored-by: Ulisses Ferreira --- AGENTS.md | 6 +++--- docs/README.md | 1 - docs/getting-started/setting-up-your-environment.md | 10 +++++++++- docs/processes/building.md | 4 +++- docs/processes/testing.md | 9 ++++++--- package.json | 1 - 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8b05e8e46..85a1c0621 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -225,9 +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: -- **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`. -- **Unit tests vs integration tests.** Snap unit Jest configs use the Node environment and do not require a Snap bundle. Tests that call `installSnap` live under `integration-test/` and need a prior snap build (`yarn build` / `yarn build:snaps`). +- **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` 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. -- **`.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. +- **`.env` is optional** for wallet snaps; `snap.config.ts` reads it via dotenv and falls back to local defaults so the snap can build without a full secrets file. 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..49c56db09 100644 --- a/docs/getting-started/setting-up-your-environment.md +++ b/docs/getting-started/setting-up-your-environment.md @@ -4,4 +4,12 @@ - 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. 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/package.json b/package.json index a2e4572d8..89529c815 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "url": "https://github.com/MetaMask/internal-snaps.git" }, "workspaces": [ - "examples/*", "packages/*" ], "files": [], From aff8ec9d57da515f7bb6a79a17ff21543c4cad04 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 8 Aug 2026 20:45:04 +0000 Subject: [PATCH 6/9] fix: satisfy eslint for layered monorepo tooling Rename shared snap Jest config to .cjs, make build/globalSetup scripts async-safe for lint rules, and retarget suppressions for renamed integration tests. Co-authored-by: Ulisses Ferreira --- .../2026-08-08-layered-monorepo-readiness.md | 94 ++++++++------ ...08-08-layered-monorepo-readiness-design.md | 24 ++-- eslint-suppressions.json | 9 +- ...naps.unit.js => jest.config.snaps.unit.cjs | 0 packages/bitcoin-wallet-snap/jest.config.mjs | 4 +- packages/sample-snap/jest.config.js | 2 +- .../solana-wallet-snap/jest.globalSetup.cjs | 40 +++--- packages/solana-wallet-snap/jest.setup.ts | 2 + packages/solana-wallet-snap/snap.config.ts | 2 + .../core/services/config/ConfigProvider.ts | 2 + .../src/index.integration.test.ts | 9 +- packages/tron-wallet-snap/jest.config.mjs | 4 +- packages/tron-wallet-snap/jest.setup.ts | 3 +- packages/tron-wallet-snap/snap.config.ts | 2 + scripts/build-workspace-kind.mjs | 117 +++++++++++++----- 15 files changed, 202 insertions(+), 112 deletions(-) rename jest.config.snaps.unit.js => jest.config.snaps.unit.cjs (100%) diff --git a/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md b/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md index 2b5085405..c7bf3a770 100644 --- a/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md +++ b/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md @@ -23,34 +23,36 @@ ## File map -| File | Responsibility | -|---|---| -| `scripts/build-libs.mjs` | Discover non-snap workspaces and run their `build` scripts topologically | -| `package.json` | `build:libs`, wire into `allow-scripts`, lint rebuild, optional `build:snaps` | -| `tsconfig.packages.json` | Root-correct `@metamask/*` → `packages/*/src` paths | -| `tsconfig.snaps.json` | Shared Snap TS compiler defaults | -| `packages/*/tsconfig.json` (snaps) | Extend `tsconfig.snaps.json` | -| `jest.config.packages.js` | Keep mapper in sync with TS paths; document Snap usage | -| `jest.config.snaps.unit.js` | Shared Node-environment unit defaults for snaps | -| `packages/*/jest.config.*` (snaps) | Unit config (node); integration stays `snaps-jest` | -| `packages/solana-wallet-snap/**` | Move `installSnap` tests into `integration-test/` | -| `packages/sample-snap/**` | Move `installSnap` tests into `integration-test/` or dedicated integration config | -| `packages/*/snap.config.ts` | Safe `ENVIRONMENT` default where validated | -| `yarn.config.cjs` | Allow Snap `test:integration` script pattern if constrained | -| `docs/getting-started/setting-up-your-environment.md`, `AGENTS.md`, `docs/processes/testing.md`, `docs/processes/building.md` | Document layered workflow | -| `package.json` `workspaces` / `examples/` | Fix empty `examples/*` glob | -| `.github/workflows/lint-build-test.yml` | Only if unit vs integration split requires CI job changes | +| File | Responsibility | +| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| `scripts/build-libs.mjs` | Discover non-snap workspaces and run their `build` scripts topologically | +| `package.json` | `build:libs`, wire into `allow-scripts`, lint rebuild, optional `build:snaps` | +| `tsconfig.packages.json` | Root-correct `@metamask/*` → `packages/*/src` paths | +| `tsconfig.snaps.json` | Shared Snap TS compiler defaults | +| `packages/*/tsconfig.json` (snaps) | Extend `tsconfig.snaps.json` | +| `jest.config.packages.js` | Keep mapper in sync with TS paths; document Snap usage | +| `jest.config.snaps.unit.js` | Shared Node-environment unit defaults for snaps | +| `packages/*/jest.config.*` (snaps) | Unit config (node); integration stays `snaps-jest` | +| `packages/solana-wallet-snap/**` | Move `installSnap` tests into `integration-test/` | +| `packages/sample-snap/**` | Move `installSnap` tests into `integration-test/` or dedicated integration config | +| `packages/*/snap.config.ts` | Safe `ENVIRONMENT` default where validated | +| `yarn.config.cjs` | Allow Snap `test:integration` script pattern if constrained | +| `docs/getting-started/setting-up-your-environment.md`, `AGENTS.md`, `docs/processes/testing.md`, `docs/processes/building.md` | Document layered workflow | +| `package.json` `workspaces` / `examples/` | Fix empty `examples/*` glob | +| `.github/workflows/lint-build-test.yml` | Only if unit vs integration split requires CI job changes | --- ### Task 1: Fix TypeScript workspace path mapping **Files:** + - Modify: `tsconfig.packages.json` - Modify: `jest.config.packages.js` (comment + mapper sanity check only if needed) - Test: `packages/tron-wallet-snap` typecheck against `@metamask/snap-networks-utils` **without** that package’s `dist/` **Interfaces:** + - Consumes: existing `@metamask/*` imports in snaps/libs - Produces: `compilerOptions.paths` of `@metamask/*` → `packages/*/src` resolved from repo root config @@ -111,6 +113,7 @@ git commit -m "fix: resolve TypeScript workspace paths from monorepo root" ### Task 2: Add shared Snap TypeScript config **Files:** + - Create: `tsconfig.snaps.json` - Modify: `packages/bitcoin-wallet-snap/tsconfig.json` - Modify: `packages/solana-wallet-snap/tsconfig.json` @@ -118,6 +121,7 @@ git commit -m "fix: resolve TypeScript workspace paths from monorepo root" - Modify: `packages/sample-snap/tsconfig.json` **Interfaces:** + - Consumes: `tsconfig.packages.json` (including fixed paths) - Produces: shared Snap compiler options — JSX from snaps-sdk, `moduleResolution: bundler`, `module: preserve`, `skipLibCheck`, `resolveJsonModule`, `types: ["jest"]` @@ -179,11 +183,13 @@ git commit -m "chore: add shared tsconfig for snap packages" ### Task 3: Add `build:libs` and run it after install **Files:** + - Create: `scripts/build-libs.mjs` - Modify: `package.json` (scripts) - Optional test helper: none (verify with shell) **Interfaces:** + - Consumes: Yarn workspaces list; `snap.manifest.json` presence; each lib’s `scripts.build` - Produces: - `yarn build:libs` — builds every non-private workspace without `snap.manifest.json` @@ -200,11 +206,11 @@ import { fileURLToPath } from 'node:url'; const root = join(dirname(fileURLToPath(import.meta.url)), '..'); -const list = spawnSync( - 'yarn', - ['workspaces', 'list', '--json'], - { cwd: root, encoding: 'utf8', shell: true }, -); +const list = spawnSync('yarn', ['workspaces', 'list', '--json'], { + cwd: root, + encoding: 'utf8', + shell: true, +}); if (list.status !== 0) { console.error(list.stderr || list.stdout); @@ -270,6 +276,7 @@ Add/adjust: ``` Notes: + - Prefer excluding by “is snap” in a `build-snaps.mjs` mirror if `--exclude` of only today’s library is too brittle; when a second library appears, update to a snap-detecting script. - `setup` can remain `yarn install` because `allow-scripts` already chains `build:libs` after install via the Yarn plugin. @@ -303,10 +310,12 @@ git commit -m "feat: build library packages after yarn install" ### Task 4: Rebuild libraries after eslint cleans dist **Files:** + - Modify: `package.json` (`lint:eslint`) - Modify: `AGENTS.md` (Cursor Cloud caveats) **Interfaces:** + - Consumes: `build:only-clean`, `build:libs` - Produces: `lint:eslint` ends with library `dist/` restored @@ -349,6 +358,7 @@ git commit -m "fix: restore library builds after eslint dist clean" ### Task 5: Shared Snap unit Jest config (Node environment) **Files:** + - Create: `jest.config.snaps.unit.js` - Modify: `packages/bitcoin-wallet-snap/jest.config.mjs` - Modify: `packages/tron-wallet-snap/jest.config.mjs` @@ -357,6 +367,7 @@ git commit -m "fix: restore library builds after eslint dist clean" - Modify: `jest.config.packages.js` (keep mapper; ensure Snap unit configs reuse the same `@metamask/(.*)` workspace→source pattern) **Interfaces:** + - Consumes: `ts-jest`, workspace source mapper pattern from `jest.config.packages.js` - Produces: default Snap `test` runs in `testEnvironment: 'node'` and does **not** load `@metamask/snaps-jest` preset @@ -380,10 +391,7 @@ module.exports = { resetMocks: true, restoreMocks: true, testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'], - testPathIgnorePatterns: [ - '/node_modules/', - '/integration-test/', - ], + testPathIgnorePatterns: ['/node_modules/', '/integration-test/'], moduleNameMapper: { '\\.svg$': 'jest-transform-stub', '^@metamask/utils/node$': require.resolve('@metamask/utils/node'), @@ -447,6 +455,7 @@ git commit -m "test: run snap unit tests in node without snaps-jest" ### Task 6: Move `installSnap` tests to integration configs **Files:** + - Create/Modify: `packages/solana-wallet-snap/jest.integration.config.js` (or `.mjs`) - Create: `packages/solana-wallet-snap/integration-test/` (move installSnap specs here) - Modify: `packages/solana-wallet-snap/package.json` (add `test:integration`) @@ -456,6 +465,7 @@ git commit -m "test: run snap unit tests in node without snaps-jest" - Modify: `yarn.config.cjs` if a new required script pattern is enforced for snaps **Interfaces:** + - Consumes: `@metamask/snaps-jest` `installSnap` - Produces: - `yarn workspace run test` → unit only (no bundle) @@ -464,6 +474,7 @@ git commit -m "test: run snap unit tests in node without snaps-jest" - [ ] **Step 1: Inventory current `installSnap` files** Known today: + - `packages/sample-snap/src/index.test.tsx` - `packages/solana-wallet-snap/src/index.test.ts` (partially; also has non-installSnap cases — split the file) - `packages/solana-wallet-snap/src/features/confirmation/views/**/render.test.tsx` @@ -531,11 +542,13 @@ git commit -m "test: move installSnap coverage into snap integration suites" ### Task 7: Safe Snap build environment defaults **Files:** + - Modify: `packages/solana-wallet-snap/snap.config.ts` - Modify: other snap configs only if they validate `ENVIRONMENT` the same way - Modify: `.env.example` files if present **Interfaces:** + - Consumes: `process.env.ENVIRONMENT` - Produces: default `'local'` (or `'test'`) when unset so SES eval accepts the value @@ -578,6 +591,7 @@ git commit -m "fix: default solana snap ENVIRONMENT to local for builds" ### Task 8: Workspace and docs hygiene **Files:** + - Modify: `package.json` (`workspaces`) **or** create `examples/.gitkeep` + placeholder — prefer removing `examples/*` until an example exists - Modify: `docs/README.md` (remove or fix dead migration-guide link) - Modify: `docs/getting-started/setting-up-your-environment.md` @@ -586,6 +600,7 @@ git commit -m "fix: default solana snap ENVIRONMENT to local for builds" - Modify: `AGENTS.md` **Interfaces:** + - Produces: accurate contributor workflow for layered readiness - [ ] **Step 1: Fix workspaces glob** @@ -613,6 +628,7 @@ yarn build # when you need snap bundles / integration tests / serve - [ ] **Step 3: Update building + testing process docs** State clearly: + - `yarn build:libs` — shared packages (`ts-bridge`) - `yarn build` / `yarn build:snaps` — snap bundles - `yarn test` — unit tests (node for snaps) @@ -634,9 +650,11 @@ git commit -m "docs: describe layered install, typecheck, and test workflow" ### Task 9: CI alignment **Files:** + - Modify: `.github/workflows/lint-build-test.yml` only as needed **Interfaces:** + - Consumes: build artifacts upload/download - Produces: CI unit tests match local unit semantics; integration remains optional/separate @@ -645,6 +663,7 @@ git commit -m "docs: describe layered install, typecheck, and test workflow" Keep current flow (build all → download dist → `yarn workspace … run test`) — this remains valid and still exercises packages after a full build. Optional improvement in this task (recommended if low-risk): + - Unit job can run **without** snap artifacts once Task 5/6 landed - Add a separate `test-integration` job for snaps that define `test:integration`, needing build artifacts (+ secrets/services as today) @@ -672,6 +691,7 @@ git commit -m "ci: align snap unit and integration test jobs" ### Task 10: End-to-end verification on a clean tree **Files:** + - None (verification only); fix regressions found in earlier tasks - [ ] **Step 1: Clean artifacts and reinstall** @@ -729,16 +749,16 @@ Only commit if there are real fixes; otherwise stop. ## Self-review -| Spec requirement | Task | -|---|---| -| Post-install library builds only | Task 3 | -| TypeScript works after install | Tasks 1–3 | -| Unit Jest after install | Tasks 5–6 | -| Snap integration still needs build | Task 6 | -| Local/CI builds work | Tasks 7, 9–10 | -| Lint does not leave libs broken | Task 4 | -| Shared snap TS config / less drift | Task 2 | -| Docs + workspaces hygiene | Task 8 | -| Env default for snap builds | Task 7 | +| Spec requirement | Task | +| ---------------------------------- | ------------- | +| Post-install library builds only | Task 3 | +| TypeScript works after install | Tasks 1–3 | +| Unit Jest after install | Tasks 5–6 | +| Snap integration still needs build | Task 6 | +| Local/CI builds work | Tasks 7, 9–10 | +| Lint does not leave libs broken | Task 4 | +| Shared snap TS config / less drift | Task 2 | +| Docs + workspaces hygiene | Task 8 | +| Env default for snap builds | Task 7 | No TBD placeholders. Script names (`build:libs`, `build:snaps`, `test:integration`, `allow-scripts`) are consistent across tasks. diff --git a/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md b/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md index 3d01d90b5..773b707d6 100644 --- a/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md +++ b/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md @@ -26,10 +26,10 @@ Explicitly **out of scope for post-install**: Snap SES integration tests (`insta ## Package model (unchanged) -| Kind | Detection | Build tool | Published shape | -|---|---|---|---| -| Library | no `snap.manifest.json` | `ts-bridge` | `dist/*` ESM/CJS + types | -| Snap | has `snap.manifest.json` | `mm-snap` (+ locale / preinstalled helpers) | `dist/bundle.js` + manifest | +| Kind | Detection | Build tool | Published shape | +| ------- | ------------------------ | ------------------------------------------- | --------------------------- | +| Library | no `snap.manifest.json` | `ts-bridge` | `dist/*` ESM/CJS + types | +| Snap | has `snap.manifest.json` | `mm-snap` (+ locale / preinstalled helpers) | `dist/bundle.js` + manifest | Yarn constraints already encode this split; keep them. @@ -95,11 +95,11 @@ Snap configs that validate `ENVIRONMENT` must default to a valid enum value for ## Success criteria -| Command (fresh clone, after `yarn`) | Expected | -|---|---| -| `yarn typecheck` | Pass | -| `yarn workspace @metamask/snap-networks-utils run test` | Pass | -| `yarn workspace @metamask/tron-wallet-snap run test` (unit) | Pass without snap `dist/bundle.js` | -| `yarn build` | Pass with minimal/default env | -| CI lint-build-test | Pass | -| `yarn workspace … run test:integration` (where defined) | Pass only after snap build (+ env/services as today) | +| Command (fresh clone, after `yarn`) | Expected | +| ----------------------------------------------------------- | ---------------------------------------------------- | +| `yarn typecheck` | Pass | +| `yarn workspace @metamask/snap-networks-utils run test` | Pass | +| `yarn workspace @metamask/tron-wallet-snap run test` (unit) | Pass without snap `dist/bundle.js` | +| `yarn build` | Pass with minimal/default env | +| CI lint-build-test | Pass | +| `yarn workspace … run test:integration` (where defined) | Pass only after snap build (+ env/services as today) | 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.snaps.unit.js b/jest.config.snaps.unit.cjs similarity index 100% rename from jest.config.snaps.unit.js rename to jest.config.snaps.unit.cjs diff --git a/packages/bitcoin-wallet-snap/jest.config.mjs b/packages/bitcoin-wallet-snap/jest.config.mjs index 3adf4e628..ee642e653 100644 --- a/packages/bitcoin-wallet-snap/jest.config.mjs +++ b/packages/bitcoin-wallet-snap/jest.config.mjs @@ -1,8 +1,8 @@ // @ts-check import { createRequire } from 'node:module'; -const require = createRequire(import.meta.url); -const shared = require('../../jest.config.snaps.unit.js'); +const requireWithCreateRequire = createRequire(import.meta.url); +const shared = requireWithCreateRequire('../../jest.config.snaps.unit.cjs'); /** * @type {import('ts-jest').JestConfigWithTsJest} diff --git a/packages/sample-snap/jest.config.js b/packages/sample-snap/jest.config.js index 02ec77b26..2dc03ba04 100644 --- a/packages/sample-snap/jest.config.js +++ b/packages/sample-snap/jest.config.js @@ -1,4 +1,4 @@ -const shared = require('../../jest.config.snaps.unit.js'); +const shared = require('../../jest.config.snaps.unit.cjs'); module.exports = { ...shared, diff --git a/packages/solana-wallet-snap/jest.globalSetup.cjs b/packages/solana-wallet-snap/jest.globalSetup.cjs index 37d9e099c..28ea468b5 100644 --- a/packages/solana-wallet-snap/jest.globalSetup.cjs +++ b/packages/solana-wallet-snap/jest.globalSetup.cjs @@ -1,33 +1,45 @@ -const { existsSync } = require('node:fs'); -const { spawnSync } = require('node:child_process'); +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 = function globalSetup() { +module.exports = async function globalSetup() { const bundlePath = join(__dirname, 'dist', 'bundle.js'); - if (existsSync(bundlePath)) { + try { + await access(bundlePath); return; + } catch { + // Bundle missing — build below. } - // eslint-disable-next-line no-console console.log( '[solana-wallet-snap] dist/bundle.js missing; running yarn build before tests…', ); - const result = spawnSync('yarn', ['build'], { - cwd: __dirname, - stdio: 'inherit', - shell: true, - env: { - ...process.env, - ENVIRONMENT: process.env.ENVIRONMENT || 'local', - }, + const code = await new Promise((resolve, reject) => { + const child = spawn('yarn', ['build'], { + cwd: __dirname, + stdio: 'inherit', + shell: true, + env: { + // eslint-disable-next-line n/no-process-env + ...process.env, + // eslint-disable-next-line n/no-process-env + ENVIRONMENT: process.env.ENVIRONMENT || 'local', + }, + }); + child.on('error', reject); + child.on('close', (exitCode) => { + resolve(exitCode ?? 1); + }); }); - if (result.status !== 0) { + 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.setup.ts b/packages/solana-wallet-snap/jest.setup.ts index a51e791cf..b078d04f5 100644 --- a/packages/solana-wallet-snap/jest.setup.ts +++ b/packages/solana-wallet-snap/jest.setup.ts @@ -26,6 +26,8 @@ const testDefaults: Record = { }; for (const [key, value] of Object.entries(testDefaults)) { + // Empty strings from dotenv should also fall back to test defaults. + // eslint-disable-next-line no-restricted-globals, @typescript-eslint/prefer-nullish-coalescing process.env[key] ||= value; } diff --git a/packages/solana-wallet-snap/snap.config.ts b/packages/solana-wallet-snap/snap.config.ts index 751358074..4b0656550 100644 --- a/packages/solana-wallet-snap/snap.config.ts +++ b/packages/solana-wallet-snap/snap.config.ts @@ -9,6 +9,8 @@ const defaultUrl = (value: string | undefined, fallback: string): string => value && value.length > 0 ? value : fallback; const environment = { + // Empty ENVIRONMENT must fall back; `??` would keep ''. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ENVIRONMENT: process.env.ENVIRONMENT || 'local', RPC_URL_MAINNET_LIST: defaultUrl( process.env.RPC_URL_MAINNET_LIST, diff --git a/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts b/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts index 4873942c3..9245222fe 100644 --- a/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts +++ b/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts @@ -124,6 +124,8 @@ export class ConfigProvider { #parseEnvironment() { const rawEnvironment = { + // Empty ENVIRONMENT must fall back for unit tests without CI secrets. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ENVIRONMENT: process.env.ENVIRONMENT || 'local', RPC_URL_MAINNET_LIST: process.env.RPC_URL_MAINNET_LIST, RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST, diff --git a/packages/solana-wallet-snap/src/index.integration.test.ts b/packages/solana-wallet-snap/src/index.integration.test.ts index 4924daa27..82d7f33d2 100644 --- a/packages/solana-wallet-snap/src/index.integration.test.ts +++ b/packages/solana-wallet-snap/src/index.integration.test.ts @@ -1,11 +1,10 @@ -import { expect } from '@jest/globals'; import { installSnap } from '@metamask/snaps-jest'; describe('onRpcRequest', () => { it('throws an error if the requested method does not exist', async () => { - const { request } = await installSnap(); + const snap = await installSnap(); - const response = await request({ + const response = await snap.request({ method: 'foo', }); @@ -19,9 +18,9 @@ describe('onRpcRequest', () => { describe('onKeyringRequest', () => { it('throws an error if the requested method does not exist', async () => { - const { request } = await installSnap(); + const snap = await installSnap(); - const response = await request({ + const response = await snap.request({ method: 'wallet_invokeSnap', params: { snapId: 'npm:@metamask/solana-wallet-snap', diff --git a/packages/tron-wallet-snap/jest.config.mjs b/packages/tron-wallet-snap/jest.config.mjs index 250cd9532..ba5232b44 100644 --- a/packages/tron-wallet-snap/jest.config.mjs +++ b/packages/tron-wallet-snap/jest.config.mjs @@ -1,8 +1,8 @@ // @ts-check import { createRequire } from 'node:module'; -const require = createRequire(import.meta.url); -const shared = require('../../jest.config.snaps.unit.js'); +const requireWithCreateRequire = createRequire(import.meta.url); +const shared = requireWithCreateRequire('../../jest.config.snaps.unit.cjs'); /** * @type {import('ts-jest').JestConfigWithTsJest} diff --git a/packages/tron-wallet-snap/jest.setup.ts b/packages/tron-wallet-snap/jest.setup.ts index 46c978416..db0ef4e4d 100644 --- a/packages/tron-wallet-snap/jest.setup.ts +++ b/packages/tron-wallet-snap/jest.setup.ts @@ -25,6 +25,7 @@ const testDefaults: Record = { }; for (const [key, value] of Object.entries(testDefaults)) { - // eslint-disable-next-line no-restricted-globals + // Empty strings from dotenv should also fall back to test defaults. + // eslint-disable-next-line no-restricted-globals, @typescript-eslint/prefer-nullish-coalescing process.env[key] ||= value; } diff --git a/packages/tron-wallet-snap/snap.config.ts b/packages/tron-wallet-snap/snap.config.ts index 134c4b9ed..0a36cf018 100644 --- a/packages/tron-wallet-snap/snap.config.ts +++ b/packages/tron-wallet-snap/snap.config.ts @@ -13,6 +13,8 @@ const config: SnapConfig = { port: 8080, }, environment: { + // Empty ENVIRONMENT must fall back; `??` would keep ''. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ENVIRONMENT: process.env.ENVIRONMENT || 'local', RPC_URL_LIST_MAINNET: defaultUrl( process.env.RPC_URL_LIST_MAINNET, diff --git a/scripts/build-workspace-kind.mjs b/scripts/build-workspace-kind.mjs index 9b2af2d3c..5d410dacf 100644 --- a/scripts/build-workspace-kind.mjs +++ b/scripts/build-workspace-kind.mjs @@ -1,55 +1,108 @@ -import { existsSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; +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 {string[]} Workspace names + * @returns {Promise} Workspace package names. */ -function listWorkspaceNames(snapsOnly) { - const list = spawnSync('yarn', ['workspaces', 'list', '--json'], { +async function listWorkspaceNames(snapsOnly) { + const list = spawn('yarn', ['workspaces', 'list', '--json'], { cwd: root, - encoding: 'utf8', shell: true, }); - if (list.status !== 0) { - console.error(list.stderr || list.stdout); - process.exit(list.status ?? 1); + 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'); } - return list.stdout + const workspaces = stdout .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => JSON.parse(line)) - .filter((workspace) => workspace.location !== '.') - .filter((workspace) => { - const isSnap = existsSync( - join(root, workspace.location, 'snap.manifest.json'), - ); - return snapsOnly ? isSnap : !isSnap; - }) - .map((workspace) => workspace.name); + .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 names = listWorkspaceNames(snapsOnly); const label = snapsOnly ? 'snaps' : 'libraries'; +const names = await listWorkspaceNames(snapsOnly); + if (names.length === 0) { console.log(`No ${label} workspaces to build.`); - process.exit(0); -} - -console.log(`Building ${label}: ${names.join(', ')}`); - -const result = spawnSync( - 'yarn', - [ +} else { + console.log(`Building ${label}: ${names.join(', ')}`); + const code = await run('yarn', [ 'workspaces', 'foreach', '--all', @@ -60,8 +113,8 @@ const result = spawnSync( ...names.flatMap((name) => ['--include', name]), 'run', 'build', - ], - { cwd: root, stdio: 'inherit', shell: true }, -); - -process.exit(result.status ?? 1); + ]); + if (code !== 0) { + throw new Error(`Failed to build ${label} (exit ${code})`); + } +} From 731976083966b206f3ebf1b0df51426c3e8356b2 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 09:20:29 +0100 Subject: [PATCH 7/9] Delete docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md --- .../2026-08-08-layered-monorepo-readiness.md | 764 ------------------ 1 file changed, 764 deletions(-) delete mode 100644 docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md diff --git a/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md b/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md deleted file mode 100644 index c7bf3a770..000000000 --- a/docs/superpowers/plans/2026-08-08-layered-monorepo-readiness.md +++ /dev/null @@ -1,764 +0,0 @@ -# Layered Monorepo Readiness Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** After a normal `yarn`, library packages are built, TypeScript works, and Snap **unit** Jest suites run; Snap bundles / `installSnap` tests still require `yarn build`. - -**Architecture:** Keep the dual package model (libraries via `ts-bridge`, snaps via `mm-snap`). Fix root TypeScript `paths`, post-install `build:libs` only, split Snap Jest into node unit vs `snaps-jest` integration, and stop lint from leaving library `dist/` wiped. - -**Tech Stack:** Yarn 4 workspaces, TypeScript 5.8 project refs/`paths`, Jest 30, `ts-bridge`, `@metamask/snaps-cli` / `@metamask/snaps-jest`, LavaMoat `allow-scripts` yarn plugin. - -**Spec:** `docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md` - -## Global Constraints - -- Do not build Snap bundles in the install hook (libraries only). -- Keep Yarn constraint: Snap `scripts.build` must start with `mm-snap build`; library `scripts.build` must remain the `ts-bridge … --no-references` command. -- Keep package `scripts.test` string required by `yarn.config.cjs` unless that constraint is updated in the same task that changes it. -- Prefer editing shared configs (`tsconfig.packages.json`, `tsconfig.snaps.json`, `jest.config.packages.js`, root `package.json`) over one-off per-snap drift. -- `installSnap` tests must not run under the default unit Jest environment. -- Every consumer-facing behavior change to a published package needs a changelog entry under Unreleased; pure tooling/docs-only root changes do not. - ---- - -## File map - -| File | Responsibility | -| ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| `scripts/build-libs.mjs` | Discover non-snap workspaces and run their `build` scripts topologically | -| `package.json` | `build:libs`, wire into `allow-scripts`, lint rebuild, optional `build:snaps` | -| `tsconfig.packages.json` | Root-correct `@metamask/*` → `packages/*/src` paths | -| `tsconfig.snaps.json` | Shared Snap TS compiler defaults | -| `packages/*/tsconfig.json` (snaps) | Extend `tsconfig.snaps.json` | -| `jest.config.packages.js` | Keep mapper in sync with TS paths; document Snap usage | -| `jest.config.snaps.unit.js` | Shared Node-environment unit defaults for snaps | -| `packages/*/jest.config.*` (snaps) | Unit config (node); integration stays `snaps-jest` | -| `packages/solana-wallet-snap/**` | Move `installSnap` tests into `integration-test/` | -| `packages/sample-snap/**` | Move `installSnap` tests into `integration-test/` or dedicated integration config | -| `packages/*/snap.config.ts` | Safe `ENVIRONMENT` default where validated | -| `yarn.config.cjs` | Allow Snap `test:integration` script pattern if constrained | -| `docs/getting-started/setting-up-your-environment.md`, `AGENTS.md`, `docs/processes/testing.md`, `docs/processes/building.md` | Document layered workflow | -| `package.json` `workspaces` / `examples/` | Fix empty `examples/*` glob | -| `.github/workflows/lint-build-test.yml` | Only if unit vs integration split requires CI job changes | - ---- - -### Task 1: Fix TypeScript workspace path mapping - -**Files:** - -- Modify: `tsconfig.packages.json` -- Modify: `jest.config.packages.js` (comment + mapper sanity check only if needed) -- Test: `packages/tron-wallet-snap` typecheck against `@metamask/snap-networks-utils` **without** that package’s `dist/` - -**Interfaces:** - -- Consumes: existing `@metamask/*` imports in snaps/libs -- Produces: `compilerOptions.paths` of `@metamask/*` → `packages/*/src` resolved from repo root config - -- [ ] **Step 1: Confirm the failure without library dist** - -```bash -rm -rf packages/snap-networks-utils/dist -yarn workspace @metamask/tron-wallet-snap exec tsc --noEmit 2>&1 | head -20 -``` - -Expected: `TS2307: Cannot find module '@metamask/snap-networks-utils'` - -- [ ] **Step 2: Fix paths in `tsconfig.packages.json`** - -Replace the paths block with root-correct mappings (paths are resolved relative to this file’s directory — the repo root): - -```json -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "paths": { - "@metamask/*": ["packages/*/src"] - } - } -} -``` - -Keep the existing comment, but update it to say paths are rooted at the monorepo root (this file), and must stay synchronized with `jest.config.packages.js`. - -- [ ] **Step 3: Re-run typecheck without library dist** - -```bash -rm -rf packages/snap-networks-utils/dist -yarn workspace @metamask/tron-wallet-snap exec tsc --noEmit -``` - -Expected: exit 0 (or only pre-existing unrelated errors — none for `snap-networks-utils`). - -If it still fails: run `tsc --traceResolution` on one file and verify the candidate is `/workspace/packages/snap-networks-utils/src`, not `/snap-networks-utils/src`. Do **not** “fix” by restoring `dist` in this task. - -- [ ] **Step 4: Verify library package still typechecks** - -```bash -yarn workspace @metamask/snap-networks-utils exec tsc --noEmit -``` - -Expected: exit 0 - -- [ ] **Step 5: Commit** - -```bash -git add tsconfig.packages.json -git commit -m "fix: resolve TypeScript workspace paths from monorepo root" -``` - ---- - -### Task 2: Add shared Snap TypeScript config - -**Files:** - -- Create: `tsconfig.snaps.json` -- Modify: `packages/bitcoin-wallet-snap/tsconfig.json` -- Modify: `packages/solana-wallet-snap/tsconfig.json` -- Modify: `packages/tron-wallet-snap/tsconfig.json` -- Modify: `packages/sample-snap/tsconfig.json` - -**Interfaces:** - -- Consumes: `tsconfig.packages.json` (including fixed paths) -- Produces: shared Snap compiler options — JSX from snaps-sdk, `moduleResolution: bundler`, `module: preserve`, `skipLibCheck`, `resolveJsonModule`, `types: ["jest"]` - -- [ ] **Step 1: Create `tsconfig.snaps.json`** - -```json -{ - "extends": "./tsconfig.packages.json", - "compilerOptions": { - "jsx": "react-jsx", - "jsxImportSource": "@metamask/snaps-sdk", - "module": "preserve", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "skipLibCheck": true, - "types": ["jest"] - } -} -``` - -- [ ] **Step 2: Slim each snap `tsconfig.json` to extend the shared config** - -Example for Tron (keep package-specific strictness flags that already differ only if required; prefer moving common flags into `tsconfig.snaps.json`): - -```json -{ - "extends": "../../tsconfig.snaps.json", - "compilerOptions": { - "lib": ["ES2023", "DOM"], - "target": "es2023", - "exactOptionalPropertyTypes": false, - "forceConsistentCasingInFileNames": true, - "noErrorTruncation": true, - "noUncheckedIndexedAccess": true - }, - "include": ["**/*.ts", "**/*.tsx", "locales/*.json"] -} -``` - -Repeat for bitcoin / solana / sample, preserving each package’s intentional `lib`/`target`/`exactOptionalPropertyTypes` differences. Remove redundant duplicates of jsx / moduleResolution / skipLibCheck / types. - -- [ ] **Step 3: Typecheck all snaps** - -```bash -yarn typecheck -``` - -Expected: exit 0 - -- [ ] **Step 4: Commit** - -```bash -git add tsconfig.snaps.json packages/*/tsconfig.json -git commit -m "chore: add shared tsconfig for snap packages" -``` - ---- - -### Task 3: Add `build:libs` and run it after install - -**Files:** - -- Create: `scripts/build-libs.mjs` -- Modify: `package.json` (scripts) -- Optional test helper: none (verify with shell) - -**Interfaces:** - -- Consumes: Yarn workspaces list; `snap.manifest.json` presence; each lib’s `scripts.build` -- Produces: - - `yarn build:libs` — builds every non-private workspace without `snap.manifest.json` - - `yarn build:snaps` — builds every workspace **with** `snap.manifest.json` (topological-dev via foreach) - - `scripts.allow-scripts` — runs LavaMoat allow-scripts then `yarn build:libs` - -- [ ] **Step 1: Add `scripts/build-libs.mjs`** - -```js -import { existsSync } from 'node:fs'; -import { spawnSync } from 'node:child_process'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const root = join(dirname(fileURLToPath(import.meta.url)), '..'); - -const list = spawnSync('yarn', ['workspaces', 'list', '--json'], { - cwd: root, - encoding: 'utf8', - shell: true, -}); - -if (list.status !== 0) { - console.error(list.stderr || list.stdout); - process.exit(list.status ?? 1); -} - -const workspaces = list.stdout - .split('\n') - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => JSON.parse(line)) - .filter((workspace) => workspace.location !== '.'); - -const libraries = workspaces.filter( - (workspace) => - !existsSync(join(root, workspace.location, 'snap.manifest.json')), -); - -if (libraries.length === 0) { - console.log('No library workspaces to build.'); - process.exit(0); -} - -const names = libraries.map((workspace) => workspace.name); -console.log(`Building libraries: ${names.join(', ')}`); - -const result = spawnSync( - 'yarn', - [ - 'workspaces', - 'foreach', - '--all', - '--no-private', - '--topological-dev', - '--parallel', - '--interlaced', - '--verbose', - ...names.flatMap((name) => ['--include', name]), - 'run', - 'build', - ], - { cwd: root, stdio: 'inherit', shell: true }, -); - -process.exit(result.status ?? 1); -``` - -If Yarn’s foreach `--include` UX differs in 4.17.1, adjust to an equivalent filter (verify with `yarn workspaces foreach --help`). Fallback: loop `yarn workspace run build` in dependency order from `yarn workspaces list --json` plus each package’s `package.json` deps. - -- [ ] **Step 2: Wire root scripts in `package.json`** - -Add/adjust: - -```json -{ - "scripts": { - "allow-scripts": "yarn exec allow-scripts && yarn build:libs", - "build:libs": "node ./scripts/build-libs.mjs", - "build:snaps": "yarn workspaces foreach --all --no-private --topological-dev --parallel --interlaced --verbose --exclude @metamask/snap-networks-utils run build", - "setup": "yarn install" - } -} -``` - -Notes: - -- Prefer excluding by “is snap” in a `build-snaps.mjs` mirror if `--exclude` of only today’s library is too brittle; when a second library appears, update to a snap-detecting script. -- `setup` can remain `yarn install` because `allow-scripts` already chains `build:libs` after install via the Yarn plugin. - -- [ ] **Step 3: Verify install hook builds libraries** - -```bash -rm -rf packages/snap-networks-utils/dist -yarn allow-scripts -test -f packages/snap-networks-utils/dist/index.d.mts && echo 'libs built' -``` - -Expected: `libs built`. Snaps’ `dist/bundle.js` should still be absent unless previously built. - -- [ ] **Step 4: Verify `mm-snap` can resolve the library** - -```bash -ENVIRONMENT=local yarn workspace @metamask/tron-wallet-snap run build -``` - -Expected: Snap bundle succeeds (no “Package path . is exported … no valid target”). - -- [ ] **Step 5: Commit** - -```bash -git add scripts/build-libs.mjs package.json -git commit -m "feat: build library packages after yarn install" -``` - ---- - -### Task 4: Rebuild libraries after eslint cleans dist - -**Files:** - -- Modify: `package.json` (`lint:eslint`) -- Modify: `AGENTS.md` (Cursor Cloud caveats) - -**Interfaces:** - -- Consumes: `build:only-clean`, `build:libs` -- Produces: `lint:eslint` ends with library `dist/` restored - -- [ ] **Step 1: Update `lint:eslint`** - -Change: - -```json -"lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=6144' yarn eslint" -``` - -to: - -```json -"lint:eslint": "yarn build:only-clean && NODE_OPTIONS='--max-old-space-size=6144' yarn eslint && yarn build:libs" -``` - -- [ ] **Step 2: Smoke-test** - -```bash -yarn lint:eslint -test -f packages/snap-networks-utils/dist/index.d.mts && echo 'libs restored' -``` - -Expected: eslint completes; `libs restored`. - -- [ ] **Step 3: Update `AGENTS.md` caveat** - -Replace the “`yarn lint` deletes `dist/`” bullet with: eslint still cleans `packages/*/dist` before linting, then `build:libs` restores library artifacts; Snap bundles still need `yarn build` / `yarn build:snaps` afterward. - -- [ ] **Step 4: Commit** - -```bash -git add package.json AGENTS.md -git commit -m "fix: restore library builds after eslint dist clean" -``` - ---- - -### Task 5: Shared Snap unit Jest config (Node environment) - -**Files:** - -- Create: `jest.config.snaps.unit.js` -- Modify: `packages/bitcoin-wallet-snap/jest.config.mjs` -- Modify: `packages/tron-wallet-snap/jest.config.mjs` -- Modify: `packages/solana-wallet-snap/jest.config.js` -- Modify: `packages/sample-snap/jest.config.js` (temporary: may still need integration-only until Task 6) -- Modify: `jest.config.packages.js` (keep mapper; ensure Snap unit configs reuse the same `@metamask/(.*)` workspace→source pattern) - -**Interfaces:** - -- Consumes: `ts-jest`, workspace source mapper pattern from `jest.config.packages.js` -- Produces: default Snap `test` runs in `testEnvironment: 'node'` and does **not** load `@metamask/snaps-jest` preset - -- [ ] **Step 1: Create `jest.config.snaps.unit.js`** - -```js -const path = require('path'); - -module.exports = { - testEnvironment: 'node', - preset: 'ts-jest', - transform: { - '^.+\\.(t|j)sx?$': 'ts-jest', - }, - collectCoverage: true, - collectCoverageFrom: ['./src/**/*.ts', './src/**/*.tsx'], - coverageDirectory: 'coverage', - coveragePathIgnorePatterns: ['.*/index\\.ts'], - coverageProvider: 'babel', - coverageReporters: ['text', 'html', 'json-summary', 'lcov'], - resetMocks: true, - restoreMocks: true, - testMatch: ['**/src/**/?(*.)+(spec|test).[tj]s?(x)'], - testPathIgnorePatterns: ['/node_modules/', '/integration-test/'], - moduleNameMapper: { - '\\.svg$': 'jest-transform-stub', - '^@metamask/utils/node$': require.resolve('@metamask/utils/node'), - '^@metamask/(.+)$': [ - path.join(__dirname, 'packages/$1/src'), - path.join(__dirname, 'node_modules/@metamask/$1'), - ], - }, -}; -``` - -- [ ] **Step 2: Point bitcoin/tron/solana unit configs at the shared config** - -Example ESM wrapper for bitcoin/tron (`jest.config.mjs`): - -```js -// @ts-check -import { createRequire } from 'node:module'; - -const require = createRequire(import.meta.url); -const shared = require('../../jest.config.snaps.unit.js'); - -/** @type {import('ts-jest').JestConfigWithTsJest} */ -const config = { - ...shared, - coverageThreshold: { - global: { - branches: 65.5, - functions: 62.64, - lines: 75.29, - statements: 74.57, - }, - }, -}; - -export default config; -``` - -Preserve each package’s existing `coverageThreshold`, `setupFilesAfterEnv`, `maxWorkers`, and SVG transformer overrides. - -**Critical:** remove `preset: '@metamask/snaps-jest'` from unit configs. - -- [ ] **Step 3: Prove a former failure now passes without Snap bundle** - -```bash -rm -rf packages/tron-wallet-snap/dist -yarn workspace @metamask/tron-wallet-snap run jest --no-coverage src/services/assets/AssetsService.test.ts -``` - -Expected: tests run (PASS or real assertion failures) — **not** `dist/bundle.js does not exist`. - -- [ ] **Step 4: Commit** - -```bash -git add jest.config.snaps.unit.js packages/*/jest.config.* -git commit -m "test: run snap unit tests in node without snaps-jest" -``` - ---- - -### Task 6: Move `installSnap` tests to integration configs - -**Files:** - -- Create/Modify: `packages/solana-wallet-snap/jest.integration.config.js` (or `.mjs`) -- Create: `packages/solana-wallet-snap/integration-test/` (move installSnap specs here) -- Modify: `packages/solana-wallet-snap/package.json` (add `test:integration`) -- Modify: `packages/sample-snap/jest.config.js`, `package.json`, move `src/index.test.tsx` → `integration-test/` -- Create: `packages/sample-snap/jest.integration.config.js` -- Modify: `packages/bitcoin-wallet-snap` / `tron-wallet-snap` only if needed for script naming consistency -- Modify: `yarn.config.cjs` if a new required script pattern is enforced for snaps - -**Interfaces:** - -- Consumes: `@metamask/snaps-jest` `installSnap` -- Produces: - - `yarn workspace run test` → unit only (no bundle) - - `yarn workspace run test:integration` → `snaps-jest` after `yarn build` - -- [ ] **Step 1: Inventory current `installSnap` files** - -Known today: - -- `packages/sample-snap/src/index.test.tsx` -- `packages/solana-wallet-snap/src/index.test.ts` (partially; also has non-installSnap cases — split the file) -- `packages/solana-wallet-snap/src/features/confirmation/views/**/render.test.tsx` -- `packages/bitcoin-wallet-snap/integration-test/*.test.ts` (already correct) - -- [ ] **Step 2: Move Solana `installSnap` tests** - -1. Create `packages/solana-wallet-snap/integration-test/`. -2. Move render tests that call `installSnap` into that folder (keep imports working; adjust relative paths). -3. Split `src/index.test.ts`: keep pure unit cases (e.g. mocked cronjob handler tests) under `src/`; move `installSnap` cases to `integration-test/on-rpc-request.test.ts` (name freely, but under `integration-test/`). -4. Add: - -```js -// packages/solana-wallet-snap/jest.integration.config.js -module.exports = { - preset: '@metamask/snaps-jest', - testMatch: ['**/integration-test/**/*.[jt]s?(x)'], -}; -``` - -5. Add script: - -```json -"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --config jest.integration.config.js --reporters=jest-silent-reporter" -``` - -- [ ] **Step 3: Handle sample-snap** - -All current tests use `installSnap`. Move them to `integration-test/` and either: - -- Make unit `jest.config.js` use `passWithNoTests: true` and `testMatch` under `src/`, or -- Keep a trivial unit smoke test that does not use `installSnap` - -Add `test:integration` like Solana. Root `test:verbose` already excludes sample-snap; keep that unless you intentionally include unit smoke tests later. - -- [ ] **Step 4: Verify unit vs integration gate** - -```bash -rm -rf packages/solana-wallet-snap/dist -yarn workspace @metamask/solana-wallet-snap run test -``` - -Expected: unit suites pass without bundle. - -```bash -ENVIRONMENT=local yarn workspace @metamask/solana-wallet-snap run build -yarn workspace @metamask/solana-wallet-snap run test:integration -``` - -Expected: integration suites discover and run under `snaps-jest` (pass/fail based on assertions, but must find the bundle). - -- [ ] **Step 5: Update yarn constraints if they reject new scripts** - -Only if `yarn constraints` complains. Do not invent constraints for `test:integration` unless useful; if you add one, require snaps to define `test:integration` with a `jest --config` integration config. - -- [ ] **Step 6: Commit** - -```bash -git add packages/solana-wallet-snap packages/sample-snap yarn.config.cjs -git commit -m "test: move installSnap coverage into snap integration suites" -``` - ---- - -### Task 7: Safe Snap build environment defaults - -**Files:** - -- Modify: `packages/solana-wallet-snap/snap.config.ts` -- Modify: other snap configs only if they validate `ENVIRONMENT` the same way -- Modify: `.env.example` files if present - -**Interfaces:** - -- Consumes: `process.env.ENVIRONMENT` -- Produces: default `'local'` (or `'test'`) when unset so SES eval accepts the value - -- [ ] **Step 1: Reproduce** - -```bash -env -u ENVIRONMENT yarn workspace @metamask/solana-wallet-snap run build 2>&1 | tail -30 -``` - -Expected (today): SES error about `ENVIRONMENT` received `""`. - -- [ ] **Step 2: Default the env in `snap.config.ts`** - -```ts -const environment = { - ENVIRONMENT: process.env.ENVIRONMENT || 'local', - // ...unchanged keys -}; -``` - -Use `||` (not `??`) so empty string also falls back. - -- [ ] **Step 3: Re-run build without ENVIRONMENT** - -```bash -env -u ENVIRONMENT yarn build -``` - -Expected: all library + snap builds succeed (CI secrets still override when present). - -- [ ] **Step 4: Commit** - -```bash -git add packages/solana-wallet-snap/snap.config.ts -git commit -m "fix: default solana snap ENVIRONMENT to local for builds" -``` - ---- - -### Task 8: Workspace and docs hygiene - -**Files:** - -- Modify: `package.json` (`workspaces`) **or** create `examples/.gitkeep` + placeholder — prefer removing `examples/*` until an example exists -- Modify: `docs/README.md` (remove or fix dead migration-guide link) -- Modify: `docs/getting-started/setting-up-your-environment.md` -- Modify: `docs/processes/building.md` -- Modify: `docs/processes/testing.md` -- Modify: `AGENTS.md` - -**Interfaces:** - -- Produces: accurate contributor workflow for layered readiness - -- [ ] **Step 1: Fix workspaces glob** - -In root `package.json`, change: - -```json -"workspaces": [ - "packages/*" -] -``` - -(unless you intentionally add an `examples` package in this same change). - -- [ ] **Step 2: Update getting-started** - -After `yarn install`, document that library packages are built automatically via `allow-scripts` → `build:libs`. Then: - -```bash -yarn typecheck -yarn test -yarn build # when you need snap bundles / integration tests / serve -``` - -- [ ] **Step 3: Update building + testing process docs** - -State clearly: - -- `yarn build:libs` — shared packages (`ts-bridge`) -- `yarn build` / `yarn build:snaps` — snap bundles -- `yarn test` — unit tests (node for snaps) -- `yarn workspace run test:integration` — requires built snap - -- [ ] **Step 4: Fix docs index** - -Remove the link to missing `./processes/snap-migration-process-guide.md` or add a stub page that says the guide is not in-tree yet (prefer remove until content exists). - -- [ ] **Step 5: Commit** - -```bash -git add package.json docs AGENTS.md -git commit -m "docs: describe layered install, typecheck, and test workflow" -``` - ---- - -### Task 9: CI alignment - -**Files:** - -- Modify: `.github/workflows/lint-build-test.yml` only as needed - -**Interfaces:** - -- Consumes: build artifacts upload/download -- Produces: CI unit tests match local unit semantics; integration remains optional/separate - -- [ ] **Step 1: Decide CI matrix behavior** - -Keep current flow (build all → download dist → `yarn workspace … run test`) — this remains valid and still exercises packages after a full build. - -Optional improvement in this task (recommended if low-risk): - -- Unit job can run **without** snap artifacts once Task 5/6 landed -- Add a separate `test-integration` job for snaps that define `test:integration`, needing build artifacts (+ secrets/services as today) - -Minimum for this plan: ensure CI still passes with the new unit configs. Do not delete the build job. - -- [ ] **Step 2: Run the same commands CI runs** - -```bash -yarn build -yarn test:scripts -yarn workspaces foreach --all --exclude @metamask/sample-snap --parallel --verbose run test -``` - -Expected: pass (sample excluded as today, or included if it now has unit smoke tests). - -- [ ] **Step 3: Commit only if workflow files changed** - -```bash -git add .github/workflows/lint-build-test.yml -git commit -m "ci: align snap unit and integration test jobs" -``` - ---- - -### Task 10: End-to-end verification on a clean tree - -**Files:** - -- None (verification only); fix regressions found in earlier tasks - -- [ ] **Step 1: Clean artifacts and reinstall** - -```bash -rm -rf packages/*/dist node_modules -yarn install -``` - -Expected: install ends with `build:libs` success; `packages/snap-networks-utils/dist` exists; snap `dist/bundle.js` files absent. - -- [ ] **Step 2: TypeScript + unit tests** - -```bash -yarn typecheck -yarn test -``` - -Expected: both pass. - -- [ ] **Step 3: Full build + one integration suite** - -```bash -yarn build -yarn workspace @metamask/bitcoin-wallet-snap run test:integration -``` - -Expected: build pass; bitcoin integration either runs (if Docker available) or fails only on missing Docker — not on missing bundle. If Docker is unavailable in the agent environment, substitute: - -```bash -yarn workspace @metamask/sample-snap run test:integration -``` - -(after sample migration), which should not need Docker. - -- [ ] **Step 4: Lint** - -```bash -yarn lint -test -f packages/snap-networks-utils/dist/index.d.mts && echo 'libs ok after lint' -``` - -Expected: lint pass; libraries restored. - -- [ ] **Step 5: Final commit if verification required fixes** - -```bash -git add -A -git commit -m "fix: address layered readiness verification gaps" -``` - -Only commit if there are real fixes; otherwise stop. - ---- - -## Self-review - -| Spec requirement | Task | -| ---------------------------------- | ------------- | -| Post-install library builds only | Task 3 | -| TypeScript works after install | Tasks 1–3 | -| Unit Jest after install | Tasks 5–6 | -| Snap integration still needs build | Task 6 | -| Local/CI builds work | Tasks 7, 9–10 | -| Lint does not leave libs broken | Task 4 | -| Shared snap TS config / less drift | Task 2 | -| Docs + workspaces hygiene | Task 8 | -| Env default for snap builds | Task 7 | - -No TBD placeholders. Script names (`build:libs`, `build:snaps`, `test:integration`, `allow-scripts`) are consistent across tasks. From e700bffbde0718f8625e18e82ecb859374a564c0 Mon Sep 17 00:00:00 2001 From: Ulisses Ferreira Date: Mon, 10 Aug 2026 09:22:37 +0100 Subject: [PATCH 8/9] docs: remove layered monorepo readiness design spec --- ...08-08-layered-monorepo-readiness-design.md | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md diff --git a/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md b/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md deleted file mode 100644 index 773b707d6..000000000 --- a/docs/superpowers/specs/2026-08-08-layered-monorepo-readiness-design.md +++ /dev/null @@ -1,105 +0,0 @@ -# Layered Monorepo Readiness Design - -**Date:** 2026-08-08 -**Status:** Approved direction (Approach C) -**Repo:** `@metamask/internal-snaps` - -## Problem - -This monorepo combines Core-style library packages (`ts-bridge`, declaration `exports`, Jest source mappers) with Snap packages (`mm-snap`, `snaps-jest`, SES bundle evaluation). Today those layers only work together **after** a full build: - -- TypeScript workspace imports resolve through `package.json` `exports` → `dist/`, not through the intended source `paths` -- `@metamask/snaps-jest` requires `dist/bundle.js` even for ordinary unit tests -- `mm-snap` cannot bundle a snap that imports a workspace library unless that library’s `dist/` exists -- CI builds before test; local `yarn` does not - -## Goals - -After a normal `yarn` (install + lifecycle hooks): - -1. Dependencies install correctly -2. TypeScript typechecking works for libraries and snaps -3. **Unit** Jest suites run without a prior Snap bundle build -4. Local and CI full builds continue to work - -Explicitly **out of scope for post-install**: Snap SES integration tests (`installSnap`), `mm-snap serve` / watch, and publish artifacts. Those still require `yarn build` (or `yarn build:snaps`). - -## Package model (unchanged) - -| Kind | Detection | Build tool | Published shape | -| ------- | ------------------------ | ------------------------------------------- | --------------------------- | -| Library | no `snap.manifest.json` | `ts-bridge` | `dist/*` ESM/CJS + types | -| Snap | has `snap.manifest.json` | `mm-snap` (+ locale / preinstalled helpers) | `dist/bundle.js` + manifest | - -Yarn constraints already encode this split; keep them. - -## Layered readiness - -```text -yarn install - └─ allow-scripts hook - └─ build:libs (ts-bridge packages only) - ├─ TypeScript via exports/types ✅ - ├─ mm-snap can resolve workspace libs ✅ - └─ Snap unit Jest (node) ✅ - -yarn build / CI - └─ build:libs then build snaps (topological-dev) - └─ snaps-jest integration / serve / publish ✅ -``` - -## Design decisions - -### 1. Post-install builds libraries only - -Add `build:libs` that runs `build` for every non-private workspace **without** `snap.manifest.json`, topological by dependency graph. - -Wire it into the existing LavaMoat `allow-scripts` after-install hook so a normal `yarn` produces library `dist/` without rebuilding every Snap bundle. - -### 2. Fix TypeScript workspace `paths` - -`tsconfig.packages.json` currently maps `@metamask/*` → `../*/src`. Because `paths` are resolved relative to the config file that **defines** them (repo root), that pattern points outside the repo and never hits source. - -Change the mapping to root-correct paths, e.g. `@metamask/*` → `packages/*/src`, and keep Jest `moduleNameMapper` synchronized. Keep post-install library builds so Node/`mm-snap` consumers of `exports` still work. - -Introduce a shared `tsconfig.snaps.json` for Snap packages (JSX from `@metamask/snaps-sdk`, bundler resolution, `skipLibCheck`) to reduce per-snap drift. - -### 3. Split Snap Jest surfaces - -Default package `test` script stays constraint-compatible, but Snap **unit** Jest configs use the Node environment (no `snaps-jest` server). - -`installSnap` / SES tests move under `integration-test/` (or an equivalent dedicated config) and run via `test:integration` after a Snap build — matching bitcoin’s existing pattern. - -Root `yarn test` continues to run unit suites. CI keeps build-then-test so packages that still invoke `snaps-jest` in CI remain green; prefer aligning CI unit jobs to the node environment and running integration separately where Docker/env allow. - -### 4. Lint must not leave the tree unusable - -`lint:eslint` currently deletes all `packages/*/dist`. After eslint, rebuild libraries (`build:libs`) so typecheck / subsequent snap builds keep working. - -### 5. Snap build env defaults - -Snap configs that validate `ENVIRONMENT` must default to a valid enum value for local builds (e.g. `local`) when unset, so `yarn build` works without a full secrets `.env`. - -### 6. Hygiene - -- Resolve `examples/*` workspace glob (add stub or remove) -- Fix docs that link to a missing migration guide -- Update getting-started / AGENTS.md for the layered workflow - -## Non-goals - -- Building all Snap bundles on every `yarn install` -- Replacing `mm-snap` or `ts-bridge` -- Making `installSnap` tests run without a Snap bundle -- Changing release / preview-publish flows beyond needing library `dist/` as they already do - -## Success criteria - -| Command (fresh clone, after `yarn`) | Expected | -| ----------------------------------------------------------- | ---------------------------------------------------- | -| `yarn typecheck` | Pass | -| `yarn workspace @metamask/snap-networks-utils run test` | Pass | -| `yarn workspace @metamask/tron-wallet-snap run test` (unit) | Pass without snap `dist/bundle.js` | -| `yarn build` | Pass with minimal/default env | -| CI lint-build-test | Pass | -| `yarn workspace … run test:integration` (where defined) | Pass only after snap build (+ env/services as today) | From 7c19b6fb38647f80a3e2a133ae28f21fb82843cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 10 Aug 2026 09:36:51 +0000 Subject: [PATCH 9/9] chore: remove fallback environment variables from wallet snaps Wallet snaps should read configuration from a proper .env file supplied by the developer. Reverts snap.config.ts defaults, jest.setup test defaults, and ConfigProvider ENVIRONMENT fallback added in the layered monorepo readiness work. Co-authored-by: Ulisses Ferreira --- AGENTS.md | 2 +- .../setting-up-your-environment.md | 3 +- .../solana-wallet-snap/jest.globalSetup.cjs | 7 +- packages/solana-wallet-snap/jest.setup.ts | 25 ----- packages/solana-wallet-snap/snap.config.ts | 82 +++----------- .../core/services/config/ConfigProvider.ts | 4 +- packages/tron-wallet-snap/jest.setup.ts | 28 ----- packages/tron-wallet-snap/snap.config.ts | 103 +++++------------- 8 files changed, 46 insertions(+), 208 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 85a1c0621..71ded5f3e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -230,4 +230,4 @@ Standard commands are documented above (see "Running tests", "Linting and format - **`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` 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. -- **`.env` is optional** for wallet snaps; `snap.config.ts` reads it via dotenv and falls back to local defaults so the snap can build without a full secrets file. +- **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/getting-started/setting-up-your-environment.md b/docs/getting-started/setting-up-your-environment.md index 49c56db09..b66574f9d 100644 --- a/docs/getting-started/setting-up-your-environment.md +++ b/docs/getting-started/setting-up-your-environment.md @@ -7,7 +7,8 @@ 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. Verify the tree: +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 diff --git a/packages/solana-wallet-snap/jest.globalSetup.cjs b/packages/solana-wallet-snap/jest.globalSetup.cjs index 28ea468b5..eff0bb16d 100644 --- a/packages/solana-wallet-snap/jest.globalSetup.cjs +++ b/packages/solana-wallet-snap/jest.globalSetup.cjs @@ -26,12 +26,7 @@ module.exports = async function globalSetup() { cwd: __dirname, stdio: 'inherit', shell: true, - env: { - // eslint-disable-next-line n/no-process-env - ...process.env, - // eslint-disable-next-line n/no-process-env - ENVIRONMENT: process.env.ENVIRONMENT || 'local', - }, + env: process.env, }); child.on('error', reject); child.on('close', (exitCode) => { diff --git a/packages/solana-wallet-snap/jest.setup.ts b/packages/solana-wallet-snap/jest.setup.ts index b078d04f5..df1465e94 100644 --- a/packages/solana-wallet-snap/jest.setup.ts +++ b/packages/solana-wallet-snap/jest.setup.ts @@ -6,31 +6,6 @@ import logger from './src/core/utils/logger'; dotenv.config(); -const testDefaults: Record = { - ENVIRONMENT: 'test', - RPC_URL_MAINNET_LIST: 'https://example.com/solana-mainnet', - RPC_URL_DEVNET_LIST: 'https://example.com/solana-devnet', - RPC_URL_TESTNET_LIST: 'https://example.com/solana-testnet', - RPC_URL_LOCALNET_LIST: 'http://127.0.0.1:8899', - RPC_WEB_SOCKET_URL_MAINNET: 'wss://example.com/solana-mainnet', - RPC_WEB_SOCKET_URL_DEVNET: 'wss://example.com/solana-devnet', - RPC_WEB_SOCKET_URL_TESTNET: 'wss://example.com/solana-testnet', - RPC_WEB_SOCKET_URL_LOCALNET: 'wss://example.com/solana-localnet', - EXPLORER_BASE_URL: 'https://solscan.io', - PRICE_API_BASE_URL: 'https://example.com/price/', - TOKEN_API_BASE_URL: 'https://example.com/token/', - STATIC_API_BASE_URL: 'https://example.com/static/', - SECURITY_ALERTS_API_BASE_URL: 'https://example.com/security/', - NFT_API_BASE_URL: 'https://example.com/nft/', - LOCAL_API_BASE_URL: 'http://127.0.0.1:3000', -}; - -for (const [key, value] of Object.entries(testDefaults)) { - // Empty strings from dotenv should also fall back to test defaults. - // eslint-disable-next-line no-restricted-globals, @typescript-eslint/prefer-nullish-coalescing - process.env[key] ||= value; -} - // Lowest precision we ever go for: MicroLamports represented in Sol amount BigNumber.config({ EXPONENTIAL_AT: 16 }); diff --git a/packages/solana-wallet-snap/snap.config.ts b/packages/solana-wallet-snap/snap.config.ts index 4b0656550..4501facac 100644 --- a/packages/solana-wallet-snap/snap.config.ts +++ b/packages/solana-wallet-snap/snap.config.ts @@ -5,73 +5,23 @@ import { resolve } from 'path'; dotenv.config(); -const defaultUrl = (value: string | undefined, fallback: string): string => - value && value.length > 0 ? value : fallback; - const environment = { - // Empty ENVIRONMENT must fall back; `??` would keep ''. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - ENVIRONMENT: process.env.ENVIRONMENT || 'local', - RPC_URL_MAINNET_LIST: defaultUrl( - process.env.RPC_URL_MAINNET_LIST, - 'https://example.com/solana-mainnet', - ), - RPC_URL_DEVNET_LIST: defaultUrl( - process.env.RPC_URL_DEVNET_LIST, - 'https://example.com/solana-devnet', - ), - RPC_URL_TESTNET_LIST: defaultUrl( - process.env.RPC_URL_TESTNET_LIST, - 'https://example.com/solana-testnet', - ), - RPC_URL_LOCALNET_LIST: defaultUrl( - process.env.RPC_URL_LOCALNET_LIST, - 'http://127.0.0.1:8899', - ), - RPC_WEB_SOCKET_URL_MAINNET: defaultUrl( - process.env.RPC_WEB_SOCKET_URL_MAINNET, - 'wss://example.com/solana-mainnet', - ), - RPC_WEB_SOCKET_URL_DEVNET: defaultUrl( - process.env.RPC_WEB_SOCKET_URL_DEVNET, - 'wss://example.com/solana-devnet', - ), - RPC_WEB_SOCKET_URL_TESTNET: defaultUrl( - process.env.RPC_WEB_SOCKET_URL_TESTNET, - 'wss://example.com/solana-testnet', - ), - RPC_WEB_SOCKET_URL_LOCALNET: defaultUrl( - process.env.RPC_WEB_SOCKET_URL_LOCALNET, - 'wss://example.com/solana-localnet', - ), - EXPLORER_BASE_URL: defaultUrl( - process.env.EXPLORER_BASE_URL, - 'https://solscan.io', - ), - PRICE_API_BASE_URL: defaultUrl( - process.env.PRICE_API_BASE_URL, - 'https://example.com/price/', - ), - TOKEN_API_BASE_URL: defaultUrl( - process.env.TOKEN_API_BASE_URL, - 'https://example.com/token/', - ), - STATIC_API_BASE_URL: defaultUrl( - process.env.STATIC_API_BASE_URL, - 'https://example.com/static/', - ), - SECURITY_ALERTS_API_BASE_URL: defaultUrl( - process.env.SECURITY_ALERTS_API_BASE_URL, - 'https://example.com/security/', - ), - NFT_API_BASE_URL: defaultUrl( - process.env.NFT_API_BASE_URL, - 'https://example.com/nft/', - ), - LOCAL_API_BASE_URL: defaultUrl( - process.env.LOCAL_API_BASE_URL, - 'http://127.0.0.1:3000', - ), + ENVIRONMENT: process.env.ENVIRONMENT ?? '', + RPC_URL_MAINNET_LIST: process.env.RPC_URL_MAINNET_LIST ?? '', + RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST ?? '', + RPC_URL_TESTNET_LIST: process.env.RPC_URL_TESTNET_LIST ?? '', + RPC_URL_LOCALNET_LIST: process.env.RPC_URL_LOCALNET_LIST ?? '', + RPC_WEB_SOCKET_URL_MAINNET: process.env.RPC_WEB_SOCKET_URL_MAINNET ?? '', + RPC_WEB_SOCKET_URL_DEVNET: process.env.RPC_WEB_SOCKET_URL_DEVNET ?? '', + RPC_WEB_SOCKET_URL_TESTNET: process.env.RPC_WEB_SOCKET_URL_TESTNET ?? '', + RPC_WEB_SOCKET_URL_LOCALNET: process.env.RPC_WEB_SOCKET_URL_LOCALNET ?? '', + EXPLORER_BASE_URL: process.env.EXPLORER_BASE_URL ?? '', + PRICE_API_BASE_URL: process.env.PRICE_API_BASE_URL ?? '', + TOKEN_API_BASE_URL: process.env.TOKEN_API_BASE_URL ?? '', + STATIC_API_BASE_URL: process.env.STATIC_API_BASE_URL ?? '', + SECURITY_ALERTS_API_BASE_URL: process.env.SECURITY_ALERTS_API_BASE_URL ?? '', + NFT_API_BASE_URL: process.env.NFT_API_BASE_URL ?? '', + LOCAL_API_BASE_URL: process.env.LOCAL_API_BASE_URL ?? '', }; const config: SnapConfig = { diff --git a/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts b/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts index 9245222fe..465100f1b 100644 --- a/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts +++ b/packages/solana-wallet-snap/src/core/services/config/ConfigProvider.ts @@ -124,9 +124,7 @@ export class ConfigProvider { #parseEnvironment() { const rawEnvironment = { - // Empty ENVIRONMENT must fall back for unit tests without CI secrets. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - ENVIRONMENT: process.env.ENVIRONMENT || 'local', + ENVIRONMENT: process.env.ENVIRONMENT, RPC_URL_MAINNET_LIST: process.env.RPC_URL_MAINNET_LIST, RPC_URL_DEVNET_LIST: process.env.RPC_URL_DEVNET_LIST, RPC_URL_TESTNET_LIST: process.env.RPC_URL_TESTNET_LIST, diff --git a/packages/tron-wallet-snap/jest.setup.ts b/packages/tron-wallet-snap/jest.setup.ts index db0ef4e4d..8902e2361 100644 --- a/packages/tron-wallet-snap/jest.setup.ts +++ b/packages/tron-wallet-snap/jest.setup.ts @@ -1,31 +1,3 @@ import { config } from 'dotenv'; config(); - -const testDefaults: Record = { - ENVIRONMENT: 'test', - RPC_URL_LIST_MAINNET: 'https://example.com/tron-mainnet', - RPC_URL_LIST_NILE_TESTNET: 'https://example.com/tron-nile', - RPC_URL_LIST_SHASTA_TESTNET: 'https://example.com/tron-shasta', - EXPLORER_MAINNET_BASE_URL: 'https://tronscan.org/', - EXPLORER_NILE_BASE_URL: 'https://nile.tronscan.org/', - EXPLORER_SHASTA_BASE_URL: 'https://shasta.tronscan.org/', - PRICE_API_BASE_URL: 'https://example.com/price/', - TOKEN_API_BASE_URL: 'https://example.com/token/', - STATIC_API_BASE_URL: 'https://example.com/static/', - SECURITY_ALERTS_API_BASE_URL: 'https://example.com/security/', - NFT_API_BASE_URL: 'https://example.com/nft/', - LOCAL_API_BASE_URL: 'http://127.0.0.1:3000', - TRONGRID_BASE_URL_MAINNET: 'https://example.com/trongrid-mainnet/', - TRONGRID_BASE_URL_NILE: 'https://example.com/trongrid-nile/', - TRONGRID_BASE_URL_SHASTA: 'https://example.com/trongrid-shasta/', - TRON_HTTP_BASE_URL_MAINNET: 'https://example.com/tron-http-mainnet/', - TRON_HTTP_BASE_URL_NILE: 'https://example.com/tron-http-nile/', - TRON_HTTP_BASE_URL_SHASTA: 'https://example.com/tron-http-shasta/', -}; - -for (const [key, value] of Object.entries(testDefaults)) { - // Empty strings from dotenv should also fall back to test defaults. - // eslint-disable-next-line no-restricted-globals, @typescript-eslint/prefer-nullish-coalescing - process.env[key] ||= value; -} diff --git a/packages/tron-wallet-snap/snap.config.ts b/packages/tron-wallet-snap/snap.config.ts index 0a36cf018..ed7d1ef21 100644 --- a/packages/tron-wallet-snap/snap.config.ts +++ b/packages/tron-wallet-snap/snap.config.ts @@ -4,90 +4,37 @@ import { resolve } from 'path'; dotenv(); -const defaultUrl = (value: string | undefined, fallback: string): string => - value && value.length > 0 ? value : fallback; - const config: SnapConfig = { input: resolve(__dirname, 'src/index.ts'), server: { port: 8080, }, environment: { - // Empty ENVIRONMENT must fall back; `??` would keep ''. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - ENVIRONMENT: process.env.ENVIRONMENT || 'local', - RPC_URL_LIST_MAINNET: defaultUrl( - process.env.RPC_URL_LIST_MAINNET, - 'https://example.com/tron-mainnet', - ), - RPC_URL_LIST_NILE_TESTNET: defaultUrl( - process.env.RPC_URL_LIST_NILE_TESTNET, - 'https://example.com/tron-nile', - ), - RPC_URL_LIST_SHASTA_TESTNET: defaultUrl( - process.env.RPC_URL_LIST_SHASTA_TESTNET, - 'https://example.com/tron-shasta', - ), - EXPLORER_MAINNET_BASE_URL: defaultUrl( - process.env.EXPLORER_MAINNET_BASE_URL, - 'https://tronscan.org/', - ), - EXPLORER_NILE_BASE_URL: defaultUrl( - process.env.EXPLORER_NILE_BASE_URL, - 'https://nile.tronscan.org/', - ), - EXPLORER_SHASTA_BASE_URL: defaultUrl( - process.env.EXPLORER_SHASTA_BASE_URL, - 'https://shasta.tronscan.org/', - ), - PRICE_API_BASE_URL: defaultUrl( - process.env.PRICE_API_BASE_URL, - 'https://example.com/price/', - ), - TOKEN_API_BASE_URL: defaultUrl( - process.env.TOKEN_API_BASE_URL, - 'https://example.com/token/', - ), - STATIC_API_BASE_URL: defaultUrl( - process.env.STATIC_API_BASE_URL, - 'https://example.com/static/', - ), - SECURITY_ALERTS_API_BASE_URL: defaultUrl( - process.env.SECURITY_ALERTS_API_BASE_URL, - 'https://example.com/security/', - ), - NFT_API_BASE_URL: defaultUrl( - process.env.NFT_API_BASE_URL, - 'https://example.com/nft/', - ), - LOCAL_API_BASE_URL: defaultUrl( - process.env.LOCAL_API_BASE_URL, - 'http://127.0.0.1:3000', - ), - TRONGRID_BASE_URL_MAINNET: defaultUrl( - process.env.TRONGRID_BASE_URL_MAINNET, - 'https://example.com/trongrid-mainnet/', - ), - TRONGRID_BASE_URL_NILE: defaultUrl( - process.env.TRONGRID_BASE_URL_NILE, - 'https://example.com/trongrid-nile/', - ), - TRONGRID_BASE_URL_SHASTA: defaultUrl( - process.env.TRONGRID_BASE_URL_SHASTA, - 'https://example.com/trongrid-shasta/', - ), - TRON_HTTP_BASE_URL_MAINNET: defaultUrl( - process.env.TRON_HTTP_BASE_URL_MAINNET, - 'https://example.com/tron-http-mainnet/', - ), - TRON_HTTP_BASE_URL_NILE: defaultUrl( - process.env.TRON_HTTP_BASE_URL_NILE, - 'https://example.com/tron-http-nile/', - ), - TRON_HTTP_BASE_URL_SHASTA: defaultUrl( - process.env.TRON_HTTP_BASE_URL_SHASTA, - 'https://example.com/tron-http-shasta/', - ), + ENVIRONMENT: process.env.ENVIRONMENT ?? '', + // RPC + RPC_URL_LIST_MAINNET: process.env.RPC_URL_LIST_MAINNET ?? '', + RPC_URL_LIST_NILE_TESTNET: process.env.RPC_URL_LIST_NILE_TESTNET ?? '', + RPC_URL_LIST_SHASTA_TESTNET: process.env.RPC_URL_LIST_SHASTA_TESTNET ?? '', + // Block explorer + EXPLORER_MAINNET_BASE_URL: process.env.EXPLORER_MAINNET_BASE_URL ?? '', + EXPLORER_NILE_BASE_URL: process.env.EXPLORER_NILE_BASE_URL ?? '', + EXPLORER_SHASTA_BASE_URL: process.env.EXPLORER_SHASTA_BASE_URL ?? '', + // APIs + PRICE_API_BASE_URL: process.env.PRICE_API_BASE_URL ?? '', + TOKEN_API_BASE_URL: process.env.TOKEN_API_BASE_URL ?? '', + STATIC_API_BASE_URL: process.env.STATIC_API_BASE_URL ?? '', + SECURITY_ALERTS_API_BASE_URL: + process.env.SECURITY_ALERTS_API_BASE_URL ?? '', + NFT_API_BASE_URL: process.env.NFT_API_BASE_URL ?? '', + LOCAL_API_BASE_URL: process.env.LOCAL_API_BASE_URL ?? '', + // TronGrid API + TRONGRID_BASE_URL_MAINNET: process.env.TRONGRID_BASE_URL_MAINNET ?? '', + TRONGRID_BASE_URL_NILE: process.env.TRONGRID_BASE_URL_NILE ?? '', + TRONGRID_BASE_URL_SHASTA: process.env.TRONGRID_BASE_URL_SHASTA ?? '', + // Tron HTTP API + TRON_HTTP_BASE_URL_MAINNET: process.env.TRON_HTTP_BASE_URL_MAINNET ?? '', + TRON_HTTP_BASE_URL_NILE: process.env.TRON_HTTP_BASE_URL_NILE ?? '', + TRON_HTTP_BASE_URL_SHASTA: process.env.TRON_HTTP_BASE_URL_SHASTA ?? '', }, polyfills: true, };