Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add build-time code exclusion using code fencing #12060

Merged
merged 30 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6d9512e
Initial code fencing transform implementation
rekmarks Sep 10, 2021
779974a
Rename some stuff
rekmarks Sep 10, 2021
6f97fa6
Concatenate file buffers before applying transform
rekmarks Sep 10, 2021
ac3d520
Begin adding tests
rekmarks Sep 10, 2021
3e7fbdb
Complete tests, reconfigure Jest
rekmarks Sep 11, 2021
6cdb3a8
Rename fileName parameter to filePath
rekmarks Sep 11, 2021
f6126d1
Lint transformed files, init lint tests
rekmarks Sep 11, 2021
28c649d
Finish transformed linting tests
rekmarks Sep 12, 2021
a90fb76
Add spaced-comment marker for fences
rekmarks Sep 12, 2021
999250c
Add build system arg for toggling fence linting
rekmarks Sep 12, 2021
20bccdd
Replace through2 with a Transform implementation
rekmarks Sep 12, 2021
2622421
Default shouldLintFenceFiles to false for dev builds
rekmarks Sep 12, 2021
c41078a
Fix spaced-comment config
rekmarks Sep 12, 2021
a950ea2
Update LavaMoat policy
rekmarks Sep 12, 2021
09ca404
fixup! Update LavaMoat policy
rekmarks Sep 12, 2021
f74378d
Check that the linter is called correctly in tests
rekmarks Sep 12, 2021
5bd2a12
Add transform function docstrings
rekmarks Sep 12, 2021
bde0241
Add some directive test cases
rekmarks Sep 12, 2021
ae4ecde
Forbid empty fences
rekmarks Sep 12, 2021
b7545fe
Add documentation, handle some parsing edge cases
rekmarks Sep 12, 2021
ff246dd
Remove linting
rekmarks Sep 12, 2021
b0c42ec
Tweak a test
rekmarks Sep 13, 2021
efd8cd3
Stop using 'key in object'
rekmarks Sep 13, 2021
0828289
Address review feedback
rekmarks Sep 13, 2021
073f7cf
Fix SC2048 violation
rekmarks Sep 14, 2021
dfadb27
Update development/build/transforms/remove-fenced-code.js
rekmarks Sep 14, 2021
ce15bf2
Update development/build/transforms/remove-fenced-code.js
rekmarks Sep 14, 2021
474ec02
Apply suggestions from code review
rekmarks Sep 14, 2021
662f5ae
Fix test scripts
rekmarks Sep 14, 2021
892a0a1
Fix tests and some bugs
rekmarks Sep 14, 2021
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
9 changes: 5 additions & 4 deletions .depcheckrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ ignores:
#
# dev deps
#

# safety fallback for npm lifecycle scripts, not used normally
- "@lavamoat/preinstall-always-fail"
# used in testing + ci
- "@metamask/auto-changelog" # invoked as `auto-changelog`
- "@metamask/forwarder"
- "@metamask/test-dapp"
- "@sentry/cli" # invoked as `sentry-cli`
- "chromedriver"
- "geckodriver"
- "depcheck" # ooo meta
- "ganache-cli"
- "geckodriver"
- "jest"
- "lavamoat-viz"
- "@sentry/cli" # invoked as `sentry-cli`
- "prettier-plugin-sort-json" # automatically imported by prettier
- "source-map-explorer"
- "depcheck" # ooo meta
# development tool
- "yarn-deduplicate"
# storybook
Expand Down
28 changes: 27 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ module.exports = {
'prefer-object-spread': 'error',
'require-atomic-updates': 'off',

// This is the same as our default config, but for the noted exceptions
'spaced-comment': [
'error',
'always',
{
markers: [
'global',
'globals',
'eslint',
'eslint-disable',
'*package',
'!',
',',
// Local additions
'/:', // This is for our code fences
],
exceptions: ['=', '-'],
},
],

'import/no-unassigned-import': 'off',

'no-invalid-this': 'off',
Expand Down Expand Up @@ -112,6 +132,7 @@ module.exports = {
'ui/**/*.test.js',
'ui/__mocks__/*.js',
'shared/**/*.test.js',
'development/**/*.test.js',
],
extends: ['@metamask/eslint-config-mocha'],
rules: {
Expand All @@ -129,7 +150,12 @@ module.exports = {
},
},
{
files: ['ui/**/*.test.js', 'ui/__mocks__/*.js', 'shared/**/*.test.js'],
files: [
'ui/**/*.test.js',
'ui/__mocks__/*.js',
'shared/**/*.test.js',
'development/**/*.test.js',
],
extends: ['@metamask/eslint-config-jest'],
rules: {
'jest/no-restricted-matchers': 'off',
Expand Down
9 changes: 2 additions & 7 deletions development/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const createScriptTasks = require('./scripts');
const createStyleTasks = require('./styles');
const createStaticAssetTasks = require('./static');
const createEtcTasks = require('./etc');
const { getNextBetaVersionMap } = require('./utils');
const { BuildTypes, getNextBetaVersionMap } = require('./utils');

// packages required dynamically via browserify configuration in dependencies
require('loose-envify');
Expand Down Expand Up @@ -150,11 +150,6 @@ function parseArgv() {
SkipStats: 'skip-stats',
};

const BuildTypes = {
beta: 'beta',
main: 'main',
};

const argv = minimist(process.argv.slice(2), {
boolean: [NamedArgs.OmitLockdown, NamedArgs.SkipStats],
string: [NamedArgs.BuildType],
Expand Down Expand Up @@ -191,7 +186,7 @@ function parseArgv() {
betaVersion: String(betaVersion),
buildType,
entryTask,
isBeta: argv[NamedArgs.BuildType] === 'beta',
isBeta: argv[NamedArgs.BuildType] === BuildTypes.beta,
isLavaMoat: process.argv[0].includes('lavamoat'),
shouldIncludeLockdown: argv[NamedArgs.OmitLockdown],
skipStats: argv[NamedArgs.SkipStats],
Expand Down
46 changes: 29 additions & 17 deletions development/build/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const {
composeSeries,
runInChildProcess,
} = require('./task');
const {
createRemoveFencedCodeTransform,
} = require('./transforms/remove-fenced-code');

module.exports = createScriptTasks;

Expand Down Expand Up @@ -144,7 +147,12 @@ function createScriptTasks({
disableConsoleSubtask,
installSentrySubtask,
phishingDetectSubtask,
].map((subtask) => runInChildProcess(subtask, { buildType, isLavaMoat }));
].map((subtask) =>
runInChildProcess(subtask, {
buildType,
isLavaMoat,
}),
);
// make a parent task that runs each task in a child thread
return composeParallel(initiateLiveReload, ...allSubtasks);
}
Expand Down Expand Up @@ -231,10 +239,11 @@ function createFactoredBuild({

const envVars = getEnvironmentVariables({ buildType, devMode, testing });
setupBundlerDefaults(buildConfiguration, {
buildType,
devMode,
envVars,
reloadOnChange,
minify,
reloadOnChange,
});

// set bundle entries
Expand Down Expand Up @@ -349,10 +358,11 @@ function createNormalBundle({

const envVars = getEnvironmentVariables({ buildType, devMode, testing });
setupBundlerDefaults(buildConfiguration, {
buildType,
devMode,
envVars,
reloadOnChange,
minify,
reloadOnChange,
});

// set bundle entries
Expand Down Expand Up @@ -399,35 +409,37 @@ function createBuildConfiguration() {

function setupBundlerDefaults(
buildConfiguration,
{ devMode, envVars, reloadOnChange, minify },
{ buildType, devMode, envVars, minify, reloadOnChange },
) {
const { bundlerOpts } = buildConfiguration;

Object.assign(bundlerOpts, {
// source transforms
// Source transforms
transform: [
// transpile top-level code
// Remove code that should be excluded from builds of the current type
createRemoveFencedCodeTransform(buildType),
// Transpile top-level code
babelify,
// inline `fs.readFileSync` files
// Inline `fs.readFileSync` files
brfs,
],
// use entryFilepath for moduleIds, easier to determine origin file
// Use entryFilepath for moduleIds, easier to determine origin file
fullPaths: devMode,
// for sourcemaps
// For sourcemaps
debug: true,
});

// ensure react-devtools are not included in non-dev builds
// Ensure react-devtools are not included in non-dev builds
if (!devMode) {
bundlerOpts.manualIgnore.push('react-devtools');
}

// inject environment variables via node-style `process.env`
// Inject environment variables via node-style `process.env`
if (envVars) {
bundlerOpts.transform.push([envify(envVars), { global: true }]);
}

// setup reload on change
// Setup reload on change
if (reloadOnChange) {
setupReloadOnChange(buildConfiguration);
}
Expand All @@ -436,21 +448,21 @@ function setupBundlerDefaults(
setupMinification(buildConfiguration);
}

// setup source maps
// Setup source maps
setupSourcemaps(buildConfiguration, { devMode });
}

function setupReloadOnChange({ bundlerOpts, events }) {
// add plugin to options
// Add plugin to options
Object.assign(bundlerOpts, {
plugin: [...bundlerOpts.plugin, watchify],
// required by watchify
// Required by watchify
cache: {},
packageCache: {},
});
// instrument pipeline
// Instrument pipeline
events.on('configurePipeline', ({ bundleStream }) => {
// handle build error to avoid breaking build process
// Handle build error to avoid breaking build process
// (eg on syntax error)
bundleStream.on('error', (err) => {
gracefulError(err);
Expand Down
123 changes: 123 additions & 0 deletions development/build/transforms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Local Browserify Transforms
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is some incredible documentation by the way. Not just this but the inline docs too.

I was expecting this transform to be difficult to understand, but it was really straightforward with all of these explanations at each step.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1000


This directory contains home-grown Browserify transforms.
Each file listed here exports a transform function factory.

## Removing Fenced Code

> `./remove-fenced-code.js`

When creating builds that support different features, it is desirable to exclude
unsupported features, files, and dependencies at build time. Undesired files and
dependencies can be excluded wholesale, but the _use_ of undesired modules in
files that should otherwise be included – i.e. import statements and references
to those imports – cannot.

To support the exclusion of the use of undesired modules at build time, we
introduce the concept of code fencing to our build system. Our code fencing
syntax amounts to a tiny DSL, which is specified below.

The transform concatenates each file into a single string, and a string parser
identifies any fences in the file. If any fences that should not be included in
the current build are found, the fences and the lines that they wrap are
deleted. The transform errors if a malformed fence line is identified.

For example, the following fenced code:

```javascript
this.store.updateStructure({
...,
GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController,
///: BEGIN:ONLY_INCLUDE_IN(beta)
PluginController: this.pluginController,
///: END:ONLY_INCLUDE_IN
});
```

Is transformed to the following if the build type is not `beta`:

```javascript
this.store.updateStructure({
...,
GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController,
});
```

Note that multiple build types can be specified by separating them with
commands inside the parameter parentheses:

```javascript
///: BEGIN:ONLY_INCLUDE_IN(beta,flask)
```

### Code Fencing Syntax

> In the specification, angle brackets, `< >`, indicate required tokens, while
> straight brackets, `[ ]`, indicate optional tokens.
>
> Alphabetical characters identify the name and purpose of a token. All other
> characters, including parentheses, `( )`, are literals.

A fence line is a single-line JavaScript comment, optionally surrounded by
whitespace, in the following format:

```text
///: <terminus>:<command>[(parameters)]

|__| |________________________________|
| |
| |
sentinel directive
```

The first part of a fence line is the `sentinel`, which is always the string
"`///:`". If the first four non-whitespace characters of a line are not the
`sentinel`, the line will be ignored by the parser. The `sentinel` must be
succeeded by a single space character, or parsing will fail.

The remainder of the fence line is called the `directive`.
The directive consists of a `terminus`, `command`, and (optionally) `parameters`.

- The `terminus` is one of the strings `BEGIN` and `END`. It must be followed by
a single colon, `:`.
- The `command` is a string of uppercase alphabetical characters, optionally
including underscores, `_`. The possible commands are listed later in this
specification.
- The `parameters` are a comma-separated list of RegEx `\w` strings. They must
be parenthesized, only specified for `BEGIN` directives, and valid for the
command of the directive.

A valid code fence consists of two fence lines surrounding one or more lines of
non-fence lines. The first fence line must consist of a `BEGIN` directive, and
the second an `END` directive. The command of both directives must be the same,
and the parameters (if any) must be valid for the command.

If an invalid fence is detected, parsing will fail, and the transform stream
will end with an error.

### Commands

#### `ONLY_INCLUDE_IN`

This, the only command defined so far, is used to exclude lines of code
depending on the type of the current build. If a particular set of lines should
only be included in a particular build type, say `beta`, they should be wrapped
as follows:

```javascript
///: BEGIN:ONLY_INCLUDE_IN(beta)
console.log('I am only included in beta builds.');
///: END:ONLY_INCLUDE_IN
```

At build time, the fences and the fenced lines will be removed if the build is
not `beta`.

Parameters are required for this command, and they must be provided as a
comma-separated list of one or more of:

- `main` (the build system default build type)
- `beta`
- `flask`
Loading