Motivation
Important
Welcome to
mono-dev- my personal standard for all my projects.Continue here if you are interested in the motivation. Otherwise if you are looking to contribute to a project of mine, proceed to Standard to understand the repo structure and tooling I use.
After creating so many projects across many different languages
(primarily Rust, TypeScript, JavaScript, Python, and C/C++),
I started to have this internal configuration hell, where
I constantly copy build scripts, package.json, CI configs,
linter rules, etc, between projects.
The worst part is - things change. So, the “source of truth” of these configs is basically what project I am currently working on. These means I either never “backport” these improvements to older projects or it’s a very tedious process when I do so - as I have to copy all the configurations from a newer project to an older project and wire up everything properly.
This was toleratable in the past when I didn’t have many projects,
maybe one new project per year. However, as I make more and more projects
faster and faster, common pattern/code emerge. At some point, I started creating
these internal libraries that all my projects would depend on.
One of the example is pure,
a pure-TypeScript library to bootstrap a web application with dark mode,
multi-languages, Result type, etc. Now, I not only have a configuration
hell, I also have my internal dependency hell!!
If I only ever work with one language, say Rust or TypeScript, this might be simple
to solve - create a dev dependency that contains all my build configurations
and common code and publish it to whatever package registry the ecosystem uses.
However, I often work in multiple languages and ecosystems in the same project.
For example, my
Breath of the Wild Weapon Modifier Corruption Database
project, had:
- Python for processing BOTW data and generating source code, along with other scripts
- Rust for building the fastest simulator for cooking in BOTW. For comparison, the first version of a cooking simulator took 9 hours (all 32 cores) to generate the database. My version can do that in under 30 seconds (all 32 cores)
- C/C++ for a Switch mod that generates the same database by calling the function in the game’s binary, to validate the database
- TypeScript for making a nice frontend for my Rust database
As I take on more and more crazy projects like these, I need to enable config- and code-sharing.
Present-me need to build abstractions for these so future-me doesn’t spend all my time copy-pasting
<ChangeDarkModeButton /> and build scripts all over the place.
Essentially, I need:
- A system to manage dependencies, and enable code-sharing and config-sharing between projects
- A system or standard to build monorepos, like how to define build steps and dependency between projects across ecosystem-boundaries
Well, that is exactly what mono-dev is. It’s not a single tool or system or service.
But a combination of a set of tools, a set of documentations, a set of scripts and config files,
and finally, this website to document everything for future-me to understand.
Caution
Note that, this standard is possible because it’s only used by me. I make assumptions about how a developer (me) works on the project. While all the source code are available on GitHub and you can feel free to use them or make PRs, I will not be implementing any fixes to support scenarios outside of what I use the tools for.
As an example, I will not add a –config-path flag to some tool, because it assumes the project follows the standard, and the config is defined in the expected place.
The standard is also unstable. Every update is a breaking update.
The Standard
Please follow these steps to setup a project locally for development.
Read the Guidelines
Ensure you have read the Contributing Guidelines prior to setting up the project. Contributing to the project assume you have agreed to everything in the guidelines.
Prerequisite
Important
I assume you are familiar with basic workflows for programming and working with version control software (i.e.
git). If not, my projects are not the place for you to make your first contribution.
Please ensure you:
- Know how to run things from a terminal/command line
- Have
gitinstalled and know the basic git workflows (clone, commit, push, Pull Request) - Have a working editor that supports the Language Server Protocol (LSP). For most people this would be Visual Studio Code
Setting up your development environment
- Ensure you have the prerequisites above.
- Setup the System Tools
- Setup the language-specific tools by following the additional sections below the System Tools page, for the language(s) used by the project.
- Understand the Project structure and Task Convention
- If the project is complicated, follow the setup guide in the project to finish setting up the project (install dependencies, build, etc).
For simple projects the task convention covers everything (like
task check,task build,task test).
System Tools
The system tools are expected to be installed and available globally on your system
(i.e. in PATH).
-
Operating System: Linux is preferred. Most projects also work with Windows. Most projects should also work with MacOS but they are not tested.
-
GNU Utils: Coreutils like
cp,rm,mkdirshould be available although they are not required for every project. For Windows users please use Microsoft fork of uutils/coreutilsWarning
The Microsoft version of coreutils is still an early product and has some rough edges. It also requires some complicated powershell integration. If you have to work on Windows, it’s recommended to test if the project already works without Coreutils, or use Windows Subsystem for Linux directly.
Some other utils like
find,xargs,sed,wget,grepmight be needed for some projects. -
Task Runner: Please install the latest version of
task. This is a great cross-platform script runnerNote
This is a great cross-platform script runner with an implementation of coreutils built-in, which is the reason why Windows don’t usually need Coreutils unless for compatibility reason). It’s also a great way to make the build system consistent across languages - instead of scripts in
package.jsonwhich would only work with the NPM ecosystem, every project has aTaskfile.ymlwhich works regardless of the language/ecosystem. -
Git submodule manager: For projects with submodules it’s recommended to use
magoo, which is a wrapper forgit submodule. If you are a veteran of git-submodule commands you can also runNote
If you are a veteran of git-submodule commands, you can also opt to run the git-submodule commands directly.
Magoo offers an interface more like a modern package manager (such as
npmorcargo); Most contributors just need to runmagoo installwhen theygit pullto checkout the submodules.
In addition to the general system tools, please setup the language-specific tool depending on the project you are working on. Some projects use multiple languages and you will need to have all of them installed.
Rust
For Rust projects:
- Install Rust (recommended through
rustup) - Windows users would need to install MSVC dependencies, please follow the guide on the Rustup download page.
- Projects may require additional tools that are written in Rust.
Run
task install-cargo-extra-toolsortask icetsfor short from the repo root. rust-analyzeris the official language server. Install therust-analyzerextension in VS Code, or if you use another editor, the corresponding extension/plugin for that editor for Rust Analyzer.
TypeScript/ECMAScript/JS
Tip
The steps below sets up PNPM and use it as the runtime/toolchain manager. If you already have another runtime/toolchain manager (such as
nvm), you can install the runtimes use your preferred method. If it also supports thedevEnginesfield, you will also get an error or warning if your currently installed engine version is not compatible.
For TypeScript/ECMAScript (aka JS) projects:
- Install PNPM which acts as a unified toolchain, engine, and package manager.
- Install NodeJS v26 with PNPM:
pnpm i -g node@26. - Some projects may also need Bun.
You can either install it standalone or through PNPM:
pnpm i -g bun - Install TypeScript and ESLint extension for VS Code, or if you use another editor, the corresponding TypeScript extension/plugin for that editor.
C/C++
For C or C++ projects:
- Install CMake
- Install Ninja. This is especially needed for Windows
to generate build commands for
clangd - Install a C compiler. For Windows this usually means you need MSVC (from Visual Studio Build Tools). Sometimes you need the GNU toolchain for Windows instead, one option is LLVM-MINGW
- Install
clang-formatandclang-tidyfrom the LLVM toolchain. For Windows you can download from theofficial LLVM release - Install the
clangdextension for VS Code (NOT the official C/C++ extension). For other editor please install the correspondingclangdextension/plugin for that editor.
Java
For Java projects:
- A decently modern JDK is expected to be installed on the system to run Gradle.
You don’t need to install the exact version the project uses because Gradle will handle that.
A JDK installed through a version manager like
jabbawill also work. - My version of gradlew which
does not require
jarfiles in the repo (Read more in the link for the security concerns of that). - I have a very custom LSP setup with JDTLS and Neovim but any editor that can work with a Gradle project should work.
Python
For Python projects or projects that contain python scripts:
- Install
uv
Project Structure
Each project should either be a single repo (for simple projects) or a monorepo (for more complex projects).
Single repo
A typical repo contains:
- A source directory, usually
src - Dependency directories, such as
node_moduleswhich are usuallygitignore-ed - Target directories, like
targetordistwhich are usually alsogitignore-ed mono-devdirectory, usually atnode_modules/mono-dev(for TS/JS projects) ormono-devat root (for other projects).Taskfile.ymland other configuration files at repo root.
All task commands can be run from the root of the repo (or any subdirectories).
Monorepo
This is common for large projects, where multiple small projects and external dependencies are integrated.
Instead of a single source directories, a monorepo contains multiple packages,
which are different subsystems for the project. Each package lives
at packages/<package_name> and its structure is a lot like a single repo.
The root of a monorepo also contains:
- Workspace configuration such as
pnpm-workspace.yamlorCargo.toml. All dependency versions should be managed by the workspace and avoid declaring individual versions inside the packages. - A
Taskfile.ymlfor root level tasks.
task commands can be run at package level (in the directory for one package)
to run a task just for that package, or at repo level which may invoke commands
from multiple packages.
The root level Taskfile.yml also contains an exec task that you can use
to run a package task at the root level:
task exec -- <package>:<task>
# x is an alias for exec
task x -- <package>:<task>
Task Convention
This section defines a convention for task names, such as
check, test, build. This way, commands are mapped to mental
tasks in developers’ brains and the same commands work everywhere.
setup/install-cargo-extra-tools
The setup task defines one-time setup on the system. such as pulling
large data or installing extra tools. It’s meant to only run once when you clone
the repo and not have to run when updating the repo with git pull.
install-cargo-extra-tools (aliased icets) is a special setup task that installs
cargo-binstall and other cargo-install-able tools needed for development on the system.
install
The install task installs dependencies, syncs dependencies, and setups the package
(like post-install hooks in some ecosystem).
Usually this delegates to installing dependencies with a package manager like pnpm.
If no install task is present, it means no special install logic is needed.
Either install the project like other projects (for example run pnpm install directly
like a regular JS project), or no installing is needed (for example running cargo build
or uv run will install the dependencies automatically).
dev
The dev task is for the inner-most loop development workflow. For example,
it can be starting a dev server for a watch-build-serve loop, or it can start
a watch-build-test loop for non-UI projects.
test
The test task runs all the tests in the project. Sometimes there will be multiple
parts of the test runnable through suffixes like test-lib, test-e2e, etc.
check
The check task runs linters and formatters to check code quality.
Run this before making a commit or Pull Request.
Note
Some people may prefer check/fix to automatically run when they save a file or when commiting (known as a pre-commit hook). This is not done for many reasons:
- Some checkers/formatters such as gofmt are really aggressive and touches the file in a way that slows the workflow. For example it may remove unused imports/variables. This means you have to write some code that use the import/variable before you can save.
- Consistency and performance: not all ecosystems provide performant formatters and linters, I don’t like the feeling when my editor is stuck when I try to save my work.
- Git hooks have well-known security issues and they also slow down the workflow. For example commiting becomes slow because it has to run all the checks. This does not make sense for the scale of my projects.
fix
Fixs the issues found with check automatically. This should always fix all formatting issues.
Some issues are not mechanically fixable (or the recommended fix doesn’t always make sense).
Manual fix is required in those cases.
build
The build task produces some artifacts to be published or used outside of the package. This task can also be used as a baseline for the package to “work” (i.e. not broken).
Contributing Guidelines
These guidelines apply to all of my projects
Contributor License Agreement
Important
By submitting code, documentation, or any other contributions to this project, you agree that your contributions are provided under the same license as the project, and you grant the project maintainers and users the rights necessary to use, modify, distribute, and sublicense your contributions in accordance with that license.
Code of Conduct
There is no official Code of Conduct in the Standard. Be reasonable and respectful.
Filing Issues
When you file a bug report or suggestion, you are contributing to the project :)
There is this sentiment spreading on the Internet that users of open-source likes to ask for more without contributing. While people like that do exist, I prefer to think of more ask = more people caring and liking and using about my project.
The catch is - you have to show that. If you are filing a bug report, please take the time to detail what is the scenario and what’s the steps to reproduce, along with expected v.s. actual outcome. If it’s a feature suggestion, give the background context or use cases for why you think the feature should be added.
Do not comment on whether you think it’s a simple thing or not. (i.e. “Why is this simple fix not done yet?” or “This feature should be easy, why not just add it?”) As a user and not a maintainer of the project, you are simply not in the position to make such call. If you have looked into the issue and implemented the “simple fix”, then prove yourself by a PR or reply in the issue.
Communication
Communication is very important. When you are not sure, ask. Also reach out with your feature idea before you work on a PR. It’s also a good idea to let me know your availability if you are going to work on something big.
Coding
Follow each language’s best practice and convention when it comes to styles. The easiest way to do this is look at existing code and use the linters often.
Documentation
Code without documentation will quickly become unworkable. The rule of thumb is I should be able to tell what a function/component does without looking at the implementation.
This is something that requires pretty high skill level and experience to do effectively. It’s very common even for large, widely-used projects to not do this properly, and it’s frustrating to having to dig into the code or setup a minimal environment to find out what the function returns for, say, empty string.
For example, this function:
/** Parses a number from an input string */
function parseNumber(input: string): number {
... // implementation hidden
}
Yes - there is documentation, but it’s useless. I already know that before I look at the comment. Here are my questions:
- What’s the output? Integer? Float? Positive Numbers?
- What’s the input? Decimal? Hex? Math expression? Automatically detected?
- What if the input is invalid? Does it return 0? -1? NaN? throw an error?
A good documentation looks like this
/**
* Parses a number from an input string.
*
* The input should be string representation of a positive integer in decimal.
* If anything other than 0-9 is in the input, it returns 0.
* Leading 0's are ignored, and an empty string also gives 0
*/
function parseNumber(input: string): number {
... // implementation hidden
}
Another good rule of thumb is that one should be able to write tests for the function by looking at the documentation. Actually, tests are documentation. They express in code instead of natural language or what the behavior of a function or component is.
Another important thing is that LLMs love to generate useless documentation/comments, see Usage of AI for more information on the my take on AI.
Pull Request
Always run checks and tests before you make a Pull Request. PR workflows need to be approved by me to run because of security. If very simple formatting issue fails the build and I was not there, you have to wait for another cycle of review.
I don’t use git hooks, also because of security.
When opening a PR, make sure the description is filled. Small PRs can have a one-to-two sentence description. If you are fixing a bug, describe what the bug is or link to the issue, explain the root cause, and explain the fix.
Also please explain how the changes have been validated, for example tested in local dev server. The automated tests can’t always catch all the bugs.
Usage of Artificial Intelligence (AI)
My take on AI/LLM is that it is a tool just like your IDE or your terminal. It’s up to you if you want to use it while contributing to this project. You are also welcomed to use AI tools to analyze the project for your learning. However please respect the open-source license when using the project.
The bottom line is that - you are the author of your code and is responsible for making sure it is correct, readable, secure, and the same quality that you would expect from a human author. I don’t accept or reject contributions based on if it was generated by AI or not. I do so based on the quality and context of the PR itself. Read contributing guidelines for more.
The same thing goes for documentation. However, be careful when the tool tries too hard - Just as an example, I prompted this to ChatGPT:
Can you generate documentation for this function? It reduces the payload to avoid hittimg payload limit:
function getMinimalPayload(): string;
This is the output:
/**
* Generates a minimal payload to optimize data transmission and prevent exceeding server payload limits.
*
* This function constructs a compact string representation of the payload, ensuring
* only essential data is included to reduce the request size.
*
* @returns {string} - A minimized payload string that the server can efficiently process.
*
* @example
* const payload = getMinimalPayload();
* console.log(payload); // Output: A compact string representation of the payload
*
* @remarks
* - The function should be used when sending requests with strict payload size limits.
* - Ensure that essential data is not lost while minimizing the payload.
*/
function getMinimalPayload(): string;
The function looks well documented right? WRONG. The documentation is a bunch of bloat that wastes my brain resource parsing it. What is “minimal payload”? What’s the format? What is left out? What is not left out? Documentation that pretends to be there is worse than no documentation.
See the contributing guidelines for more about writing documentation.
Setup HTTPS for Development
Some Web features requires a secure context. For example, copying stuff into clipboard.
Usually, for developer experience, the localhost host is considered secure.
If you only use localhost when developing (i.e. you are running the dev server
and visiting the page on the same machine), then you don’t need HTTPS.
If you are like me, who uses a VM for development and hosts the dev server in local network, then your host computer needs to be configured to trust the web app hosted by the VM’s dev server.
The steps for Windows are currently documented here. I might move them to this page instead in the future.
The mono-dev Standard will look for .cert/cert.key and .cert/cert.pem 2 levels up.
So the recommendation is to put the .cert folder in the repo root.
Signing
minisign is used to sign binary
releases (e.g. on GitHub Releases).
If a signature file (*.sig) is present in the release, you can verify the signature with the following command:
minisign -Vm <file> -P RWThJQKJaXayoZBe0YV5LV4KFkQwcqQ6Fg9dJBz18JnpHGdf/cHUyKs+
cargo-binstall
(This is for me not for you)
To support signature checks with cargo-binstall, add the following
to the Cargo.toml of the package to be published:
[package.metadata.binstall.signing]
algorithm = "minisign"
pubkey = "RWThJQKJaXayoZBe0YV5LV4KFkQwcqQ6Fg9dJBz18JnpHGdf/cHUyKs+"
Tooling and Templates
Note
This chapter is mainly documentation for myself as a single source of truth for configurations.
If you are looking for setup documentation, see Standard
Common
Note
This chapter is mainly documentation for myself as a single source of truth for configurations.
If you are looking for setup documentation, see Standard
Template: root Taskfile.yml
version: '3'
includes:
common:
taskfile: ./mono-dev/task/common.yaml
flatten: true
optional: true
TypeScript/ECMA
Note
This chapter is mainly documentation for myself as a single source of truth for configurations.
If you are looking for setup documentation for TypeScript, see here
Dev Engines
The root package.json must define a devEngines field to guide users
for setting up the project
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "^12",
"onFail": "error"
},
"runtime": [
{ "name": "node", "version": "^26", "onFail": "error" },
{ "name": "bun", "version": "^1.4.0", "onFail": "warn" }
]
},
The onFail field can either be error if the engine is required,
or warn if the engine is only needed for some parts of the project.
We don’t use download, since that will automatically install the engines
if the user’s package manager supports it, even when the user does not want
to use such feature.
Tools
Important
We will soon migrate from
eslinttooxlintandprettiertooxfmt, if everything works out and there aren’t any compatibility issues discovered during the migration.
TypeScript projects use:
prettierfor formattingeslintfor checking code style and issuestscfor type checking and generatingd.tsfiles for librariesvitestfor testingvitefor bundling library or apptypedocfor generating documentation usingrustdoctheme
mono-dev packages itself as a node module. In TypeScript/ECMAScript
projects, the package needs to be declared in package.json to be
managed by the package manager.
Running pnpm up mono-dev will resolve the latest commit and update it.
Template: package.json
-
{ "devDependencies": { "mono-dev": "github:Pistonight/mono-dev#dist" }, "pistonight/mono-dev": { "nocheck": [ "/src/generated" ] } }- Paths in
nocheckwill not be processed byeslintorprettier. If the path is in the form of/fooorfoo, thefoodirectory will also not be type-checked. pistonight/mono-devis the options for mono-dev. See MonoDevOptions
- Paths in
Template: Taskfile.yml
-
version: '3' includes: ecma: taskfile: ./node_modules/mono-dev/task/ecma.yaml internal: true optional: true
Template: .gitignore
-
# mono-dev: ecma gitignores node_modules package-lock.json .prettierignore .eslintcache /eslint.config.js /tsconfig*.json /mono-dev /dist /docs
Type Checking & Linting
mono-dev automatically generates type checking configs based on directory structure:
- Each directory is type-checked separately
and allow for different env config (for example,
srcvsscripts) - no DOM and no types exist by default. They need to be manually included in
env.d.tsin each directory. Only directories withenv.d.tswill be checked. - If root directory contain any TypeScript stuff, it will be checked as well
- ESLint only checks the TypeScript projects. If you use ECMAScript, you opt-out of safety anyway
Note
For LSP and compatibility with other tools, ESLint and tsconfig files will be generated at the project root (like how they are for a regular project). For eslint-lsp, you may need to add
eslintdependency to downstream in order for the server to find the eslint library.
Import Path remapping
We now use the NodeJS subpath imports to map internal imports to avoid the “relative parent import hell”. This has a few advantages over mapping it in TS:
- Generated
.d.tsfiles would have the correct imports - Bundler tools such as
viteorbundoesn’t need extra TS-specific configuration.
Since the bundler would remove these imports when bundling, they are only significant
in type declarations generated by TSC (since they keep the original imports).
This is why the field is mapped to the source typescript files instead of files in dist.
The mapping is always automatically generated for the src directory and
can be manuall disabled with mono-dev option importmap: false.
Tip
For example, the following directory structure:
- src/ - app/ - util/ - image/ - index.ts - data/ - index.ts - lib/ - foo/ - index.ts - index.tsgenerates:
#lib -> ./src/lib/index.ts #util/image -> ./src/util/image/index.ts #util/data -> ./src/util/data/index.tsNote that in the published
package.json, the imports will be replaced with the.d.tsfiles
Test
mono-dev re-exports vitest for testing. This ensures the version of vitest
is managed by the version of mono-dev. Import anything from mono-dev/vitest
instead of vitest.
Use ecma:test task to run test once and ecma:test-dev task to run in watch mode.
Library Exports
The tooling supports 3 kinds of exports:
- Raw: As-configured in
exportsfield; anything that is not one of the below - Auto-compiled: Any
exportskey that satisfies*.(c|m)?tsx?- In the published package, the export is transformed to an object like below.
{ "exports": { "./foo": "./src/bar/foo.ts" } } // becomes: { "exports": { "./foo": { "import": "./dist/bar/foo.js", "types": "./dist/_dts_/src/bar/foo.d.ts" } } }Note
In this configuration, only the published package contains ESM + Type Declaration. Internal consumers (like other packages in a monorepo) consume the TS source directly for more streamlined dev experience. For example running vite dev server will not require internal packages to be built into ESM first. Editing TS source also does not require rebuilding the packages
- Use the mono-dev
nocompileoption to exclude auto-configured compilation, which means the published package will also export the TS file directly.
- In the published package, the export is transformed to an object like below.
- Manual-compiled: Object
exportskey of the form:{ "pistonight/mono-dev": { "compile": { "./foo": "./src/bar/foo.ts" } }, "exports": { "./foo": { "import": "./dist/bar/foo.js", "types": "./dist/_dts_/src/bar/foo.d.ts" } } }Warning
the
compileoption must be specified to enable this. [!TIP] This configuration is useful if the compiled ESM of the package needs to be consumed as-is from within the monorepo. For example when testing bundling the package (importing transpiled ESM vs. TS directly would not produce the same output)
Note that src must be where TS files are exported from, and dist must be where
output ESM is emitted. These 2 directory names are NOT configurable.
Build Library
Building library from TS source into consumable package without TS supports zero-config.
Under the hood, the library mode of vite is used. vite.config.ts will be generated
on-the-go when running the ecma:lib-build task.
Currently the library is only packaged as ESM.
The build uses dependencies, devDependencies and peerDependencies to automatically
externalize dependencies.
dependencies- Will be installed by package manager when consumer adds the library
- Externalized: The code of the dependency will not be in the library
peerDependencies:- Will NOT be installed by package manager when consumer adds the library; They have to also install it in the downstream.
- Externalized: The code of the dependency will not be in the library
devDependencies:- Will NOT be installed by package manager when consumer adds the library.
- NOT Externalized: The code of the dependency is bundled into the library.
Consideration: Unless the library is meant to be used as a framework, DO NOT add global states. It’s very easy to cause duplicated global states when resolving dependencies.
See the above section for out library exports are configured.
If needed, the lib-types export defines the types for library builds.
Put in env.d.ts
/// <reference types="mono-dev/lib-types" />
Vite
mono-dev ships a baseline vite config that adds common plugins
and configs to my projects.
vite.config.ts at the root:
import { configure } from "mono-dev/app-build-config";
// config options are referenced from pistonight/mono-dev options
// in package.json
// use configure just like defineConfig
export default configure({
/** normal vite config here */
});
Plugins are automatically added:
- YAML loader (always added)
- React and React Compiler (if react is installed)
- WASM (if configured)
Define vite types in src/env.d.ts:
/// <reference types="mono-dev/app-types" />
/// <reference types="dom" />
Use the ecma:app-dev and ecma:app-build tasks to execute vite
dev server and build.
import.meta.env
Mono-dev options can be used to specify auto import.meta.env values.
See MonoDevOptions
Publish
The tool need to zap the package.json before publishing.
Therefore, private must be true to prevent accidental
publishing of the original package.json.
Use the ecma:publish or pnpm exec mono publish to publish the package.
--access public is always specified.
Rust (Cargo)
Note
This chapter is mainly documentation for myself as a single source of truth for configurations.
If you are looking for setup documentation for Rust, see here
Tools
Rust projects use:
rustfmtfor formattingclippyfor checking code styles and issuescargofor compiling/package managementrustdocfor generating API documentation
Check and Fix
mono-dev provides wrapper for clippy with common clippy flags
Template: Taskfile.yml
version: '3'
includes:
cargo:
taskfile: mono-dev/task/cargo.yaml
internal: true
optional: true
tasks:
check:
cmds:
- task: cargo:clippy-all
- task: cargo:fmt-check
fix:
cmds:
- task: cargo:fmt-fix
dev-doc:
cmds:
- task: cargo:watch-serve-doc
Note
Clippy fix is not automated, because IMO sometimes the suggestions lead to worse code style and should be ignored.
For other clippy options, including feature flags and targets,
see the included Taskfile.
The dev-doc command uses shwoop and
to serve the documentation generated by cargo doc and watch for changes.
Using rustfmt
When running cargo fmt, it may not format all of the source files, especially if some
source files are conditionally compiled. In this case use the rustfmt-check and rustfmt-fix tasks
version: '3'
includes:
cargo:
taskfile: mono-dev/task/cargo.yaml
internal: true
tasks:
check:
cmds:
- task: cargo:clippy-all
- task: cargo:rustfmt-check
vars:
PATH: src
fix:
cmds:
- task: cargo:rustfmt-fix
vars:
PATH: src
Test
There’s no wrapper for test - just run cargo test
Note
TODO - add section for wasm-pack-build task and WASM stuff in general
C/C++
Note
This chapter is mainly documentation for myself as a single source of truth for configurations.
If you are looking for setup documentation for C/C++ projects, see here
Template: Taskfile.yml
version: '3'
includes:
ccpp:
taskfile: ../mono-dev/task/ccpp.yaml
internal: true
optional: true
tasks:
check:
cmds:
- task: ccpp:clang-tidy
- task: ccpp:fmt-check
fix:
cmds:
- task: ccpp:fmt-fix
Go
Go support is experimental
Check And Fix
includes:
go:
taskfile: ../mono-dev/task/go.yaml
internal: true
tasks:
check:
cmds:
- go vet
- task: go:fmt-check
fix:
cmds:
- go fmt
Docker
Containerization steps can be either its own package or the same package as the server for simple containers.
The Dockerfile should be in the root of that package, may be something like:
FROM alpine:latest
EXPOSE 80
ENV APP_DIR=/app
COPY ./dist $APP_DIR
RUN chmod +x $APP_DIR/bin/changeme
WORKDIR $APP_DIR
ENV FOO=BAR \
BIZ=BAZ
ENTRYPOINT ["/app/bin/changeme"]
Usually, docker workflow downloads artifact from CI, then build container locally.
It’s recommended to setup a pull which downloads from CI, and a pull-local to
copy local artifacts
tasks:
pull:
desc: Pull build artifacts from CI using current commit
cmds:
- magnesis
pull-local:
desc: Copy build artifacts from local build output
cmds:
- cp ...
The actual container operations:
includes:
docker:
taskfile: ../mono-dev/task/docker.yaml
internal: true
vars:
DOCKER_IMAGE: pistonite/foo
tasks:
build:
cmds:
- task: docker:build
run:
cmds:
- task: docker:run
vars:
DOCKER_RUN_FLAGS: >
-p 8000:80
-e FOO=BAR
-e GIZ=BAZ
connect:
cmds:
- task: docker:connect
stop:
cmds:
- task: docker:stop
clean:
cmds:
- task: docker:clean
Mdbook
mdbook is a Rust tool for generating documentation website from Markdown.
Install theme
mono-dev ships a mdbook template with catppuccin themes.
The theme files are forked and modified from the official catppuccin mdbook theme to my liking.
version: '3'
includes:
mdbook:
taskfile: ../mono-dev/task/mdbook.yaml
internal: true
tasks:
install:
cmds:
- task: mdbook:install-theme
vars:
MONO_DEV: mono-dev # path to mono-dev
MDBOOK_TARGET: packages/book # path to the mdbook project
Also ignore the generated theme directory
GitHub Actions
Note
This chapter is documentation for myself.
You do not need to modify workflows when contributing (including bumping versions). Please understand that all PRs that touch
.githubwill be closed for supply-chain security.
Since the development activity is on GitHub, I use GitHub Actions to run CI. I use runners from Blacksmith to speed up some hot workflows.
The Standard provides composite actions for common workflows as well as copy-paste configs for composing workflows
File Structure
The workflow files should be placed in the following directory structure:
- .github/
- steps/
- setup/
- action.yml
- ... (more repo-specific reusable steps)
- workflows/
- pr.yml
- ... (more .yml for workflows)
Blacksmith Runners
Blacksmith now has all of Ubuntu, MacOS (very expensive) and Windows runners:
blacksmith-4vcpu-ubuntu-2404
blacksmith-4vcpu-ubuntu-2404-arm
blacksmith-4vcpu-windows-2025
blacksmith-6vcpu-macos-latest
See https://docs.blacksmith.sh/blacksmith-runners/overview
Action: Setup
jobs:
change-me:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: Pistonight/mono-dev/actions/setup@main
with:
# ALL VALUES BELOW ARE OPTIONAL
# -- clone mono-dev in the repo --
# Default: 'auto' - special value:
# - if pnpm-lock.yaml exists at root, then pnpm install will run, to pull
# in mono-dev and also all other dependencies.
# pnpm and node will also be installed without having to specify ecma-pnpm
# - otherwise, cloned to `/mono-dev` of the repo
#
# Set to `false` to not clone
# Anything else indicates the ref to clone instead of `main`
mono-dev: auto
# Where to clone mono-dev, /mono-dev is the default
mono-dev-path: mono-dev
# -- whether to clone submodules, default false
submodules: true
# -- setup NodeJS/PNPM --
# ecma-pnpm sets up both NodeJS and PNPM. If ecma-pnpm is set, ecma-node is ignored.
# Note that the setup action requires NodeJS to run. If NodeJS is not available
# on the runner image, then you need to set at least ecma-node to true
# even if the project is not NodeJS
ecma-node: true
ecma-pnpm: false
# setup Bun, default is false
ecma-bun: true
# setup Rust, default is false
# use `stable`, `nightly` value
# When using `nightly`, the toolchain is pinned to the nightly
# version on the 1st of the previous UTC month
# - this is for efficient caching of CI
# also setup at least one target below
rust: stable
# setup wasm32-unknown-unknown target for Rust, default false
# also will install wasm-pack
rust-wasm: true
# setup native targets for the runner's OS
# default is x64, only `x64` (x86_64) and `arm64` (aarch64)
# are supported, arm is ignored on windows right now
rust-native: x64,arm64
# install the rust-src component, default false
rust-src: true
# install mdbook
tool-mdbook: true
# install extra tools with cargo-binstall
# installed tools here are not cached and falling
# back to compile from source is banned
#
# formats:
# - crates.io: <crate>
# - github: <crate>=<user>/<repo>[#<rev>]
# - specific binary: <binary>(<crate>)[=<user>/<repo>[#<rev>]]
# - specific version: replace <crate> with <crate>@<version>
tool-cargo-binstall: ripgrep,workex=Pistonite/workex
# same format as above, but uses cargo install
# this is cached by rust-cache
# note specifying anything here will also
# setup rust for the current OS/Arch if not already done
tool-cargo-install: ...
# Setup C/CPP tooling
ccpp-lint: true # clang-tidy and clang-format
ccpp-cmake: true
# note ninja is installed by GH runners by default
# setup python, use `true` for vanilla python
python: uv
# repo-specific setup (if needed ..)
- uses: ./.github/steps/setup
- run: task install
Action: Permanent Cache
Use this action to permanently cache a directory until manually busted, with a task to generate the data to cache if miss
- uses: Pistonight/mono-dev/actions/permanent-cache@main
with:
path: |
path/to/cache/dir1
path/to/cache/dir2
key: my-cache
version: v1
# task to run to generate the data to cache (task exec --)
task: generate-data
Action: Rust
Use the first action to build Rust CLI tool and upload it to CI artifacts, then use the second action to download the artifacts, sign them, and create a release
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- image: blacksmith-4vcpu-ubuntu-2404
target: x64
- image: blacksmith-4vcpu-ubuntu-2404-arm
target: arm64
- image: blacksmith-6vcpu-macos-latest
target: arm64
- image: blacksmith-4vcpu-windows-2025
target: x64
# if needed: blacksmith currently doesn't have windows runners
# - image: windows-11-arm
# target: arm64
runs-on: ${{ matrix.image }}
steps:
- uses: Pistonight/mono-dev/actions/setup@main
with:
rust: stable
rust-native: ${{ matrix.target }}
# if needed
# rust-src: true
- uses: Pistonight/mono-dev/actions/rust-xplat@main
with:
arch: ${{ matrix.target }}
binary: botwrdb
# optional: install tauri build dependencies, default false
tauri: true
# optional: additional build arguments
# default build args included:
# --bin <binary>
# --release
# --target <triple> (for apple only)
build-args: "--feature too"
# optional: target directory for the build (default is `target`)
target-dir: my-target-dir
# optional: Task to run before building
pre-task: exec -- pre-build
# optional: Task to run after building (not ran if build fails)
post-task: exec -- post-build
# optional: Task to run instead of cargo build. cargo build args are passed in as .CLI_ARGS
build-task: exec -- build
name: Release
on:
push:
tags:
- v*.*.*
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- if: startsWith(github.ref, 'refs/tags/v')
uses: Pistonight/mono-dev/actions/release@main
with:
artifacts-workflow: build.yml
artifacts-path: release
pack: botwrdb-*
minisign-key: ${{ secrets.MINISIGN_KEY }}
Action: Docker
Use this action to download artifacts from a previous workflow, build a docker image, and publish it to GitHub Packages.
# permission for publishing docker image
permissions:
contents: read
packages: write
steps:
- uses: Pistonight/mono-dev/actions/docker-image@main
with:
# optional: download artifacts from a previous workflow
artifacts-workflow: build.yml
artifacts-path: packages/server/dist
# optional: run a task after downloading artifacts
task: server:package-assets
image: pistonite/skybook-server
path: packages/server
version: ${{ github.events.input.version }}
Action: Release
Use this action to create release notes automatically and draft a release with artifacts
Uses RELEASE_NOTES_HEADER.md and RELEASE_NOTES_FOOTER.md in the .github directory
name: Release
on:
push:
tags:
- v*.*.*
jobs:
release:
runs-on: ubuntu-latest
# permission for publish release
permissions:
contents: write
steps:
- uses: Pistonight/mono-dev/actions/release@main
with:
# optional: download artifacts from a previous workflow
artifacts-workflow: build.yml
artifacts-name: packages/server/dist
# optional: run a task after downloading artifacts
task: server:package-assets
# optional: determine how releases are packed
# by default, each artifact is packed into an archive
# use pattern with * in the beginning or end to match the artifact
# name. ** or true matches everything (the default). false disables packing
# and only files in `files` are uploaded
pack: server-*
# whether to append the version tag to the archive name
# default is true
append-version: true
# optional. if provided, release artifacts will be signed
minisign-key: ${{ secrets.MINISIGN_KEY }}
files: |
path/to/file1.zip
path/to/file2.tar.gz
# default
tag: ${{ github.ref_name }}
Action: GCloud (Pistonite Storage)
Use this action to setup gcloud for Pistonite Storage.
# permissions for gcloud
permissions:
contents: read
id-token: write
steps:
- uses: Pistonight/mono-dev/actions/pistonstor@main
Workflow Templates
Copy to a workflow .yml file, and delete things not needed
General Config
name: _____CHANGEME_____
on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- v*.*.*
workflow_dispatch:
inputs:
version:
description: "Version tag of the image (e.g. 0.2.0-beta)"
required: true
Full Job > GitHub Pages
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: Pistonight/mono-dev/actions/setup@main
with:
tool-mdbook: true
- run: task build-pages
- uses: actions/upload-pages-artifact@v5
with:
path: packages/manual/book
retention-days: 3
deploy-pages:
needs:
- build
if: github.event_name != 'pull_request'
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- id: deployment
uses: actions/deploy-pages@v5
Full Job > Rust CLI Build & Release
See Rust Action
Job > Single Platform
jobs:
_____CHANGEME_____:
runs-on: ubuntu-latest
# runs-on: blacksmith-4vcpu-ubuntu-2404
Job > Multiple Platforms
jobs:
_____CHANGEME_____:
strategy: { matrix: { os: [ ubuntu-latest, macos-latest, windows-latest ] } }
runs-on: ${{ matrix.os }}
Job > Multiple Platforms + Different Args
jobs:
_____CHANGEME_____:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x64
build_args: "--target x86_64-unknown-linux-gnu"
build_output: target/x86_64-unknown-linux-gnu/release/botwrdb
- os: ubuntu-latest
target: arm64
build_args: "--target aarch64-unknown-linux-gnu"
build_output: target/aarch64-unknown-linux-gnu/release/botwrdb
- os: macos-latest
target: x64
build_args: "--target x86_64-apple-darwin"
build_output: target/x86_64-apple-darwin/release/botwrdb
- os: macos-latest
target: arm64
build_args: "--target aarch64-apple-darwin"
build_output: target/aarch64-apple-darwin/release/botwrdb
- os: windows-latest
target: x64
build_output: target/release/botwrdb.exe
runs-on: ${{ matrix.os }}
# use the args like ${{ matrix.target }} or ${{ matrix.build_args }}
Permissions
# permission for publish release
permissions:
contents: write
# permission for publishing docker image
permissions:
contents: read
packages: write
# permission for gcloud
permissions:
contents: read
id-token: write
Steps > Setup
See Setup Action
Steps > Upload Artifacts
- uses: actions/upload-artifact@v7
with:
path: dist/foo
name: foo
retention-days: 3
Steps > Download Artifacts
Note that the Docker Image Action automatically downloads artifacts
- run: mkdir -p package
shell: bash
- uses: dawidd6/action-download-artifact@v20
with:
github_token: ${{ github.token }}
workflow: CHANGEME.yml
commit: ${{ github.sha }}
path: package
Steps > Download Release
This is helpful if there are data files in previous releases
# download release
- uses: robinraju/release-downloader@v1
with:
tag: CHANGEME
fileName: CHANGEME.7z
out-file-path: package
extract: false
Ifs > Pull Request
Only run the step if the event is or is not a pull request
- if: github.event_name == 'pull_request'
- if: github.event_name != 'pull_request'
Ifs > Release Tag
- if: startsWith(github.ref, 'refs/tags/v')