Skip to content

Commit 0b75d37

Browse files
authored
Add @metamask/build-utils package (#3577)
## Explanation In MetaMask/metamask-mobile#7734, code fence support was added to MetaMask Mobile by essentially copypasting the implementation from the extension. This PR extracts the shared logic from the extension and mobile implementations of code fencing into a new package, `@metamask/build-utils`, which can be imported into the respective clients. A draft integration in the extension can be found in MetaMask/metamask-extension#22033, which uses local copies of the built version of the new package. `@metamask/build-utils` currently implements the shared logic for code fence parsing and removal,, as well as linting transformed files. The consumer is responsible for integrating the fence parser into the given build system, and configuring the ESLint instance used for linting. Noteworthy changes / decisions made: - `ONLY_INCLUDE_IN` has been renamed to `ONLY_INCLUDE_IF`, which is more in line with its actual usage. - When code fencing was originally implemented, we imagined that it would be used to differentiate between specific builds, e.g. "main" and "beta". Since then, the parameters of the command are labels corresponding to different features. Thus, fenced code is not included _in_ certain builds so much as _if_ certain features are enabled. - The decision to only extract the `removeFencedCode` and `lintTransformedFile` functions was made for two reasons primarily: - The integrations of code fence removal in [the extension / Browserify](https://github.com/MetaMask/metamask-extension/blob/develop/development/build/transforms/remove-fenced-code.js/#L12-L66) and [mobile / Metro](https://github.com/MetaMask/metamask-mobile/blob/3ba70c4d083ac01d63cdfc06c4257f47fa808cc2/metro.transform.js) are significantly different. - The different applications have different ESLint configurations. It seems inappropriate to make this package responsible for being aware of the ESLint configs of the consumers. - See also: https://github.com/MetaMask/metamask-mobile/pull/7837/files#r1401408233 ## References * Related to MetaMask/metamask-mobile#7734 * Related to MetaMask/metamask-extension#22033 ## Changelog ### `@metamask/build-utils` - **Added**: Initial release ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate
1 parent 9f9e935 commit 0b75d37

18 files changed

+1723
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ linkStyle default opacity:0.5
5050
approval_controller(["@metamask/approval-controller"]);
5151
assets_controllers(["@metamask/assets-controllers"]);
5252
base_controller(["@metamask/base-controller"]);
53+
build_utils(["@metamask/build-utils"]);
5354
composable_controller(["@metamask/composable-controller"]);
5455
controller_utils(["@metamask/controller-utils"]);
5556
ens_controller(["@metamask/ens-controller"]);
@@ -134,9 +135,9 @@ linkStyle default opacity:0.5
134135
signature_controller --> approval_controller;
135136
signature_controller --> base_controller;
136137
signature_controller --> controller_utils;
138+
signature_controller --> keyring_controller;
137139
signature_controller --> logging_controller;
138140
signature_controller --> message_manager;
139-
signature_controller --> keyring_controller;
140141
transaction_controller --> approval_controller;
141142
transaction_controller --> base_controller;
142143
transaction_controller --> controller_utils;

packages/build-utils/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [Unreleased]
8+
9+
[Unreleased]: https://github.com/MetaMask/core/

packages/build-utils/LICENSE

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
MIT License
2+
3+
Copyright (c) 2023 MetaMask
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE

packages/build-utils/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# `@metamask/build-utils`
2+
3+
Utilities for building MetaMask applications.
4+
5+
## Installation
6+
7+
`yarn add @metamask/build-utils`
8+
9+
or
10+
11+
`npm install @metamask/build-utils`
12+
13+
## Usage
14+
15+
### `/transforms`
16+
17+
See [the transforms readme](https://github.com/MetaMask/core/packages/build-utils/src/transforms/README.md).
18+
19+
## Contributing
20+
21+
This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).

packages/build-utils/jest.config.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
const merge = require('deepmerge');
7+
const path = require('path');
8+
9+
const baseConfig = require('../../jest.config.packages');
10+
11+
const displayName = path.basename(__dirname);
12+
13+
module.exports = merge(baseConfig, {
14+
// The display name when running multiple projects
15+
displayName,
16+
17+
// An object that configures minimum threshold enforcement for coverage results
18+
coverageThreshold: {
19+
global: {
20+
branches: 100,
21+
functions: 100,
22+
lines: 100,
23+
statements: 100,
24+
},
25+
},
26+
});

packages/build-utils/package.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@metamask/build-utils",
3+
"version": "0.0.0",
4+
"description": "Utilities for building MetaMask applications",
5+
"keywords": [
6+
"MetaMask",
7+
"Ethereum"
8+
],
9+
"homepage": "https://github.com/MetaMask/core/tree/main/packages/build-utils#readme",
10+
"bugs": {
11+
"url": "https://github.com/MetaMask/core/issues"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "https://github.com/MetaMask/core.git"
16+
},
17+
"license": "MIT",
18+
"main": "./dist/index.js",
19+
"types": "./dist/index.d.ts",
20+
"files": [
21+
"dist/"
22+
],
23+
"scripts": {
24+
"build:docs": "typedoc",
25+
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/build-utils",
26+
"publish:preview": "yarn npm publish --tag preview",
27+
"test": "jest --reporters=jest-silent-reporter",
28+
"test:clean": "jest --clearCache",
29+
"test:verbose": "jest --verbose",
30+
"test:watch": "jest --watch"
31+
},
32+
"dependencies": {
33+
"@metamask/utils": "^8.2.0"
34+
},
35+
"devDependencies": {
36+
"@metamask/auto-changelog": "^3.4.3",
37+
"@types/eslint": "^8.44.7",
38+
"@types/jest": "^27.4.1",
39+
"deepmerge": "^4.2.2",
40+
"eslint": "^8.44.0",
41+
"jest": "^27.5.1",
42+
"ts-jest": "^27.1.4",
43+
"typedoc": "^0.24.8",
44+
"typedoc-plugin-missing-exports": "^2.0.0",
45+
"typescript": "~4.8.4"
46+
},
47+
"engines": {
48+
"node": ">=16.0.0"
49+
},
50+
"publishConfig": {
51+
"access": "public",
52+
"registry": "https://registry.npmjs.org/"
53+
}
54+
}

packages/build-utils/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type { FeatureLabels } from './transforms/remove-fenced-code';
2+
export { removeFencedCode } from './transforms/remove-fenced-code';
3+
export { lintTransformedFile } from './transforms/utils';
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# Source file transforms
2+
3+
This directory contains home-grown transforms for the build systems of the MetaMask applications.
4+
5+
## Remove Fenced Code
6+
7+
> `./remove-fenced-code.ts`
8+
9+
### Usage
10+
11+
Let's imagine you've added some fences to your source code.
12+
13+
```typescript
14+
this.store.updateStructure({
15+
/** ..., */
16+
GasFeeController: this.gasFeeController,
17+
TokenListController: this.tokenListController,
18+
///: BEGIN:ONLY_INCLUDE_IF(snaps)
19+
SnapController: this.snapController,
20+
///: END:ONLY_INCLUDE_IF
21+
});
22+
```
23+
24+
The transform should be applied on your raw source files as they are committed to
25+
your repository, before anything else (e.g. Babel, `tsc`, etc.) parses or modifies them.
26+
27+
```typescript
28+
import {
29+
FeatureLabels,
30+
removeFencedCode,
31+
lintTransformedFile,
32+
} from '@metamask/build-utils';
33+
34+
// Let's imagine this function exists in your build system and is called immediately
35+
// after your source files are read from disk.
36+
async function applyTransforms(
37+
filePath: string,
38+
fileContent: string,
39+
features: FeatureLabels,
40+
shouldLintTransformedFiles: boolean = true,
41+
): string {
42+
const [newFileContent, wasModified] = removeFencedCode(
43+
filePath,
44+
fileContent,
45+
features,
46+
);
47+
48+
// You may choose to disable linting during e.g. dev builds since lint failures cause
49+
// an error to be thrown.
50+
if (wasModified && shouldLintTransformedFiles) {
51+
// You probably only need a singleton ESLint instance for your linting purposes.
52+
// See the lintTransformedFile documentation for important notes about usage.
53+
const eslintInstance = getESLintInstance();
54+
await lintTransformedFile(eslintInstance, filePath, newFileContent);
55+
}
56+
return newFileContent;
57+
}
58+
59+
// Then, in the relevant part of your build process...
60+
61+
const features: FeatureLabels = {
62+
active: new Set(['foo']), // Fences with these features will be included.
63+
all: new Set(['snaps', 'foo' /** etc. */]), // All extant features must be listed here.
64+
};
65+
66+
const transformedFile = await applyTransforms(
67+
filePath,
68+
fileContent,
69+
features,
70+
shouldLintTransformedFiles,
71+
);
72+
73+
// Do something with the results.
74+
// continueBuildProcess(transformedFile);
75+
```
76+
77+
After the transform has been applied as above, the example source code will look like this:
78+
79+
```typescript
80+
this.store.updateStructure({
81+
/** ..., */
82+
GasFeeController: this.gasFeeController,
83+
TokenListController: this.tokenListController,
84+
});
85+
```
86+
87+
### Overview
88+
89+
When creating builds that support different features, it is desirable to exclude
90+
unsupported features, files, and dependencies at build time. Undesired files and
91+
dependencies can be excluded wholesale, but the _use_ of undesired modules in
92+
files that should otherwise be included – i.e. import statements and references
93+
to those imports – cannot.
94+
95+
To support the exclusion of the use of undesired modules at build time, we
96+
introduce the concept of code fencing to our build system. Our code fencing
97+
syntax amounts to a tiny DSL, which is specified below.
98+
99+
The transform expects to receive the contents of individual files as a single string,
100+
which it will parse in order to identify any code fences. If any fences that should not
101+
be included in the current build are found, the fences and the lines that they wrap
102+
are deleted. An error is thrown if a malformed fence is identified.
103+
104+
For example, the following fenced code:
105+
106+
```javascript
107+
this.store.updateStructure({
108+
...,
109+
GasFeeController: this.gasFeeController,
110+
TokenListController: this.tokenListController,
111+
///: BEGIN:ONLY_INCLUDE_IF(snaps)
112+
SnapController: this.snapController,
113+
///: END:ONLY_INCLUDE_IF
114+
});
115+
```
116+
117+
Is transformed as follows if the current build should not include the `snaps` feature:
118+
119+
```javascript
120+
this.store.updateStructure({
121+
...,
122+
GasFeeController: this.gasFeeController,
123+
TokenListController: this.tokenListController,
124+
});
125+
```
126+
127+
Note that multiple features can be specified by separating them with
128+
commands inside the parameter parentheses:
129+
130+
```javascript
131+
///: BEGIN:ONLY_INCLUDE_IF(build-beta,build-flask)
132+
```
133+
134+
### Code Fencing Syntax
135+
136+
> In the specification, angle brackets, `< >`, indicate required tokens, while
137+
> straight brackets, `[ ]`, indicate optional tokens.
138+
>
139+
> Alphabetical characters identify the name and purpose of a token. All other
140+
> characters, including parentheses, `( )`, are literals.
141+
142+
A fence line is a single-line JavaScript comment, optionally surrounded by
143+
whitespace, in the following format:
144+
145+
```text
146+
///: <terminus>:<command>[(parameters)]
147+
148+
|__| |________________________________|
149+
| |
150+
| |
151+
sentinel directive
152+
```
153+
154+
The first part of a fence line is the **sentinel** which is always the string
155+
"`///:`". If the first four non-whitespace characters of a line are not exactly the
156+
**sentinel** the line will be ignored by the parser. The **sentinel** must be
157+
succeeded by a single space character, or parsing will fail.
158+
159+
The remainder of the fence line is called the **directive**
160+
The directive consists of a **terminus** **command** and **parameters**
161+
162+
- The **terminus** is one of the strings `BEGIN` and `END`. It must be followed by
163+
a single colon, `:`.
164+
- The **command** is a string of uppercase alphabetical characters, optionally
165+
including underscores, `_`. The possible commands are listed later in this
166+
specification.
167+
- The **parameters** are a string of comma-separated RegEx `\w` strings. The parameters
168+
string must be parenthesized, only specified for `BEGIN` directives, and valid for its
169+
command.
170+
171+
A valid code fence consists of two fence lines surrounding one or more lines of
172+
non-fence lines. The first fence line must consist of a `BEGIN` directive, and
173+
the second an `END` directive. The command of both directives must be the same,
174+
and the parameters (if any) must be valid for the command. Nesting is not intended
175+
to be supported, and may produce undefined behavior.
176+
177+
If an invalid fence is detected, parsing will fail, and the transform will throw
178+
an error.
179+
180+
### Commands
181+
182+
#### `ONLY_INCLUDE_IF`
183+
184+
This, the only command defined so far, is used to exclude lines of code depending
185+
on flags provided to the current build process. If a particular set of lines should
186+
only be included in e.g. the beta build type, they should be wrapped as follows:
187+
188+
```javascript
189+
///: BEGIN:ONLY_INCLUDE_IF(build-beta)
190+
console.log('I am only included in beta builds.');
191+
///: END:ONLY_INCLUDE_IF
192+
```
193+
194+
At build time, the fences and the fenced lines will be removed if the `build-beta`
195+
flag is not provided to the transform.
196+
197+
The parameters must be provided as a comma-separated list of features that are
198+
valid per the consumer's build system.

0 commit comments

Comments
 (0)