Monorepo Setup
Learn how to share presets across a monorepo. Monorepo guide →
Learn how to create your own architecture presets for Stricture - from defining boundaries and rules to publishing and sharing them with your team.
Consider creating a custom preset when:
A Stricture preset is an npm package with this structure:
my-preset/├── src/│ ├── boundaries.ts # Boundary definitions│ ├── rules.ts # Architectural rules│ ├── types.ts # TypeScript types (optional)│ ├── diagram.ts # Mermaid diagram (optional)│ ├── scaffolding.ts # CLI scaffolding (optional)│ └── index.ts # Main export├── tests/│ └── preset.test.ts # Tests for your preset├── package.json├── tsconfig.json├── README.md└── LICENSELet’s create a real preset that enforces feature flag architecture patterns.
Our feature flags architecture has these boundaries:
Rules:
Create a new npm package:
mkdir stricture-feature-flagscd stricture-feature-flagsnpm init -ynpm install -D @stricture/core typescript tsup vitestmkdir stricture-feature-flagscd stricture-feature-flagspnpm initpnpm add -D @stricture/core typescript tsup vitestmkdir stricture-feature-flagscd stricture-feature-flagsyarn init -yyarn add -D @stricture/core typescript tsup vitestUpdate package.json:
{ "name": "@my-company/stricture-feature-flags", "version": "1.0.0", "description": "Feature flags architecture preset for Stricture", "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } }, "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": ["dist"], "scripts": { "build": "tsup", "test": "vitest", "type-check": "tsc --noEmit" }, "dependencies": { "@stricture/core": "^0.1.0" }, "keywords": ["stricture", "preset", "feature-flags", "architecture"]}Create tsconfig.json:
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": true, "outDir": "./dist" }, "include": ["src/**/*"]}Create tsup.config.ts:
import { defineConfig } from 'tsup'
export default defineConfig({ entry: ['src/index.ts'], format: ['esm', 'cjs'], dts: true, clean: true, splitting: false, sourcemap: true})Create src/boundaries.ts:
import type { BoundaryDefinition } from '@stricture/core'
/** * Feature flags architecture boundary definitions */export const boundaries: BoundaryDefinition[] = [ { name: 'flags', pattern: 'src/flags/**', mode: 'file', tags: ['flags'], metadata: { description: 'Flag definitions and types - pure data', layer: 0 } }, { name: 'providers', pattern: 'src/providers/**', mode: 'file', tags: ['providers'], metadata: { description: 'Flag evaluation providers (LaunchDarkly, env vars, etc.)', layer: 1 } }, { name: 'consumers', pattern: 'src/{features,pages,components}/**', mode: 'file', tags: ['consumers'], metadata: { description: 'Application code that uses feature flags', layer: 2 } }, { name: 'admin', pattern: 'src/admin/**', mode: 'file', tags: ['admin'], metadata: { description: 'Flag management UI and tools', layer: 3 } }]Create src/rules.ts:
import type { ArchRule } from '@stricture/core'
/** * Feature flags architecture rules */export const rules: ArchRule[] = [ // Flag isolation - flags are pure data { id: 'flags-isolation', name: 'Flags Are Pure Data', description: 'Flag definitions cannot import anything', severity: 'error', from: { tag: 'flags', mode: 'file' }, to: { tag: '*', mode: 'file' }, allowed: false, message: 'Flag definitions must be pure data with no dependencies', examples: { bad: [ "import { evaluate } from '../providers/launchdarkly'", "import axios from 'axios'" ], good: [ "export const MY_FLAG = 'my-feature' as const", "export type MyFlag = { enabled: boolean }" ] } },
// Allow flags to import other flags { id: 'flags-self-imports', name: 'Flags Can Import Flags', description: 'Flag files can reference other flag definitions', severity: 'error', from: { tag: 'flags', mode: 'file' }, to: { tag: 'flags', mode: 'file' }, allowed: true },
// Providers implement evaluation logic { id: 'providers-to-flags', name: 'Providers Use Flag Definitions', description: 'Providers read flag definitions', severity: 'error', from: { tag: 'providers', mode: 'file' }, to: { tag: 'flags', mode: 'file' }, allowed: true }, { id: 'providers-self-imports', name: 'Providers Can Import Providers', description: 'Providers can share utilities', severity: 'error', from: { tag: 'providers', mode: 'file' }, to: { tag: 'providers', mode: 'file' }, allowed: true }, { id: 'providers-external', name: 'Providers Can Use External SDKs', description: 'Providers can use LaunchDarkly, Split, etc.', severity: 'error', from: { tag: 'providers', mode: 'file' }, to: { tag: 'external', mode: 'file' }, allowed: true },
// Consumers use flags but not providers directly { id: 'consumers-to-flags', name: 'Consumers Import Flags', description: 'Application code imports flag definitions', severity: 'error', from: { tag: 'consumers', mode: 'file' }, to: { tag: 'flags', mode: 'file' }, allowed: true }, { id: 'consumers-not-providers', name: 'Consumers Cannot Import Providers', description: 'Use a facade/hook instead of importing providers directly', severity: 'error', from: { tag: 'consumers', mode: 'file' }, to: { tag: 'providers', mode: 'file' }, allowed: false, message: 'Consumers should use a flag facade/hook, not import providers directly', examples: { bad: [ "import { LaunchDarklyProvider } from '../providers/launchdarkly'" ], good: [ "import { useFlag } from '../hooks/use-flag'", "const isEnabled = useFlag(MY_FLAG)" ] } }, { id: 'consumers-self-imports', name: 'Consumers Can Import Consumers', description: 'Application code can import other application code', severity: 'error', from: { tag: 'consumers', mode: 'file' }, to: { tag: 'consumers', mode: 'file' }, allowed: true }, { id: 'consumers-external', name: 'Consumers Can Use External Libraries', description: 'Application code can use normal dependencies', severity: 'error', from: { tag: 'consumers', mode: 'file' }, to: { tag: 'external', mode: 'file' }, allowed: true },
// Admin has full access for management { id: 'admin-to-flags', name: 'Admin Can Access Flags', description: 'Admin UI needs flag definitions', severity: 'error', from: { tag: 'admin', mode: 'file' }, to: { tag: 'flags', mode: 'file' }, allowed: true }, { id: 'admin-to-providers', name: 'Admin Can Access Providers', description: 'Admin UI can manage provider configuration', severity: 'error', from: { tag: 'admin', mode: 'file' }, to: { tag: 'providers', mode: 'file' }, allowed: true }, { id: 'admin-self-imports', name: 'Admin Can Import Admin', description: 'Admin components can reference each other', severity: 'error', from: { tag: 'admin', mode: 'file' }, to: { tag: 'admin', mode: 'file' }, allowed: true }, { id: 'admin-external', name: 'Admin Can Use External Libraries', description: 'Admin UI can use frameworks and libraries', severity: 'error', from: { tag: 'admin', mode: 'file' }, to: { tag: 'external', mode: 'file' }, allowed: true }]Create src/index.ts:
import type { ArchPreset } from '@stricture/core'import { boundaries } from './boundaries'import { rules } from './rules'
/** * Feature Flags Architecture Preset * * Enforces clean separation between: * - Flags: Pure flag definitions * - Providers: Flag evaluation logic * - Consumers: Application code using flags * - Admin: Management UI */export const featureFlagsPreset: ArchPreset = { id: '@my-company/stricture-feature-flags', name: 'Feature Flags Architecture', description: 'Enforces clean feature flag patterns with provider abstraction', boundaries, rules}
export default featureFlagsPreset
// Re-export for advanced usageexport { boundaries } from './boundaries'export { rules } from './rules'Create tests/preset.test.ts:
import { describe, it, expect } from 'vitest'import { featureFlagsPreset } from '../src'
describe('Feature Flags Preset', () => { it('exports a valid preset', () => { expect(featureFlagsPreset).toBeDefined() expect(featureFlagsPreset.id).toBe('@my-company/stricture-feature-flags') expect(featureFlagsPreset.boundaries).toHaveLength(4) expect(featureFlagsPreset.rules.length).toBeGreaterThan(0) })
it('defines all required boundaries', () => { const boundaryNames = featureFlagsPreset.boundaries.map(b => b.name) expect(boundaryNames).toContain('flags') expect(boundaryNames).toContain('providers') expect(boundaryNames).toContain('consumers') expect(boundaryNames).toContain('admin') })
it('has rules for flag isolation', () => { const flagIsolationRule = featureFlagsPreset.rules.find( r => r.id === 'flags-isolation' ) expect(flagIsolationRule).toBeDefined() expect(flagIsolationRule?.allowed).toBe(false) })
it('prevents consumers from importing providers', () => { const rule = featureFlagsPreset.rules.find( r => r.id === 'consumers-not-providers' ) expect(rule).toBeDefined() expect(rule?.allowed).toBe(false) expect(rule?.message).toContain('facade') })})Run tests:
npm testnpm run buildThis creates:
dist/index.js - ESM builddist/index.cjs - CommonJS builddist/index.d.ts - TypeScript definitionsFor public or scoped npm packages:
# Login to npmnpm login
# Publish (first time)npm publish --access public
# Or for scoped packagesnpm publishFor internal company presets, keep it in your monorepo:
my-monorepo/├── packages/│ ├── stricture-presets/│ │ └── feature-flags/│ │ ├── src/│ │ └── package.json # @my-company/stricture-feature-flags│ ├── api/│ │ └── .stricture/│ │ └── config.json # Uses: "@my-company/stricture-feature-flags"│ └── web/│ └── .stricture/│ └── config.json└── package.jsonIn workspace package.json:
{ "name": "@my-company/stricture-feature-flags", "version": "1.0.0", "main": "./dist/index.cjs", "module": "./dist/index.js", "types": "./dist/index.d.ts"}Other packages reference it as a workspace dependency:
{ "devDependencies": { "@my-company/stricture-feature-flags": "workspace:*" }}For company-wide distribution via private registry:
{ "publishConfig": { "registry": "https://npm.mycompany.com" }}npm publishAfter publishing/installing, use it like any other preset:
{ "preset": "@my-company/stricture-feature-flags"}Or with customization:
{ "preset": "@my-company/stricture-feature-flags", "boundaries": [ { "name": "experiments", "pattern": "src/experiments/**", "mode": "file", "tags": ["consumers"] } ], "rules": [ { "id": "experiments-to-flags", "from": { "tag": "experiments" }, "to": { "tag": "flags" }, "allowed": true } ]}Create a comprehensive README:
# Feature Flags Architecture Preset
Stricture preset for enforcing clean feature flag patterns.
## Installation
\`\`\`bashnpm install -D @my-company/stricture-feature-flags\`\`\`
## Usage
\`\`\`json{ "preset": "@my-company/stricture-feature-flags"}\`\`\`
## Architecture
[Diagram and explanation of your architecture]
## Boundaries
- **flags/** - Pure flag definitions- **providers/** - Flag evaluation logic- **consumers/** - Application code using flags- **admin/** - Management UI
## Rules
[List key rules and rationale]
## Examples
[Code examples of correct usage]Create src/diagram.ts:
export const diagram = `%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#b3d9ff', 'primaryBorderColor':'#1976d2', 'secondaryColor':'#b3ffcc', 'secondaryBorderColor':'#2e7d32'}}}%%graph TB subgraph Consumers["📁 Consumers<br/><i>App Code</i>"] Features["Features"] Pages["Pages"] end
subgraph Core["Core"] Flags["📁 Flags<br/><i>Definitions</i>"] Providers["📁 Providers<br/><i>Evaluation</i>"] end
subgraph Admin["📁 Admin<br/><i>Management UI</i>"] UI["Admin UI"] end
Features --> Flags Pages --> Flags Providers --> Flags UI --> Flags UI --> Providers
style Consumers fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style Core fill:#f1f8e9,stroke:#2e7d32,stroke-width:2px style Admin fill:#fff3e0,stroke:#f57c00,stroke-width:2px linkStyle default stroke:#22c55e,stroke-width:2px`Export from index.ts:
import { diagram } from './diagram'
export const featureFlagsPreset: ArchPreset = { // ... diagram}Create src/scaffolding.ts:
export const scaffolding = { directories: [ { path: 'src/flags', description: 'Flag definitions' }, { path: 'src/providers', description: 'Flag evaluation providers' }, { path: 'src/features', description: 'Application features' }, { path: 'src/admin', description: 'Management UI' } ], files: [ { path: 'src/flags/index.ts', content: `// Feature flag definitionsexport const MY_FEATURE = 'my-feature' as const
export type FeatureFlags = { [MY_FEATURE]: boolean}` }, { path: 'src/providers/provider.ts', content: `import type { FeatureFlags } from '../flags'
export interface FlagProvider { evaluate<K extends keyof FeatureFlags>(flag: K): Promise<FeatureFlags[K]>}` } ]}Users can scaffold with:
npx stricture scaffoldTest your preset with real violations:
import { describe, it, expect } from 'vitest'import { validateImport } from '@stricture/core'import { featureFlagsPreset } from '../src'
describe('Feature Flags Preset - Integration', () => { it('prevents consumers from importing providers', () => { const result = validateImport( '/project/src/features/checkout.ts', '/project/src/providers/launchdarkly.ts', featureFlagsPreset.rules, featureFlagsPreset.boundaries )
expect(result.allowed).toBe(false) expect(result.message).toContain('facade') })
it('allows consumers to import flags', () => { const result = validateImport( '/project/src/features/checkout.ts', '/project/src/flags/index.ts', featureFlagsPreset.rules, featureFlagsPreset.boundaries )
expect(result.allowed).toBe(true) })
it('prevents flags from importing anything', () => { const result = validateImport( '/project/src/flags/index.ts', '/project/node_modules/axios/index.js', featureFlagsPreset.rules, featureFlagsPreset.boundaries )
expect(result.allowed).toBe(false) expect(result.message).toContain('pure data') })})Create a test project:
mkdir test-projectcd test-projectnpm init -ynpm install -D @stricture/core @stricture/eslint-pluginnpm install -D ../stricture-feature-flags # Local linkCreate .stricture/config.json:
{ "preset": "@my-company/stricture-feature-flags"}Add some test files with violations and verify ESLint catches them.
allowed: true rules{features,modules}/** to work with different structuresdriving vs driven, not just adapters)Maintain a CHANGELOG.md:
# Changelog
## [1.1.0] - 2024-01-15
### Added- New `experiments` boundary for A/B testing code- Rule allowing experiments to access providers
### Changed- Improved error message for consumers-not-providers rule
## [1.0.0] - 2024-01-01
Initial releaseIf your preset could benefit the community:
packages/Share your preset with the community:
Monorepo Setup
Learn how to share presets across a monorepo. Monorepo guide →
Troubleshooting
Debug common issues with custom presets. Troubleshoot →
Core Concepts
Deep dive into boundaries, rules, and specificity. Learn concepts →
Explore Presets
Study existing presets for inspiration. Browse presets →