Buildless Development
.ts files directly in demos—no build step needed. The dev server transforms TypeScript on-demand with source maps. Use --target es2020 to control browser compatibility and --css-transform patterns to import CSS as JavaScript modules.
The cem dev server transforms TypeScript and CSS on-demand, letting you write modern code without running build tools.
Write TypeScript directly
in your demos with full source map support,
import CSS as modules
using constructable stylesheets, and configure
automatic path rewrites
for projects with separate source and output directories. The server respects your tsconfig.json settings and provides
browser compatibility control
through esbuild targets.
This buildless approach eliminates tsc --watch, CSS preprocessors, and bundlers from your development workflow. You write TypeScript and CSS, import them directly in demos, and the dev server handles transformation transparently with full debugging support.
The dev server handles TypeScript files (.ts), CSS files as modules (with opt-in patterns), ES modules from npm via import maps, and modern JavaScript syntax. JavaScript files (.js, .mjs), HTML files, and JSON files work as-is without building them ahead-of-time.
Write TypeScript Directly
Import TypeScript files directly in your demos without compilation:
<!-- In your demo -->
<script type="module" src="../src/my-element.ts"></script>The dev server handles TypeScript transformation on-demand with full source map support for debugging. You don’t need to run tsc --watch, set up build pipelines, wait for compilation before testing, or manage separate source and output directories.
Browser Compatibility
Control which browsers your code supports using the --target flag:
cem serve --target es2020The default target is es2022. See
esbuild’s target documentation
for all available targets.
Your tsconfig.json Works
The dev server respects your existing TypeScript configuration including compilerOptions.target, compilerOptions.module, compilerOptions.rootDir and compilerOptions.outDir for src/dist separation, and include and exclude patterns. The command-line --target flag overrides tsconfig.json settings if provided.
Separate dist Directory
If your project uses separate source and output directories (e.g., src/ and dist/), the dev server automatically handles path resolution so you can reference compiled output paths while serving source files.
How it works:
- The dev server reads your
tsconfig.jsonto detectrootDirandoutDir - It creates URL rewrites automatically (e.g.,
"/dist/:path*" → "/src/{{.path}}") - Requests to
/dist/components/button.jsresolve to/src/components/button.ts - Your demos can reference the output path, but the server serves the source
Example tsconfig.json:
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"target": "ES2022"
}
}With this configuration, a request to /dist/components/button.js resolves to /src/components/button.ts, which the server transforms and serves as TypeScript source.
This lets you write demos using the same paths your production build uses without changing import paths between development and production. It works seamlessly with TypeScript’s project references and supports monorepo/workspace setups with per-package tsconfig.json files.
Fallback behavior:
If the dev server can’t find a source file via URL rewrites, it tries co-located files (in-place compilation). This ensures backward compatibility with projects that compile TypeScript in the same directory as source files.
Manual configuration:
If you need custom URL rewrites beyond what tsconfig.json provides, configure them using URLPattern syntax and Go templates:
serve:
urlRewrites:
# Simple prefix mapping
- urlPattern: "/dist/:path*"
urlTemplate: "/src/{{.path}}"
# Custom library paths
- urlPattern: "/lib/:path*"
urlTemplate: "/sources/{{.path}}"
# Pattern with template function
- urlPattern: "/api/:version/:endpoint*"
urlTemplate: "/{{.version | lower}}/api/{{.endpoint}}"Pattern syntax:
URL patterns use URLPattern syntax:
:param- Matches a single path segment and captures it as a named parameter**- Matches any number of path segments (wildcard)*- Matches characters within a single segment
URL templates use Go template syntax:
{{.param}}- Inserts a captured parameter{{.param | lower}}- Applies thelowerfunction (converts to lowercase){{.param | upper}}- Applies theupperfunction (converts to uppercase){{.param | slug}}- Applies theslugfunction (URL-safe slug)
Example: Element name transformation for demos
If your project serves demos at URLs that don’t match the on-disk structure:
- On disk:
elements/my-card/demo/card.html - Served URL:
/elements/card/demo/card/ - CSS reference in demo:
<link rel="stylesheet" href="../my-card-lightdom.css"> - Browser resolves to:
/elements/card/demo/my-card-lightdom.css❌
This can happen when your demo files load files by relative path from, since the
dev server prettifies their URLs (card.html => card/index.html).
Configure URL rewrites to fix the mismatch:
serve:
urlRewrites:
- urlPattern: "/elements/:slug/demo/:rest*.css"
urlTemplate: "/elements/my-{{.slug}}/{{.rest}}.css"This resolves /elements/card/demo/my-card-lightdom.css → elements/my-card/my-card-lightdom.css ✓
See Configuration for details.
Import CSS as Modules
The dev server supports two ways to import CSS files as constructable stylesheets. Import attributes with with { type: 'css' } work automatically without configuration, while plain imports require glob pattern configuration.
Import Attributes
Use the standard import attributes syntax for automatic CSS transformation:
import styles from './my-element.css' with { type: 'css' };
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [styles];
}
}This modern, standards-based syntax always transforms CSS to modules—no configuration needed. The dev server rewrites with { type: 'css' } to query parameters during TypeScript transformation, then transforms the CSS file to a JavaScript module that exports a CSSStyleSheet object.
Plain Imports (Requires Configuration)
Alternatively, use plain imports without the with keyword:
import styles from './my-element.css';
class MyElement extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [styles];
}
}Plain imports only transform CSS files matching glob patterns that you configure via command line or config file:
Via command line:
cem serve --css-transform 'src/**/*.css' --css-transform 'elements/**/*.css'Via config file:
serve:
transforms:
css:
include:
- src/**/*.css
- elements/**/*.css
exclude:
- demo/**/*.csswith { type: 'css' } work. This prevents <link rel="stylesheet"> tags from breaking unexpectedly.
Debugging
Source maps work automatically, so stack traces point to your original TypeScript, browser DevTools show your source files, and breakpoints work in TypeScript rather than generated JavaScript.
Configuration
Via Command Line
cem serve --target es2020 --css-transform 'src/**/*.css'Via Config File
serve:
target: es2020
transforms:
css:
include:
- 'src/**/*.css'See Configuration for all options.
What’s Next?
- Import Maps - Use npm packages without bundling
- Configuration - Configuration reference
- Getting Started - Set up your first demo