developing-lt-frontend
Installation
SKILL.md
lenne.tech Frontend Development
Gotchas
- The current
nuxt-base-starterruns vitest+oxlint+oxfmt, not eslint+prettier — Projects originally generated from older starters still shipeslint+prettier+ Playwright-only. When aligning a project with the current starter, expect to migrate the entire toolchain: installoxlint+oxfmt+vitest+@vitejs/plugin-vue+happy-dom, dropeslint+prettier+jsdom, addtests/unit/setup.ts+tests/unit/mocks/nuxt-imports.ts, and updatepackage.jsonscripts totest:unit,test:e2e,lint,format,format:check, plus thecheck/check:fixaggregate. Full recipe lives in themodernizing-toolchainskill (Phase 4). - Nuxt's PORT vs NITRO_PORT — Some Nitro versions read
process.env.PORTas a string and feed it directly intonet.Server#listen, which crashes withERR_SOCKET_BAD_PORT options.port should be >= 0 and < 65536. Received type string. Always preferNITRO_PORT=<num>for the production build (node .output/server/index.mjs) —NITRO_PORTis the documented Nitro-specific knob, goes through Nitro's own env loader, and is coerced to number reliably. The Nuxt dev server (nuxt dev) is unaffected —nuxt.config.tsdevServer.portworks as expected. - Aligning with the upstream starter is a wholesale dep sync, not a curated pick — When the project is being brought to the current
nuxt-base-starterbaseline, sync every dep version (bothdependenciesanddevDependencies) to what the starter ships and read the CHANGELOG of any package whose major moved. The recurring trap that a blanket version-sync does not fix is missing direct deps after a peer-restructure: whenRollup failed to resolve import "X"(or an equivalent module-not-found at install time) appears, the wrapper package no longer pulls "X" transitively — declare X as a direct dependency inpackage.json, even if no app code imports it directly. pnpm run generate-typesneeds a RUNNING API — and on the current starter it refuses to guess which one. The generator fetches the OpenAPI schema from the API. Since DEV-2802 the starter resolves that URL instead of defaulting:NUXT_API_URLfrom the shell (a.envfile is not read), else<repo-root>/.lt-dev/.env(written bylt dev up, which also carries theNODE_EXTRA_CA_CERTSthe Caddy HTTPS host needs), else a hard exit 1 with an actionable message. It additionally refuses a URL belonging to a differentlt devproject. So underlt dev upthe documented call needs no extra env, and a stale or foreign API can no longer be generated from silently. Do not "fix" that hard failure by re-adding a fallback — the oldhttp://localhost:3000default is exactly the bug: on a machine with parallel worktrees that port belongs to whichever project holds it, and the generator then wrotetypes.gen.ts/sdk.gen.tsfrom a foreign contract, reported success and exited 0. Older projects that have not adopted the guard still carry that silent fallback; there, verify the API is up before regenerating (curl -k https://api.<slug>.localhost/health, orcurl http://localhost:3000/healthin classic mode) and check that an expected endpoint really appears insdk.gen.ts.- UI language: detect it from the project instead of assuming German. UI text (labels, buttons, placeholders, toasts) must match the language the project already uses. Determine it in this order: (1) an explicit project rule wins — check the project's
CLAUDE.md, a conventions doc, or i18n config; (2) otherwise infer from existing UI files — match the language already used across*.vuepages/components; (3) only default to German for a true greenfield with no rule and no existing UI text. Never bulk-translate an existing app from one language to another — silently flipping an established English UI to German (or vice-versa) is a destructive, review-failing change that has broken a whole project before. The project — not this plugin — decides the language; once detected, stay consistent with it (incl.duvsSietone for German). - Use
useOverlay()for modals — NOT conditional rendering — The default instinct is<MyModal v-if="showModal" />. This bypasses Nuxt UI's modal stack, breaks focus trapping, and causes z-index issues with nested dialogs. The correct pattern isuseOverlay().create(ModalComponent)from composables. Seereference/modals.md. types.gen.tsandsdk.gen.tsare GENERATED — never hand-edit — Manual changes are overwritten on nextgenerate-typesrun. If a type is missing, the fix is on the API side (add@ApiProperty,@Field, etc.) not in the generated file..gitignoredoes NOT ignore these files — they ARE committed, but only via the regeneration command.- Better Auth derives its origins from BASE_URL/APP_URL — not from a hardcoded port number. When
lt dev upis used, these env vars are set automatically to the project's stable HTTPS URLs (https://api.<slug>.localhost,https://<slug>.localhost) and auth works regardless of internal port. The legacy "3000/3001 only" rule applies ONLY to non-migrated projects with hardcoded URLs. Runlt dev initonce to migrate. Seemanaging-dev-serversskill for the full URL rules. - Nothing is fetched from a foreign host at runtime — ever. No CDN scripts, no Google Fonts, no icon sets pulled over the wire, no web-worker files loaded from
unpkg/jsdelivr. Every asset ships in the build or is served from our own host. This is not a preference: a PDF viewer that loaded itspdfjs-distworker fromhttps://unpkg.com/...meant (1) no PDFs at all whenever that CDN was slow, blocked by a corporate firewall, or down — surfacing as the useless "Setting up fake worker failed", and turning a 5-second test into an 18-minute hang; (2) a request leaving for a foreign CDN on every single view, which breaks the DE/DSGVO hosting promise these projects are sold on; (3) a third party able to serve different code tomorrow — a supply-chain hole in the middle of the product. Wire assets through the bundler instead: with Vite/Nuxt that isimport workerUrl from "pkg/file.mjs?url". When adding any dependency that renders, decodes or workers in the browser, check whether it fetches something at runtime —grep -rnoE "https?://" app/over the source, and check the browser's network tab against the built app for third-party hosts. - Limit local Playwright runs to new + affected specs to keep TDD loops fast — The full Playwright suite is slow and runs in CI. During local development / TDD, default to running only the new + affected specs via
lt dev test -- <spec>(lt-projects) orpnpm dlx playwright test <spec>(non-lt). Backend Unit + API stay unrestricted — they're fast and catch cross-pillar regressions. Only run the full local Playwright suite when the user explicitly asks. Full recipe:reference/e2e-testing.md→ "Local TDD Loop — Affected Specs Only".
Ecosystem Context
Developers typically work in a Lerna fullstack monorepo created via lt fullstack init: