https://github.com/niieani/condu
https://github.com/niieani/condu
Last synced: 13 days ago
JSON representation
- Host: GitHub
- URL: https://github.com/niieani/condu
- Owner: niieani
- Created: 2023-05-27T00:09:32.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2025-08-10T21:28:44.000Z (2 months ago)
- Last Synced: 2025-09-06T04:39:48.796Z (about 1 month ago)
- Language: TypeScript
- Size: 49 MB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# condu - Configuration as Code
condu is a configuration management tool for JavaScript/TypeScript projects that solves the "config hell" problem by providing a unified approach to manage all project configuration in code.
## Why condu?
Modern JavaScript/TypeScript projects require numerous configuration files:
- tsconfig.json
- eslintrc/eslint.config.js
- .prettierrc
- .editorconfig
- package.json
- .gitignore
- And many more...These configurations:
- Use different formats (JSON, YAML, JS)
- Are scattered across your project
- Are hard to keep in sync across multiple projects
- Often require changes to multiple files when adding a new toolcondu solves these problems by:
1. Allowing you to define all configuration in TypeScript
2. Providing a system to share and reuse configurations
3. Making it easy to override only what you need
4. Automating updates across your entire codebase## Getting Started
### Installation
```bash
# Using npm
npm install condu --save-dev# Using yarn
yarn add condu --dev# Using pnpm
pnpm add condu -D
```### Basic Usage
1. Create a `.config/condu.ts` file in your project root:
```typescript
import { configure } from "condu";
import { typescript } from "@condu-feature/typescript";
import { eslint } from "@condu-feature/eslint";
import { prettier } from "@condu-feature/prettier";export default configure({
features: [typescript(), eslint(), prettier()],
});
```2. Run condu to apply your configuration:
```bash
npx condu apply
```This will generate all the necessary configuration files based on your `.config/condu.ts` file.
## Features
Features are the building blocks of condu. Each feature manages configuration for a specific tool or aspect of your project.
### Core Features
condu comes with many built-in features:
- **typescript**: Manages TypeScript configuration
- **eslint**: Configures ESLint
- **prettier**: Sets up Prettier formatting
- **gitignore**: Creates and manages .gitignore files
- **vscode**: Configures VS Code workspace settings
- **editorconfig**: Sets up EditorConfig
- **pnpm/yarn/npm**: Package manager configuration
- **moon**: Task runner integration
- **vitest**: Testing framework setup
- **release-please**: Release management
- And more...### Using Features
Each feature can be configured with options:
```typescript
typescript({
preset: "esm-first",
tsconfig: {
compilerOptions: {
strict: true,
skipLibCheck: true,
},
},
});
```## Monorepo Support
condu excels at managing monorepo configurations. Define your workspace structure:
```typescript
export default configure({
projects: [
{
parentPath: "packages/features",
nameConvention: "@myorg/feature-*",
},
{
parentPath: "packages/core",
nameConvention: "@myorg/*",
},
],
features: [
// ...features
],
});
```## Creating Custom Features
You can create custom features to encapsulate your own configuration logic.
A feature's primary purpose is to define a _recipe_ - a list of changes that should be made whenever `condu apply` is run. Think of the calls to condu recipe APIs similar to React component hooks.
### Inline Features (Simplest Approach)
For one-off or simple modifications, you can define features inline directly in your config file:
```typescript
import { configure } from "condu";
import { typescript } from "@condu-feature/typescript";export default configure({
features: [
typescript(),// Anonymous arrow function feature
(condu) => {
condu.in({ kind: "package" }).modifyPublishedPackageJson((pkg) => ({
...pkg,
// Add sideEffects: false to all packages for better tree-shaking
sideEffects: false,
}));
},// Named functions will use their name as the feature name
function addLicense(condu) {
condu.root.generateFile("LICENSE", {
content: `MIT License\n\nCopyright (c) ${new Date().getFullYear()} My Organization\n\n...`,
});
},
],
});
```Inline features:
- Are perfect for quick, project-specific configurations
- Don't participate in the PeerContext system
- Are applied in the order they appear in the features array### Reusable Features with `defineFeature`
For creating proper reusable features, use the `defineFeature` function:
```typescript
import { defineFeature } from "condu";export const myFeature = (options = {}) =>
defineFeature("myFeature", {
// The main recipe that runs during configuration application
defineRecipe(condu) {
// Generate a configuration file
condu.root.generateFile("my-config.json", {
content: {
enabled: options.enabled ?? true,
settings: options.settings ?? {},
},
stringify: JSON.stringify,
});// Add required dependencies
condu.root.ensureDependency("my-library");// Target specific packages in a monorepo
condu.in({ kind: "package" }).modifyPackageJson((pkg) => ({
...pkg,
scripts: {
...pkg.scripts,
"my-script": "my-command",
},
}));
},
});
```### Using PeerContext for Feature Coordination
When you want features to influence each other, use the PeerContext system.
For example, the TypeScript feature could automatically enable TypeScript-specific ESLint rules as in the example below:```typescript
// ESLint feature definition
declare module "condu" {
interface PeerContext {
eslint: {
rules: Record;
plugins: string[];
extends: string[];
};
}
}export const eslint = (options = {}) =>
defineFeature("eslint", {
initialPeerContext: {
rules: {
"no-unused-vars": "error",
},
plugins: [],
extends: ["eslint:recommended"],
},defineRecipe(condu, peerContext) {
// Generate eslint config using the final peer context
// which may have been modified by other features
condu.root.generateFile(".eslintrc.js", {
content: `module.exports = {
extends: ${JSON.stringify(peerContext.extends)},
plugins: ${JSON.stringify(peerContext.plugins)},
rules: ${JSON.stringify(peerContext.rules, null, 2)}
}`,
});// Ensure ESLint dependency
condu.root.ensureDependency("eslint");// Ensure any plugins are installed
for (const plugin of peerContext.plugins) {
condu.root.ensureDependency(`eslint-plugin-${plugin}`);
}
},
});// TypeScript feature that influences ESLint
export const typescript = (options = {}) =>
defineFeature("typescript", {
initialPeerContext: {
// TypeScript-specific context
config: {
strict: true,
// ...other TypeScript options
},
},// Here TypeScript feature modifies ESLint's context
modifyPeerContexts: (project, initialContext) => ({
eslint: (current) => ({
...current,
// Add TypeScript ESLint plugin
plugins: [...current.plugins, "typescript"],
// Add TypeScript ESLint config
extends: [...current.extends, "plugin:@typescript-eslint/recommended"],
// Add/modify TypeScript-specific rules
rules: {
...current.rules,
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
// Disable the base ESLint rule in favor of TypeScript-specific one
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "error",
},
}),
}),defineRecipe(condu, peerContext) {
// Generate tsconfig.json
condu.root.generateFile("tsconfig.json", {
content: {
compilerOptions: peerContext.config,
},
stringify: (obj) => JSON.stringify(obj, null, 2),
});// Ensure TypeScript dependencies
condu.root.ensureDependency("typescript");// Also add TypeScript ESLint dependencies if ESLint is used
if (condu.project.hasFeature("eslint")) {
condu.root.ensureDependency("@typescript-eslint/parser");
condu.root.ensureDependency("@typescript-eslint/eslint-plugin");
}
},
});
```With this setup:
1. The ESLint feature defines its initial rules and plugin configuration
2. The TypeScript feature enhances ESLint configuration with TypeScript-specific rules
3. When both features are used together, you automatically get TypeScript-aware linting### Advanced Usage: `defineGarnish` for Post-Processing
For final adjustments after all features have run their main recipes, use `defineGarnish`:
```typescript
import { defineFeature } from "condu";export const packageScripts = () =>
defineFeature("packageScripts", {
// Standard recipe for basic setup
defineRecipe(condu) {
// Basic script setup
condu.root.modifyPackageJson((pkg) => ({
...pkg,
scripts: {
...pkg.scripts,
start: "node index.js",
},
}));
},// Garnish runs after all other features have applied their recipes
defineGarnish(condu) {
// Access the complete state after all features have run
const allTasks = condu.globalRegistry.tasks;// Generate scripts based on tasks defined by other features
condu.root.modifyPackageJson((pkg) => {
const scripts = { ...pkg.scripts };// Create aggregate scripts based on task types
const buildTasks = allTasks.filter(
(task) => task.taskDefinition.type === "build",
);if (buildTasks.length > 0) {
scripts["build:all"] = buildTasks
.map((t) => `npm run build:${t.taskDefinition.name}`)
.join(" && ");// Add individual build scripts for each task
for (const task of buildTasks) {
scripts[`build:${task.taskDefinition.name}`] =
task.taskDefinition.command;
}
}// Create test scripts for all test tasks
const testTasks = allTasks.filter(
(task) => task.taskDefinition.type === "test",
);if (testTasks.length > 0) {
scripts["test:all"] = testTasks
.map((t) => `npm run test:${t.taskDefinition.name}`)
.join(" && ");
}return { ...pkg, scripts };
});
},
});
```The `defineGarnish` function:
- Runs after all features have completed their main recipes
- Has access to `globalRegistry` with information about all tasks, dependencies, and files
- Is perfect for generating aggregate configurations or scripts that depend on what other features defined
- Enables post-processing of files or configurations## API Reference
### `condu` object
The main `condu` object available in feature recipes contains the following:
- `condu.project`: Information about the project
- `condu.root`: Recipe API for the root package
- `condu.in(criteria)`: Recipe API for the packages matching the criteriaAdditionally, when used in `defineGarnish`:
- `condu.globalRegistry`: Contains the summary of all the recipes, including:
- which files were modified
- what tasks were registered### Recipe API
Methods for declaring configuration changes:
#### generateFile
Creates files that are fully managed by condu.
```typescript
generateFile(path: PathT, options: GenerateFileOptionsForPath): ScopedRecipeApi
```- **Purpose**: Generate new files that are completely managed by condu
- **Features**:
- Type-safe content generation
- Custom serialization support
- File attributes for special handling (e.g., gitignore)**Examples**:
```typescript
// Generate a standard JSON configuration file
condu.root.generateFile("tsconfig.json", {
content: {
compilerOptions: {
strict: true,
target: "ES2020",
},
include: ["**/*.ts"],
},
// Automatically stringify JSON with formatting
stringify: (obj) => JSON.stringify(obj, null, 2),
});// Generate a YAML file
condu.root.generateFile("pnpm-workspace.yaml", {
content: {
packages: ["packages/*"],
},
// Use a custom YAML stringifier
stringify: getYamlStringify(),
// Set file attributes for special handling
attributes: {
gitignore: false, // Don't add to .gitignore
},
});// Generate a text file with raw content
condu.root.generateFile(".gitignore", {
content: ["node_modules", "build", ".DS_Store", "*.log"].join("\n"),
// No stringification needed for plain text
});
```#### modifyGeneratedFile
Modifies a file that was previously generated by condu.
```typescript
modifyGeneratedFile(path: PathT, options: ModifyGeneratedFileOptions>): ScopedRecipeApi
```- **Purpose**: Update or extend files already managed by condu
- **Features**:
- Access to current content
- Preserves format of the file
- Can be used to add or modify portions of existing files in a typesafe way
- Can be used without providing a parse/stringify, as it uses the one provided by the feature**Examples**:
```typescript
// Modify an existing tsconfig.json
condu.root.modifyGeneratedFile("tsconfig.json", {
content: ({ content = {} }) => ({
...content,
compilerOptions: {
...content.compilerOptions,
// Add or update specific compiler options
declaration: true,
sourceMap: true,
},
}),
});
```#### modifyUserEditableFile
Modifies files that should remain editable by users.
```typescript
modifyUserEditableFile(path: PathT, options: ModifyUserEditableFileOptions): ScopedRecipeApi
```- **Purpose**: Update portions of user-editable files while preserving other user changes
- **Features**:
- Custom parsing and stringification for different formats
- Can create the file if it doesn't exist with `ifNotExists: "create"`
- Preserves content not explicitly modified by condu**Examples**:
```typescript
// Modify a JSON file with type safety
condu.root.modifyUserEditableFile(".vscode/settings.json", {
// Get default JSON parsers and stringifiers
...getJsonParseAndStringify(),
// Create the file if it doesn't exist (that's the default)
ifNotExists: "create", // other options: "ignore" | "error"
// Modify or provide content
content: ({ content = {} }) => ({
...content,
// Add or update specific settings while preserving others
"typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
}),
});// Modify a custom format file
condu.root.modifyUserEditableFile(".npmrc", {
// Custom parser for the specific file format
parse: (rawContent) => customParse(rawContent),
// Custom stringifier for the specific file format
stringify: (data) => customStringify(data),
// Merge content
content: ({ content = {} }) => ({
...content,
"my-setting": "value",
}),
// Set file attributes (e.g., for .gitignore)
attributes: { gitignore: false },
});
```#### ensureDependency
Ensures a dependency is installed in the package.
```typescript
ensureDependency(name: string, dependency?: DependencyDefinitionInput): ScopedRecipeApi
```- **Purpose**: Manage dependencies in package.json
- **Features**:
- Installation target customization (dev, peer, regular)
- Version specification
- Support for aliased packages
- Ability to mark as "built" for pnpm's `onlyBuiltDependencies`**Examples**:
```typescript
// Add a simple dev dependency with default settings
condu.root.ensureDependency("typescript");// Add a dependency with specific options
condu.root.ensureDependency("react", {
// Specify which dependency list to use
list: "dependencies",
// Specify exact version
version: "18.2.0",
// Use a custom name for the dependency
installAsAlias: "react-aliased",
// Specify how versioning is managed
managed: "version", // or "presence" to preserve existing versions
});// Add peer dependencies
condu.root.ensureDependency("react-dom", {
list: "peerDependencies",
// Use semver range prefix
rangePrefix: ">=",
// Mark as built for pnpm
built: true,
});
```#### setDependencyResolutions
Sets dependency resolutions to override specific package versions.
```typescript
setDependencyResolutions(resolutions: Record): ScopedRecipeApi
```- **Purpose**: Override dependency versions for all packages in the workspace
- **Features**:
- Works with different package managers (npm, yarn, pnpm)
- Adapts to the correct syntax for each package manager**Examples**:
```typescript
// Force specific versions of packages
condu.root.setDependencyResolutions({
lodash: "4.17.21",
"webpack/tapable": "2.2.1",
"@types/react": "18.0.0",
});
```#### modifyPackageJson
Modifies the package.json file with a custom transformer function.
```typescript
modifyPackageJson(modifier: PackageJsonModifier): ScopedRecipeApi
```- **Purpose**: Make changes to package.json
- **Features**:
- Full access to the package.json content
- Type-safe with package.json type definitions
- Can access the global registry state**Examples**:
```typescript
// Add custom scripts based on project structure
condu.root.modifyPackageJson((pkg) => ({
...pkg,
scripts: {
...pkg.scripts,
build: "tsc -p tsconfig.json",
test: "vitest run",
lint: "eslint .",
},
// Add custom metadata
keywords: [...(pkg.keywords || []), "condu-managed"],
}));// Add or modify specific fields
condu.in({ kind: "package" }).modifyPackageJson((pkg) => ({
...pkg,
// Add TypeScript configuration
types: "./build/index.d.ts",
// Ensure sideEffects flag is set for tree-shaking
sideEffects: false,
}));
```#### modifyPublishedPackageJson
Modifies the package.json that will be used during publishing.
```typescript
modifyPublishedPackageJson(modifier: PackageJsonModifier): ScopedRecipeApi
```- **Purpose**: Configure how the package.json appears when published to registries like npm
- **Features**:
- Only affects the published version, not the development version
- Perfect for export maps, types path adjustments, etc.**Examples**:
```typescript
// Configure exports map for published packages
condu.in({ kind: "package" }).modifyPublishedPackageJson((pkg) => ({
...pkg,
// Add standard entry points
main: "./build/index.js",
module: "./build/index.js",
types: "./build/index.d.ts",
// Configure exports map
exports: {
".": {
import: "./build/index.js",
require: "./build/index.cjs",
types: "./build/index.d.ts",
},
"./package.json": "./package.json",
},
// Remove development-only fields
devDependencies: undefined,
}));
```#### defineTask
Defines a task that can be run using a task runner.
```typescript
defineTask(name: string, taskDefinition: Omit): ScopedRecipeApi
```- **Purpose**: Define tasks for build, test, etc. that can be run by task runners or package scripts
- **Features**:
- Task type categorization
- Dependencies between tasks
- Command definition**Examples**:
```typescript
// Define a build task
condu.root.defineTask("build", {
type: "build",
command: "tsc -p tsconfig.json",
inputs: ["**/*.ts", "tsconfig.json"],
outputs: ["build/**"],
});// Define a test task that depends on the build task
condu.root.defineTask("test", {
type: "test",
command: "vitest run",
deps: ["build"],
});
```#### ignoreFile
Marks a file to be ignored by certain tools.
```typescript
ignoreFile(path: string, options?: Omit): ScopedRecipeApi
```- **Purpose**: Add files to gitignore or configure other file attributes without generating content
- **Features**:
- Control file visibility in editors and VCS**Examples**:
```typescript
// Add a file to .gitignore
condu.root.ignoreFile("build/");// Configure file attributes
condu.root.ignoreFile("temp/debug.log", {
gitignore: true,
vscode: false, // will still be visible in VSCode
});
```### PeerContext System
The PeerContext system enables features to share information and coordinate with each other:
1. **Declaring Context**: Features declare what data is exposed and modifiable via TypeScript interface augmentation
```typescript
declare module "condu" {
interface PeerContext {
myFeature: {
config: MyConfigType;
};
}
}
```2. **Initializing Context**: Features provide their initial context data
```typescript
initialPeerContext: {
config: {
/* initial data */
}
}
```3. **Modifying Other Contexts**: Features can modify other features' contexts
```typescript
modifyPeerContexts: (project, initialContext) => ({
otherFeature: (current) => ({
...current,
someOption: true,
}),
});
```4. **Using Context**: Features get the _final_ (merged) context data passed in to their recipes when they are applied
```typescript
defineRecipe(condu, peerContext) {
// Use peerContext.config
}
```This system enables powerful coordination between features without tight coupling.
To resolve any type-system issues when building a feature that might influence others, be sure to include the peer features as an optional peerDependency, with a broad version requirement (such as `*` or `>=1.0.0`).
## CLI Reference
Condu provides a comprehensive CLI for managing your projects.
### Core Commands
#### `condu init [project-name]`
Initializes a new condu project in the current directory or creates a new directory with the specified name.
```bash
# Initialize in current directory
condu init# Create a new project directory
condu init my-new-project
```Options: None
The init command will:
- Create a `.config` directory with a default `condu.ts` file
- Set up a package.json with the necessary dependencies
- Initialize a git repository if one doesn't exist
- Add a postinstall script that runs `condu apply`#### `condu apply`
Applies configuration from your `.config/condu.ts` file, generating or updating all configuration files.
```bash
condu apply
```Options: None
This is the primary command you'll use to apply changes after modifying your condu configuration.
#### `condu create [options]`
Creates a new package in a monorepo according to your project conventions.
```bash
# Create a basic package
condu create features/my-feature# Create a package with a custom name
condu create features/my-feature --as @myorg/custom-name
```Options:
- `--as `: Specify a custom package name
- `--description `: Add a description to the package.json
- `--private`: Mark the package as private#### `condu tsc [options]`
Builds TypeScript code and additionally creates CommonJS (.cjs) or ES Module (.mjs) versions of your code.
```bash
# Build with CommonJS output
condu tsc --preset ts-to-cts# Build with ES Module output
condu tsc --preset ts-to-mts
```Options:
- `--preset ts-to-cts|ts-to-mts`: Generate CommonJS or ES Module versions
- All standard TypeScript compiler options are supported#### `condu release [packages...] [options]`
Prepares packages for release by generating distributable files and optionally publishing to npm.
```bash
# Release all packages
condu release# Release specific packages
condu release @myorg/package1 @myorg/package2# Do a dry run without publishing
condu release --dry-run
```Options:
- `--ci`: Mark non-released packages as private (useful in CI environments)
- `--npm-tag `: Specify the npm tag to use (default: latest)
- `--dry-run`: Prepare packages without actually publishing#### `condu exec [args...]`
Executes a command in the context of a selected package.
```bash
# Run in current directory
condu exec npm run test# Run in a specific package
condu exec --pkg @myorg/my-package npm run test
```Options:
- `--cwd `: Specify the working directory
- `--pkg `: Specify the target package### Helper Commands
- `condu help`: Shows help information
- `condu version`: Shows the current condu version## Best Practices
1. **Keep features focused**: Each feature should manage one aspect of configuration
2. **Use peer contexts** for cross-feature coordination, e.g. if you know that the project is TypeScript-based, you might want to enable TS-specific linters in your linter feature
3. **Use presets** to combine common feature sets, making common boilerplates like `create-react-app` obsolete
4. **Create custom features** for organization-specific configuration patterns
5. **Commit the generated files** to source control for transparency## Preset Example
Presets combine multiple features with sensible defaults:
```typescript
// monorepo.ts
export const monorepo =
(options = {}) =>
(pkg) => ({
projects: [
{
parentPath: "packages",
nameConvention: `@${pkg.name}/*`,
},
],
features: [
typescript(options.typescript),
eslint(options.eslint),
prettier(options.prettier),
pnpm(options.pnpm),
// Add more features
],
});
```Use a preset in your project:
```typescript
import { configure } from "condu";
import { monorepo } from "@condu-preset/monorepo";export default configure(
monorepo({
// Override specific feature options
typescript: {
preset: "commonjs-first",
},
}),
);
```## Conclusion
condu streamlines configuration management by:
- Centralizing all configuration in code
- Providing strong typing with TypeScript
- Enabling reuse across projects
- Minimizing boilerplate
- Making updates easier to applySay goodbye to config hell and focus on building your application!