Setting Up Lit
🔄 Updated on Thu Sep 12 2024
📅 Published on Sat Aug 28 2021
Lit is a library for building Web Components.
Since Lit is distributed as npm packages, before shipping to browser, your code must be bundled; and if you use TypeScript for developing, the code must also be transpiled. Something must be set up before starting developing with Lit.
However, the initial setup process is not very clearly described in the official documentation. The Starter Kit project is also kind of bloated: You have to install a bundle tool with several plugins, a local web server, some polyfills and even a static site generator, and all of them need to be configured separately.
Gone are the days when Notepad program alone is enough for writing websites.
In this article I'd like to share an alternative, simpler setup to start using Lit. It would serve as a boilerplate for building Lit-powered web components / apps. Some goals are:
- TypeScript-based
- For modern browsers only
- As few dependencies as possible, non-bloated
- As few configurations as possible
- Code-splitting: multiple inputs multiple outputs
- Code linting
Add Lit
Make a directory e.g. "lit-setup", start an npm project:
mkdir lit-setup
cd lit-setup
npm init -y
Then install Lit as a dependency:
npm install lit
Directory Structure
We put source files under a src
directory, target/built files under dest
, and an additional assets
for possible static resources such as favicons. Something like this:
.
├── assets <- favicons, images, manifests...
├── dest <- target directory to deploy
└── src <- source files
The source code will be processed and put into dest
, The assets
would also be copied into dest
. This dest
directory can then be deployed.
mkdir src
mkdir dest
mkdir assets
Bundle Tool
Lit source files must be transpiled and bundled together with Lit library.
When it comes to build/bundle tool there are many choices. Lit officially uses Rollup. I'll be using esbuild here, because it:
- Is much, much faster
- Comes with built-in support for
- Transpiling TypeScript
- Node module resolution
- ES code minification
Rollup needs at least three additional plugins for those. Why more dependencies if it can be less?
npm install --save-dev esbuild
When specifying input files, esbuild doesn't support glob pattern yet esbuild supports glob style since v0.19, but it's not even mentioned in the documentation, I'll still prefer to use globby:
npm i -D globby
Then create a function to invoke esbuid:
import {globby} from 'globby';
import {build} from 'esbuild';
const bundle = async () => {
// get files for bundling
const entries = await globby('src/**/*.ts');
// bundle
return build({
entryPoints: entries,
bundle: true,
format: 'esm',
splitting: true,
outdir: 'dest/wc'
});
};
// call bundle() to trigger the build process
Now each time you run node build.mjs
, esbuild will read every .ts file from src
, transpile and bundle it, then create the corresponded .js file in dest/wc
directory. E.g. from src/foo/foo.ts
to dest/wc/foo/foo.js
.
Here we added a wc
(stands for "web component") directory for better organization of target files.
Add TypeScript
When using esbuild as bundle tool, you don't have to install TypeScript (esbuild implemented its own TypeScript parser). But for linting and/or publishing code, TypeScript is still needed.
npm i -D typescript
TS Config
Some configs cannot be avoided. Let's start by installing the recommended tsconfig file:
npm i -D @tsconfig/recommended
Then create our tsconfig.json
(with minimal overwrites):
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"declaration": true,
"experimentalDecorators": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ES2022",
"moduleResolution": "Bundler",
"target": "ES2023"
},
"include": ["src/**/*.ts"]
}
Add Lint Tool
ESLint, what else? Although it does introduce tons of dependencies, static linting for coding is like spell checking for writing: essential.
npm i -D eslint @eslint/js
Configuring ESLint is very easy thanks to its interactive guide:
npm init @eslint/config
Answer the questions, notably choose JavaScript modules (import/export)
for module, None of these
for frameworks, Yes
for TypeScript, Browser
for environment and let eslint install dependencies afterwards.
The auto-generated eslint.config.mjs
is already usable, we'd ignore the bundled files, since they don't need to be linted.
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{files: ['**/*.{js,mjs,cjs,ts}']},
{languageOptions: {globals: globals.browser}},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{ignores: ['dest/*']}
];
Local Web Server
Lit's documentation recommends using Web Dev Server, which means additional installations and configurations.
Alternative: Ubuntu 22.04 has Python installed by default, a (static) web server can be started quickly:
python -m http.server
Copy Static Assets
We write a function to copy the assets
directory to dest
, using the cpy
package:
npm i -D cpy
import cpy from 'cpy';
// copy assets directory and more
const copy = () => cpy(['assets', 'index.html'], 'dest');
Clean Up
Each build should have a clean start. Again, write a function that utilizes the del
package:
npm i -D del
import {deleteAsync} from 'del';
// empty the output directory
const clean = () => deleteAsync('dest/*');
Build Script
The final build process should be:
- Clean up output directory
- Copy assets
- Build and bundle source files
Step 1 must be run first, while step 2 and 3 can be run parallel after that. We could utilize Promise
here for simple "series" and "parallel":
// light parallel
const copyAndBundle = () => Promise.all([copy(), bundle()]);
// series...
clean().then(copyAndBundle);
Put together, the complete build.mjs
:
import {deleteAsync} from 'del';
import cpy from 'cpy';
import {globby} from 'globby';
import {build} from 'esbuild';
// delete dest
const clean = () => deleteAsync('dest/*');
// copy assets
const copy = () => cpy(['assets', 'index.html'], 'dest');
const bundle = async () => {
// get files for bundling
const entries = await globby('src/**/*.ts');
// bundle
return build({
entryPoints: entries,
bundle: true,
format: 'esm',
splitting: true,
outdir: 'dest/wc'
});
};
// some lite parallel
const copyAndBundle = () => Promise.all([copy(), bundle()]);
// go
clean().then(copyAndBundle);
Example Code
Web Components
Let's write two Lit-powered web components that do nothing:
Their markups would be:
<ui-foo title="Lit Setup">
<ui-bar items="Lit,esbuild,TypeScript,ESLint"></ui-bar>
</ui-foo>
So, create a foo.ts
under src/foo
directory:
import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('ui-foo')
export class Foo extends LitElement {
static styles = css`h3 {color: darkcyan;}`;
@property()
title = 'Untitled';
render() {
return html`
<div class="foo">
<h3>${this.title}</h3>
<slot></slot>
</div>
`;
}
}
Then a bar.ts
under src/bar
:
import {html, css, LitElement} from 'lit';
import {customElement, property} from 'lit/decorators.js';
@customElement('ui-bar')
export class Bar extends LitElement {
@property()
items = '';
render() {
return html`
<ul>
${this.items.split(',').map((i, idx) => html`<li class="top${(idx + 1).toString()}">${i}</li>`)}
</ul>
`;
}
}
Now run our "build.mjs":
node build.mjs
Under the dest
directory you'll get the foo/foo.js
and bar/bar.js
, as well as a chunk-XXX.js
which is automatically created by esbuild with the splitting: true
option, this is basically the Lit library shared by both web components.
Finally we can use the components on our page index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Setup</title>
<script type="module" src="wc/foo/foo.js"></script>
<script type="module" src="wc/bar/bar.js"></script>
</head>
<body>
<ui-foo title="Lit Setup">
<ui-bar items="Lit,esbuild,TypeScript,ESLint"></ui-bar>
</ui-foo>
</body>
</html>
This index.html
will be copied to dest
directory after executing build.mjs
. Now switch to dest
and start the Python static web server:
cd dest
python -m http.server
Start a browser and visit http://0.0.0.0:8000/
, you should see the result.
And that's it. The complete source code can be found here.
🔚