Best Practices for Organizing Cordis Plugins in a Monorepo
Monorepo organization in Cordis relies on Yarn workspaces, scoped package names under @cordisjs/plugin-<name>, and TypeScript path mapping to keep plugins isolated yet composable.
The cordiverse/cordis repository demonstrates a battle-tested approach to managing multiple plugins within a single codebase. By following the architectural patterns established in the core framework, developers can maintain independent versioning, enable cross-package imports without relative paths, and ensure clean runtime composition. This guide covers the specific conventions used in the Cordis source code, from directory layout to dependency management.
Package Structure and Workspace Configuration
Each Cordis plugin resides in its own package under the packages/ directory. This layout mirrors the structure seen in packages/loader/, packages/group/, and packages/hmr/.
Directory layout: Create a dedicated folder for every plugin containing its own package.json, tsconfig.json, and source files. This encapsulation allows independent versioning and targeted publishing to npm.
Naming convention: Use the scoped format @cordisjs/plugin-<name> in the name field of package.json. As seen in [packages/loader/package.json](https://github.com/cordiverse/cordis/blob/main/packages/loader/package.json), this prevents naming collisions and signals first-party status.
Workspace integration: Declare all plugin folders in the root package.json workspaces array. Cordis uses Yarn workspaces to resolve intra-repo imports without registry round-trips, as defined in the root [package.json](https://github.com/cordiverse/cordis/blob/main/package.json).
TypeScript Path Mapping for Clean Imports
To enable seamless imports like import { Loader } from '@cordisjs/plugin-loader' without relative paths, Cordis configures a wildcard path alias in the root [tsconfig.json](https://github.com/cordiverse/cordis/blob/main/tsconfig.json):
{
"compilerOptions": {
"paths": {
"@cordisjs/plugin-*": ["./packages/*/src"]
}
}
}
This mapping directs TypeScript to resolve any @cordisjs/plugin-* import to the corresponding packages/*/src directory. The configuration supports IDE auto-completion and ensures that imports mimic the public npm package structure during development.
Plugin Entry Points and Export Patterns
Every plugin must expose a clean public API through a single entry point while avoiding side effects in the module initialization phase.
Entry file: Export all public functionality from src/index.ts. The loader plugin in [packages/loader/src/index.ts](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) demonstrates this pattern by exporting the Loader class and related types without invoking ctx.plugin at the top level.
Side-effect free exports: Keep plugin registration logic out of the entry file. Expose pure functions or classes that accept a Context instance, allowing the consuming application or loader to control activation timing.
Example plugin structure:
// packages/my-plugin/src/index.ts
import type { Context } from '@cordisjs/core'
export function myPlugin(ctx: Context) {
ctx.logger?.('my-plugin').info('Plugin loaded')
return {
doSomething() {
ctx.logger?.('my-plugin').info('Doing something')
}
}
}
Runtime Loading and Composition
Cordis provides specialized plugins for dynamic loading and grouping, enabling flexible runtime architecture.
Dynamic loading: The Loader plugin in [packages/loader/src/index.ts](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts) resolves and registers plugins by name at runtime. Configure it with a baseUrl pointing to your plugin directory:
import { Cordis } from '@cordisjs/core'
import Loader from '@cordisjs/plugin-loader'
const app = new Cordis()
await app.plugin(Loader, { baseUrl: import.meta.url })
await app.loader.import('@cordisjs/plugin-group')
Plugin grouping: The group utility exported from [packages/group/src/index.ts](https://github.com/cordiverse/cordis/blob/main/packages/group/src/index.ts) combines multiple plugins into a single logical unit:
import { group } from '@cordisjs/plugin-group'
await ctx.plugin(
group([
'@cordisjs/plugin-loader',
'@cordisjs/plugin-hmr'
])
)
Testing and Documentation Standards
Maintain quality and discoverability by colocating tests and documentation with each plugin.
Test isolation: Place tests in a tests/ subfolder within the plugin directory, as shown in [packages/loader/tests/index.spec.ts](https://github.com/cordiverse/cordis/blob/main/packages/loader/tests/index.spec.ts). Configure the monorepo's vitest setup to run these tests, allowing imports via package names rather than relative paths.
Documentation: Include a README.md in each plugin folder describing purpose, usage, and configuration options. The loader plugin's [README.md](https://github.com/cordiverse/cordis/blob/main/packages/loader/README.md) serves as the reference for consuming the package.
Dependency Management with Peer Dependencies
Prevent runtime duplication of the core framework by declaring @cordisjs/core as a peer dependency rather than a direct dependency.
Peer dependency pattern: In [packages/loader/package.json](https://github.com/cordiverse/cordis/blob/main/packages/loader/package.json), @cordisjs/core is listed under peerDependencies. This ensures all plugins share a single core instance at runtime while allowing independent plugin updates.
Runtime dependencies: Declare only required runtime libraries in each plugin's package.json. Avoid bundling @cordisjs/core to prevent version mismatches and memory overhead from multiple core instances.
Summary
- Package isolation: Place each plugin in
packages/<name>/with its ownpackage.jsonand use the@cordisjs/plugin-<name>naming convention. - TypeScript paths: Configure
"@cordisjs/plugin-*": ["./packages/*/src"]in roottsconfig.jsonto enable clean imports. - Clean exports: Expose functionality through
src/index.tswithout side effects, returning functions or classes rather than auto-registering. - Dynamic composition: Use the
Loaderplugin for runtime resolution andgroupfor logical bundling of plugins. - Peer dependencies: List
@cordisjs/coreas apeerDependencyto ensure singleton core instances across all plugins. - Colocated tests: Store tests in
packages/<name>/tests/and import via package names to simulate real-world usage.
Frequently Asked Questions
How do I scaffold a new plugin in a Cordis monorepo?
Create a directory under packages/<plugin-name>/ containing src/index.ts and a package.json with "name": "@cordisjs/plugin-<plugin-name>". Add @cordisjs/core to peerDependencies, then update the root tsconfig.json paths if your naming follows the standard convention. The Yarn workspace configuration will automatically recognize the new package.
What is the difference between the Loader and Group plugins?
The Loader plugin dynamically resolves and imports plugins by string name at runtime, enabling configuration-driven architectures. The Group plugin, defined in packages/group/src/index.ts, statically combines multiple plugin references into a single callable unit for organizational purposes. Use Loader for modularity and Group for composition.
Should Cordis plugins use dependencies or peerDependencies for the core framework?
Always declare @cordisjs/core as a peerDependency in your plugin's package.json. This prevents npm from installing separate copies of the core framework for each plugin, ensuring all plugins interact with the same Context instance and avoiding runtime conflicts.
How does TypeScript resolve @cordisjs/plugin-* imports during development?
The root tsconfig.json maps the wildcard pattern @cordisjs/plugin-* to ./packages/*/src, allowing TypeScript to resolve imports locally without publishing or relative path tricks. This mapping enables IDE features like "Go to Definition" and auto-import to work across package boundaries as if the modules were already published to npm.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →