Contexts

This document explains the context-centric package organization used in morph and generated projects.

Overview

Morph organizes code by bounded contexts - each domain area gets its own directory containing related packages. This mirrors Domain-Driven Design principles where each context is autonomous and self-contained.

Per-Context Package Structure

Each context generates up to three package types:

contexts/{context-name}/
├── dsl/                     # Types, schemas, errors
├── core/                    # Operations, handlers, services
└── impls/                   # Hand-written implementations (optional)

DSL Package (dsl/)

Contains domain types and schemas (100% generated):

  • schemas.ts - Effect Schema definitions for entities and value objects
  • errors.ts - Domain error types
  • events.ts - Domain event types (if events defined)
  • arbitraries.ts - fast-check arbitraries for property testing

Package name: @{scope}/{context}-dsl

Core Package (core/)

Contains business logic (mostly generated, with re-exports from impls):

  • operations/ - Operation definitions with handlers
    • {operation}/index.ts - Operation definition
    • {operation}/handler.ts - Handler interface (Context.Tag)
    • {operation}/impl.ts - Real implementation (from impls/ fixtures)
    • {operation}/mock-impl.ts - Mock implementation for testing
    • {operation}/impl.template.ts - Template for custom implementations
  • services/ - Repository interfaces and implementations
  • subscribers/ - Event subscribers (if events defined)
  • invariants/ - Domain invariant validators (if invariants defined)
  • layers.ts - Layer compositions (InMemoryLayer, etc.)
  • prose.ts - Re-exports prose from impls
  • text.ts - Re-exports text catalog from impls (if i18n configured)
  • test/scenarios.test.ts - Scenario tests
  • test/properties.test.ts - Property tests (if invariants defined)

Package name: @{scope}/{context}-core

Impls Package (impls/)

Hand-written implementations and fixtures. This is the only place for hand-written context code:

Handler implementations: Structure mirrors core/operations/:

impls/{operation}.ts  →  core/src/operations/{operation}/impl.ts

Fixtures (prose and text):

  • prose.ts - Human-readable templates for test output and feature files
  • text.ts - Text catalog with translations for UI

These fixtures are re-exported from core, so consumers import from core, not impls:

// ✅ Correct - import fixtures from core
import { prose } from "@my-app/tasks-core";

// ❌ Wrong - never import from impls directly
import { prose } from "@my-app/tasks-impls";

Important: The impls package is an internal implementation detail. Users should never import from impls directly. All functionality is exposed through operations in the core package:

// ✅ Correct - use operations via ops namespace
import { ops, HandlersLayer } from "@my-app/tasks-core";

const result = await Effect.runPromise(
  ops.createTask.execute({ title: "Buy milk" }, {}).pipe(
    Effect.provide(HandlersLayer)
  )
);

// ❌ Wrong - never import from impls directly
import { createTask } from "@my-app/tasks-impls";

This ensures:

  1. A consistent public API surface where core is the single entry point
  2. Operations are always executed through the Effect layer system
  3. Implementation details (impls) remain internal

Generated Project Structure

A generated project follows this layout:

{project}/
├── contexts/                # Domain contexts
│   └── {context}/
│       ├── dsl/             # @{scope}/{context}-dsl
│       └── core/            # @{scope}/{context}-core
├── apps/                    # Application targets
│   ├── api/                 # REST API
│   ├── cli/                 # Command-line interface
│   ├── mcp/                 # MCP server
│   └── ui/                  # Web UI
├── libs/                    # Shared libraries
│   └── client/              # API client
├── tests/                   # Cross-cutting tests
│   ├── scenarios/           # Scenario definitions
│   └── properties/          # Property definitions
├── docs/                    # Generated documentation
├── config/                  # Shared config (tsconfig, eslint)
└── schema.json              # Domain schema (includes extensions config)

Morph’s Own Structure

Morph dogfoods itself. Its schema defines two contexts: generation (code generation operations) and schema-dsl (schema parsing, compilation, and decompilation). Auth and storage are not contexts – they are extensions that live in a separate directory.

morph/
├── contexts/
│   ├── generation/              # Code generation context
│   │   ├── dsl/                 # @morphdsl/generation-dsl
│   │   ├── core/                # @morphdsl/generation-core
│   │   ├── impls/               # Hand-written generation implementations
│   │   │
│   │   ├── targets/             # Generation targets
│   │   │   ├── api/
│   │   │   ├── cli/
│   │   │   ├── cli-client/
│   │   │   ├── client/
│   │   │   ├── core/
│   │   │   ├── dsl/
│   │   │   ├── mcp/
│   │   │   ├── monorepo/
│   │   │   ├── proto/
│   │   │   ├── ui/
│   │   │   ├── verification/
│   │   │   └── vscode/
│   │   │
│   │   ├── builders/            # Code builders
│   │   │   ├── app/
│   │   │   ├── readme/
│   │   │   ├── test/
│   │   │   └── scaffold/
│   │   │
│   │   ├── generators/          # Cross-cutting generators
│   │   │   ├── types/
│   │   │   ├── openapi/
│   │   │   ├── diagrams/
│   │   │   └── ...
│   │   │
│   │   └── plugin/              # Plugin system
│   │
│   └── schema-dsl/              # Schema DSL context
│       ├── dsl/                 # @morphdsl/schema-dsl-dsl
│       ├── core/                # @morphdsl/schema-dsl-core
│       ├── impls/               # Hand-written schema-dsl implementations
│       ├── compiler/            # Schema compiler
│       ├── decompiler/          # Schema decompiler
│       └── parser/              # Schema parser
│
└── extensions/                  # Infrastructure extensions
    ├── auth/                    # Auth interfaces
    ├── auth-password/           # Password hashing (with dsl/ + impls/)
    ├── auth-{none,apikey,jwt,session}/
    ├── codec/                   # Codec interfaces
    ├── codec-{json,yaml,protobuf}/
    ├── eventstore/              # Event store interfaces
    ├── eventstore-{memory,jsonfile,redis}/
    ├── storage/                 # Storage interfaces
    └── storage-{memory,jsonfile,sqlite,redis,eventsourced}/

The generation context is special because it contains both:

  1. Generated packages (dsl/, core/) from morph’s own schema
  2. Generation infrastructure (targets/, builders/, generators/, plugin/)

The schema-dsl context handles parsing .morph schema files, compiling them to the internal representation, and decompiling back. It also contains generated packages (dsl/, core/) plus domain-specific tooling (compiler/, decompiler/, parser/).

Extensions are not contexts – they provide reusable infrastructure (auth providers, storage backends) that generated projects can opt into via the extensions field in their domain schema. Extension packages use @morphdsl/{name}-dsl and @morphdsl/{name}-impls naming (not -core).