Strict CSP
Expression-in-attribute frameworks normally compile :onclick="count++" with new Function — which a strict Content-Security-Policy (no 'unsafe-eval') blocks, and Chrome MV3 extensions forbid outright. Banking, government, healthcare, extensions — same wall.
Sprae’s evaluator is pluggable. The CSP build swaps new Function for jessie — a JS interpreter, no eval anywhere — and keeps standard JS expressions:
<!-- all of these run under script-src 'self' -->
<button :onclick="user.name = 'John'">Set name</button>
<li :each="item in items.filter(i => i.active)" :text="item.name"></li>
<input :onkeydown.enter="todos.push({ text: draft, done: false })" />
Setup
<!-- CDN (self-host the file for script-src 'self') -->
<script src="/js/sprae-csp.umd.js" data-start></script>
// or ESM
import sprae from 'sprae'
import jessie from 'subscript/jessie'
sprae.use({ compile: jessie })
Get the bundle: sprae-csp.umd.js · sprae-csp.js (ESM)
vs Alpine’s CSP build
Alpine offers a CSP build that restricts expressions to a small subset. Per its own docs:
| expression | sprae CSP | Alpine CSP |
|---|---|---|
user.name = 'John' (property assignment) |
✅ | ❌ |
items.filter(i => i.active) (arrow functions) |
✅ | ❌ |
Math.max(a, b), JSON.stringify(x) (globals) |
✅ | ❌ |
:html / x-html |
✅ | ❌ |
| size, min+gzip | 18.1kb | 20.3kb |
Alpine’s recommended workaround is moving logic into Alpine.data() components — sprae just runs your expression.
Known limits
The full sprae test suite runs against the CSP build on every commit (CI). Differences from the eval build:
awaitis not supported inside expressions — fetch in:mountor state methods instead.thisbinding is not preserved in compiled functions — use:reffor element access.- Getters and template literals have edge cases — prefer
storecomputeds and string concatenation.
Chrome extension (MV3)
MV3 disallows remotely-hosted code and string evaluation — so: bundle sprae-csp with the extension, no CDN.
// manifest.json
{
"manifest_version": 3,
"action": { "default_popup": "popup.html" }
}
<!-- popup.html -->
<script src="sprae-csp.umd.js" data-start></script>
<div :scope="{ on: false }">
<button :onclick="on = !on" :text="on ? 'Disable' : 'Enable'"></button>
</div>
Header example
Content-Security-Policy: default-src 'self'; script-src 'self'
No 'unsafe-eval', no 'unsafe-inline' needed — directives are attributes, not inline scripts.