Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Package in this monorepo are represented by subdirectories in `packages/`. Each
- `LICENSE` — Each package has a license that describes how engineers can use it in projects.
- Configuration files — See below.

Note that the package template in `scripts/create-package/package-template` also uses this same structure.
Note that the package template in `scripts/create-package/library-template` also uses this same structure.

### Configuration files

Expand All @@ -55,15 +55,15 @@ The monorepo uses a hierarchical configuration approach for different tools. For
- `tsconfig.scripts.json` defines shared TypeScript settings for directories in `scripts/`.
- The root `lint:tsc` script checks repository scripts with `tsconfig.json`, then checks each
workspace package configuration directly. It does not build Snap bundles with `tsc`.
- `packages/**/tsconfig.json` (and `scripts/create-package/package-template/tsconfig.json`) defines TypeScript settings for each package that are meant to be used by code editors and type checking.
- `packages/**/tsconfig.json` (and `scripts/create-package/library-template/tsconfig.json`) defines TypeScript settings for each package that are meant to be used by code editors and type checking.
- Non-Snap packages and the package template also have `tsconfig.build.json` files for `ts-bridge` declaration builds. Snap packages do not have build configs because `mm-snap` builds their bundles.
- `scripts/create-package/tsconfig.json` customizes TypeScript settings for the `create-package` tool.

#### Jest

- `jest.config.packages.js` defines shared Jest settings for all directories in `packages/`.
- `jest.config.scripts.js` defines shared Jest settings for all directories in `scripts/`.
- `packages/**/jest.config.js` (and `scripts/create-package/package-template/jest.config.js`) customizes Jest settings for each package.
- `packages/**/jest.config.js` (and `scripts/create-package/library-template/jest.config.js`) customizes Jest settings for each package.

#### ESLint

Expand All @@ -76,7 +76,7 @@ The monorepo uses a hierarchical configuration approach for different tools. For

#### TypeDoc

- `packages/**/typedoc.json` (and `scripts/create-package/package-template/typedoc.js`) defines TypeDoc settings for each package.
- `packages/**/typedoc.json` (and `scripts/create-package/library-template/typedoc.json`) defines TypeDoc settings for each package.

#### Other files

Expand Down Expand Up @@ -169,7 +169,7 @@ Each consumer-facing change to a package should be accompanied by one or more en

## Adding new packages

Use `yarn create-package --name <name> --description <description>` to add a new package to the monorepo.
Use `yarn create-package --type <snap|library> --name <name> --description <description>` to add a new package to the monorepo. The `--type` option is required: use `snap` to scaffold a new Snap, or `library` (shorthand: `lib`) to scaffold a new non-Snap package.

## Code guidelines

Expand Down Expand Up @@ -225,12 +225,12 @@ Use the `sample-gas-prices-service/` directory in the `sample-controllers` packa

## Cursor Cloud specific instructions

This repo is a Yarn 4 monorepo of MetaMask Snaps. The two products live in `packages/`: `@metamask/bitcoin-wallet-snap` and `@metamask/sample-snap`. Dependencies are already installed by the startup update script (`corepack enable` + `yarn install`), so no install step is needed at session start. Yarn is pinned via `packageManager` (`yarn@4.17.1`) and provisioned by corepack; use `yarn ...`, not the classic global yarn.
This repo is a Yarn 4 monorepo of MetaMask Snaps. The products live in `packages/`, including `@metamask/bitcoin-wallet-snap`. Dependencies are already installed by the startup update script (`corepack enable` + `yarn install`), so no install step is needed at session start. Yarn is pinned via `packageManager` (`yarn@4.17.1`) and provisioned by corepack; use `yarn ...`, not the classic global yarn.

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 <pkg> build` before `yarn workspace <pkg> 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.
- **Running a snap:** `yarn workspace <pkg> run serve` serves the pre-built bundle at `http://localhost:8080` (`/snap.manifest.json` and `/dist/bundle.js`); `yarn workspace <pkg> 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` test suites (they install the snap and invoke its JSON-RPC methods, e.g. the generated 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.
9 changes: 6 additions & 3 deletions docs/processes/adding-new-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
Manually creating a new monorepo package can be a tedious, even frustrating process. To alleviate that problem, we have created a CLI that automates most of the job for us, creatively titled [`create-package`](../../scripts/create-package/). To create a new monorepo package, follow these steps:

1. Create a new package using `yarn create-package`.
- You must specify the kind of package you want with the `--type` option. There is no default:
- `--type snap` creates a new Snap, whose bundle is built with `mm-snap`.
- `--type library` (or the `--type lib` shorthand) creates a new non-Snap package, whose declarations are built with `ts-bridge`.
- Use the `--help` flag for usage information.
- Once this is done, you can find a package with your chosen name in `/packages`.
2. Make sure your license is correct.
Expand All @@ -14,16 +17,16 @@ Manually creating a new monorepo package can be a tedious, even frustrating proc
3. Update `.github/CODEOWNERS` to assign a team as the owner of the new package.
4. Add your dependencies.
- Do this as normal using `yarn`.
- Remember, if you are adding other monorepo packages as dependents, don't forget to add them to the `references` array in your package's `tsconfig.json` and `tsconfig.build.json`.
- Remember, if you are adding other monorepo packages as dependents, don't forget to add them to the `references` array in your package's `tsconfig.json` and, for non-Snap packages, `tsconfig.build.json`.

And that's it!

### Contributing to `create-package`

Along with this documentation, `create-package` is intended to be the source of truth for the process of adding new packages to the monorepo. Consequently, to change that process, you will want to change `create-package`.

The `create-package` directory contains a [template package](../../scripts/create-package/package-template/). The CLI is not aware of the contents of the template, only that its files have [placeholder values](../../scripts/create-package/constants.ts). When a new package is created, the template files are read from disk, the placeholder values are replaced with real ones, and the updated files are added to a new directory in `/packages`. To modify the template package:
The `create-package` directory contains a template for each package type: a [template Snap](../../scripts/create-package/snap-template/) and a [template library](../../scripts/create-package/library-template/). The CLI is not aware of the contents of the templates, only that their files have [placeholder values](../../scripts/create-package/constants.ts). When a new package is created, the template files for the requested type are read from disk, the placeholder values are replaced with real ones, and the updated files are added to a new directory in `/packages`. To modify the templates:

- If you need to add or modify any files or folders, just go ahead and make your changes in [`/scripts/create-package/package-template`](../../scripts/create-package/package-template/). The CLI will read whatever's in that directory and write it to disk.
- If you need to add or modify any files or folders, just go ahead and make your changes in [`/scripts/create-package/snap-template`](../../scripts/create-package/snap-template/) or [`/scripts/create-package/library-template`](../../scripts/create-package/library-template/). The CLI will read whatever's in those directories and write it to disk.
- If you need to add or modify any placeholders, make sure that your desired values are added to both the relevant file(s) and [`/scripts/create-package/constants.ts`](../../scripts/create-package/constants.ts). Then, update the implementation of the CLI accordingly.
- As with placeholders, updating the monorepo files that the CLI interacts with begins by updating [`/scripts/create-package/constants.ts`](../../scripts/create-package/constants.ts).
3 changes: 2 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ const config = createConfig([
'**/dist/**',
'**/docs/**',
'.yarn/**',
'scripts/create-package/package-template/**',
'scripts/create-package/snap-template/**',
'scripts/create-package/library-template/**',
],
},
{
Expand Down
4 changes: 2 additions & 2 deletions jest.config.scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
coverageDirectory: '<rootDir>/scripts/coverage',

// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/package-template/'],
coveragePathIgnorePatterns: ['/(snap|library)-template/'],

// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'babel',
Expand Down Expand Up @@ -65,7 +65,7 @@ module.exports = {
],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
testPathIgnorePatterns: ['/package-template/'],
testPathIgnorePatterns: ['/(snap|library)-template/'],

// Default timeout of a test in milliseconds.
testTimeout: 5000,
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"test:clean": "yarn workspaces foreach --all --parallel --verbose run test:clean && yarn test",
"test:packages": "yarn test:verbose --silent --collectCoverage=false --reporters=jest-silent-reporter",
"test:scripts": "NODE_OPTIONS=--experimental-vm-modules yarn jest --config ./jest.config.scripts.js --silent",
"test:verbose": "yarn workspaces foreach --all --exclude @metamask/sample-snap --parallel --verbose run test:verbose",
"test:verbose": "yarn workspaces foreach --all --parallel --verbose run test:verbose",
"workspaces:list-versions": "./scripts/list-workspace-versions.sh"
},
"devDependencies": {
Expand All @@ -53,7 +53,6 @@
"@typescript-eslint/eslint-plugin": "^8.48.0",
"@typescript-eslint/parser": "^8.48.0",
"@yarnpkg/types": "^4.0.0",
"comment-json": "^4.5.1",
"depcheck": "^1.4.7",
"eslint": "^9.39.1",
"eslint-config-prettier": "^9.1.0",
Expand All @@ -72,7 +71,6 @@
"lodash": "^4.17.21",
"nock": "^13.3.1",
"oxfmt": "^0.45.0",
"prettier": "^3.3.3",
"prettier-2": "npm:prettier@^2.8.8",
"rimraf": "^6.0.1",
"semver": "^7.6.3",
Expand Down
19 changes: 0 additions & 19 deletions packages/sample-snap/CHANGELOG.md

This file was deleted.

12 changes: 0 additions & 12 deletions packages/sample-snap/README.md

This file was deleted.

106 changes: 81 additions & 25 deletions scripts/create-package/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,31 @@ function getMockArgv(...args: string[]): string[] {
}

/**
* Returns the parsed `yargs.Arguments` object for a given package name and
* description.
* Returns the parsed `yargs.Arguments` object for a given package name,
* description, and type.
*
* @param name - The package name.
* @param description - The package description.
* @param type - The package type.
* @returns The parsed argv object.
*/
function getParsedArgv(
name: string,
description: string,
type: string,
): {
_: [];
$0: 'create-package';
name: `@metamask/${string}`;
description: string;
type: string;
} {
return {
_: [],
$0: 'create-package',
name: `@metamask/${name}`,
description,
type,
};
}

Expand Down Expand Up @@ -67,9 +71,9 @@ describe('create-package/cli', () => {
const defaultCommand = commandMap.$0;
jest.spyOn(defaultCommand, 'handler').mockImplementation();

await expect(cli(getMockArgv('--name', ' '), commands)).rejects.toThrow(
'exit: 1',
);
await expect(
cli(getMockArgv('--name', ' ', '--type', 'snap'), commands),
).rejects.toThrow('exit: 1');

expect(console.error).toHaveBeenCalledWith(
'The argument "name" was processed to an empty string. Please provide a value with non-whitespace characters.',
Expand All @@ -82,24 +86,20 @@ describe('create-package/cli', () => {
jest.spyOn(defaultCommand, 'handler');

jest.spyOn(utils, 'readMonorepoFiles').mockResolvedValue({
tsConfig: {},
tsConfigBuild: {},
nodeVersions: '>=18.0.0',
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
});
jest.spyOn(utils, 'finalizeAndWriteData').mockResolvedValue();

expect(
await cli(
getMockArgv('--name', 'foo', '--description', 'bar'),
getMockArgv('--name', 'foo', '--description', 'bar', '--type', 'lib'),
commands,
),
).toBeUndefined();

expect(defaultCommand.handler).toHaveBeenCalledTimes(1);
expect(defaultCommand.handler).toHaveBeenCalledWith(
expect.objectContaining(getParsedArgv('foo', 'bar')),
expect.objectContaining(getParsedArgv('foo', 'bar', 'library')),
);
});

Expand All @@ -108,24 +108,44 @@ describe('create-package/cli', () => {
jest.spyOn(defaultCommand, 'handler');

jest.spyOn(utils, 'readMonorepoFiles').mockResolvedValue({
tsConfig: {},
tsConfigBuild: {},
nodeVersions: '>=18.0.0',
// TODO: Replace `any` with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
});
jest.spyOn(utils, 'finalizeAndWriteData').mockResolvedValue();

expect(
await cli(
getMockArgv('--name', '@metamask/foo', '--description', 'bar'),
getMockArgv(
'--name',
'@metamask/foo',
'--description',
'bar',
'--type',
'snap',
),
commands,
),
).toBeUndefined();

expect(defaultCommand.handler).toHaveBeenCalledTimes(1);
expect(defaultCommand.handler).toHaveBeenCalledWith(
expect.objectContaining(getParsedArgv('foo', 'bar')),
expect.objectContaining(getParsedArgv('foo', 'bar', 'snap')),
);
});

it('should normalize the "lib" shorthand to "library"', async () => {
const defaultCommand = commandMap.$0;
jest.spyOn(defaultCommand, 'handler').mockImplementation();

expect(
await cli(
getMockArgv('--name', 'foo', '--description', 'bar', '--type', 'lib'),
commands,
),
).toBeUndefined();

expect(defaultCommand.handler).toHaveBeenCalledTimes(1);
expect(defaultCommand.handler).toHaveBeenCalledWith(
expect.objectContaining(getParsedArgv('foo', 'bar', 'library')),
);
});

Expand All @@ -135,14 +155,21 @@ describe('create-package/cli', () => {

expect(
await cli(
getMockArgv('--name', 'foo', '--description', 'bar'),
getMockArgv(
'--name',
'foo',
'--description',
'bar',
'--type',
'library',
),
commands,
),
).toBeUndefined();

expect(defaultCommand.handler).toHaveBeenCalledTimes(1);
expect(defaultCommand.handler).toHaveBeenCalledWith(
expect.objectContaining(getParsedArgv('foo', 'bar')),
expect.objectContaining(getParsedArgv('foo', 'bar', 'library')),
);
});

Expand All @@ -151,7 +178,7 @@ describe('create-package/cli', () => {
jest.spyOn(defaultCommand, 'handler').mockImplementation();

await expect(
cli(getMockArgv('--description', 'bar'), commands),
cli(getMockArgv('--description', 'bar', '--type', 'snap'), commands),
).rejects.toThrow('exit: 1');

expect(console.error).toHaveBeenCalledWith(
Expand All @@ -163,13 +190,42 @@ describe('create-package/cli', () => {
const defaultCommand = commandMap.$0;
jest.spyOn(defaultCommand, 'handler').mockImplementation();

await expect(cli(getMockArgv('--name', 'foo'), commands)).rejects.toThrow(
'exit: 1',
);
await expect(
cli(getMockArgv('--name', 'foo', '--type', 'snap'), commands),
).rejects.toThrow('exit: 1');

expect(console.error).toHaveBeenCalledWith(
'Missing required argument: "description"',
);
});

it('should error if the package type is missing', async () => {
const defaultCommand = commandMap.$0;
jest.spyOn(defaultCommand, 'handler').mockImplementation();

await expect(
cli(getMockArgv('--name', 'foo', '--description', 'bar'), commands),
).rejects.toThrow('exit: 1');

expect(console.error).toHaveBeenCalledWith(
'Missing required argument: "type". Please specify either "snap" or "library".',
);
});

it('should error if the package type is invalid', async () => {
const defaultCommand = commandMap.$0;
jest.spyOn(defaultCommand, 'handler').mockImplementation();

await expect(
cli(
getMockArgv('--name', 'foo', '--description', 'bar', '--type', 'foo'),
commands,
),
).rejects.toThrow('exit: 1');

expect(console.error).toHaveBeenCalledWith(
'Invalid package type: "foo". Valid types are: "snap", "library", "lib".',
);
});
});
});
Loading
Loading