0x-labs
is the home of 0x's private codebase.
Working in the repository
Technology
The repository relies on the following technologies:
- Yarn classic: A JavaScript package manager.
0x-labs
is built on Yarn workspaces. - Turborepo: Provides a caching solution to speed up workflows (
build
,test
, etc.)
ℹ️ 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 the0x-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 runbuild
in each of those workspaces first" - "
build
each workspace, but make surebuild
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
afterbuild:no-diff
, which will return an exit code1
, and thus fail CI, ifbuild:no-diff
produced outputs that were not included in the PR.
Configuration
- The primary branch of the repository is
main
main
is protected from pushesmain
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
- Commit messages should be in the form of a present-tense “action”, i.e.
- 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 inpackage.json
) matches the top-level directory name of the workspace - The workspace version (
version
field inpackage.json
) is0.0.0
. Since no workspace is published as an npm package, there is no concept of a "version". - The workspace
private
field is set totrue
to preventyarn
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
, orout
directory in the package’s 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 fromlib
before migrating the project into0x-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 wanteslint
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 tomain
to go into production) - Set "Git > Ignored Build Step" to
npx turbo-ignore
. This causes commits tomain
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:
- The root
package.json
script to run each pipeline first runsturbo-prerun.sh
(e.g."build": ". ./turbo-prerun.sh && turbo run build"
) - When a workspace
package.json
script requires an "external" binary, it is preceded withturbo-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
- Create script in workspace
package.json
- Add binary to
turbo-prerun.sh
- Create Turbo workspace configuration with the appropriate environment variables
- 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