kopular 0.23.1 → 1.0.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/LLM.md +83 -32
- package/README.md +23 -1
- package/package.json +2 -2
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/
|
|
13
|
-
(
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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
|
|
714
|
-
|
|
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
|
-
(`
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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 (
|
|
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.
|
|
3
|
+
"version": "1.0.0",
|
|
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",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@types/jsdom": "^30.0.0",
|
|
64
64
|
"@types/node": "^20.14.0",
|
|
65
65
|
"jsdom": "^25.0.1",
|
|
66
|
-
"kopscript": "^0.
|
|
66
|
+
"kopscript": "^1.0.0",
|
|
67
67
|
"typescript": "^5.5.0",
|
|
68
68
|
"vitest": "^4.1.11"
|
|
69
69
|
},
|