Warning
This repo still links out to private documentation. We're working on making those public, but it will take time.
This is the mono repo that is home to the open source modules of the Exodus eco-system.
Before you leave your loving aunt and uncle and your comfortable attic on Privet Drive and venture down to fight Voldemort below, we recommend you go to Hogwarts and at least learn Lumos.
This repository uses a modern version of yarn that doesn't support .npmrc
files anymore. To gain access to Exodus'
private packages, you have to invoke yarn npm login
and login with your credentials. This has to be done once only.
After that, you can install dependencies as usual.
This section describes how to migrate an existing module and keep its git commit history.
GH SSH authentication has to be configured or alternatively the --https
flag has to be used.
Install @exodus/migrate
globally (npm i -g @exodus/migrate
), run exodus-migrate
and specify the path to the repository, or subdirectory within the repository.
For the latter you can simply navigate to the subdirectory in the GH UI and copy the URL from your browser's
address bar. If the subdirectory does not contain a package.json
, a basic package.json
will be created on your
behalf. More info can be found in the @exodus/migrate
repository
Examples:
# from a repository URL
exodus-migrate --url https://github.com/ExodusMovement/some-other-repo --target-dir modules/auto-enable-assets --scope @exodus --rename-tags
# from a subdirectory within a repository (can also be from a different branch than master)
exodus-migrate --url https://github.com/ExodusMovement/some-other-repo/tree/master/src/_local_modules/enabled-assets --target-dir modules/auto-enable-assets --scope @exodus --rename-tags
The script will replace the repository
, homepage
, and bugs.url
properties in package.json
to point to hydra
and set the homepage to the module's folder on master.
-
You should check for potentially broken badges in your README.md, no longer required ci folders, eslint configs, .gitignore files, and lockfiles on package level.
yarn postmigrate
can help to identify unwanted files and create new config files to extend the root configuration in this repository. If the last commit affects files inside the imported package's folder,yarn postmigrate
will be able to determine the package automatically. Otherwise you can supply the module path manually:yarn postmigrate modules/orders-monitor
. -
Many
devDependencies
may no longer be required as they are hoisted to avoid duplication and use the same versions across all modules. Prune what you can from your imported module. -
The changes cannot be merged using the GH UI without losing the history. Merging has to be done locally to
master
as fast-forward merge. This only works if no other PR has been merged in-between. Using the--ff-only
flag will make git abort should a fast-forward merge not be possible. All the work was in vain then and you have to start over from1.
Better be fast this time!
git checkout master
git merge $IMPORT_BRANCH --ff-only
- Last, push to master directly.
Note: if your package is missing them you will most likely need to add babel.config.js
and jest.config.js
. yarn postmigrate
also offers to add them (see 2.)
Different templates are available to scaffold a module.
To generate a library, use the following:
yarn generate:library my-library
This will create a basic scaffold under the folder ./libraries/my-library
.
To generate a module, use the following and select the language you'd like to use in the interactive prompt that comes up:
yarn generate:module my-module
This will create a folder ./modules/my-module
in your desired target language. It has a basic test setup and
logging pre-configured.
While developing a package in this monorepo, you may want to test it in an app, e.g. your mobile dapp. Unfortunately we can't use npm link
because mobile's packager metro
doesn't support symlinks (yet). However, we have a similar tool here to help you sync your changes to the client repos before you publish a new version.
To link your module to a client repo, run:
yarn run -T sync module-name,other-module-name /path/to/client-repo
This will start a watch process that syncs the specified modules to src/node_modules
in that repo. If you need them synced elsewhere, specify a different path as the 2nd argument.
Examples
yarn run -T sync module-name,other-module-name ../my-app
Examples:
# test one library
yarn test --scope @exodus/fiat-client
If your module needs transpiling (i.e. Babel or Typescript) before publishing, make sure to add
a build
script to the package.json
of the module.
Examples:
# build all
yarn build
# build one library
yarn build --scope @exodus/fiat-client
Lerna uses npm/yarn pack and does not allow to specify a custom folder for packing, i.e.
the module's root where package.json
resides is used for packing. This is less than ideal
for modules that require transpilation (i.e. Typescript modules) and do not re-export everything
from the entrypoint. Without further steps, imports would have to include the dist folder name such as
@exodus/networking-spec/lib/shared
.
One option to achieve a clean import structure anyway, is to copy the build output to the top
level before packing. The lifecycle script prepack
can be used for that.
For further information on the topic please refer to this GH issue
To version your packages, either:
- merge a PR with eligible commit type. The following don't trigger a release:
chore
,docs
,test
,ci
- run
yarn release
and select the package(s) you want to release - run
yarn release
and supply packages as a positional argument:yarn release networking-mobile,kyc,storage-mobile
- run the version workflow directly through the GH UI.
All of these derive version bumps from the conventional commit history and create a release PR, labeled with publish-on-merge
. Make sure that the checks on the release PR pass, especially when releasing packages that depend on other packages from this repository.
For more options to yarn release
, see the CLI docs.
All packages that received a version bump in the previous step are automatically published to npm after merging the release PR. The tags listed in the PR body will be added to the merge commit.
Initial versions can be published by manually executing the publish workflow. All packages with versions not currently present in the registry will be published. If unclear how to run the publish workfow, please follow these instructions.
Commit messages and PR titles should follow the conventional commits specification. Breaking changes are denoted with a bang (!
) before the colon (:
) in the commit message and will result in a major version bump.
feat!: all roads lead to Gotham
If your PR only affects a section of a package, you may use a scope. Please refrain from using scopes for package names as they will show up in the CHANGELOG.md
and the scope is redundant there. PRs are labelled with the package names they affect, so it also doesn't add any value in the GH UI.
🟩 Good
feat(redux): add hardware wallet account selector
🟥 Bad
feat(wallet-accounts): add hardware wallet account selector
Occasionally, it is necessary to introduce a breaking change. Fixing downstream packages may only require a patch or minor, and not always warrant a breaking change. The solution is to create a PR chain. Changes that are breaking from a consumer perspective are isolated in the first PR, and non-breaking downstream errors are fixed in follow-up PRs. To avoid failing checks on master for an extended period, the chain should not be merged manually. Instead, apply the label action/merge-chain
to the tip of the PR chain. It will merge the first PR, rebase the following PR onto master, and continue on merging until the entire chain is merged.
If your package requires referencing one of the packages maintained in this mono repo and you want
to consume the latest unpublished changes without having to set a specific version, you have to
manually add that dependency to package.json
and set the version to *
. This manual step is
currently required because of an incompatibility between more recent yarn versions (berry) and
lerna.
Latest code changes are automatically reflected in the import and versioning/publishing
takes care of keeping the version in the module's package.json
up-to-date.
For TS modules, a path mapping to resolve the import correctly from the src
folder has to be added. This can be done in the top level tsconfig.json
:
{
// ...
compilerOptions: {
// ...
paths: {
// ...
'@exodus/networking-spec': ['./modules/networking-spec/src'],
'@exodus/networking-spec/*': ['./modules/networking-spec/src/*'],
},
},
}
The jest config for these modules, then also has to define a custom moduleNameMapper
that can be created from the paths definitions in our tsconfig to avoid duplication:
module.exports = {
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
prefix: '<rootDir>/../../',
}),
}
A full example can be found here
Please note that referencing files outside/above the module's directory, will cause tsc to nest
its build output in module-name/src
and copy the locally referenced dependency to outDir
.
Make sure an adequate prepack
script is provided to cleanup and prepare the build output before packing.
This repo uses sophisticated caching courtesy of nx and Github Actions. When changing non-module-local
configuration/code, you may want to clear the cache in the CI to force checks to re-run. You can do so
by running yarn cache:delete
or use the GH page for
managing caches. The CLI client has the advantage of being able to purge all caches for a given branch. This is
currently not supported in the UI.
First see general conventions.
Refer to our Lego manual to understand key concepts/differences in our architecture.
If your module requires some async initialization, e.g. to load data from storage or an API, expose a public load()
method, and call it the load
and unlock
application lifecycle hooks.
load = async () => {
this.#cache = await fetchival(this.#someServerUrl).get()
}
Your module may need to consume data from storage
, fusion
, remoteConfig
and/or other modules like walletAccounts
, blockchainMetadata
, etc.
In all of these cases, you should prefer accepting an atom
for that piece of data rather than any of those modules. This lets your module avoid worrying about the specifics of where a value is coming from and instead have a simple API for retrieving and monitoring that value ({ get, observe, set? }
). It will also make your module much easier to test.
Similarly, modules should produce/export data by writing it to atoms.
If your module accepts some static configuration, e.g. { someServerUrl, maxSlippage }
, accept that as an option called config
in your module's constructor.
If you're using Exodus's @exodus/dependency-injection
or @exodus/headless
to wire up the dependency tree, config values will be auto-magically binded for you from the passed global config by module id:
import createHeadless from '@exodus/headless'
import createPreprocessors from '@exodus/dependency-preprocessors'
const config = {
feri: {
likesSandwiches: true,
maxCachacaCapacity: Number.MAX_SAFE_INTEGER + 1,
},
}
const exodus = createHeadless({ adapters, config })
exodus.register({
definition: {
id: 'feri',
factory: createFeri, // will get called as `createFeri({ config: config.feri })`
dependencies: ['config'],
},
})
exodus.resolve()