pawikan
v0.2.1
Published
A TypeScript-first frontend framework that compiles .paw to TypeScript to JavaScript. Svelte/Solid-style, no virtual DOM, with SSR. Pairs with Bao; backend agnostic.
Maintainers
Readme
Pawikan
A TypeScript-first frontend framework that compiles .paw to TypeScript to
JavaScript. Svelte/Solid-style compile-time reactivity, no virtual DOM, with SSR.
Built to pair with Bao (first-class),
but backend agnostic.
Pawikan shares the .paw syntax with Pating
(the server-rendered Python templating engine). Pating renders .paw on the
server in Python; Pawikan compiles the same syntax to JS for SPA/SSR/desktop.
This is a private, work-in-progress repository. It has no remote.
Pipeline
.paw -> lexer -> parser -> AST -> TypeScript generator -> esbuild/SWC -> JavaScriptSee docs/SPEC.md for the design and locked decisions.
Develop
npm install
npm run build # tsc -> dist/
npm test # vitest unit suite (133 tests, fast, no browser)
npm run test:e2e # Playwright headless-Chromium browser test (see below)Browser e2e (Playwright)
The unit suite runs under Node/happy-dom. A separate, heavier Playwright suite
(e2e/, npm run test:e2e) drives a real headless Chromium to prove the
Precognition live-validation flow end to end in a browser: it compiles a
.paw form with p-precognition, serves it with the real pawikan/runtime,
stands up a /validate stub (422 with messages for a bad value, 204 for a good
one, honoring the Precognition: true header), then types into the email field
and asserts the .field-error appears after the debounce + fetch and clears on
a valid value. First run downloads Chromium with npx playwright install
chromium. It is intentionally kept out of npm test so the unit run stays fast.
Status
First working build of the full pipeline .paw -> lexer -> parser -> AST ->
TypeScript generator -> esbuild -> JavaScript. Svelte/Solid-style compile-time
reactivity, no virtual DOM. Verified with npm run build, npm run typecheck
(clean), and npm test (133 tests passing). The minified client runtime is
5.64 KB (budget: < 20 KB).
Production-ready (built and tested)
- Compiler core — lexer, expression sub-parser, parser, and AST. Mirrors the
Pating grammar for portability:
{expr}interpolation,<if>/<elif>/<else>,<for x in items>withloophelpers and<else>,<set>,<include>,<extends>/<block>,<macro>/<call>, comments, whitespace control, and the client directives (p-data/state:,p-text/p-html/p-show/p-if,:bind/p-bind,@event/p-onwith modifiers,p-model,p-drag,p-canvas). Plus the section-style component form (state:/load:/watch:/template:). Line/column-accurate diagnostics throughout. - Client/TypeScript generator — emits a reactive
mount()module with fine-grained bindings (text, attrs, show, model, events,<if>/<for>blocks). Only emits interactive code when the template needs it. - Client runtime (
pawikan/runtime) — signals/computed/effect + DOM binding helpers, no virtual DOM. Tiny (6.85 KB minified). - Drag & canvas (Tier 1/2) —
p-dragmakes an element draggable via pointer events, writing position tostyle.transform(GPU-composited, no layout); with an expression it is controlled (p-drag="pos"reads/writes a reactive{x, y}, likep-modelfor position) and.x/.ylock an axis.p-canvasbinds a<canvas>to a reactive draw expression (p-canvas="drawScene(ctx, bars())") that repaints, coalesced to one frame viarequestAnimationFrame, whenever a signal it reads changes. Both are direct-DOM bindings — still no virtual DOM. Backed by thedraggable()/bindCanvas()runtime helpers. - SSR generator + layer — AST to an HTML-string
render(ctx)function; interpolation with autoescaping, filters,<if>/<for>, macros, attributes. - Component slots —
<slot/>(default) and<slot name="x"/>with fallback content; a<call name(args)>...</call>site fills them with bare children (default slot) and<template slot="x">...</template>(named slots). Compiled in both generators (SSR slot functions; client__props.__slots). - Built-in UI components (
pawikan/components) — Tier 1:field-errors,pagination,flash(dismissible, reactive on the client),csrf, andmethod, authored in.pawand matching the Pating contract (same names, props, and data shapes).renderComponent(name, ctx)server-renders them. - Authorization components (Tier 2) —
<auth>,<guest>, and<can ability="update" of="post">wrap children and render them only when the condition holds. Context/props providecurrent_user(truthy when authenticated) and a callablecan(ability, resource?) => boolean, matching the Pating/Bao contract. They desugar to<if>at parse time, so they work in both the SSR and reactive client output with zero added runtime weight. - Asset stacks (Tier 2) —
<stack name="scripts"/>is a placeholder; any<push name="scripts">...</push>(anywhere, including nested components/macros/loops) appends to it, and the content surfaces at the stack in push order.<push ... once>dedupes a block to a single emission per render. SSR collects pushes into a per-render registry and resolves the placeholders at the end ofrender(), so a push can target a stack declared earlier or later. On the reactive client they are intentionally no-ops (see known limits) since stacks are a server/document-head concern. - Localization (Tier 2) —
t(key, params?)is available in template scope (SSR viactx.t/ctx.translations, client via__props.t/__props.translations). A missing key returns the key;{param}placeholders interpolate fromparams. Use it inline ({ t('welcome', { name }) }, with object shorthand) or as a component (<t key="welcome" name="{user}"/>). A caller-suppliedtfunction takes precedence over the built-in table lookup. Matches the Bao + Pating contract. - Notifications (Tier 2) —
<notifications>(inpawikan/components) renders an inbox fromnotifications: [{ id, level, title, message, read, created_at }]. Unread items get theis-unreadclass (read itemsis-read) so unread is distinct, the header shows an unread count (derived, or passed asunread_count), and an empty list renders anis-emptyplaceholder. SSR + reactive client (thereadflag is reactive). - Reactive Precognition live validation (Tier 2) —
useForm({ url })(and thep-precognition="/url"form directive viabindPrecognition(root)) debounce changed fields, POST them with thePrecognition: trueheader, and bind the returned{ field: [messages] }into a reactiveerrorssignal that the.field-errormarkup reads. 422 shows messages; 204 clears them. This is now verified end to end in a real headless browser (Chromium) via the Playwright suite (npm run test:e2e), not just under happy-dom: the browser test types into a compiled.pawform and asserts the message appears and clears across the real debounce +fetch+ reactive DOM update. compile()API —source -> { code, css, map, ast, ts, meta }via esbuild.- Router manifest + matcher — file-based routing:
index, static,[param],[...catchAll],_layoutassociation; URL matching with param extraction.
Functional first cut (works, with known limits)
- CLI (
paw build web|ssr|static,paw routes) — compiles/writes real output.paw devshells out to Vite if installed (thin launcher, not a custom dev server). - Vite plugin (
pawikan/vite) — transforms.pawon import; injects extracted CSS via a dev style tag (no production CSS extraction yet). - SSR hydration —
renderToString({ hydrate: true })emits adata-paw-hmarker + a bootstrap script that mounts the client component. This is full client remount, not true partial/resumable hydration.
Gaps / TODO (not done or partial)
- Cross-file layout composition.
<extends>/<block>parse into the AST and single-template blocks render, but the SSR layer does not yet load a parent layout file and merge child blocks into it.<include>likewise parses but is not resolved against the filesystem at render time. - SPA routing runtime is a first cut: history push/pop, click interception, and param extraction work, but nested-layout composition at runtime and route data-loading are left to the caller. No code-splitting.
paw build staticdoes not inject dynamic route params, so[slug]pages render with empty params. Static export of dynamic routes needs a params source.- Macros /
<set>/<include>in client components are not compiled (they work in SSR). Client components focus on the reactive subset. - Asset stacks are SSR-only.
<stack>/<push>collect and emit during server render; in a client (mount) component they are no-ops. Document-head asset injection is a server concern, so this is by design rather than a TODO. p-transitionis emitted as a marker attribute only; no transition engine.- Selective/resumable hydration, islands, and SSR streaming are not implemented.
- CSS is extracted from
<style>blocks but is not scoped per-component. - List reconciliation is correctness-first (full re-render on change), not a keyed minimal-diff reconciler.
