Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Know how to run things from a terminal/command line
  2. Have git installed and know the basic git workflows (clone, commit, push, Pull Request)
  3. 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

  1. Ensure you have the prerequisites above.
  2. Setup the System Tools
  3. Setup the language-specific tools by following the additional sections below the System Tools page, for the language(s) used by the project.
  4. Understand the Project structure and Task Convention
  5. 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).

  1. Operating System: Linux is preferred. Most projects also work with Windows. Most projects should also work with MacOS but they are not tested.

  2. GNU Utils: Coreutils like cp, rm, mkdir should be available although they are not required for every project. For Windows users please use Microsoft fork of uutils/coreutils

    Warning

    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, grep might be needed for some projects.

  3. Task Runner: Please install the latest version of task. This is a great cross-platform script runner

    Note

    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.json which would only work with the NPM ecosystem, every project has a Taskfile.yml which works regardless of the language/ecosystem.

  4. Git submodule manager: For projects with submodules it’s recommended to use magoo, which is a wrapper for git submodule. If you are a veteran of git-submodule commands you can also run

    Note

    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 npm or cargo); Most contributors just need to run magoo install when they git pull to 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-tools or task icets for short from the repo root.
  • rust-analyzer is the official language server. Install the rust-analyzer extension 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 the devEngines field, 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-format and clang-tidy from the LLVM toolchain. For Windows you can download from the official LLVM release
  • Install the clangd extension for VS Code (NOT the official C/C++ extension). For other editor please install the corresponding clangd extension/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 jabba will also work.
  • My version of gradlew which does not require jar files 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_modules which are usually gitignore-ed
  • Target directories, like target or dist which are usually also gitignore-ed
  • mono-dev directory, usually at node_modules/mono-dev (for TS/JS projects) or mono-dev at root (for other projects).
  • Taskfile.yml and 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.yaml or Cargo.toml. All dependency versions should be managed by the workspace and avoid declaring individual versions inside the packages.
  • A Taskfile.yml for 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:

  1. 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.
  2. 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.
  3. 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 eslint to oxlint and prettier to oxfmt, if everything works out and there aren’t any compatibility issues discovered during the migration.

TypeScript projects use:

  • prettier for formatting
  • eslint for checking code style and issues
  • tsc for type checking and generating d.ts files for libraries
  • vitest for testing
  • vite for bundling library or app
  • typedoc for generating documentation using rustdoc theme

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 nocheck will not be processed by eslint or prettier. If the path is in the form of /foo or foo, the foo directory will also not be type-checked.
    • pistonight/mono-dev is the options for mono-dev. See MonoDevOptions

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, src vs scripts)
  • no DOM and no types exist by default. They need to be manually included in env.d.ts in each directory. Only directories with env.d.ts will 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 eslint dependency 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.ts files would have the correct imports
  • Bundler tools such as vite or bun doesn’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.ts

generates:

#lib ->  ./src/lib/index.ts
#util/image -> ./src/util/image/index.ts
#util/data -> ./src/util/data/index.ts

Note that in the published package.json, the imports will be replaced with the .d.ts files

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:

  1. Raw: As-configured in exports field; anything that is not one of the below
  2. Auto-compiled: Any exports key 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 nocompile option to exclude auto-configured compilation, which means the published package will also export the TS file directly.
  3. Manual-compiled: Object exports key 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 compile option 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:

  • rustfmt for formatting
  • clippy for checking code styles and issues
  • cargo for compiling/package management
  • rustdoc for 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 .github will 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

Workflow file

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

Workflow file

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

Workflow file

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

Workflow file

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

Workflow file

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)

Workflow file

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')