Output modes
Every outputs entry in tako.config.ts picks a mode. Two
exist. The import surface is identical in both — only the location and packaging
differ.
Layout common to both modes
Each generator owns the prefix <namespace>/<generatorName>/ and writes its own barrel
there. core then synthesizes a <namespace>/index.ts that re-exports every generator
which contributed to that namespace, except the generators that export nothing
importable from the root: the UI-framework generators (angular, react-tanstack),
whose output imports its framework as soon as it loads — re-exporting it would make a
plain Node process (an API importing @myapp/db) require @angular/forms; they stay
reachable from their own path, @myapp/db/angular — and openapi, which emits only an
openapi.json/openapi.yaml document per namespace and no TypeScript module at all.
<root>/
db/
zod/
user.schema.ts
enums.ts
index.ts # gen-zod's own barrel
angular/
user.form.ts
index.ts # gen-angular's own barrel
index.ts # SYNTHESIZED by core: export * from './zod'; (angular is left out, see above)
index.ts # SYNTHESIZED: re-exports every namespace
Import surface:
import { UserDto } from '@myapp/db'; // synthesized root barrel
import { UserSchema } from '@myapp/db/zod'; // one generator's barrel
import { UserForm } from '@myapp/db/angular'; // UI-framework generators: this path only
import { UserSchema } from '@myapp/db/zod/user.schema'; // fine-grained, no eager sibling load
Mode A — directory (default)
mode: 'dir' (the default). The tree above is written under dir
(default ./generated/kurotako), relative to the config file. You commit it — or add it
to .gitignore and regenerate in CI — and import it by relative path:
outputs: [{ dir: './generated/kurotako' }]
import { UserSchema } from './generated/kurotako/db/zod';
Mode B — npm package per source
mode: 'package'. Each namespace becomes its own installable npm package under
packagesDir, named ${scope}/${namespace}. tako writes each package's package.json,
builds it, and auto-installs it into your workspace.
outputs: [
{
mode: 'package',
packagesDir: './packages',
scope: '@myapp',
// packageManager: 'bun', // optional — auto-detected
},
]
packagesDir and scope are required for mode B; loadConfig fails without them.
The result is imported by package name:
import { UserSchema } from '@myapp/db/zod';
The generated package.json declares an explicit exports entry for the package root and
for every generator that wrote into the namespace (./zod, ./typescript, ./angular, …),
each with types, import and require conditions, plus a ./* pattern for single files
(@myapp/db/zod/user.schema). A sub-path such as @myapp/db/zod therefore resolves to the
generator's dist/zod/index.* for ESM, CommonJS and TypeScript alike. Before
@kurotako/core 0.4.0 (kurotako 0.3.0) only the ./* pattern existed, which sent @myapp/db/zod to a non-existent
dist/zod.js; the workaround was @myapp/db/zod/index, which keeps working.
Prerequisites
tako generate checks these before writing anything, and reports everything that is
missing in a single error (with ready-to-paste content for the two files). The workspace
root is the directory one level above packagesDir.
tsconfig.base.jsonin the workspace root. The generatedtsconfig.jsonextends it. Minimal content:target: ES2022,module: ESNext,moduleResolution: bundler,strict: true,skipLibCheck: true.tsup.config.base.{ts,js,mjs,cjs}in the workspace root, exporting abasePreset(tsupOptions:format: ['esm', 'cjs'],dtsenabled,outDir: 'dist'). The generatedtsup.config.tsspreads it.typescriptresolvable from the workspace root (needed for the.d.tsbuild).- Every peer dependency of the active generators resolvable from the generated package,
that is, installed in the workspace root (or in
packagesDir's ancestors):zodforgen-zod,@angular/coreand@angular/formsforgen-angular, and so on. Without them the.d.tsbuild fails withTS2307(cannot find module) followed byTS7006(implicitany).
To create the two base files, run tako init --package-base (add --packages-dir <dir>
if it is not ./packages). It never overwrites an existing file. Install the
typescript and peer dependencies with your package manager.
Bootstrapping a workspace that already depends on the generated package
tako generate writes each package's package.json and then installs it into your
workspace for you — so on a project with no consumer yet, the very first generate just
works: nothing depends on @myapp/db until you add it.
A git-cloned monorepo is usually past that point: apps/*/package.json already declare
"@myapp/db": "workspace:*" so the app's own source can import it, and packagesDir
itself is gitignored (the package is fully regenerated, so committing its dist/src
would be redundant build output). That combination is a bootstrap deadlock for npm/Bun/pnpm
workspaces, independent of tako: bun install (or npm/pnpm install) resolves every
workspace:* reference in the graph up front, including @myapp/db, and refuses to
proceed while packages/db/ doesn't exist yet — but nothing (not even tako generate) can
run before that same install has succeeded once, since it's what links tako's own
binary into the project.
The fix is to commit a minimal stub package.json for the generated package — just enough
for the package manager's workspace resolution to find a name and version:
{
"name": "@myapp/db",
"version": "0.0.0",
"private": true,
"type": "module",
"//": "Generated by tako. Do not edit."
}
The "//" value matters: it is the same marker tako looks for before wiping and
rewriting an output-package directory (OutputNotGeneratedError otherwise) — without it,
tako generate refuses to touch a non-empty directory it doesn't recognize as its own. With the stub in place, bun install resolves cleanly on a fresh
clone, tako generate runs, and immediately overwrites the stub with the real generated
package.json — which itself carries the same marker, so it stays a valid seed for the
next clone too. Everything else under packages/db/ (dist/, src/, tsup.config.ts,
…) stays gitignored as before; only this one file needs the exception.
Several generators in one namespace
When several generators describe the same model (for example typescript, zod and
angular), many declarations share a name (UserDto, BoolFilter, ...). The synthesized
<namespace> entry point can expose only one declaration per name, so it keeps the one
from the lexically first generator (usually typescript). Nothing is lost, and this is
not an error: tako only logs it at debug level (--debug). To get a specific
generator's declaration, import it explicitly:
import { UserDto } from '@myapp/db'; // typescript's declarations for clashing names
import { UserSchema } from '@myapp/db/zod'; // zod
import { UserForm } from '@myapp/db/angular'; // angular (never in the root entry point)
Choosing
- Mode A is the default and the simplest: no build, no install, just files.
- Mode B suits a monorepo where the generated code is consumed like any other
workspace package, with a real
package.jsonand version.
The rationale is in
docs/architecture.md.