laghu 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/README.md +211 -29
- package/SECURITY.md +45 -0
- package/dist/laghu.d.ts +32 -8
- package/dist/laghu.min.js +1 -1
- package/dist/router.min.js +1 -0
- package/package.json +5 -3
- package/src/laghu.js +577 -87
- package/src/router.js +123 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to laghu are recorded here. Versioning follows
|
|
4
|
+
semver: patch for fixes, minor for backwards-compatible additions,
|
|
5
|
+
major for breaking changes.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.1.0]
|
|
10
|
+
|
|
11
|
+
Security baseline (breaking only for attack patterns):
|
|
12
|
+
|
|
13
|
+
- Default HTML sanitizer: `data-html` and swaps strip scripts,
|
|
14
|
+
frames, event-handler attributes, and scriptable URLs out of the
|
|
15
|
+
box. Override with `Laghu.sanitize` (DOMPurify recommended for
|
|
16
|
+
untrusted content).
|
|
17
|
+
- URL policy: `data-bind:href/src/action/...` refuses
|
|
18
|
+
`javascript:`, `data:text/html`, and `vbscript:` values.
|
|
19
|
+
- Prototype pollution guards in path reads, writes, and action
|
|
20
|
+
strings (`__proto__`, `constructor`, `prototype` blocked).
|
|
21
|
+
- `DEV` now defaults off (`__LAGHU_DEV__ === true` enables). Error
|
|
22
|
+
visibility moves to events that always fire: `laghu:error`,
|
|
23
|
+
`laghu:request-error`, `laghu:unknown-action`.
|
|
24
|
+
|
|
25
|
+
Reliability:
|
|
26
|
+
|
|
27
|
+
- One throwing binding no longer kills the page; failures report
|
|
28
|
+
`laghu:error` and the scan continues.
|
|
29
|
+
- Effects unsubscribe on stop and prune stale deps on re-run;
|
|
30
|
+
nested effects compose through a stack.
|
|
31
|
+
- Swapped content keeps the requesting element's actions;
|
|
32
|
+
`morph:outer` forwards its mode.
|
|
33
|
+
- Fetch overlap uses a controller set (correct `queue:all`),
|
|
34
|
+
`data-timeout` aborts hung requests, GET arrays repeat params,
|
|
35
|
+
`+json` content types merge, failures record `state.$error`
|
|
36
|
+
with `data-pending` on the element.
|
|
37
|
+
- `onUnmount` setup hooks now run on disconnect.
|
|
38
|
+
- Row scopes own their variables; keyed lists use item-relative
|
|
39
|
+
`data-key` and rebind only on identity change; the observer
|
|
40
|
+
treats intra-batch moves as moves, not removals.
|
|
41
|
+
|
|
42
|
+
Expressions (still no `new Function`, still CSP-safe):
|
|
43
|
+
|
|
44
|
+
- Ternary (`a ? b : c`), arithmetic (`+ - * / %` with precedence),
|
|
45
|
+
parenthesized groups, calls with evaluated arguments,
|
|
46
|
+
bracket indexes (`list[0]`, `map["key"]`).
|
|
47
|
+
- Plain kebab-case action names stay literal; hyphens mean
|
|
48
|
+
subtraction only inside larger expressions.
|
|
49
|
+
|
|
50
|
+
New API:
|
|
51
|
+
|
|
52
|
+
- `Laghu.computed(fn)`, `Laghu.watch(scope, expr, cb)`,
|
|
53
|
+
`Laghu.persist(name, opts)` (localStorage, versioned).
|
|
54
|
+
- `LaghuRouter` opt-in plugin (`dist/router.min.js`): hash routes
|
|
55
|
+
with params, guards, notfound, programmatic navigate.
|
|
56
|
+
- Late-added state keys wake readers (miss tracking); `$error`
|
|
57
|
+
seeded at hydrate.
|
|
58
|
+
|
|
59
|
+
Docs: full JSDoc with examples on all public API, TSDoc
|
|
60
|
+
declarations, 17-section README with linked contents.
|
|
61
|
+
|
|
62
|
+
## [1.0.0]
|
|
63
|
+
|
|
64
|
+
Initial release: zero-build IIFE core, Light-DOM components, Proxy
|
|
65
|
+
reactivity, HTML-first templates, JSON/HTML fetch, morph plugin,
|
|
66
|
+
9 examples, 2 demo apps, 5-way benchmark.
|
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Contents:
|
|
|
27
27
|
- [15. Checking your setup](#15-checking-your-setup)
|
|
28
28
|
- [16. Troubleshooting](#16-troubleshooting)
|
|
29
29
|
- [17. Project files](#17-project-files)
|
|
30
|
+
- [18. Roadmap (explicit non-goals included)](#18-roadmap-explicit-non-goals-included)
|
|
30
31
|
|
|
31
32
|
## 1. When to use laghu
|
|
32
33
|
|
|
@@ -84,14 +85,29 @@ like `github.com/.../blob/...` serves HTML, and
|
|
|
84
85
|
`raw.githubusercontent.com` serves plain text, which browsers refuse
|
|
85
86
|
to run as a script.
|
|
86
87
|
|
|
87
|
-
Use jsDelivr instead. It serves
|
|
88
|
-
JavaScript content type:
|
|
88
|
+
Use jsDelivr instead. It serves files with the correct
|
|
89
|
+
JavaScript content type. Two flavors, pick one per page:
|
|
90
|
+
|
|
91
|
+
npm package (preferred, versioned by the registry):
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<script src="https://cdn.jsdelivr.net/npm/laghu@1.0.0/dist/laghu.min.js"></script>
|
|
95
|
+
<script src="https://cdn.jsdelivr.net/npm/laghu@1.0.0/dist/morph.min.js"></script>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
GitHub release files (works without any registry):
|
|
89
99
|
|
|
90
100
|
```html
|
|
91
101
|
<script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/laghu.min.js"></script>
|
|
92
102
|
<script src="https://cdn.jsdelivr.net/gh/prosubodh/laghu@v1.0.0/dist/morph.min.js"></script>
|
|
93
103
|
```
|
|
94
104
|
|
|
105
|
+
With a bundler or Node tooling:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
npm install laghu
|
|
109
|
+
```
|
|
110
|
+
|
|
95
111
|
Rules for production pages:
|
|
96
112
|
|
|
97
113
|
- Always pin a version (`@v1.0.0`). Never use `@master`, because
|
|
@@ -167,8 +183,9 @@ One-way text binding. Same behavior, two spellings. Sets
|
|
|
167
183
|
### data-html
|
|
168
184
|
|
|
169
185
|
Renders a value as HTML. Explicit opt-in because HTML parsing is an
|
|
170
|
-
XSS risk. Content passes through the
|
|
171
|
-
`Laghu.sanitize`
|
|
186
|
+
XSS risk. Content passes through the baseline sanitizer, replaceable
|
|
187
|
+
with `Laghu.sanitize` (DOMPurify for untrusted sources). Prefer
|
|
188
|
+
`data-bind` unless you trust the source.
|
|
172
189
|
|
|
173
190
|
```html
|
|
174
191
|
<div data-html="post.body"></div>
|
|
@@ -352,6 +369,43 @@ Fetch companions:
|
|
|
352
369
|
- `data-headers`: extra headers as JSON.
|
|
353
370
|
- `data-indicator="#spin"`: element shown while the request flies.
|
|
354
371
|
- `data-disable`: disables the element while its request flies.
|
|
372
|
+
- `data-timeout="5000"`: aborts hung requests (milliseconds);
|
|
373
|
+
timeouts report `laghu:request-error` with `timeout: true`.
|
|
374
|
+
- `data-pending`: set on the element during flight (style hooks
|
|
375
|
+
like `[data-pending] { opacity: .5; }`).
|
|
376
|
+
- `state.$error`:fetch failures record `{ status }` or `{ message,
|
|
377
|
+
timeout }` here (null when healthy). Bind it directly:
|
|
378
|
+
`<span data-bind="$error.status"></span>`.
|
|
379
|
+
|
|
380
|
+
### Hash routing (router plugin)
|
|
381
|
+
|
|
382
|
+
For pages with a handful of views, load `router.min.js` after the
|
|
383
|
+
core and declare hash routes. Past about five guarded routes, use
|
|
384
|
+
a real router; this one stays small on purpose.
|
|
385
|
+
|
|
386
|
+
```html
|
|
387
|
+
<script src="laghu.min.js"></script>
|
|
388
|
+
<script src="router.min.js"></script>
|
|
389
|
+
<div id="view"></div>
|
|
390
|
+
<script>
|
|
391
|
+
LaghuRouter.route({
|
|
392
|
+
target: '#view',
|
|
393
|
+
scope: state,
|
|
394
|
+
routes: [
|
|
395
|
+
{ path: '/', render: function () { return '<h1>Home</h1>'; } },
|
|
396
|
+
{ path: '/user/:id', render: function (p) { return '<h1>User ' + p.id + '</h1>'; } }
|
|
397
|
+
],
|
|
398
|
+
notfound: function () { return '<h1>Missing</h1>'; },
|
|
399
|
+
beforeEach: function (to) { return true; }
|
|
400
|
+
});
|
|
401
|
+
LaghuRouter.navigate('/user/42');
|
|
402
|
+
</script>
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Links are plain anchors (`href="#/user/42"`); hash changes drive
|
|
406
|
+
renders, params decode automatically, guards cancel by returning
|
|
407
|
+
false, and rendered fragments hydrate with the scope and actions
|
|
408
|
+
you pass in.
|
|
355
409
|
|
|
356
410
|
### data-trigger
|
|
357
411
|
|
|
@@ -451,34 +505,51 @@ trigger. Nested objects wrap on first read.
|
|
|
451
505
|
### Laghu.effect(fn)
|
|
452
506
|
|
|
453
507
|
Runs `fn` now and again whenever state it read changes. Returns a
|
|
454
|
-
stop function
|
|
508
|
+
stop function that fully unsubscribes. Subscriptions refresh every
|
|
509
|
+
run, so untaken branches release. Nested effects compose; a throwing
|
|
510
|
+
run reports `laghu:error` and the page survives. One effect per
|
|
511
|
+
binding is the normal pattern.
|
|
455
512
|
|
|
456
513
|
### Laghu.eval(scope, expr)
|
|
457
514
|
|
|
458
515
|
Reads one expression without `new Function` (CSP-safe). Supports
|
|
459
|
-
paths,
|
|
460
|
-
|
|
516
|
+
dotted paths, bracket indexes (`list[0]`, `map["k"]`), literals,
|
|
517
|
+
`!` and unary `-`, arithmetic (`+ - * / %`), comparisons, `&&`,
|
|
518
|
+
`||`, ternary (`a ? b : c`), parenthesized groups, and calls with
|
|
519
|
+
evaluated arguments like `total(qty)`. Anything else is undefined.
|
|
520
|
+
|
|
521
|
+
State keys must be valid identifiers (camelCase): a hyphen means
|
|
522
|
+
subtraction inside expressions. Action names are exempt; exact
|
|
523
|
+
action lookup runs before expression parsing, so
|
|
524
|
+
`data-click="my-action"` stays literal.
|
|
461
525
|
|
|
462
526
|
### Laghu.get(scope, path) and Laghu.set(scope, path, val)
|
|
463
527
|
|
|
464
528
|
Dotted path access with scope-chain lookup. `$store.name.key`
|
|
465
|
-
reaches global stores.
|
|
529
|
+
reaches global stores. Segments `__proto__`, `constructor`, and
|
|
530
|
+
`prototype` are refused on read and write. Late-added keys wake
|
|
531
|
+
readers: bindings on keys that do not exist yet update when the
|
|
532
|
+
keys appear. Writes follow the chain to the owning scope.
|
|
466
533
|
|
|
467
534
|
### Laghu.parseTrigger(spec, el)
|
|
468
535
|
|
|
469
536
|
Parses `data-trigger` text into a modifier object. Useful for
|
|
470
537
|
testing and custom wiring.
|
|
471
538
|
|
|
472
|
-
### Laghu.swap(target, html, mode, scope)
|
|
539
|
+
### Laghu.swap(target, html, mode, scope, actions)
|
|
473
540
|
|
|
474
541
|
Swaps HTML and hydrates the result. See modes in section 5.
|
|
475
542
|
Dispatches `laghu:before-swap` and `laghu:after-swap`, preserves
|
|
476
|
-
`data-preserve` nodes, restores focus and caret.
|
|
543
|
+
`data-preserve` nodes, restores focus and caret. The optional
|
|
544
|
+
`actions` map wires handlers in the new content; fetch swaps reuse
|
|
545
|
+
the requesting element's actions automatically.
|
|
477
546
|
|
|
478
547
|
### Laghu.sanitize(fn)
|
|
479
548
|
|
|
480
|
-
Replaces the HTML pass-through used by `data-html` and swaps.
|
|
481
|
-
|
|
549
|
+
Replaces the HTML pass-through used by `data-html` and swaps. The
|
|
550
|
+
default is a baseline cleaner (scripts, frames, event handlers,
|
|
551
|
+
scriptable URLs stripped), not a policy. Install a real sanitizer
|
|
552
|
+
when rendering untrusted HTML:
|
|
482
553
|
|
|
483
554
|
```js
|
|
484
555
|
Laghu.sanitize(function (h) { return DOMPurify.sanitize(h); });
|
|
@@ -498,6 +569,32 @@ Laghu.store('cart').n++;
|
|
|
498
569
|
<span data-bind="$store.cart.n"></span>
|
|
499
570
|
```
|
|
500
571
|
|
|
572
|
+
### Laghu.computed(fn)
|
|
573
|
+
|
|
574
|
+
Derived value kept fresh by an effect. Call the getter inside
|
|
575
|
+
bindings or other effects to subscribe.
|
|
576
|
+
|
|
577
|
+
```js
|
|
578
|
+
var total = Laghu.computed(function () { return state.price * state.qty; });
|
|
579
|
+
Laghu.effect(function () { el.textContent = total(); });
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Laghu.watch(scope, expr, cb)
|
|
583
|
+
|
|
584
|
+
Calls back with `(next, prev)` whenever an expression's value
|
|
585
|
+
changes. Reads without subscribing the caller's own effect.
|
|
586
|
+
Returns the stop function.
|
|
587
|
+
|
|
588
|
+
### Laghu.persist(name, opts)
|
|
589
|
+
|
|
590
|
+
Persists a named store to localStorage (debounced) with versioned
|
|
591
|
+
restore. Mismatched versions reseed instead of loading.
|
|
592
|
+
|
|
593
|
+
```js
|
|
594
|
+
Laghu.store('cart', { n: 0 });
|
|
595
|
+
Laghu.persist('cart', { key: 'cart:v1', version: 1 });
|
|
596
|
+
```
|
|
597
|
+
|
|
501
598
|
### Laghu.directive(name, fn)
|
|
502
599
|
|
|
503
600
|
Registers `data-name`. The function receives `{ el, get, effect,
|
|
@@ -535,7 +632,9 @@ replacing. `opts.mode` is `inner` (default) or `outer`.
|
|
|
535
632
|
|
|
536
633
|
## 7. Events reference
|
|
537
634
|
|
|
538
|
-
All names are CustomEvents
|
|
635
|
+
All names are CustomEvents, and all of them bubble, so one listener
|
|
636
|
+
on a section or on document observes every child. Listen with
|
|
637
|
+
`addEventListener`:
|
|
539
638
|
|
|
540
639
|
- `laghu:before-request`: cancelable, `detail` has `url`, `method`.
|
|
541
640
|
Call `preventDefault()` to stop the fetch (for confirm dialogs).
|
|
@@ -653,14 +752,30 @@ laghu runs under strict CSP with no `unsafe-eval` and no
|
|
|
653
752
|
`unsafe-inline`:
|
|
654
753
|
|
|
655
754
|
- No `new Function`, no `eval`, no string `setTimeout`. Expressions
|
|
656
|
-
use a small built-in reader (paths, literals, `!`,
|
|
657
|
-
`&&`, `||`,
|
|
755
|
+
use a small built-in reader (paths, indexes, literals, `!`,
|
|
756
|
+
arithmetic, comparisons, `&&`, `||`, ternary, calls with args).
|
|
658
757
|
- No inline handlers. All listeners attach with
|
|
659
758
|
`addEventListener` from `data-*` attributes.
|
|
660
|
-
- Ship laghu as an external file with a Subresource Integrity hash
|
|
759
|
+
- Ship laghu as an external file with a Subresource Integrity hash
|
|
760
|
+
(`node scripts/sri.mjs` prints them for release notes).
|
|
661
761
|
- `data-scope` takes JSON only, for the same no-eval reason.
|
|
662
|
-
|
|
663
|
-
|
|
762
|
+
|
|
763
|
+
No-eval is not the whole of web security. The rest of the posture:
|
|
764
|
+
|
|
765
|
+
- `data-html` and swaps pass a baseline sanitizer (scripts, frames,
|
|
766
|
+
event-handler attributes, scriptable URLs stripped). It is a
|
|
767
|
+
floor: point `Laghu.sanitize` at DOMPurify for untrusted content,
|
|
768
|
+
and set `script-src-attr 'none'` in your policy for defense in
|
|
769
|
+
depth.
|
|
770
|
+
- Attribute bindings refuse `javascript:`, `data:text/html`, and
|
|
771
|
+
`vbscript:` URLs on navigation and loading attributes.
|
|
772
|
+
- State paths refuse `__proto__`, `constructor`, and `prototype`
|
|
773
|
+
segments on read and write, including inside action strings.
|
|
774
|
+
- Runtime failures dispatch `laghu:error` (bindings, directives,
|
|
775
|
+
init, unmount) and `laghu:request-error` (fetch) in every
|
|
776
|
+
environment; consoles stay quiet unless you enable dev mode.
|
|
777
|
+
- See `SECURITY.md` for reporting, supported versions, and the
|
|
778
|
+
audit-backed regression list.
|
|
664
779
|
|
|
665
780
|
## 12. Loading flash and SEO notes
|
|
666
781
|
|
|
@@ -684,13 +799,28 @@ never run.
|
|
|
684
799
|
|
|
685
800
|
Measured after minification:
|
|
686
801
|
|
|
687
|
-
- Core `dist/laghu.min.js`: about
|
|
802
|
+
- Core `dist/laghu.min.js`: about 27.5kB raw, 9.2kB gzip, 8.2kB brotli.
|
|
688
803
|
- Morph `dist/morph.min.js`: about 2.3kB raw, 1kB gzip.
|
|
804
|
+
- Router `dist/router.min.js`: about 1.7kB raw, 0.9kB gzip.
|
|
689
805
|
- Budgets enforced by `node scripts/size.mjs`: core at or under
|
|
690
|
-
|
|
806
|
+
10kB gzip, morph at or under 3kB gzip, router at or under
|
|
807
|
+
2.5kB gzip.
|
|
808
|
+
|
|
809
|
+
The core grew from 7.1kB to 9.2kB for two consensus items: the
|
|
810
|
+
security baseline (default sanitizer, URL policy, pollution
|
|
811
|
+
guards, error isolation, effect unsubscribe) and the expression
|
|
812
|
+
grammar (ternary, arithmetic, argued calls, indexing). Both are
|
|
813
|
+
audited-in requirements, not creep: the budget moved openly, and
|
|
814
|
+
the total stays under half of htmx alone (~16kB gzip).
|
|
691
815
|
|
|
692
816
|
For scale: htmx alone is about 16kB gzip, Alpine with a few plugins
|
|
693
817
|
is 12 to 15kB. laghu core plus morph stays under half of either.
|
|
818
|
+
End-to-end proof lives in `bench/RESULTS.md`: the same TicTacToe in
|
|
819
|
+
laghu, vanilla, React, Vue, and Angular, measured in headless
|
|
820
|
+
Chromium. laghu ships 3 to 10 times less JavaScript than the
|
|
821
|
+
frameworks, holds the lowest heap of the four, and ties them on
|
|
822
|
+
interaction latency (all sub-millisecond); vanilla remains the
|
|
823
|
+
floor, as zero abstraction must.
|
|
694
824
|
|
|
695
825
|
## 14. Worked examples
|
|
696
826
|
|
|
@@ -699,8 +829,23 @@ See the `examples/` folder. Each file opens directly from disk.
|
|
|
699
829
|
- `counter.html`: one component, one binding, one action.
|
|
700
830
|
- `todo.html`: island with `data-model` input, keyed `data-for`
|
|
701
831
|
list, checkbox models, `data-show` counter.
|
|
832
|
+
- `model.html`: every two-way input type (text, number, single
|
|
833
|
+
checkbox, checkbox group, radio, single select, multi select,
|
|
834
|
+
textarea) with live mirrors and reset.
|
|
835
|
+
- `triggers.html`: throttle, once, interval, load, init, change
|
|
836
|
+
write-through, and prevented form submit.
|
|
837
|
+
- `swaps.html`: every swap mode, the swap event log,
|
|
838
|
+
`data-preserve`, and a morph swap that keeps typing and focus.
|
|
839
|
+
- `components.html`: two independent instances, attribute sync,
|
|
840
|
+
mount and unmount observation, removal and restore.
|
|
841
|
+
- `store.html`: two islands sharing one `$store` counter and name.
|
|
842
|
+
- `directives.html`: custom ticker and autofocus directives,
|
|
843
|
+
template removal running directive cleanup, class binding.
|
|
702
844
|
- `fetch.html`: JSON button plus morph list swap (needs a server
|
|
703
|
-
for the URLs, or swap in
|
|
845
|
+
for the URLs, or swap in local JSON files).
|
|
846
|
+
|
|
847
|
+
`test/examples.mjs` boots every file above and drives it: 48
|
|
848
|
+
assertions, all passing alongside the suites below.
|
|
704
849
|
|
|
705
850
|
## 15. Checking your setup
|
|
706
851
|
|
|
@@ -708,13 +853,18 @@ See the `examples/` folder. Each file opens directly from disk.
|
|
|
708
853
|
npm install
|
|
709
854
|
npx tsc --noEmit -p jsconfig.json
|
|
710
855
|
node scripts/size.mjs
|
|
711
|
-
node
|
|
856
|
+
node scripts/sri.mjs
|
|
857
|
+
npm test
|
|
712
858
|
```
|
|
713
859
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
860
|
+
154 tests cover (71 in `test/run.mjs` plus 48 in
|
|
861
|
+
`test/examples.mjs`, 23 in `test/apps.mjs`, 12 in
|
|
862
|
+
`test/router.mjs`): counter increments, text and attribute
|
|
863
|
+
bindings, conditionals, keyed reorder reuse, two-way inputs,
|
|
864
|
+
attribute sync, triggers, JSON merge, all swap modes, morph focus
|
|
865
|
+
preservation, stores, directives with cleanup, unknown-action
|
|
866
|
+
warnings, every shipped example page driven end to end, both demo
|
|
867
|
+
apps played to completion, and the router contract.
|
|
718
868
|
|
|
719
869
|
Releasing a new version (publishes to npm through CI):
|
|
720
870
|
|
|
@@ -758,14 +908,46 @@ Request never fires with `changed`: the input value never differed.
|
|
|
758
908
|
morph warns and falls back: `morph.min.js` is not loaded. Add the
|
|
759
909
|
script tag after the core.
|
|
760
910
|
|
|
911
|
+
`data-click="my-action"` does nothing: check the actions map first;
|
|
912
|
+
exact names win before expression parsing. `data-bind="a-b"` on the
|
|
913
|
+
other hand always subtracts; rename the key to camelCase.
|
|
914
|
+
|
|
915
|
+
New key renders blank: bindings subscribe to keys that appear later,
|
|
916
|
+
but prefer declaring the full shape up front so first paint is
|
|
917
|
+
complete.
|
|
918
|
+
|
|
761
919
|
## 17. Project files
|
|
762
920
|
|
|
763
921
|
- `src/laghu.js`: core source with full JSDoc, loaded directly in
|
|
764
922
|
development.
|
|
765
923
|
- `src/morph.js`: opt-in morph plugin source with full JSDoc.
|
|
766
|
-
- `
|
|
924
|
+
- `src/router.js`: opt-in hash router plugin with full JSDoc.
|
|
925
|
+
- `dist/laghu.min.js`, `dist/morph.min.js`, `dist/router.min.js`:
|
|
926
|
+
minified builds.
|
|
767
927
|
- `dist/laghu.d.ts`: TypeScript declarations for consumers.
|
|
768
|
-
- `test/run.mjs`: jsdom suite,
|
|
928
|
+
- `test/run.mjs`: jsdom suite, 71 assertions including security
|
|
929
|
+
regressions.
|
|
930
|
+
- `test/examples.mjs`: boots every example page and drives it, 48 assertions.
|
|
931
|
+
- `test/apps.mjs`: plays both demo apps to completion, 23 assertions.
|
|
932
|
+
- `test/router.mjs`: router contract, 12 assertions.
|
|
769
933
|
- `scripts/size.mjs`: minifies and enforces gzip budgets.
|
|
770
|
-
- `
|
|
934
|
+
- `scripts/sri.mjs`: prints SRI hashes for release notes.
|
|
935
|
+
- `examples/`: nine pages opening from disk.
|
|
936
|
+
- `apps/`: full demo projects with the prescribed structure.
|
|
937
|
+
- `bench/`: five-way benchmark harness and results.
|
|
938
|
+
- `CHANGELOG.md`, `SECURITY.md`, `CONTRIBUTING.md`: release,
|
|
939
|
+
security, and contribution contracts.
|
|
771
940
|
- `jsconfig.json`: strict `checkJs` config used by editors and CI.
|
|
941
|
+
|
|
942
|
+
## 18. Roadmap (explicit non-goals included)
|
|
943
|
+
|
|
944
|
+
Next, in order: declarative validation (`data-validate` with
|
|
945
|
+
schemas), transitions (`data-transition`), async fallback templates
|
|
946
|
+
for fetch, icon and Tailwind interop recipes (`data-icon`,
|
|
947
|
+
class merging), then docs site, real-browser matrix, and devtools.
|
|
948
|
+
|
|
949
|
+
Deliberately out of scope: JSX or any compiler toolchain, VDOM
|
|
950
|
+
diffing in core, built-in auth/i18n/charts/offline-sync, a Node
|
|
951
|
+
SSR server, full SPA parity. laghu owns small interactive pages
|
|
952
|
+
and embeds; it graduates users to bigger tools with documented
|
|
953
|
+
boundaries instead of cloning them badly.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Only the latest minor release receives security fixes. The
|
|
6
|
+
published provenance attests every release build on GitHub Actions;
|
|
7
|
+
prefer registry installs over copying files.
|
|
8
|
+
|
|
9
|
+
| Version | Supported |
|
|
10
|
+
| ------- | --------- |
|
|
11
|
+
| 1.1.x | yes |
|
|
12
|
+
| 1.0.x | no |
|
|
13
|
+
|
|
14
|
+
## Reporting a vulnerability
|
|
15
|
+
|
|
16
|
+
Open a private security advisory on GitHub, or email the
|
|
17
|
+
maintainer through the profile listed on the repo. Include a
|
|
18
|
+
minimal reproduction (HTML plus state or response payload). Expect
|
|
19
|
+
an acknowledgement within 72 hours and a fix or mitigation plan
|
|
20
|
+
within 14 days. Do not open public issues for unpatched holes.
|
|
21
|
+
|
|
22
|
+
## Threat model and defaults
|
|
23
|
+
|
|
24
|
+
- `data-html` and HTML swaps pass a baseline sanitizer (scripts,
|
|
25
|
+
frames, event handlers, scriptable URLs stripped). It is a
|
|
26
|
+
floor: render untrusted content only behind DOMPurify via
|
|
27
|
+
`Laghu.sanitize`, and serve a Content Security Policy that also
|
|
28
|
+
sets `script-src-attr 'none'`.
|
|
29
|
+
- Attribute bindings refuse `javascript:`, `data:text/html`, and
|
|
30
|
+
`vbscript:` URLs on navigation and loading attributes.
|
|
31
|
+
- State paths reject `__proto__`, `constructor`, and `prototype`
|
|
32
|
+
segments on read and write, including inside action strings.
|
|
33
|
+
- Expressions never use `new Function` or `eval`, so
|
|
34
|
+
`script-src 'self'` holds without `unsafe-eval`. That covers
|
|
35
|
+
code injection through expressions, not XSS through HTML: those
|
|
36
|
+
are separate layers, configure both.
|
|
37
|
+
- Fetch sends JSON with same-origin default credentials; set
|
|
38
|
+
`data-headers` and CORS server-side for cross-origin APIs.
|
|
39
|
+
|
|
40
|
+
## Audits
|
|
41
|
+
|
|
42
|
+
Dependency and secret scanning run in CI. XSS and pollution
|
|
43
|
+
vectors from past audits live as regression tests in
|
|
44
|
+
`test/run.mjs` (block 7). Re-run `npm test` before every release;
|
|
45
|
+
the publish workflow refuses dirty trees and mismatched tags.
|
package/dist/laghu.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface DirectiveContext {
|
|
|
44
44
|
|
|
45
45
|
/** Full public surface available as window.Laghu. */
|
|
46
46
|
export interface LaghuAPI {
|
|
47
|
-
/** Release string, for example "1.
|
|
47
|
+
/** Release string, for example "1.1.0". */
|
|
48
48
|
version: string;
|
|
49
49
|
/**
|
|
50
50
|
* Register a Light-DOM custom element.
|
|
@@ -52,18 +52,19 @@ export interface LaghuAPI {
|
|
|
52
52
|
* @param setup Factory returning fresh state, actions, and options.
|
|
53
53
|
*/
|
|
54
54
|
define(tag: string, setup: () => SetupResult): any;
|
|
55
|
-
/** Make a plain object or array reactive. Primitives pass through. */
|
|
55
|
+
/** Make a plain object or array reactive. Primitives pass through. Re-wraps return the same proxy. */
|
|
56
56
|
reactive<T extends object>(obj: T): T;
|
|
57
57
|
/**
|
|
58
|
-
* Run fn now and again on dependency change.
|
|
58
|
+
* Run fn now and again on dependency change. Subscriptions refresh
|
|
59
|
+
* per run; stop() unsubscribes fully. Never throws out.
|
|
59
60
|
* @returns Stop function that unsubscribes the effect.
|
|
60
61
|
*/
|
|
61
62
|
effect(fn: () => void): () => void;
|
|
62
|
-
/** Evaluate a
|
|
63
|
+
/** Evaluate a CSP-safe expression: paths, indexes, literals, arithmetic, comparisons, ternary, calls. */
|
|
63
64
|
eval(scope: any, expr: string): any;
|
|
64
|
-
/** Read a dotted path (or $store.name.key) without throwing. */
|
|
65
|
+
/** Read a dotted path (or $store.name.key) without throwing. Prototype segments blocked. */
|
|
65
66
|
get(scope: any, path: string): any;
|
|
66
|
-
/** Write a dotted path, creating intermediate objects. */
|
|
67
|
+
/** Write a dotted path, creating intermediate objects. Prototype segments refused. */
|
|
67
68
|
set(scope: any, path: string, val: any): void;
|
|
68
69
|
/** Parse data-trigger text into a TriggerSpec. */
|
|
69
70
|
parseTrigger(spec: string, el: Element): TriggerSpec;
|
|
@@ -71,11 +72,12 @@ export interface LaghuAPI {
|
|
|
71
72
|
* Swap HTML into target, hydrate the result, preserve focus.
|
|
72
73
|
* @param swap One of inner, outer, text, append, prepend, beforebegin,
|
|
73
74
|
* afterend, delete, none, morph:inner, morph:outer.
|
|
75
|
+
* @param actions Named actions for handlers in the new content.
|
|
74
76
|
*/
|
|
75
|
-
swap(target: Element, html: string, swap: string, scope: any): void;
|
|
77
|
+
swap(target: Element, html: string, swap: string, scope: any, actions?: Record<string, Function>): void;
|
|
76
78
|
/** FOUC guard CSS string. Inject it or copy the rule. */
|
|
77
79
|
css: string;
|
|
78
|
-
/** Replace the HTML pass-through used by data-html and swaps. */
|
|
80
|
+
/** Replace the HTML pass-through used by data-html and swaps. Defaults to a baseline sanitizer. */
|
|
79
81
|
sanitize(fn: (html: string) => string): void;
|
|
80
82
|
/**
|
|
81
83
|
* Scan root, wire all bindings, return the reactive state.
|
|
@@ -86,6 +88,12 @@ export interface LaghuAPI {
|
|
|
86
88
|
destroyTree(root: Element): void;
|
|
87
89
|
/** Create (with init) or fetch (without) a named global reactive store. */
|
|
88
90
|
store(name: string, init?: any): any;
|
|
91
|
+
/** Derived value kept fresh; call the getter inside bindings or effects to subscribe. */
|
|
92
|
+
computed(fn: () => any): () => any;
|
|
93
|
+
/** Call back with (next, prev) when an expression's value changes. Returns the stop function. */
|
|
94
|
+
watch(scope: any, expr: string, cb: (next: any, prev: any) => void): () => void;
|
|
95
|
+
/** Persist a named store to localStorage (debounced) with versioned restore. */
|
|
96
|
+
persist(name: string, opts?: { key?: string; version?: number; debounce?: number }): any;
|
|
89
97
|
/**
|
|
90
98
|
* Register a data-name directive. Return a cleanup function optionally.
|
|
91
99
|
* @example
|
|
@@ -104,6 +112,22 @@ declare global {
|
|
|
104
112
|
morphEl(a: Element, b: Element): void;
|
|
105
113
|
version: string;
|
|
106
114
|
};
|
|
115
|
+
LaghuRouter?: {
|
|
116
|
+
route(opts: {
|
|
117
|
+
target?: string;
|
|
118
|
+
scope?: any;
|
|
119
|
+
actions?: Record<string, Function>;
|
|
120
|
+
routes?: Array<{ path: string; render: (params: Record<string, string>, to: any) => string }>;
|
|
121
|
+
notfound?: (to: any) => string;
|
|
122
|
+
beforeEach?: (to: any, from: any) => boolean | void;
|
|
123
|
+
afterEach?: (to: any, from: any) => void;
|
|
124
|
+
}): () => void;
|
|
125
|
+
compile(pattern: string): (path: string) => Record<string, string> | null;
|
|
126
|
+
current(): { path: string; params: Record<string, string> };
|
|
127
|
+
navigate(path: string): void;
|
|
128
|
+
afterEach(fn: (to: any, from: any) => void): void;
|
|
129
|
+
version: string;
|
|
130
|
+
};
|
|
107
131
|
__LAGHU_DEV__?: boolean;
|
|
108
132
|
}
|
|
109
133
|
}
|