Skip to content

New packages in a monorepo

You’re adding the Nth package to a TypeScript monorepo. You copy packages/auth/ to packages/analytics/, rename the directory, update package.json, update tsconfig.json. You open index.ts and the JSDoc comment still says @acme/auth. You grep for auth and find it in the describe() block in index.test.ts too. Then a week later CI fails because someone missed auth in a file that wasn’t open at the time.

With a template in the repo, you type the package name once.

acme/
_templates/
package/ ← template lives here
packages/
auth/
package.json
tsconfig.json
src/
index.ts
auth.test.ts
payments/
package.json
tsconfig.json
src/
index.ts
payments.test.ts
turbo.json
package.json

auth and payments already exist. You’re adding analytics.

_templates/package/
diecut.toml
template/
package.json.die
tsconfig.json
src/
index.ts.die
index.test.ts.die
[template]
name = "package"
[variables.package_name]
type = "string"
prompt = "Package name"
validation = '^[a-z][a-z0-9-]*$'
validation_message = "Lowercase letters, numbers, and hyphens only."
[variables.package_scope]
type = "string"
computed = "@acme/{{ package_name }}"

Two variables. package_name is prompted once. package_scope is derived from it automatically.

In the copy-paste workflow, this is where mistakes happen: package.json gets updated to @acme/analytics but the JSDoc comment in index.ts still says @acme/auth because it was easy to miss. Here, both come from the same computed value — if one is right, they’re all right.

{
"name": "{{ package_scope }}",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^1.0.0"
}
}
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}

No variables here — this file is identical for every package. diecut copies it as-is.

/**
* {{ package_scope }}
*/
export {};
import { describe, it } from "vitest";
describe("{{ package_name }}", () => {
it("has tests", () => {
// add your tests here
});
});

package_name appears in the describe block. package_scope appears in package.json and the index comment. Both come from the same single answer.

From the monorepo root:

Terminal window
diecut new ./_templates/package -o packages/analytics

diecut prompts for one variable:

Package name: analytics

That’s it.

Not sure what you’ll get? Add --dry-run --verbose to see the rendered output without writing any files:

Terminal window
diecut new ./_templates/package -o packages/analytics --dry-run --verbose
packages/analytics/
package.json
tsconfig.json
src/
index.ts
index.test.ts
.diecut-answers.toml

The generated package.json:

{
"name": "@acme/analytics",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^1.0.0"
}
}

The generated src/index.ts:

/**
* @acme/analytics
*/
export {};

The generated src/index.test.ts:

import { describe, it } from "vitest";
describe("analytics", () => {
it("has tests", () => {
// add your tests here
});
});

analytics was typed once. Notice that package.json and index.ts use @acme/analytics (the scoped name) while index.test.ts uses analytics (the bare name). In a copy-paste workflow, getting these two forms right across all files is exactly the step that gets missed under time pressure. Here, both are computed from the same package_name.

_templates/ is committed to the repo alongside the source code. There’s nothing to share or document separately — a new teammate clones the repo and the template is already there.

Without a template, package 3 gets created by copying package 1, and package 5 gets created by copying package 3. Each copy carries forward whatever adjustments the author made at the time. After a few rounds, the packages have quietly diverged. With a template, every new package starts from the same place.

When you decide the standard setup needs a vitest.config.ts, you add it once to _templates/package/template/. The next diecut new picks it up. Packages that already exist keep their own files — you update them separately, on your own schedule, if at all.


To learn more about computed variables and template features, see Creating Templates. For all CLI options, see the Commands reference.