@jacare/compiler
v0.1.17
Published
Jacaré compiler — .jcr templates to optimized DOM code
Readme
@jacare/compiler
The Jacaré compiler transforms .jcr modules into optimized JavaScript for the client, server, or both.
A .jcr file is plain JavaScript by default, with export <view> / export <style> / export <contract> blocks (recommended), or view\...`/style`...`tagged templates. Optional TypeScript: sibling*.jcr.tslogic files and/or a// @jacare-tspragma (types stripped with esbuild). The compiler parses the template, detects reactive bindings, and emits fine-grained DOM code that uses@jacare/core`.
Table of contents
- Install
- What it compiles
- Basic usage
- Compile modes
- Template syntax
- Component props
- CLI
- Error reporting
- Source maps
- Integration
- Links
Install
npm install @jacare/compilerUsually installed as a dependency of @jacare/vite-plugin or @jacare/cli. Use directly when building custom tooling.
npm: https://www.npmjs.com/package/@jacare/compiler
What it compiles
Input — a .jcr module:
import { signal } from '@jacare/core'
const count = signal(0)
export <view>
<button on-click=${() => count.update((n) => n + 1)}>
Count: ${count}
</button>
</view>Output — optimized JavaScript:
export function mount(target) { /* direct DOM operations */ }
export function render() { /* SSR HTML */ }
export function resume(target, state) { /* hydration */ }
export default mountThe compiler imports only the runtime helpers each file actually uses (bindText, branch, bindModel, etc.).
Basic usage
import { compile } from '@jacare/compiler'
import { readSiblingJcrTs } from '@jacare/compiler/fs'
import { readFileSync, writeFileSync } from 'node:fs'
const filename = 'src/app.jcr'
const source = readFileSync(filename, 'utf-8')
const result = compile(source, {
filename,
mode: 'client',
siblingScript: readSiblingJcrTs(filename) ?? false,
})
writeFileSync('dist/app.js', result.code)Node tooling that needs sibling .jcr.ts files should import @jacare/compiler/fs (keeps node:fs out of browser bundles such as Lab / Studio).
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| filename | string | — | Source path for error messages and source maps |
| mode | 'client' \| 'server' \| 'full' | 'full' | Output mode |
| runtimeImport | string | '@jacare/core' | Import path for runtime helpers |
| scriptLang | 'js' \| 'ts' | — | Force TypeScript strip (also enabled by // @jacare-ts) |
| siblingScript | string \| false | — | Sibling .jcr.ts contents, or false to skip |
Return value
{
code: string
map?: RawSourceMap
}Compile modes
| Mode | Emits | Use case |
|------|-------|----------|
| client | mount() + resume() | Browser bundles |
| server | render() | SSR |
| full | All three | Tooling, inspection |
compile(source, { mode: 'client' }) // dev / client-only
compile(source, { mode: 'server' }) // SSR-only
compile(source, { mode: 'full' }) // defaultTemplate syntax
Text interpolation
view`<p>${message}</p>`
view`<p>Hello, ${name()}!</p>`Bare pulse references compile to bindText. Mixed text uses reactive effects.
Events
view`<button on-click=${save}>Save</button>`
view`<button on-click=${() => remove(id)}>Delete</button>`Attribute bindings
view`<a bind-href=${url}>Link</a>`
view`<img bind-src=${avatar} bind-alt=${name} />`Arrow functions in attributes are invoked reactively:
view`<a jacare-go=${() => href(id)} href=${() => href(id)}>Go</a>`Two-way bindings
view`
<input bind-value=${text} />
<input type="checkbox" bind-checked=${done} />
`Conditionals
view`
#if items().length === 0
<p class="empty">No items</p>
#else
<ul>...</ul>
#end
`Contract pulses unwrap bare names (#if open → open()). Attribute form (same IR as a single #if arm):
<p jacare-when={error()} role="alert">${error}</p>Route / search address sugar
${@route/id} compiles to getRouteParam('id') (active nav params).${@search/q} compiles to getRouteSearch('q') (active ?q=).
Keep createRoute(nav.where) for JS. Reserved bag ids route and search are not collected as Mesh ports.
Keyed lists
view`
#for items() as item (item.id)
<li>${item.label}</li>
#end
`The expression in parentheses is the key for reconcileKeyedList.
Components
import Field from './Field.jcr'
view`<Field :label=${'Name'} :field=${form.fields.name} />`Component props
Variables used in the template but not declared in the module script become mount props:
// Field.jcr
export default view`
<label>
<span>${label}</span>
<input bind-value=${field} />
</label>
`Compiles to:
export function mount(target, props = {}) {
const label = props['label']
const field = props['field']
// ...
}Template contracts
Declare the component surface with export <contract> — props (with type, required, default, model), pulses, slots, and emits. The compiler emits emit() + defaults; jacare check / the Vite plugin validate parents. Names listed under pulses are rewritten like local signals in #if / effects (bare open → open()); calling open() explicitly still works.
export <contract>
props: {
label: { type: 'string', required: true }
value: { type: 'string', model: true }
}
emits: ['change']
</contract>
export <view>
<label>${label}</label>
<input bind-value=${value} />
<button on-click=${() => emit('change')}>OK</button>
</view>Parent must use bind-value for model props (not :value). See API — Template contracts.
Module imports (import { topics } from './topics.js') are not treated as props.
CLI
npx jacare-compile src/app.jcr dist/app.jsWithout an output path, writes next to the input with .js extension.
jacare-compile src/app.jcr
# → src/app.jsError reporting
Compile errors throw JacareCompileError with file, line, column, and a source snippet:
Jacaré: unclosed view template literal
at src/app.jcr:12:3
export default view`
<div>import { compile, formatCompileError, JacareCompileError } from '@jacare/compiler'
try {
compile(source, { filename: 'app.jcr' })
} catch (error) {
if (error instanceof JacareCompileError) {
console.error(formatCompileError(error))
}
}Source maps
When filename is provided, the compiler emits source maps mapping generated JS back to .jcr lines:
const result = compile(source, { filename: 'src/app.jcr' })
console.log(result.map?.sources) // ['src/app.jcr']Integration
With Vite
Use @jacare/vite-plugin — it calls this compiler automatically for .jcr files.
With custom bundlers
Call compile() in your transform step and pass through the result.
Detecting lazy loaders
The runtime uses Symbol.for('jacare.lazy') on loaders returned by lazy(). The compiler does not need special handling for lazy imports.
Links
- npm — @jacare/compiler
- Repository
- API — Template contracts
- Compiler docs
- Syntax guide
- Jacaré Lab
- Related: @jacare/core · @jacare/vite-plugin · @jacare/cli
License
MIT
