Generated types
npx copy-ink types
An AST scan of your JSX, written to .copy-ink/:
| File | Contents |
|---|---|
manifest.json | Every <Copy> usage — component, field, resolved scope, collection, item, locale, and source location — plus the route rendering each collection's items |
copy-ink-env.d.ts | Module augmentation: field, scope, collection and locale unions, plus each collection's item shape |
image-hosts.json | The allowedHosts array, for next.config |
Wiring it up
Add .copy-ink to your tsconfig include:
{
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".copy-ink"]
}
copy-ink doctor warns (tsconfig-include) when this is missing — without it
the generated declarations are never loaded and autocomplete stays empty.
.copy-ink/ is generated output. Add it to .gitignore and regenerate in CI,
or commit it — either works, but pick one.
What the declarations do
declare module 'copy-ink' {
interface CopyInkRegistry {
fields: 'heroTitle' | 'hero.subtitle' | 'body'
scopes: '' | 'about' | 'contact'
collections: 'posts'
locales: 'en' | 'de'
}
interface CopyInkCollectionData {
readonly posts: {
readonly title: string
readonly date?: string
readonly cover?: ImageNode
}
}
}
KnownField, KnownScope, KnownCollection and KnownLocale resolve from
that registry. Before the file exists every slot is absent, so each one widens
to plain string and nothing you write is rejected.
Fields come from three places: every <Copy> usage in your JSX, every path
present in a YAML file (whether anything reads it or not), and every collection
field in the config. meta and its children are excluded — that block is
metadata, not copy.
Suggestions, not constraints
The unions are typed as Known | (string & {}). Autocomplete offers the known
values; an unknown string still type-checks.
That is deliberate. A stale manifest — a field added to YAML after the last
types run, a component written before the scan — would otherwise turn a
working project red, and the developer's first instinct would be to stop running
the tool.
For the check that does fail, run copy-ink check: it reads the same
manifest and reports missing-key for a field JSX references but no file
defines.
Why a static scan
A field inside a branch that never renders would never register at runtime, so
runtime registration alone could not tell you it exists. Scanning the source
sees every usage, whether or not it executes — which is what makes check
trustworthy rather than merely encouraging.
The scan knows the difference between a scope it can resolve (a literal
scope="about", or the route a file maps to) and one it cannot (a scope from a
variable). Unresolvable entries record scope: null rather than guessing, and
check does not report on them.
When to re-run
- After adding or renaming a
<Copy>usage - After adding a key to a content file
- After changing
collectionsorlocales - After changing
images.allowedHosts— plus anext devrestart
A predev / prebuild script keeps it honest:
{
"scripts": {
"predev": "copy-ink types",
"prebuild": "copy-ink types && copy-ink check"
}
}