# create-lit-app

[![CI/CD Pipeline](https://github.com/chris-briddock/create-lit-app/actions/workflows/ci.yml/badge.svg)](https://github.com/chris-briddock/create-lit-app/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@chris-briddock/create-lit-app)](https://www.npmjs.com/package/@chris-briddock/create-lit-app)

Scaffold a production-ready [Lit](https://lit.dev) application with best
practices baked in and optional server-side rendering — what `create-next-app`
is for React, for web components.

```bash
npm create @chris-briddock/lit-app my-app
```

No arguments are required; the CLI asks for what it needs. It has **zero
runtime dependencies**, so there is nothing to download before the prompts
appear.

## What you get

- **[Lit 3](https://lit.dev) + [Vite 8](https://vite.dev)** — Vite 8 bundles
  with [Rolldown](https://rolldown.rs) and transforms with Oxc, so builds are
  fast without extra configuration.
- **TypeScript 6**, configured for the decorator flavour Lit and the bundler
  actually agree on (see [Decorators](#a-note-on-decorators)).
- **Optional SSR** with [`@lit-labs/ssr`](https://lit.dev/docs/ssr/overview/):
  streaming server rendering, declarative shadow DOM, and client hydration.
- **Optional routing**, with an example page tree and a 404 route.
- **Optional [Tailwind CSS 4](https://tailwindcss.com)**, wired through the
  `@tailwindcss/vite` plugin and made to work inside shadow roots (see
  [Tailwind and shadow DOM](#tailwind-and-shadow-dom)).
- **Vitest 4 in browser mode** — component tests run in real Chromium, because
  shadow DOM and custom element upgrade timing are exactly what a DOM emulator
  approximates worst.
- **ESLint 10 + Prettier**, including `eslint-plugin-lit`, which understands
  `html` tagged templates and catches binding mistakes a general-purpose
  linter cannot see.
- **A custom elements manifest** for editor autocomplete and hover docs.

Every generated project passes its own `lint`, `format:check`, `typecheck` and
`build` on the first run.

## Usage

```bash
npm  create @chris-briddock/lit-app my-app
pnpm create @chris-briddock/lit-app my-app
yarn create @chris-briddock/lit-app my-app
bun  create @chris-briddock/lit-app my-app
```

The package manager you invoke is detected and used for the install and for
the commands printed at the end.

### Options

Every prompt has a flag, so the CLI is fully scriptable:

| Flag | Description |
| --- | --- |
| `--typescript` / `--javascript` | Language (default: TypeScript) |
| `--ssr` / `--spa` | Rendering strategy (default: SPA) |
| `--router` / `--no-router` | Routing and example pages |
| `--tailwind` / `--no-tailwind` | Tailwind CSS (default: no) |
| `--eslint` / `--no-eslint` | ESLint + Prettier |
| `--tests` / `--no-tests` | Vitest browser tests |
| `--git` / `--no-git` | Initialise a git repository |
| `--install` / `--no-install` | Install dependencies |
| `--use-npm`, `--use-pnpm`, `--use-yarn`, `--use-bun` | Force a package manager |
| `-y`, `--yes` | Accept defaults, skip prompts |
| `--force` | Scaffold into a non-empty directory |

```bash
# Fully non-interactive
npm create @chris-briddock/lit-app my-app -- --ssr --router --yes

# Scaffold into the current directory
npm create @chris-briddock/lit-app . --javascript --no-tests
```

Prompts are skipped automatically when stdin is not a TTY, so CI needs no
special handling.

## SSR

Choosing `--ssr` produces an Express server that runs Vite in middleware mode
during development — so you get server rendering *and* hot module replacement
— and serves pre-built assets in production.

```bash
npm run dev              # server-rendered, with HMR
npm run build            # type-check, then build client + server bundles
npm run start            # production server
```

The response is streamed: the document head is flushed before rendering
begins, so the browser starts fetching CSS and JS while the server is still
producing markup.

Two details the template gets right, and which are easy to get wrong by hand:

- `@lit-labs/ssr-client/lit-element-hydrate-support.js` is imported **before**
  anything that imports `lit`. Otherwise elements discard the server-rendered
  shadow DOM and re-render from scratch.
- With `--ssr --router`, the route table is plain data shared by the server and
  the browser, and the active path is passed as an **attribute**. Attributes
  survive into the HTML, so the client hydrates with the same value the server
  rendered. A property binding would exist only on the server.

`@lit-labs/router` is used for SPA routing only — it reads `window` on connect,
so it cannot run during server rendering.

## Tailwind and shadow DOM

`--tailwind` installs Tailwind 4 the documented way: the `@tailwindcss/vite`
plugin, and `@import 'tailwindcss'` in `src/styles/global.css`. No config file,
no PostCSS step.

That alone is not enough for Lit. Components render into a shadow root, and
document stylesheets do not cross that boundary, so utility classes in a
component template would silently do nothing. Generated projects bridge it with
two small files:

```css
/* src/styles/shadow.css — utilities, without the document-level reset */
@reference './global.css';
@import 'tailwindcss/utilities.css';
```

```ts
// src/styles/tailwind.ts
import { unsafeCSS } from 'lit';
import sheet from './shadow.css?inline';

export const tailwind = unsafeCSS(sheet);
```

Components then adopt it with ``static styles = [tailwind, css`...`]``, keeping
the handwritten block last for what utilities cannot express — `:host` and
`::slotted()`.

Three decisions behind that, each verified against a real build:

- **Preflight is excluded from the shadow sheet.** It is a document-level
  reset; re-applying it inside every shadow root is wasteful. The cost is that
  components needing `font: inherit` on form controls, or no underline on
  links, restate those rules themselves — and the templates do.
- **The theme comes from `@reference './global.css'`.** That loads the project
  stylesheet for its theme while emitting none of its CSS, so a token declared
  in its `@theme` block generates utilities inside components as well as in the
  document. Compiling the shadow sheet against Tailwind's stock theme instead
  would leave custom tokens working in `index.html` and silently doing nothing
  in every component. Referenced values are inlined as fallbacks, so the sheet
  stays self-contained rather than depending on variables reaching it from
  `:root`.
- **Light DOM is not used instead.** Overriding `createRenderRoot()` would let
  `global.css` reach components and remove the need for both files, but
  `@lit-labs/ssr` still serialises output into a declarative shadow root. Under
  `--ssr` that yields the server's copy sealed in a shadow root plus a second
  copy rendered on hydration, so it only works for `--spa`.

## A note on decorators

Generated TypeScript projects use `experimentalDecorators: true` with
`useDefineForClassFields: false`.

This is deliberate and verified rather than inherited. Standard ES decorators
with the `accessor` keyword type-check under TypeScript 6, but Vite 8 emits
them **untransformed** — the resulting bundle contains `@dec accessor x`, which
is a syntax error in every current JavaScript engine. Experimental decorators
lower correctly to `__decorate` calls and honour `useDefineForClassFields`,
which Lit's reactive properties depend on.

The generated `tsconfig.json` says as much, so nobody "modernises" it and
ships a broken bundle.

## Development

```bash
npm install
npm test                    # unit + filesystem integration tests
npm run smoke               # scaffold, install and build every combination
npm run smoke -- --quick    # TypeScript only, faster
npm run check-versions      # verify every pinned dependency resolves
npm run format:templates    # keep templates format-clean
```

`npm test` is offline and fast. `npm run smoke` hits the network and is the
check that matters before publishing.

### How templates work

Templates are composed in ordered layers, each copied over the last:

```
base → lang/<ts|js> → app/<spa|ssr> → app/<spa|ssr>-<lang>
     → router/<rendering>-<lang>
     → tailwind/base → tailwind/lang/<lang> → tailwind/app/<rendering>-<lang>
     → tailwind/router/<rendering>-<lang>
     → tooling/eslint-<lang> → tooling/test-<lang>
```

A later layer overrides a single file from an earlier one — routing replaces
`app-root`, for instance — which avoids needing a template per combination of
answers.

Within a layer:

- `*.tpl` files are rendered with `{{variable}}` interpolation and
  `{{#if flag}}` / `{{#unless flag}}` blocks, and lose the suffix.
- `_gitignore`, `_prettierrc` and friends are renamed to their dotfile form,
  because npm refuses to publish files named `.gitignore`.

`package.json` is built in code (`src/manifest.ts`) rather than templated,
since its dependencies and scripts vary along all four axes at once. All
versions are pinned in one place, `src/versions.ts`.

## Requirements

Node.js 20.19+ or 22.12+, matching Vite 8's supported range.

## License

MIT
