{ } Lips v0.2.0

Precompile & CSP

Lips compiles templates in the browser by default. You can also compile them at build time. Two things follow:

The IR is plain JSON. It can be embedded in a bundle, fetched from a server, or stored.

Entry points

Import Contents gzip
@lipsjs/lips full — runtime, parser/compiler, styles, router ~21 KB
@lipsjs/lips/runtime precompiled-only — no parser, compiler, Stylis or router ~13 KB
@lipsjs/lips/precompile build-time helpers and the bundler plugin
@lipsjs/lips/dev unminified full build

The compiler, the CSS preprocessor and <router> are injected by the full entry rather than imported by the core, which is what lets the runtime entry drop them completely.

Precompiling a template

import { precompile } from '@lipsjs/lips/precompile'

const { template, diagnostics } = precompile({
  state: { count: 0 },
  handler: { inc(){ this.state.count++ } },
  default: `<button on-click(inc)>{state.count}</button>`
})

diagnostics.length && console.error( diagnostics )

// template → { ir, state, handler }  — `default` is gone

Macros are inlined during precompilation, so the macros source never reaches the client.

At runtime, register the result as usual:

import Lips from '@lipsjs/lips/runtime'

const lips = new Lips({ mode: 'interpreted' })
lips.register('counter', template)

A template carrying ir skips parsing entirely — even on the full build.

Single-file components

A .lips file is a script followed by a template. Anything before the first top-level tag is the script; the rest is the template.

const state = {
  count: 0,
  step: 1
}

const handler = {
  increment(){ this.state.count += this.state.step }
}

const stylesheet = `
  .counter { display: flex; gap: .5rem; align-items: center }`

<div class="counter">
  <span>{state.count}</span>
  <button on-click(increment)>+{state.step}</button>
</div>

The plugin picks up state, handler, _static, context and stylesheet when they are declared with const, and merges them with the compiled IR into the default export.

Bundler setup

// vite.config.js
import { lipsPlugin } from '@lipsjs/lips/precompile'

export default {
  plugins: [ lipsPlugin() ]
}
import Counter from './counter.lips'   // already-compiled IR

lips.register('counter', Counter)

Options:

Option Default Meaning
include /\.lips$/ which files to transform
reportDiagnostics true emit parser/compiler warnings as build warnings

Template errors fail the build, reported with file:line:col — a broken template is caught before it ships, not at runtime.

Compiling by hand

For full control — a build script, a server, or a generated component:

import { compileTemplate, parseTemplate, parseSFC } from '@lipsjs/lips'

const { ir, diagnostics } = compileTemplate(`<p>{state.msg}</p>`, {
  macros: `<macro [x] name="chip"><b>{x}</b></macro>`
})

if( diagnostics.some( d => d.severity === 'error' ) )
  throw new Error('template failed to compile')

JSON.stringify( ir )   // ship it anywhere

Diagnostics never throw — you always get an IR plus a list of what went wrong:

{
  code: 'LIPS-C013',
  severity: 'error',
  message: '',
  hint: '',
  loc: { line: 4, col: 12, offset: 88, length: 17 }
}

Running under a strict CSP

const lips = new Lips({ mode: 'interpreted' })

In interpreted mode expressions run through a sandboxed AST walker instead of a compiled Function. Both modes are proven to render identical DOM — the difference is only how an expression is evaluated.

Content-Security-Policy: script-src 'self'
  compiled (default) interpreted
Expression evaluation one cached Function per source AST walker
Needs unsafe-eval yes no
Speed faster slower per expression

interpreted mode removes the need for unsafe-eval on its own. Precompiling additionally removes the parser — pair them for the smallest, strictest deployment.

Putting it together

// vite.config.js
import { lipsPlugin } from '@lipsjs/lips/precompile'
export default { plugins: [ lipsPlugin() ] }
// app.js — the 13 KB runtime, no eval, no parser
import Lips from '@lipsjs/lips/runtime'
import Counter from './counter.lips'
import App from './app.lips'

const lips = new Lips({ mode: 'interpreted' })

lips.register('counter', Counter)
lips.root( App, '#app')

The runtime build has no CSS preprocessor, so component stylesheet sources are skipped there with a warning. Ship those styles as a static CSS file, or use the full entry.