| name | monorepo |
| description | Manage the @robonen/tools monorepo. Use when: installing dependencies, creating new packages, linting, building, testing, publishing, or scaffolding workspace packages. Covers pnpm catalogs, tsdown builds, eslint presets, vitest projects, JSR/NPM publishing. |
Monorepo Management
Overview
This is a pnpm workspace monorepo (@robonen/tools) with shared configs, strict dependency management via catalogs, and automated publishing.
Workspace Layout
| Directory | Purpose | Examples |
|---|
configs/* | Shared tooling configs | eslint, tsconfig, tsdown |
core/* | Platform-agnostic TS libraries | stdlib, platform, encoding |
vue/* | Vue 3 packages | primitives, toolkit |
docs | Nuxt 4 documentation site | — |
infra/* | Infrastructure configs | renovate |
Installing Dependencies
Always use pnpm. Never use npm or yarn.
Add a dependency to a specific package
pnpm -C <package-path> add <dep-name>
pnpm -C <package-path> add -D <dep-name>
Examples:
pnpm -C core/stdlib add -D eslint
pnpm -C vue/primitives add vue
Use catalogs for shared versions
Versions shared across multiple packages MUST use the pnpm catalog system. The catalog is defined in pnpm-workspace.yaml:
catalog:
vitest: ^4.0.18
tsdown: ^0.21.0
eslint: ^10.4.1
vue: ^3.5.28
In package.json, reference catalog versions with the catalog: protocol:
{
"devDependencies": {
"vitest": "catalog:",
"eslint": "catalog:"
}
}
When to add to catalog: If the dependency is used in 2+ packages, add it to pnpm-workspace.yaml under catalog: and use catalog: in each package.json.
When NOT to use catalog: Package-specific dependencies used in only one package (e.g., citty in root).
Internal workspace dependencies
Reference sibling packages with the workspace protocol:
{
"devDependencies": {
"@robonen/eslint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*"
}
}
Nearly every package depends on these three shared config packages.
After installing
Always run pnpm install at the root after editing pnpm-workspace.yaml or any package.json manually.
Creating a New Package
Note: The existing bin/cli.ts (pnpm create) is outdated — it generates Vite configs instead of tsdown and lacks eslint/vitest setup. Follow the manual steps below instead.
1. Create the directory
Choose the correct parent based on package type:
core/<name> — Platform-agnostic TypeScript library
vue/<name> — Vue 3 library (needs jsdom, vue deps)
configs/<name> — Shared configuration package
2. Create package.json
{
"name": "@robonen/<name>",
"version": "0.0.1",
"license": "Apache-2.0",
"description": "",
"packageManager": "pnpm@10.29.3",
"engines": { "node": ">=24.13.1" },
"type": "module",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"lint:check": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run",
"dev": "vitest dev",
"build": "tsdown"
},
"devDependencies": {
"@robonen/eslint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*",
"eslint": "catalog:",
"tsdown": "catalog:"
}
}
For Vue packages, also add:
{
"dependencies": {
"vue": "catalog:",
"@vue/shared": "catalog:"
},
"devDependencies": {
"@vue/test-utils": "catalog:"
}
}
For packages with sub-path exports (like core/platform):
{
"exports": {
"./browsers": {
"types": "./dist/browsers.d.ts",
"import": "./dist/browsers.js",
"require": "./dist/browsers.cjs"
}
}
}
3. Create tsconfig.json
Node/core package:
{
"extends": "@robonen/tsconfig/tsconfig.json"
}
Vue package (needs DOM types and path aliases):
{
"extends": "@robonen/tsconfig/tsconfig.json",
"compilerOptions": {
"lib": ["DOM"],
"baseUrl": ".",
"paths": { "@/*": ["src/*"] }
}
}
4. Create tsdown.config.ts
Standard:
import { defineConfig } from 'tsdown';
import { sharedConfig } from '@robonen/tsdown';
export default defineConfig({
...sharedConfig,
entry: ['src/index.ts'],
});
Vue package (externalize vue, bundle internal deps):
import { defineConfig } from 'tsdown';
import { sharedConfig } from '@robonen/tsdown';
export default defineConfig({
...sharedConfig,
entry: ['src/index.ts'],
deps: {
neverBundle: ['vue'],
alwaysBundle: [/^@robonen\//, '@vue/shared'],
},
inputOptions: {
resolve: {
alias: { '@vue/shared': '@vue/shared/dist/shared.esm-bundler.js' },
},
},
define: { __DEV__: 'false' },
});
5. Create eslint.config.ts
Standard (node packages):
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic);
ESLint auto-discovers eslint.config.ts (loaded via jiti, which @robonen/eslint depends on). No defineConfig wrapper is needed — compose() returns the flat-config array directly.
6. Create vitest.config.ts
Node package:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});
Vue package:
import { defineConfig } from 'vitest/config';
import { resolve } from 'node:path';
export default defineConfig({
define: { __DEV__: 'true' },
resolve: {
alias: { '@': resolve(__dirname, './src') },
},
test: {
environment: 'jsdom',
},
});
7. Create jsr.json (for publishable packages)
{
"$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@robonen/<name>",
"version": "0.0.1",
"exports": "./src/index.ts"
}
8. Create source files
mkdir -p src
touch src/index.ts
9. Register with vitest projects
Add the new vitest.config.ts path to the root vitest.config.ts projects array.
10. Install dependencies
pnpm install
11. Verify
pnpm -C <package-path> build
pnpm -C <package-path> lint
pnpm -C <package-path> test
Linting
Uses ESLint (flat config) with composable presets from @robonen/eslint.
Run linting
pnpm -C <package-path> lint:check
pnpm -C <package-path> lint:fix
pnpm lint:check
pnpm lint:fix
Available presets
| Preset | Purpose |
|---|
base | ESLint core + Unicorn rules + global ignores |
typescript | TypeScript rules (via typescript-eslint, on *.ts + *.vue) |
imports | Import ordering, cycles, duplicates (eslint-plugin-import-x) |
stylistic | Code style via @stylistic/eslint-plugin |
vue | Vue 3 Composition API rules (eslint-plugin-vue) |
vitest | Test file rules (@vitest/eslint-plugin) |
node | Node.js-specific rules (eslint-plugin-n) |
Compose presets in eslint.config.ts:
import { base, compose, imports, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports);
Recommended: Include stylistic preset for code formatting:
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint';
export default compose(base, typescript, imports, stylistic);
All plugins (including @stylistic/eslint-plugin) ship as dependencies of @robonen/eslint, so packages only need @robonen/eslint + eslint in devDependencies.
Building
pnpm -C <package-path> build
pnpm build
All packages use tsdown with shared config from @robonen/tsdown. Output: ESM (.js/.mjs) + CJS (.cjs) + type declarations (.d.ts). Every bundle includes an Apache-2.0 license banner.
Testing
pnpm -C <package-path> test
pnpm test
pnpm test:ui
pnpm -C <package-path> dev
Uses vitest with project-based configuration. Root vitest.config.ts lists all package vitest configs as projects.
Publishing
Publishing is automated via GitHub Actions on push to master:
- CI builds and tests all packages
- Publish workflow compares each package's
version in package.json against npm registry
- If version changed →
pnpm publish --access public
To publish: Bump the version in package.json (and jsr.json if present), then merge to master.
NPM scope: All packages publish under @robonen/.
Documentation
pnpm docs:dev
pnpm docs:generate
pnpm docs:preview
pnpm docs:extract
Key Conventions
- ESM-first: All packages use
"type": "module"
- Strict TypeScript:
strict: true, noUncheckedIndexedAccess: true, verbatimModuleSyntax: true
- License: Apache-2.0 for all published packages
- Node version: ≥24.13.1 (set in
engines and CI)
- pnpm version: Pinned in
packageManager field
- No barrel re-exports of entire modules — export explicitly
__DEV__ global: false in builds, true in tests (Vue packages only)