Emit modes and targets
EmitMode chooses which files are written. TargetProfile chooses which Effect import paths they use. Both are output policy and never enter the IR or the semantic hash.
Two settings change the generated files without changing the IR.
| Setting | Question it answers | Literals |
|---|---|---|
EmitMode | Which projections does this pass write? | contract, handlers, all |
TargetProfile | Which Effect package paths and API names do the files use? | effect-4.0 |
Source: packages/compiler/src/Collected.ts (EmitMode, TargetProfile, ProjectResolution, ProjectConfig), packages/compiler/src/Extension.ts (GenerationContext), docs/specs/0010-target-profiles-and-emit-modes.md.
Output policy never enters the IR
GenerationContext carries the policy to generators. Its doc comment says: "Output policy never enters ApplicationIR or its semantic hash."
| Field | Type | Meaning |
|---|---|---|
target | TargetProfile | the Effect API profile |
emit | EmitMode | the projection choice |
allowImportingTsExtensions | boolean | emit .ts instead of .js in relative imports |
canonicalImportBase | string, optional | one output-independent base for local symbols |
outputDir | string, optional | where this pass writes |
resolveEffectModule | function, optional | runtime-only resolver for the target project's installed Effect modules |
defaultGenerationContext is target: "effect-4.0", emit: "all", allowImportingTsExtensions: false. It applies when the pipeline runs without project resolution, for example in tests (compileCollected).
Spec 0010 requires the same source to give a byte-identical canonical IR and the same semantic hash in every mode, target and output directory.
Emit modes
Pass --emit to effx build or effx check. The default is all.
| Mode | Writes | Source |
|---|---|---|
contract | <group>-contract.ts for each HTTP group. No handlers, no http.ts, rpc.ts, cli.ts, client.ts, foldkit.ts or guards.ts. | packages/compiler/src/generate/http.ts and the other generators, which return nothing unless emit is all |
handlers | <group>-handlers.ts for each declaration-only group. Nothing else. | generate/http.ts |
all | The contract and handlers files for declaration-only groups, plus the projections for locally implemented operations: http.ts, rpc.ts, cli.ts, client.ts, and when applicable foldkit.ts and guards.ts. | generate/*.ts |
For an external-only group named profile, spec 0010 gives the exact outputs:
| Mode | Output |
|---|---|
contract | profile-contract.ts only |
handlers | profile-handlers.ts only |
all | both files, plus the usual projections for any locally implemented operations |
Rules from spec 0010:
| Rule | Detail |
|---|---|
| Equal group names under different roots | Both file names are prefixed with the root identifier. Files are never overwritten. |
| Placeholders | No mode writes an empty placeholder file for a missing implementation. |
| Manifest | manifest.json records the selected mode in emit and the exact files in generated. |
| Mode change | Files the previous manifest owned and this pass does not write are removed. See effx build. |
Why two passes
A backend package and a shared contract package can build from the same source with different modes. The contract pass writes the group the shared package exports. The handlers pass writes the factory the backend binds. Both passes see one IR.
profile.effx.ts ──effx build --project <contract tsconfig> --emit=contract──▶ <contract dir>/.effx/generated/profile-contract.ts
│
└────────effx build --project <backend tsconfig> --emit=handlers──▶ <backend dir>/.effx/generated/profile-handlers.ts
same IR, same semanticHashThe two projects set one shared source identity with effx.projectRoot in each tsconfig, a path relative to that tsconfig. ProjectConfig.projectRoot supplies the same value programmatically. Local symbols and StableIds use that root, not the output directory (spec 0010, packages/frontend-ts/src/project.ts).
The output directory is selected by --out-dir, config outDir, or tsconfig
effx.outDir. The default is .effx/generated next to the tsconfig.
Stable Effect target
effect-4.0 is the only target. It requires a stable installed Effect release
in >=4.0.0 <5. Prereleases are not supported. The compiler reads the target
project's own effect/package.json, not the CLI's bundled Effect. An explicit
target cannot override an unsupported installed version. Missing or unsupported
packages produce EFFX2701 and prevent generation.
Every generator uses the same native module names:
| Import | Module |
|---|---|
| core | effect |
| http | effect/http |
| http api | effect/http-api |
| net | effect/net |
| rpc | effect/rpc |
| cli | effect/cli |
| sql | effect/sql |
Application module leaves are checked against the installed package exports.
Unsupported subpaths produce a diagnostic; the compiler does not rename them.
Source: packages/compiler/src/generate/target.ts,
packages/frontend-ts/src/project.ts, spec 0010's stable-only amendment.
Relative import suffix
Relative imports in generated files end in .js, unless the target tsconfig sets allowImportingTsExtensions. With that option they keep .ts. This applies to generated-to-generated imports and to imports of your own schema and service symbols. The suffix never affects a StableId or the hash (spec 0010).
Status
| Item | Status |
|---|---|
--emit contract, --emit handlers, --emit all | implemented (scripts/effx.ts). STATE.md: "Gate 3 compiler prerequisites (target/split emit)", build, DoD met locally. |
--target effect-4.0 | stable Effect 4 only; isolated fixture: packages/frontend-ts/test/fixtures/stable-v4 |
effx.config.ts | implemented (spec 0015): emit, target, outDir, strictAccess and project are optional config fields; precedence is flag, then config, then the tsconfig effx block, then the default (CLI). |
Semantic hash
Canonical JSON per RFC 8785 over the normalized IR, and the sha-256 hash taken from it. Same meaning gives the same bytes.
External bindings
A declaration-only operation has a contract but no handler. The application binds it. What the IR records, what is generated, and what the compiler refuses.