docs/solutions/developer-experience/2026-03-25-templates-update-check-need-arg-safe-wrapper-template-scoped-lint-and-ts6-baseurl-opt-out.md
baseUrl opt-outpnpm templates:update and pnpm templates:check were failing for several different reasons, and the failures stacked on top of each other in a way that made the whole path look more mysterious than it was.
The first break was a shell bug in the wrapper script. After that was fixed, the next breaks were template-local lint and typecheck issues exposed by TypeScript 6 and by generated code that needed stronger normalization than the current lint:fix path was doing.
templates:update failure as a template problemThe command was dying before template generation even started:
./tooling/scripts/update-templates.sh: line 7: EXTRA_ARGS[@]: unbound variable
That had nothing to do with shadcn, Biome, or TypeScript. The wrapper was just expanding an empty array under set -euo pipefail.
biome check . inside template packagesFrom inside templates/plate-template, both of these still walked the repo root:
bun run lint:fix
pnpm lint
That meant template lint was reporting errors from files like .codex/skills/... instead of only checking the template.
baseUrlOnce template typecheck actually ran, TS6 stopped on:
Option 'baseUrl' is deprecated and will stop functioning in TypeScript 7.0.
The templates still need @/*, so simply deleting baseUrl was the wrong move for this repo shape.
The generated playground template kept reintroducing this Biome warning:
if (!code || !code.trim() || !drawingType)
Even after patching the local source and rebuilding apps/www/public/r, default pnpm templates:update still pulled the stale version. That was the tell: the default updater path was not consuming the local rebuilt registry payload.
In tooling/scripts/update-templates.sh, do not splat an empty array under set -u.
Use a simple branch instead:
if (($# > 0)); then
"$SCRIPT_DIR/update-template.sh" "$@" basic
"$SCRIPT_DIR/update-template.sh" "$@" ai
else
"$SCRIPT_DIR/update-template.sh" basic
"$SCRIPT_DIR/update-template.sh" ai
fi
That gets templates:update into the real work instead of dying in shell plumbing.
In both template package manifests:
replace biome check . with explicit paths:
"lint": "biome check src biome.jsonc components.json tsconfig.json eslint.config.mjs next.config.ts postcss.config.mjs && eslint"
and make lint:fix use the same explicit scope.
That keeps Biome inside the template instead of letting it drift up to the git root.
--unsafeFor generated template files, safe fixes were not enough. The updater was still failing on rules like useOptionalChain that Biome only rewrites in unsafe mode.
So the template lint:fix scripts should be:
"lint:fix": "biome check src biome.jsonc components.json tsconfig.json eslint.config.mjs next.config.ts postcss.config.mjs --fix --unsafe"
This is the right place for the stronger rewrite. These files are generated and normalized output, not hand-written library code.
baseUrl deprecation where the repo intentionally keeps aliasesIn both template tsconfigs:
add:
"ignoreDeprecations": "6.0"
while keeping:
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
That matches the decision to keep alias-based imports instead of rewriting the templates to relative paths.
In templates/plate-playground-template/biome.jsonc, use the same preset path shape as the other template:
"extends": [
"ultracite/biome/core",
"ultracite/biome/react",
"ultracite/biome/next"
]
Without that, template-local Biome resolution was broken before lint even had a chance to check code.
The upstream source still had the lint-triggering condition:
Change:
if (!code || !code.trim() || !drawingType) {
to:
if (!code?.trim() || !drawingType) {
This did not fix default templates:update by itself, but it still keeps the source registry component honest and prevents the same drift in local registry builds.
The failures were coming from four separate layers:
Each layer needed its own fix.
Once the wrapper stopped exploding, Biome was constrained to template files, TS6 deprecation noise was explicitly silenced where aliases are still intentional, and generated files were allowed to take unsafe normalization rewrites, both commands became boring again:
pnpm templates:updatepnpm templates:checkThat is exactly what you want here.
templates:update is not the same as local registry reproductionRebuilding apps/www/public/r did not change the generated playground template during the default updater flow.
So if you need to prove a local registry fix end-to-end, use the local path explicitly rather than assuming the default updater is reading your rebuilt local payload.
lint:fix is not regular hand-written-code lintFor templates, lint:fix is part of generation cleanup. Using --unsafe here is justified because the command is normalizing generated output, not rewriting product code behind a developer's back.
baseUrl means opting out explicitlyIf the repo chooses to keep @/* in template tsconfigs, TypeScript 6 needs the opt-out spelled out. Otherwise tsc --noEmit will keep failing before it checks any real types.
These commands passed after the fix:
pnpm templates:update
pnpm templates:check
pnpm --filter www build:registry
pnpm --filter www typecheck
pnpm lint:fix