Multi-file output & library imports¶
Two features for libraries that need to scale beyond a single file — either because they generate a directory tree as output, or because the library itself wants to split shared bits across files.
Multi-file output (file "path" ... end)¶
A library can declare any number of file "..." blocks at the top
level. Each block has a relative path (subdirectories with / are
fine) and an inner-DSL body of write calls (plus optional
for / if control flow). The engine runs each block against the
same final context + body and writes the result to disk under the
--out-dir you pass to capy run.
Basic shape¶
extension py
file "README.md"
write `# ${unquote context.title}
Generated by Capy.
`
end
file "src/main.py"
write `"""Generated app."""
print("hello from ${unquote context.title}")
`
end
file "src/__init__.py"
write `# auto-generated package marker
`
end
Run it:
wrote generated/README.md (...)
wrote generated/src/main.py (...)
wrote generated/src/__init__.py (...)
Subdirectories are created automatically.
Dynamic filenames¶
The path on each file "..." block is itself rendered against the
same context + body data — ${EXPR} interpolations inside the
path work just like inside a write literal:
file "${pascalCase (unquote context.name)}Page.tsx"
write `export function ${pascalCase (unquote context.name)}Page() { ... }
`
end
file "components/${dasherize (unquote context.name)}.css"
write `.${dasherize (unquote context.name)} { ... }
`
end
A source that says page "Account Settings" then emits
AccountSettingsPage.tsx and components/account-settings.css
side by side. See
samples/design-system-components/
for a real one.
Tip: subdirectories work fine in the filename template too —
"${context.feature}/index.ts" lets the source choose its own
folder.
What's in scope inside file blocks¶
Same as file_template:
context.X— the final accumulated context after every function body has executed.body— the concatenated rendered output from every top-level statement.- All template helpers via
${func arg arg}interpolation:indent,unquote,upper,lower,toQuoted,toJSON,toJSONIndent,add,percent,stars,trimSuffix,trimPrefix,split,join,pascalCase,dasherize, … for/ifcontrol flow withwritecalls inside.
State-mutation statements (set / append / …) are NOT allowed
inside file / file_template blocks — those are pure renderers;
the context is already finalised by then.
When file blocks coexist with file_template¶
If your library has both, --out-dir is required to write the
file blocks; the file_template output is what the CLI prints
to stdout (or writes to --out). Most multi-file libraries omit
file_template entirely.
The killer sample¶
samples/multi-file-project/
turns a 9-line route declaration into a 6-file FastAPI project tree
(README, pyproject.toml, .gitignore, src/main.py, src/handlers.py,
tests/test_smoke.py).
Library imports (import)¶
Any library can import other libraries by path. Imported types, functions, context entries, and file blocks are merged in before the importer's own declarations.
# common/types.capy
type Email
pattern "^[^@]+@[^@]+\\.[^@]+$"
end
type Semver
pattern "^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9.]+)?$"
end
# common/syntax.capy
function tag
arg literal "tag"
arg capture name ident
append context.tags name
end
function note
arg literal "note"
arg capture text string
write `> **Note:** ${unquote text}
`
end
After loading lib.capy, the engine sees one merged library with
the post function from lib.capy plus tag, note from syntax.capy
plus Email, Semver from types.capy.
Conflict resolution¶
If the importer and an import both declare the same function name, the importer wins. Imports fill in defaults; the main library can specialize.
This lets you import a shared base and override one or two functions without forking the whole thing.
Path resolution¶
Import paths are relative to the file containing the import
directive. So lib.capy importing common/types.capy resolves to
<dir-of-lib.capy>/common/types.capy.
Absolute paths also work but are discouraged — they break repo portability.
Cycles¶
If a.capy imports b.capy and b.capy imports a.capy, the
loader stops with import cycle detected at .... Cycles are
tracked by absolute path so symlinks and .. shortcuts don't fool
it.
Source-file inclusion (library-declared @import)¶
The library-import mechanism above is for the LIBRARY file. For SCRIPT files, Capy offers an OPT-IN line-level preprocessor: a library can declare one or more inclusion directives, and the engine recognises only the ones the library named. Zero directives are built in — Capy's "no predefined grammar" promise extends to source-level inclusion.
A library opts in with a top-level preprocess block:
With this in place, lines like @import "shared/drinks.capy" (or
the synonym, or any other names you declared, e.g. @use "...")
are replaced with the contents of the referenced file BEFORE
lexing. With no preprocess block, the same @import line is
just regular text and the lexer will treat it as unknown tokens.
menu "Capy Cafe — Spring 2026"
section "Mains"
item "House pasta" "$16"
item "Sheet-pan salmon" "$22"
end
@import "shared/drinks.capy"
@import "shared/desserts.capy"
note "All dishes are made fresh."
end
Each @import is replaced with the contents of the referenced
file, with the same leading indentation as the @import line —
so imported content nests naturally inside the surrounding block.
Rules¶
- Path resolution is relative to the file containing the
@importdirective. - Indentation auto-tracks: a
@import "x"(4 spaces of indent) inlines the imported content with 4 spaces prepended to each non-blank line. - Cycles are detected by absolute path.
- Directive names are whatever the library declared.
@importand@includeare conventional, but a library could equally name the directive@use,@require, or@from— the script must use whatever names the library opts into viapreprocess. - Default = nothing. A library with no
preprocessblock recognises NO inclusion directive. That's the zero-grammar promise: even universal-looking constructs are library-declared.
When to reach for source vs. library imports¶
Library import |
Source @import |
|
|---|---|---|
| Where it appears | Top of lib.capy |
Inside script.capy, any indent |
| What it imports | Functions, types, context | Verbatim source statements |
| Processing stage | Library load | Source preprocessing |
| Conflict rule | Importer-wins merge | n/a (text inclusion) |
| Typical use | Share DSL building blocks | Share pieces of authored content |
See samples/source-imports/
for a worked menu example.
When to split a library¶
Split when…
- A type or function is reused across more than one library (Email validation, semver checks, common HTML primitives).
- The main library grows past ~200 lines and the boilerplate is drowning out the project-specific bits.
- A team wants to publish a "base library" that downstream projects extend.
Don't split prematurely — a single 100-line lib.capy is easier to
read than five 20-line files. Refactor when the pain shows up.
The composition sample¶
samples/lib-composition/
ships a minimal layout that demonstrates the pattern end-to-end:
lib-composition/
├── lib.capy
├── common/
│ ├── types.capy ← Email, URL, Semver, Slug
│ └── syntax.capy ← meta, tag, note
└── script.capy
capy check lib.capy reports 6 functions and 4 types — three of
the functions and all four types come from imports, three are
local. The merged library behaves exactly as if it had been a
single 100-line file.
Putting it together¶
The two features compose: a multi-file project generator can import shared scaffolding from a base library, override one or two file templates, and emit a fully customized project tree:
# base/project-scaffold.capy
file "README.md"
write `# ${unquote context.name}
`
end
file ".gitignore"
write `*.pyc
.venv/
`
end
file "pyproject.toml"
write `[project]
name = ${toQuoted context.name}
...
`
end
# my-project/lib.capy
import "../base/project-scaffold.capy"
# Override the README with something more elaborate.
file "README.md"
write `# ${unquote context.name}
## Custom intro just for this project
...
`
end
The base scaffold ships the standard files; the consumer overrides specific ones. Same pattern as software libraries everywhere — defaults + overrides.