kopular 1.0.1 → 1.1.1

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/GUIDE.md ADDED
@@ -0,0 +1,238 @@
1
+ # Kopular quick guide
2
+
3
+ Everything you need for typical app work, in one page. For anything not covered here —
4
+ generics, nullable types, advanced routing, the full diagnostic list — see this package's
5
+ own `LLM.md` and `node_modules/kopscript/LLM.md`.
6
+
7
+ ## Setup
8
+
9
+ Every file starts with `using "kopular";` for the DOM and every Kopular type, plus
10
+ `using "./other_file";` per project file it needs (`using` isn't transitive — list every
11
+ file you reference, not just direct dependencies' dependencies).
12
+
13
+ ```ks
14
+ using "kopular";
15
+ using "./counter";
16
+
17
+ Counter app = new Counter();
18
+ app.Mount(document.body);
19
+ ```
20
+
21
+ Run `npm run build && npm run serve`, or `npm start` (a scaffolded project already has
22
+ both scripts). `ks check src/app.ks` type-checks without building, if you just want errors.
23
+
24
+ ## The language you need
25
+
26
+ - `Type name = value;` for every local — no `let`/`var`, no inference on declarations.
27
+ - No implicit `this` — every member reference is `this.Field`/`this.Method()`, always,
28
+ including inside a lambda.
29
+ - `if`/`else`/`while`/`for`/`foreach (Type x in xs)` — all statements, no ternary. Use
30
+ `match` for a conditional value: `match x { 1 => "one", _ => "other" }` (the `_` arm is
31
+ required unless every case is covered, e.g. an enum).
32
+ - Lambdas need explicit parameter types: `(number x) => x * 2`.
33
+ - Nullable: `string? name` — a nullable field forces `if (x != null) { ... }` before use;
34
+ narrowing is scoped to that `if` block, not reachability-based (an early
35
+ `if (x == null) { return; }` does NOT narrow `x` afterward — wrap the rest in
36
+ `if (x != null) { ... }` instead).
37
+ - Strings: `s.Length`, `.Contains()/.StartsWith()/.EndsWith()`, `.Trim()/.ToUpper()/.ToLower()`,
38
+ `.Split(sep)`, `.Replace(from, to)`, `.CompareTo(other)` (sort comparator), `+` concatenates.
39
+ Both `"double"` and `'single'` quotes work — use whichever the other isn't, e.g. inside
40
+ a template attribute (`*if="Name != ''"`).
41
+ - Arrays: `xs.Length`, `.Map()/.Filter()/.ForEach()/.Find()/.FindIndex()/.Includes()/.IndexOf()`,
42
+ `.Sort(cmp)/.Reverse()/.Push(x)` (all three **non-mutating**, return a new array),
43
+ `.Slice(a, b)/.Concat(ys)/.Join(sep)/.Reduce(fn, initial)`.
44
+ - `x == null` / `x != null` also match `undefined` — a missed `Array.Find`, an absent
45
+ optional field, etc. all read as `null`.
46
+ - No object-literal syntax anywhere (`{ key: value }` doesn't exist as a value). A JS API
47
+ that needs one (rare — `fetch`'s options, `addEventListener`'s options object) needs a
48
+ small hand-written `.js` shim; Kopular's own `Http`/`FormField` cover the common cases so
49
+ you'll rarely hit this.
50
+
51
+ ## Components
52
+
53
+ ```ks
54
+ class Counter : Component {
55
+ public state<number> Count;
56
+ constructor() : base() { this.Count = state(0); }
57
+ public void Increment() { this.Count.Value = this.Count.Value + 1; }
58
+ template from "./counter.html";
59
+ }
60
+ ```
61
+ ```html
62
+ <button (click)="Increment()">Count: {{ Count.Value }}</button>
63
+ ```
64
+
65
+ `state<T>` referenced directly in a template auto-subscribes — no manual `Subscribe`
66
+ needed. State reached *indirectly* (through a method, or `this.SomeService.Count`) does
67
+ need one: `this.SomeService.Count.Subscribe((v) => this.Update());` in the constructor.
68
+
69
+ **Template bindings**: `{{ expr }}` text, `[prop]="expr"` (real fields for
70
+ `id`/`className`/`value`/`disabled`/`checked`; anything else is a plain attribute),
71
+ `(click)`/`(input)`/`(blur)`/`(change)` events, `[(value)]="Field"` two-way binding
72
+ (`Field` can be a bare name, `this.Field`, or a path like `Qty.Value` — not a method call),
73
+ `*if="expr"`, `*for="Type v of expr"` (element type required, no inference), `*mount="expr"`
74
+ (embeds a live child component, composes with `*for`).
75
+
76
+ **Hand-written `Render()`** (needed when logic is too dynamic for a template, or a template
77
+ would obscure more than it clarifies):
78
+
79
+ ```ks
80
+ public override VElement Render() {
81
+ VElement button = VElement.Create("button");
82
+ button.TextContent = "Count: " + this.Count.Value;
83
+ button.OnClick = (Event e) => { this.Increment(); };
84
+ return button;
85
+ }
86
+ ```
87
+ `VElement.Create(tag)`, `.TextContent/.ClassName/.Id/.Value/.RawHtml/.Disabled/.Checked`,
88
+ `.OnClick/.OnInput/.OnBlur/.OnChange`, `.AppendChild(child)`, `.SetAttr(name, value)` (the
89
+ escape hatch for anything without a named field). Both styles compile to the same thing and
90
+ mix freely across a project.
91
+
92
+ **Error boundary**: override `RenderError(string message)` to show a fallback instead of an
93
+ uncaught crash if `Render()` throws.
94
+
95
+ ## Nested components & content projection
96
+
97
+ ```ks
98
+ this.Items.ForEach((Item item) => {
99
+ VElement slot = VElement.Mount(item); // item : Component, e.g. its own `public string Id;`
100
+ slot.Id = item.Id; // stable key for reordering
101
+ list.AppendChild(slot);
102
+ });
103
+ ```
104
+ A template does the same via `<li *for="Item i of Items" *mount="i"></li>`.
105
+
106
+ Content projection (React's `children`): pass a `() => VElement` into a constructor, call
107
+ it from `Render()` — no separate mechanism needed.
108
+
109
+ ## Services — no DI container
110
+
111
+ A service is a plain class; "injecting" it is a constructor argument. Wire everything once
112
+ in a composition root:
113
+
114
+ ```ks
115
+ class AppContainer {
116
+ public Router Nav;
117
+ constructor() {
118
+ CounterService counter = new CounterService();
119
+ this.Nav = new Router(new NotFoundPage());
120
+ this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
121
+ }
122
+ }
123
+ ```
124
+
125
+ ## Router
126
+
127
+ ```ks
128
+ Router nav = new Router(new NotFoundPage()); // fallback page, required
129
+ nav.AddRoute("/", new HomePage(nav));
130
+ nav.AddRoute("/dogs/:id", new DogDetailPage(nav)); // nav.Param inside that page
131
+ nav.SetGuard("/login", (string path) => {
132
+ if (path == "/admin") { return authService.LoggedIn; }
133
+ return true;
134
+ });
135
+ nav.Navigate("/dogs/1");
136
+ nav.Mount(document.body);
137
+ ```
138
+ - Routes hold already-built `Component`s, not factories — built once, state survives
139
+ navigating away and back.
140
+ - `nav.Param` — the first `:name` segment, a plain `string` (not `state<T>`; the outlet
141
+ re-renders on every navigation already).
142
+ - `SetGuard(redirectPath, guard)`: one guard for the whole router; `guard` returns `false`
143
+ to redirect. Defaults to always-allow.
144
+ - `AddLazyRoute(path, loader)` for real code-splitting — see "Lazy routes" below.
145
+ - Every deploy target needs its own SPA fallback (serve `index.html` for any route with no
146
+ matching file) — a plain HTTP limitation, not a Kopular one.
147
+
148
+ ## Forms
149
+
150
+ ```ks
151
+ FormField<string> email = new FormField<string>("", (string v) => {
152
+ string? required = Validators.Required(v);
153
+ if (required != null) { return required; }
154
+ return Validators.Email(v);
155
+ });
156
+ email.Value.Value = "not-an-email";
157
+ print(email.Error.Value); // "Must be a valid email"
158
+ ```
159
+ `Validators`: `Required/MinLength/MaxLength/Email/Min/Max`, each returns an error message
160
+ or `null`. No array-of-validators param — chain checks in one lambda, as above.
161
+
162
+ ## HTTP
163
+
164
+ ```ks
165
+ Response r = await Http.Get("/api/dogs");
166
+ if (r.ok) { string body = await r.text(); }
167
+ await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
168
+ ```
169
+ No typed JSON deserialization built in — describe the shape as `extern class` and parse
170
+ with `extern MyShape Parse(string json) as "JSON.parse";`.
171
+
172
+ ## Async: `Delay`, `Computed`, `Resource<T>`
173
+
174
+ ```ks
175
+ await Delay(500); // real setTimeout-backed delay
176
+
177
+ Computed2<number, number, number> total = new Computed2<number, number, number>(
178
+ qty, price, (number q, number p) => q * p
179
+ );
180
+ total.Value.Subscribe((number v) => this.Update()); // total.Value is itself state<number>
181
+
182
+ Resource<Response> r = new Resource<Response>(Http.Get(url)); // task already in flight
183
+ match r.Status.Value {
184
+ AsyncStatus.Loading => BuildSpinner(),
185
+ AsyncStatus.Success => BuildContent(r.Data.Value),
186
+ AsyncStatus.Failure => BuildError(r.Error.Value)
187
+ };
188
+ ```
189
+
190
+ ## Scoped styles
191
+
192
+ ```ks
193
+ class Widget : Component {
194
+ template from "./widget.html";
195
+ styles from "./widget.css";
196
+ }
197
+ ```
198
+ Every selector in `widget.css` is rewritten to only match this class's own elements —
199
+ never a sibling's or child's. Needs a constructor to exist on the class.
200
+
201
+ ## Lazy routes (real code-splitting)
202
+
203
+ ```js
204
+ // admin_page_loader.js — hand-written, not compiled from .ks
205
+ export async function LoadAdminPage() {
206
+ const { AdminPage } = await import("./admin_page.js");
207
+ return new AdminPage();
208
+ }
209
+ ```
210
+ ```ks
211
+ extern task<Component> LoadAdminPage() from "./admin_page_loader";
212
+ nav.AddLazyRoute("/admin", LoadAdminPage);
213
+ ```
214
+ The lazy page (`admin_page.ks`) must be built as its own entry too — it's deliberately not
215
+ `using`'d from your app's entry (that's what keeps it out of the eager bundle), so add a
216
+ second build line: `ks build src/app.ks && ks build src/admin_page.ks`. Testing it via
217
+ `kopular/testing`'s `runKopularApp` needs no extra setup — it compiles any file in your
218
+ `srcDir` the entry doesn't reach.
219
+
220
+ ## Testing
221
+
222
+ ```js
223
+ import { runKopularApp } from "kopular/testing";
224
+ const { window, cleanup } = await runKopularApp(srcDir, "app.ks", { includeKopularPackage: true });
225
+ // assert against window.document, then:
226
+ cleanup();
227
+ ```
228
+ Real jsdom, real compile, real DOM assertions — not a mock.
229
+
230
+ ## Common mistakes
231
+
232
+ - Forgetting `this.` on a member reference inside a lambda — it's a real undefined-identifier
233
+ error, not automatic.
234
+ - `if (x == null) { return; } use(x);` does NOT narrow `x` — see "Nullable" above.
235
+ - A two-way binding target must be a field path (`Field`, `this.Field`, `Field.Value`), never
236
+ a method call.
237
+ - `*mount` accepts only `id`/`[id]` on its element — any other attr/binding is a compile
238
+ error, since the mounted child's own `Render()` owns all of its content.
package/LLM.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Kopular — LLM reference
2
2
 
3
3
  Complete reference for generating correct Kopular code. This is a spec, not a tutorial —
4
- see `README.md` for narrative/rationale. Kopular is 10 files total; this covers all of
5
- them. For the host language, see KopScript's own `LLM.md` in the `KopScript` repo (or its
6
- published `LLM.md` on the `kopscript` npm package) that reference is a prerequisite,
7
- not repeated here.
4
+ see `README.md` for narrative/rationale, or this package's own **`GUIDE.md`** for a short,
5
+ task-oriented page covering everything a typical app needs (start there for most tasks;
6
+ come back here for anything it doesn't cover). Kopular is 10 files total; this covers all
7
+ of them. For the host language, see KopScript's own `LLM.md` in the `KopScript` repo (or
8
+ its published `LLM.md` on the `kopscript` npm package, `kopscript@1.1.0`+) — that
9
+ reference is a prerequisite, not repeated here.
8
10
 
9
11
  Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
10
12
  `kopular/velement` (VElement — what `Render()` returns; see "Component" below),
@@ -13,30 +15,50 @@ Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Compo
13
15
  (Computed1, Computed2), `kopular/resource` (Resource, AsyncStatus), `kopular/vdom`
14
16
  (`ScopedStyles` — the runtime half of `styles from`; everything else in this file, the
15
17
  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
18
- <dir>` scaffolds a new project (the `extern` bindings below, plus the vendor/serve
19
- scripts needed to run in a browser) rather than requiring it be reconstructed by hand;
20
- prefer it over hand-writing the section below for a new project. Templates (`template
21
- from "./x.html";`, see below) are a KopScript language feature, not a Kopular export —
22
- there is no `kopular/template` entry point to import.
18
+ directly), `kopular/timers` (`Delay`), `kopular/testing` (runKopularApp,
19
+ runKopularFixture — see below). Also ships a bin, `kp` — `npx kp new <dir>` scaffolds a
20
+ new project (an `index.html` with every module already wired, a `Counter` component, and
21
+ the vendor/serve scripts needed to run in a browser). Templates (`template from
22
+ "./x.html";`, see below) are a KopScript language feature, not a Kopular export there is
23
+ no `kopular/template` entry point to import.
23
24
 
24
25
  ## Consuming Kopular from your own KopScript project
25
26
 
26
- `using` only resolves same-project relative paths reaching into an npm package (Kopular
27
- included) always goes through `extern`, re-describing exactly the members you use.
28
-
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
27
+ `using "kopular";` (`kopscript@1.1.0`+) resolves this package's own declarations
28
+ everything in the "Entry points" list above — through `node_modules`, the same way `npm
29
+ install` already makes them available to plain JS. One line, every Kopular type in scope,
30
+ each consuming file's own compiled output importing only the bindings it actually
31
+ references (never the whole surface). This is the normal way to consume Kopular now; `kp
32
+ new` generates it by default.
33
+
34
+ ```ks
35
+ using "kopular";
36
+ using "./counter";
37
+
38
+ Counter app = new Counter();
39
+ app.Mount(document.body);
40
+ ```
41
+
42
+ `document`/`Element`/`Event`/etc. come from the same `using "kopular";` — no separate ambient
43
+ block needed. The rest of this file describes every one of those bindings' real shapes, for
44
+ when you need something `GUIDE.md` doesn't cover, or need to know exactly what a type looks
45
+ like.
46
+
47
+ ### The hand-copied `extern` block (older projects, or trimming what you import)
48
+
49
+ Before `kopscript@1.1.0`, every consuming project hand-declared its own copy of Kopular's
50
+ `extern` bindings (`using` couldn't reach into a package). That still works — a real,
51
+ hand-written `extern` block is exactly as valid as one resolved via `using "kopular";",
52
+ just more to maintain — and is occasionally still the right call if you want a visibly
53
+ trimmed subset. **Don't reach for `Element`/`Document`/`Event` via `extern ... from
54
+ "kopular/dom";`, even though that actually works** (`kopular/dom` genuinely re-exports real
55
+ `globalThis` bindings, same as any other Kopular export) — Kopular's own internal `dom.ks`
56
+ only declares what Kopular's own framework code itself touches, a smaller surface than a
57
+ real app typically needs. `using "kopular";`'s own declarations (`src/kopular.ks` in this
58
+ package) don't have that problem — they're written for consumers, not just for Kopular's
59
+ own internals — so prefer it over a hand-copied ambient block for new code. If you do
60
+ hand-copy one anyway, copy the block below rather than hand-rolling a smaller one from
61
+ scratch and adding members as compile errors demand them — a real, complete first-attempt
40
62
  implementation of a Kopular app hit the exact same missing property (`Element.value`)
41
63
  twice from two independently-trimmed subsets, because the compile error only ever names
42
64
  the one member actually touched, never warns that a *sibling* feature (a template's
@@ -95,6 +117,10 @@ extern class VElement {
95
117
  string Id { get; set; }
96
118
  string Value { get; set; }
97
119
  string RawHtml { get; set; }
120
+ // Real bool properties, not attribute strings — a boolean attribute is on
121
+ // whenever it's present at all, so only a real property can turn it back off.
122
+ bool Disabled { get; set; }
123
+ bool Checked { get; set; }
98
124
  (Event) => void OnClick { get; set; }
99
125
  (Event) => void OnInput { get; set; }
100
126
  (Event) => void OnBlur { get; set; }
package/README.md CHANGED
@@ -836,11 +836,11 @@ error, not a runtime blank screen.
836
836
 
837
837
  ## Starting a new project: `kp new`
838
838
 
839
- Everything in the next section the `extern` bindings, plus a `vendor/kopular/` copy of
840
- this package's browser files and an import map pointing at it (a browser can't resolve a
841
- bare specifier like `"kopular/component"` the way Node's own module resolution does) is
842
- boilerplate every Kopular project needs verbatim. Generate it instead of reconstructing it
843
- by hand (or from memory, if you're an AI agent):
839
+ `npx kp new my-app` scaffolds a real, working project: a `Counter` component
840
+ (`src/counter.ks`), an `index.html` with every Kopular module already mapped, and the
841
+ vendor/serve scripts needed to run in a browser (a browser can't resolve a bare specifier
842
+ like `"kopular/component"` the way Node's own module resolution does, so `vendor-kopular.mjs`
843
+ copies Kopular's runtime into `vendor/` and `index.html`'s import map points there).
844
844
 
845
845
  ```bash
846
846
  npx kp new my-app
@@ -849,37 +849,19 @@ npm install
849
849
  npm start # builds, vendors kopular's browser files, and serves at :8080
850
850
  ```
851
851
 
852
- This scaffolds a real, working `Component` (`src/counter.ks` the same Counter shown
853
- above), the ambient DOM/Kopular `extern` bindings it needs (`src/kopular_bindings.ks`),
854
- and a `README.md` that points an AI agent at this package's own `LLM.md` before it starts
852
+ Its `README.md` points an AI agent at this package's own `GUIDE.md` before it starts
855
853
  generating code. `kp` ships from this package (not from `kopscript`'s own `ks` CLI) since
856
854
  scaffolding a *Kopular* app is a framework concern, not a language one — `ks` stays a
857
855
  pure-language tool with no framework knowledge baked in.
858
856
 
859
857
  ## Using Kopular from another KopScript project
860
858
 
861
- KopScript's own `using "./path";` only resolves relative paths within a project — it has
862
- no package-import mechanism yet. Cross-package consumption goes through `extern`
863
- instead, the same way KopScript already describes any other JS/npm dependency
864
- (`kp new` above generates exactly this, if you'd rather not hand-write it):
859
+ `using "kopular";` (`kopscript@1.1.0`+) resolves this package's own KopScript declarations
860
+ through `node_modules` one line, every Kopular type in scope, with each file's own
861
+ compiled output importing only what it actually references:
865
862
 
866
863
  ```ks
867
- extern class VElement {
868
- static VElement Create(string tag);
869
- string TextContent { get; set; }
870
- } from "kopular/velement";
871
-
872
- extern class Component {
873
- constructor();
874
- virtual VElement Render();
875
- void Mount(Element parent);
876
- } from "kopular/component";
877
-
878
- extern class Router {
879
- constructor(Component notFoundPage);
880
- void AddRoute(string path, Component page);
881
- void Navigate(string path);
882
- } from "kopular/router";
864
+ using "kopular";
883
865
 
884
866
  class MyWidget : Component {
885
867
  public override VElement Render() {
@@ -890,19 +872,14 @@ class MyWidget : Component {
890
872
  }
891
873
  ```
892
874
 
893
- Marking `Render()` `virtual` in the `extern` declaration is what lets a real subclass
894
- `override` it see [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)
895
- for a full working example (components, a service, and routing, all consuming Kopular
896
- this way).
875
+ See [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) for
876
+ a full working example (components, a service, and routing).
897
877
 
898
- The snippet above is illustrative, not exhaustive real code needs a fuller
899
- `VElement`/`Component`/`Router`, and `Element` itself (used above as `Mount`'s
900
- parameter type but never shown declared) is a plain ambient browser global your own
901
- project declares, not something Kopular exports — `LLM.md`'s "Consuming Kopular from
902
- your own KopScript project" section has the complete, copy-ready block for all of
903
- these, `Element`/`Document`/`Event` included. `npx kp new` (above) generates this
904
- boilerplate for a fresh project either way — reach for `LLM.md`'s block when adding to
905
- an existing one instead.
878
+ Before `kopscript@1.1.0`, every project hand-declared its own copy of Kopular's `extern`
879
+ bindings instead `using` couldn't reach into a package yet. That still works (`kp new`
880
+ above can generate it), and is still the right call if you want a visibly trimmed subset —
881
+ see `LLM.md`'s "Consuming Kopular from your own KopScript project" for the complete,
882
+ copy-ready block, `Element`/`Document`/`Event` included.
906
883
 
907
884
  `extern class` can carry its own `<T>` (kopscript >= 0.5.0), so a generic export like
908
885
  `FormField<T>` describes the same way a real generic class does — see `LLM.md`'s