kopular 0.23.1 → 0.23.2

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.
Files changed (3) hide show
  1. package/LLM.md +83 -32
  2. package/README.md +23 -1
  3. package/package.json +1 -1
package/LLM.md CHANGED
@@ -9,10 +9,12 @@ not repeated here.
9
9
  Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
10
10
  `kopular/velement` (VElement — what `Render()` returns; see "Component" below),
11
11
  `kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives`
12
- (If), `kopular/http` (Http), `kopular/forms` (FormField, Validators), `kopular/testing`
13
- (runKopularApp, runKopularFixture see below). `kopular/vdom` (the diff/patch engine
14
- behind `Update()`) is internal nothing outside `component.ks` needs to import it
15
- directly. Also ships a bin, `kp` `npx kp new
12
+ (If), `kopular/http` (Http), `kopular/forms` (FormField, Validators), `kopular/computed`
13
+ (Computed1, Computed2), `kopular/resource` (Resource, AsyncStatus), `kopular/vdom`
14
+ (`ScopedStyles` the runtime half of `styles from`; everything else in this file, the
15
+ diff/patch engine behind `Update()`, is internal, nothing else here needs importing
16
+ directly), `kopular/testing` (runKopularApp, runKopularFixture — see below). Also ships a
17
+ bin, `kp` — `npx kp new
16
18
  <dir>` scaffolds a new project (the `extern` bindings below, plus the vendor/serve
17
19
  scripts needed to run in a browser) rather than requiring it be reconstructed by hand;
18
20
  prefer it over hand-writing the section below for a new project. Templates (`template
@@ -24,21 +26,22 @@ there is no `kopular/template` entry point to import.
24
26
  `using` only resolves same-project relative paths — reaching into an npm package (Kopular
25
27
  included) always goes through `extern`, re-describing exactly the members you use.
26
28
 
27
- **`Element`/`Document`/`Event` are NOT part of Kopular's own exports** — they're plain
28
- ambient browser globals (`extern class Element { ... };`, no `from` clause), genuinely
29
- present at runtime with no import needed, but Kopular has no re-exportable copy of them
30
- to reach for: `kopular/dom` is Kopular's own *internal* ambient binding, itself made of
31
- erased `extern` declarations with nothing real behind them at runtime, so `extern class
32
- Element { ... } from "kopular/dom";` doesn't work — there's no `Element` symbol actually
33
- living in that module to bind to. Every consuming project re-declares its own
34
- Element/Document/Event, same as this one does. **Copy the block below rather than
35
- hand-rolling a smaller one from scratch and adding members as compile errors demand
36
- them** a real, complete first-attempt implementation of a Kopular app hit the exact
37
- same missing property (`Element.value`) twice from two independently-trimmed subsets,
38
- because the compile error only ever names the one member actually touched, never warns
39
- that a *sibling* feature (a template's `placeholder="..."` attribute, a `[(value)]`
40
- binding's generated `e.target.value` read) will need one you didn't happen to write by
41
- hand:
29
+ **Don't reach for `Element`/`Document`/`Event` via `extern ... from "kopular/dom";`, even
30
+ though that actually works** (`kopular/dom` genuinely re-exports real `globalThis`
31
+ bindings, same as any other Kopular export) **redeclare your own ambient block
32
+ instead.** The reason isn't that importing wouldn't work; it's that Kopular's own internal
33
+ `dom.ks` only declares what Kopular's own framework code itself touches, a smaller
34
+ surface than a real app typically needs (`Element.value`/`.placeholder`, `.href`/`.src`/
35
+ `.alt`, `.querySelector`, ...) importing it would silently cap you at that subset with
36
+ no signal you're missing something until you hit a real compile error for each one, one
37
+ at a time. Every consuming project re-declares its own Element/Document/Event, same as
38
+ this one does. **Copy the block below rather than hand-rolling a smaller one from scratch
39
+ and adding members as compile errors demand them** — a real, complete first-attempt
40
+ implementation of a Kopular app hit the exact same missing property (`Element.value`)
41
+ twice from two independently-trimmed subsets, because the compile error only ever names
42
+ the one member actually touched, never warns that a *sibling* feature (a template's
43
+ `placeholder="..."` attribute, a `[(value)]` binding's generated `e.target.value` read)
44
+ will need one you didn't happen to write by hand:
42
45
 
43
46
  ```ks
44
47
  extern class Event {
@@ -109,6 +112,10 @@ extern class VElement {
109
112
  extern class Component {
110
113
  constructor();
111
114
  virtual VElement Render(); // `virtual` here is what lets your subclass `override` it
115
+ // Renders a fallback UI instead of an uncaught crash if Render() throws
116
+ // — see "Component" below. Purely additive; not overriding it keeps
117
+ // today's exact (uncaught) behavior.
118
+ virtual VElement RenderError(string message);
112
119
  virtual void AfterRender(Element root); // see "Component" below
113
120
  void Mount(Element parent);
114
121
  void Update();
@@ -120,8 +127,26 @@ extern class Component {
120
127
  extern class Router {
121
128
  constructor(Component notFoundPage);
122
129
  void AddRoute(string path, Component page);
130
+ // Real code-splitting — see "Router" below. loader is a real dynamic
131
+ // import(), reached via a hand-written loader shim.
132
+ void AddLazyRoute(string path, () => task<Component> loader);
133
+ // Every registered path (AddRoute + AddLazyRoute), in registration
134
+ // order — see "Router" below.
135
+ string[] AllPaths();
123
136
  void Navigate(string path);
124
137
  void Mount(Element parent);
138
+ // One guard for the whole Router, not per-route — see "Router" below.
139
+ void SetGuard(string redirectPath, (string) => bool guard);
140
+ // The FIRST captured :name segment from whatever route just matched —
141
+ // "" if the matched route has no dynamic segment. Not state<T>; no
142
+ // Subscribe() needed — see "Common mistakes" below for why.
143
+ string Param { get; }
144
+ // Any captured :name segment (or a trailing * wildcard, as "*") by
145
+ // name, for a route with more than one — see "Router" below.
146
+ string Params(string name);
147
+ // A query-string value by key ("" if absent), independent of which
148
+ // route matched.
149
+ string Query(string key);
125
150
  } from "kopular/router";
126
151
 
127
152
  extern VElement If(bool condition, () => VElement whenTrue, () => VElement whenFalse) from "kopular/directives";
@@ -231,7 +256,11 @@ w.Bump(); // re-render + diff + patch
231
256
  - `VElement`: `Tag`, `TextContent`/`ClassName`/`Id`/`Value` (direct fields — `Value` is the
232
257
  one that must be a live DOM *property*, not an attribute: `SetAttr("value", x)` sets the
233
258
  default value, not the current one), `RawHtml` (an opaque, undiffed leaf — set instead of
234
- `TextContent`/children, for a raw-HTML-then-wire-handlers pattern), the four fixed named
259
+ `TextContent`/children, for a raw-HTML-then-wire-handlers pattern **sets real
260
+ `innerHTML`, unescaped: only ever assign static/developer-authored content — a `raw
261
+ string ... from "<path>.html";` compile-time constant, in every real use in this
262
+ ecosystem today — never anything reachable from user input, a fetched `Http` response
263
+ body, or a `FormField<T>`'s `.Value`, or it's a real XSS hole**), the four fixed named
235
264
  event fields `OnClick`/`OnInput`/`OnBlur`/`OnChange` (each a real no-op by default, never
236
265
  null — no nullable function type to fall back on), `AppendChild(child)`, and
237
266
  `SetAttr(name, value)` (the escape hatch for any other real HTML attribute — `href`,
@@ -370,6 +399,11 @@ class Counter : Component {
370
399
  `blur`/`change` — `VElement`'s own fixed set — anything else is a compile error
371
400
  (`KS5016`). `[prop]`/static `attr="..."` assign directly for `id`/`className`/`value`;
372
401
  anything else goes through `SetAttr` instead.
402
+ - **`*mount="expr"`** embeds a live child Component declaratively — desugars to
403
+ `VElement.Mount(expr)`, the same mechanism a hand-written `Render()` uses (see "Nested
404
+ component composition" below). Unlike `*if`/`*for`, it composes with either — `*for="Row
405
+ r of Rows" *mount="r"` (one mounted child per loop item) is the headline case, not an
406
+ error. See kopscript's own `LLM.md`/`README.md` for the full syntax.
373
407
  - A `state<T>` field declared directly on the class and referenced directly in the
374
408
  template (`Count` above) gets `Subscribe((v) => this.Update())` wired automatically —
375
409
  no manual `Subscribe` in the constructor for that field. State reached indirectly
@@ -380,7 +414,8 @@ class Counter : Component {
380
414
  `this.Field` (see KopScript's own LLM.md `KS5017`/`KS5018` for the two rejected cases).
381
415
  - One top-level element per template (hard error otherwise); no mixing text and element
382
416
  children under one element (`VElement` has no text-node sibling concept, only
383
- `.TextContent`); no pipes, at most one structural directive per element.
417
+ `.TextContent`); no pipes, at most one of `*if`/`*for` per element (`*mount` is
418
+ orthogonal to both — see above — and isn't included in that limit).
384
419
  - This is entirely a KopScript compiler feature (parsed/desugared before type-checking
385
420
  runs) — Kopular's own framework code (`component.ks`, `dom.ks`) is unmodified and
386
421
  unaware templates exist; a template-generated `Render()` is indistinguishable from a
@@ -708,10 +743,14 @@ try {
708
743
  Compiles `entryFileName` (plus everything else in `srcDir` it `using`s) via kopscript's
709
744
  `compileGraph`, binds jsdom onto `globalThis` (`document`/`Element`/`Event`/`location`/
710
745
  `history`/`window`/`fetch`) for the compiled ambient `extern` declarations to find, and
711
- runs it. Every `.ks`/`.html`/`.js` file in `srcDir` is copied along with it — `.js`
746
+ runs it. Every `.ks`/`.html`/`.js`/`.css` file in `srcDir` is copied along with it — `.js`
712
747
  included specifically so a hand-written (not compiled) sibling like a `Router.AddLazyRoute`
713
- loader shim just works with no extra setup; `extraFiles` is only for a real dependency with
714
- some OTHER extension (e.g. a `.json` config file). `runKopularFixture(source, options)` is the sibling export for an inline fixture
748
+ loader shim just works with no extra setup, `.css` the same way for a `styles from
749
+ "<path>.css";` stylesheet; `extraFiles` is only for a real dependency with some OTHER
750
+ extension (e.g. a `.json` config file). `runKopularFixture(source, options)` also takes
751
+ an `options.extraSource` (`Record<string, string>` — filename to inline content) for
752
+ supplying a fixture's own auxiliary `.html`/`.css` file without a real file on disk, since
753
+ `runKopularFixture` only ever writes the one entry-file string you pass it. This is the sibling export for an inline fixture
715
754
  string instead of a real file (used by Kopular's own test suite; copies Kopular's *own*
716
755
  `.ks` sources alongside the fixture, so `using "./component"` resolves — only meaningful
717
756
  for testing Kopular itself, not an external consumer, which should use `runKopularApp`
@@ -769,6 +808,15 @@ class CounterService {
769
808
 
770
809
  ## Common mistakes (seeded from real generation failures)
771
810
 
811
+ - **`VElement.RawHtml` sets real `innerHTML`, completely unescaped — never assign it
812
+ anything reachable from user input.** It exists for a raw-HTML-then-wire-a-delegated-
813
+ listener pattern (see `header.ks`/`nav.ks` in KopularDemo), and every real use in this
814
+ ecosystem is a `raw string ... from "<path>.html";` compile-time constant — genuinely
815
+ static content, baked in at build time, never a runtime value. There is nothing in the
816
+ type system stopping `el.RawHtml = someFetchedString;` or `el.RawHtml =
817
+ formField.Value.Value;` from compiling — both are a real XSS hole if that content is
818
+ ever attacker-influenced. Use `TextContent` (always escaped) for any dynamic string;
819
+ `RawHtml` is for static markup only.
772
820
  - **`Router.Param` is a plain `string`, not `state<T>` — don't `Subscribe()` to it.** A
773
821
  `state<T>`-based design for it was tried and genuinely crashes: `Navigate()` sets `Param`
774
822
  *before* the newly-matched page finishes mounting, so a page `Subscribe`-ing to it fires
@@ -819,13 +867,16 @@ imperative `Render()` code as the hand-written form, checked at compile time, no
819
867
  interpreted at runtime (see "Templates" above) · two-way binding in a hand-written
820
868
  `Render()`, or on anything but `value` even in a template (`[(value)]="Field"` exists —
821
869
  see "Templates" above — but it's `value`-only, and templates-only) ·
822
- embedding a child Component from a *template* (`.html`) — `VElement.Mount(component)`
823
- works only from a hand-written `Render()` today, see "Component" above ·
824
870
  a generic/arbitrary `VElement` event binding — only `click`/`input`/`blur`/`change` ·
825
871
  pipes · animations · typed/generic HTTP responses (`Http` returns raw text — see above) ·
826
- SSR.
827
-
828
- (`FormField<T>`/`Validators` and `kp new` see "FormField<T> / Validators" and "Starting
829
- a new project" above are real, shipped features; they used to be listed here as gaps
830
- before those landed and this list wasn't updated at the time. Leaving this parenthetical
831
- rather than quietly deleting it, as a reminder to keep this list in sync going forward.)
872
+ live, per-request SSR — no framework-level renderer ships for this, but build-time static
873
+ prerendering is a real, demonstrated pattern built entirely on existing pieces
874
+ (`Router.AllPaths()` + `kopular/testing`'s `runKopularApp`, no Kopular framework code
875
+ needed)see KopularDemo's own `scripts/prerender.mjs`.
876
+
877
+ (`FormField<T>`/`Validators`, `kp new`, and embedding a child Component from a *template*
878
+ via `*mount="expr"` — see "FormField<T> / Validators", "Starting a new project", and
879
+ "Nested component composition" above — are all real, shipped features; they used to be
880
+ listed here as gaps before those landed and this list wasn't updated at the time. Leaving
881
+ this parenthetical rather than quietly deleting it, as a reminder to keep this list in
882
+ sync going forward.)
package/README.md CHANGED
@@ -615,6 +615,26 @@ class Panel : Component {
615
615
  }
616
616
  ```
617
617
 
618
+ ## Raw HTML, and its real security boundary
619
+
620
+ `VElement.RawHtml` sets a real, opaque `innerHTML` — an undiffed leaf, in place of
621
+ `TextContent`/children, for a raw-markup-plus-one-delegated-listener pattern (this site's
622
+ own `header.ks`/`nav.ks` use it exactly this way):
623
+
624
+ ```ks
625
+ VElement header = VElement.Create("header");
626
+ header.RawHtml = SiteHeaderHtml; // a raw string ... from "./header.html"; constant
627
+ header.OnClick = (Event e) => { /* one delegated listener over the whole subtree */ };
628
+ ```
629
+
630
+ **It's unescaped, real `innerHTML` — never assign it anything reachable from user input.**
631
+ Every real use in this ecosystem is a `raw string ... from "<path>.html";` compile-time
632
+ constant (see KopScript's own docs) — genuinely static markup, baked in at build time,
633
+ never a runtime value. Nothing in the type system stops `el.RawHtml =
634
+ someFetchedString;` or `el.RawHtml = formField.Value.Value;` from compiling — both are a
635
+ real XSS hole if that content is ever attacker-influenced. Use `TextContent` (always
636
+ escaped) for any dynamic string; `RawHtml` is for static markup only.
637
+
618
638
  ## HTTP
619
639
 
620
640
  ```ks
@@ -910,7 +930,9 @@ npm run build # compiles src/*.ks -> src/*.js (compiled output is gitignored)
910
930
  npm test # runs test/kopular.test.ts against a real DOM via jsdom
911
931
  ```
912
932
 
913
- `kopscript` is a real published dependency (`^0.1.0`) this repo doesn't need KopScript
933
+ `kopscript` is a real published dependency (see `package.json`'s own `devDependencies` for
934
+ the exact version this repo currently requires — not restated by hand here, since it
935
+ changes far more often than this paragraph does) — this repo doesn't need KopScript
914
936
  checked out as a sibling directory or anything else local to build or test.
915
937
 
916
938
  ## Status
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "0.23.1",
3
+ "version": "0.23.2",
4
4
  "description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
5
5
  "type": "module",
6
6
  "license": "MIT",