2023-04-07 19:04:45 -06:00
2023-04-07 19:04:45 -06:00
2023-03-22 15:47:28 -06:00
2023-02-09 15:31:30 -07:00
2023-02-16 17:20:16 -07:00
2023-02-21 21:55:45 -07:00
2023-03-09 21:15:45 -08:00

0x-labs is the home of 0x's private codebase.

Working in the repository

Technology

The repository relies on the following technologies:

Tip: the Turborepo Monorepo Handbook explains many of the concepts you'll want to know to work in the repository

Remote caching

Turborepo has a remote caching feature which allows build artifacts to be cached and shared between your local machine, other developers' machines, and CI. This means that if you've built and tested your changes locally, they won't need to be rebuilt and retested in CI.

To enable remote caching on your machine, you'll need to be a member of the Vercel 0x-eng team. Once you've been added, run:

yarn turbo login
yarn turbo link

Tip: after the link script asks "Would you like to enable Remote Caching for ~/0x-labs?", make sure to select the 0x-eng team, not your personal account.

Depending on shared code

See the Internal Packages section of the Turborepo Monorepo Handbook.

Working with workspaces

If you're only dealing with one workspace, and all its dependencies have been built, you can cd into the workspace root and mostly ignore that you're in a monorepo:

cd apps-node/api
yarn install
yarn build
yarn test

👆🏾 Note: Working in a specific workspace this way won't get you any of the caching benefits of Turborepo.

Working from the repository root allows you perform actions on some or all workspaces.

Use yarn workspace <workspace_name> <command> to run a command in a specific workspace:

pwd # ~/0x-labs
yarn workspace api add -D ts-node

Use yarn workspaces <command> to run a script in every workspace, if it is defined:

pwd # ~/0x-labs
yarn workspaces fix

To perform complex actions across workspaces, we use Turborepo pipelines. Turborepo pipelines are cached for speed and allow dependencies between pipelines to be specified. For example:

  • "run test in every workspace that has one, but run build in each of those workspaces first"
  • "build each workspace, but make sure build is first run on any dependencies within the repository"

Pipelines are specified in turbo.json. Each pipeline can be run with yarn <pipeline name> via the scripts defined in the root package.json.

For example, the test:ci pipeline first ensures each workspace under test has been built, then runs the test:ci script in each workspace that has one defined:

pwd # ~/0x-labs
yarn test:ci # runs script `turbo run test:ci`

To run a Turbo pipeline on a single workspace, use the --filter flag:

pwd # ~/0x-labs
yarn build --filter=api # Builds workspace "api" and its dependencies

Tip: The --filter flag has syntax to match on a number of dimensions. See "Filtering Workspaces" for more.

Structure

Code in the repository is organized into "workspaces", which are directories with a package.json file in the root. Workspaces are created for logical pieces of architecture: servers, websites, shared libraries, etc., and are not organized based on team structure.

Files in the root directory contain code that either (a) applies to all workspaces in the repository or (b) is necessary for repository operation. Generally, adding a new workspace should not involve any changes to files in the root directory.

For example, if your workspace contains an .xyz/ directory which shouldn't be included in git, then .xyz/* shall be added to the .gitignore in the workspace root, not in the root .gitignore.

Workspaces exist in the following directories:

  • apps-node
  • packages
  • sites

apps-node

Workspaces in the apps-node directory have a Docker image built for each using .github/Dockerfile-node. The image is uploaded to AWS Elastic Container Registry and is tagged with both the commit hash of the commit where the image was built and the Turborepo hash of the workspace. Either of these tags can be used in 0x-main-infra to specify the image to use.

If there are no changes to a workspace, then no new image is created.

As an example, a new image for my-app could be accessed at:

  • ***.dkr.ecr.us-east-1.amazonaws.com/apps:my-app__789355d868cd646f (Turborepo hash, note the double underscore)
  • ***.dkr.ecr.us-east-1.amazonaws.com/apps:my-app_2a4810fbd3f195bf8da8c161d7d5b03e9626cd2e (Commit hash)

Tip: The GitHub bot will comment on each commit in a PR or merged to main with the image tags that correspond to that commit. Look for the word bubble icon 💬 where a commit is mentioned in a PR.

packages

Workspaces in packages contain shared code meant to be used by app and website workspaces. They do not create any "runnable" output.

See Depending on shared code for more on how to use packages.

sites

Workspaces in sites represent a type of app which will be deployed through means other than a Docker image, most commonly Vercel or similar.

Functionally, there is no difference between workspaces in packages and in sites.

CI & pipelines

GitHub Actions runs the following pipelines on each pull request and commit to the repository:

  • build
  • build:no-diff
  • test:ci
  • lint:ci

Each pipeline will run the corresponding script in the package.json of each workspace, if it exists. To pass CI, each pipeline must finish with a 0 exit code. Additionally, the build:no-diff pipeline must not produce any new build artifacts.

FYI: There are some advanced use cases, such as auto-generating documentation, where one might want to run a build step and commit the output. CI will run git diff --exit-code after build:no-diff, which will return an exit code 1, and thus fail CI, if build:no-diff produced outputs that were not included in the PR.

Configuration

  • The primary branch of the repository is main
  • main is protected from pushes
  • main has a linear commit history
  • Commits to main are accomplished by a pull request (PR)
    • PRs require an approval to submit
    • PRs require CI to pass to submit
    • PRs are submitted via “Squash and Merge”. One PR will translate to one commit.
      • Commit messages should be in the form of a present-tense “action”, i.e. Add prometheus metric for TokenPriceOracle
      • Commit messages may be prefixed with one or more “tags” describing the portions of the codebase the commit affects, i.e. [rfqm] Add support for BSC
    • A CODEOWNERS file may be used to require PR approvals from specific people for specific sub-directories of the repository

Creating workspaces

At the most basic level, creating a workspace is as simple as adding a directory under the appropriate top-level directory and adding a package.json.

However, there are some conventions meant to make sure that workspaces play well with each other in the repository:

  • The workspace name (name field in package.json) matches the top-level directory name of the workspace
  • The workspace version (version field in package.json) is 0.0.0. Since no workspace is published as an npm package, there is no concept of a "version".
  • The workspace private field is set to true to prevent yarn from complaining
  • Source files are located in the src/ directory
  • Test files are located in either a test/ or __tests__ directory, or a nested child directory thereof
  • Build artifacts not committed to the repository are written to the __build__, dist, or out directory in the packages top level folder

Some other things to keep in mind:

The CI runs the scripts specified in the CI & pipelines section. If your repository needs some special check in CI, make sure to run it as part of one of the CI checks:

{
    "scripts": {
        "circular": "madge --circular --extensions ts ./",
        "lint": "eslint .",
        "format": "prettier --list-different --config .prettierrc",
        "lint:ci": "yarn circular && yarn format && yarn lint"
    }
}

If your workspace needs to deviate from the conventions above, see "Configuring Workspaces"

Warning: some 0x projects write their build outputs to lib. This collides with conventions of many frameworks (i.e. Foundry, SvelteKit, Rust). Make sure to change the build target from lib before migrating the project into 0x-labs.

Common tooling

For the most part, you can use whatever tooling you'd like in your workspace. However, 0x-labs has some built in tooling setup which can make bootstrapping your workspace easier.

Prettier

Prettier is enabled globally and is a root dependency of 0x-labs. (This isn't ideal, but it's a limitation of Prettier as of February 2023).

To use Prettier in your workspace, first setup a .prettierignore in the workspace root. Note that, unlike .gitignore files, .prettierignore files do not "extend" .prettierignore files higher up the directory tree. (See "Feature: handle .prettierignore location like .gitignore and .npmignore (#4081)")

Next, add scripts as you'd like to your package.json. Some suggestions:

  • "fix": "prettier --write ." (Available via a turbo pipeline from the repo root)
  • "lint:ci": "prettier --check ." (You probably also want eslint here)

tsconfig and eslint

The repository has common configurations for both TypeScript and eslint. See the linked READMEs for setup instructions.

Websites

Since Vercel is the author of Turborepo, it's no surprise that deploying websites from the repository to Vercel is a cinch. Key points of the Vercel project settings follow:

  • Set "General > Root Directory" to the workspace directory, e.g. sites/matcha
  • In "General > Root Directory" ensure that "Include source files outside of the Root Directory in the Build Step" is checked
  • Set "Git > Production Branch" to main (assuming you want commits to main to go into production)
  • Set "Git > Ignored Build Step" to npx turbo-ignore. This causes commits to main which don't affect your site to not trigger a new production deployment.

That's it! For more information, see the Vercel Monorepo and the Ignored Build Step documentation.

Running non-node binaries

Consider the scenario where a project wishes to run Foundry tests in CI.

{
    "scripts": {
        "test:ci": "forge test -vvv"
    }
}

The CI machine will have forge installed, so the test will run as expected.

Locally, developers would need forge installed to successfully run the test:ci turbo pipeline, and this presents a problem. As the number of binaries not installed by yarn increases, the developer would need to install more and more binaries which they may not even need for the workspaces they work on.

The solution requires two steps:

  1. The root package.json script to run each pipeline first runs turbo-prerun.sh (e.g. "build": ". ./turbo-prerun.sh && turbo run build")
  2. When a workspace package.json script requires an "external" binary, it is preceded with turbo-bin.sh (e.g. "build": "./../../turbo-bin.sh forge build")

In the first step, the turbo-prerun.sh runs the --version command on the binaries specified at the bottom of turbo-prerun.sh. It stores the output of <bin> --version as the environment variable TURBO_VERSION_<bin>, if the binary is present. This variable gets specified as part of the Turbo workspace hash, which ensures that if the local version of the binary differs from the version in CI there won't be a false cache hit.

In the second step, turbo-bin.sh looks for the env variable TURBO_VERSION_<bin>. If it is present, the script continues to run as normal. If the env variable is not present, the script exits with a code 0.

This solution allows developers not working in specialized workspaces to still be able to run and remote cache turbo pipelines, while the complete pipeline runs in CI and locally if the necessary binaries are installed.

Setup

  1. Create script in workspace package.json
  2. Add binary to turbo-prerun.sh
  3. Create Turbo workspace configuration with the appropriate environment variables
  4. Add the binary to the CI setup

As an example, we'll add scripts requiring forge to a foundry-demo workspace:

(1) First we create a package.json file to name the workspace and specify the scripts. Unfortunately, this is the only way to expose the scripts to turbo, even if it isn't idiomatic for the language of the workspace.

Example package.json:

{
    "name": "foundry-demo",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "build": "../../turbo-bin.sh forge build",
        "test:ci": "../../turbo-bin.sh forge test"
    }
}

(2) Next, we add a line to the bottom of turbo-prerun.sh to have it check for forge:

########################################
# ADD MORE BINARIES BELOW AS NECESSARY #
########################################

set_binary_version forge

(3) In the workspace root, add a turbo.json to extend the root Turbo configuration. In this configuration file, modify the pipeline tasks to include the environment variable for the forge binary. See the Turborepo docs: Altering Caching Based on Environment Variables to learn more about how environment variables affect the pipeline.

// apps/foundry-demo/turbo.json
{
    "extends": ["//"],
    "pipeline": {
        "build": {
            "dependsOn": ["^build"],
            "outputs": ["out/**"],
            "env": ["TURBO_VERSION_FORGE"]
        },
        "test:ci": {
            "dependsOn": ["build"],
            "env": ["TURBO_VERSION_FORGE"]
        }
    }
}

(4) In .github/workflows/ci.yml build_and_test job, ensure the binary is installed:

- name: Add foundry
  uses: foundry-rs/foundry-toolchain@v1
  with:
      version: nightly

Migrating existing repositories

See the notes in Notion.

Troubleshooting

End of line sequences (CLRF, LF)

While we're on the verge of creating machines who surpass humans in intelligence, we still haven't decided upon a single method to represent the end of a line in text files.

If you're having issues running commands, or git status lists file changes not staged for commit which then go away when you git add them to stage, then you likely have files with line endings which don't match your OS.

For example, this output of yarn build indicates the incorrect line ending in turbo-prerun.sh:

$ . ./turbo-prerun.sh && turbo run build
: command not foundline 2:
: command not foundline 62:
'/turbo-prerun.sh: line 63: syntax error near unexpected token `{
'/turbo-prerun.sh: line 63: `set_binary_version() {
error Command failed with exit code 1.

These errors also show up in the logs as ^M at the end of lines.

To understand the problem and how to configure git to avoid it, see "CRLF vs. LF: Normalizing Line Endings in Git".

To quickly fix individual files in VSCode, run "Change End of Line Sequence" in the command pallet.

To bulk fix files, consider dos2unix.

Caching issues

If you suspect a problem with Turborepo caching, you can disable it with the --force flag:

yarn build --force

Repo maintenance

Remote caching

Accessing the Vercel remote cache in GitHub Actions requires the secrets TURBO_TEAM and TURBO_TOKEN to be set in GitHub. Unfortunately, the token is tied to specific user's account. If that person leaves the Vercel "0x-eng" organization, a new token must be generated.

See the Turborepo GitHub Actions CI Recipe for more.

GitHub Personal Access Token

Some GitHub Actions, such as "Deploy" actions which publish PRs in other repositories, require a Personal Access Token stored in the repository secrets as PAT.

The current PAT is for the 0xEng account.

The PAT expires on February 16th, 2024

Description
protocol for MEV related stuff
Readme 166 MiB
Languages
TypeScript 59.4%
Solidity 31.6%
MDX 9%