kopular 0.2.0 → 0.4.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 ADDED
@@ -0,0 +1,244 @@
1
+ # Kopular — LLM reference
2
+
3
+ Complete reference for generating correct Kopular code. This is a spec, not a tutorial —
4
+ see `README.md` for narrative/rationale. Kopular is 6 files total; this covers all of
5
+ them. For the host language, see KopScript's own `LLM.md` in the `Kop` repo (or its
6
+ published `LLM.md` on the `kopscript` npm package) — that reference is a prerequisite,
7
+ not repeated here.
8
+
9
+ Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
10
+ `kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives`
11
+ (If), `kopular/http` (Http).
12
+
13
+ ## Consuming Kopular from your own KopScript project
14
+
15
+ `using` only resolves same-project relative paths — reaching into an npm package (Kopular
16
+ included) always goes through `extern`, re-describing exactly the members you use:
17
+
18
+ ```ks
19
+ extern class Component {
20
+ constructor();
21
+ virtual Element Render(); // `virtual` here is what lets your subclass `override` it
22
+ void Mount(Element parent);
23
+ void Update();
24
+ } from "kopular/component";
25
+
26
+ extern class Router {
27
+ constructor(Component notFoundPage);
28
+ void AddRoute(string path, Component page);
29
+ void Navigate(string path);
30
+ void Mount(Element parent);
31
+ } from "kopular/router";
32
+
33
+ extern Element If(bool condition, () => Element whenTrue, () => Element whenFalse) from "kopular/directives";
34
+
35
+ extern class Response {
36
+ bool ok { get; }
37
+ number status { get; }
38
+ task<string> text(); // no `async` on an extern signature — see KopScript's own LLM.md
39
+ };
40
+ extern class Http {
41
+ static task<Response> Get(string url);
42
+ static task<Response> Post(string url, string jsonBody);
43
+ static task<Response> Put(string url, string jsonBody);
44
+ static task<Response> Patch(string url, string jsonBody);
45
+ static task<Response> Delete(string url);
46
+ } from "kopular/http";
47
+ ```
48
+
49
+ You also need your own ambient DOM `extern` block (`document`, `Element`, `Event`, ...) —
50
+ Kopular's own copy in `dom.ks` isn't reachable across the package boundary; redeclare the
51
+ handful of members you actually use. See KopularDemo's `src/kopular_bindings.ks` for a
52
+ complete, real example of both.
53
+
54
+ ## `Component` (`component.ks`)
55
+
56
+ ```ks
57
+ class MyWidget : Component {
58
+ private number count;
59
+
60
+ constructor() : base() {
61
+ this.count = 0;
62
+ }
63
+
64
+ public override Element Render() {
65
+ Element el = document.createElement("div");
66
+ el.textContent = "Count: " + this.count;
67
+ return el;
68
+ }
69
+
70
+ public void Bump() {
71
+ this.count = this.count + 1;
72
+ this.Update(); // re-runs Render(), swaps the old root for the new one
73
+ }
74
+ }
75
+
76
+ MyWidget w = new MyWidget();
77
+ w.Mount(document.body); // first Render() + append
78
+ w.Bump(); // re-render
79
+ ```
80
+
81
+ - `Render()`: `virtual`, override it to build a fresh DOM subtree from current state.
82
+ Called by both `Mount()` and `Update()`. **No template language, no diffing** — every
83
+ call rebuilds the whole subtree from scratch.
84
+ - `Mount(parent)`: calls `Render()` once, appends the result to `parent`, remembers both
85
+ for `Update()` to use later.
86
+ - `Update()` (protected — called from within the component, not externally): calls
87
+ `Render()` again and `replaceChild`s the old root with the new one.
88
+ - **Known limitation**: if a *parent* component's `Update()` runs while it has mounted
89
+ children, those children are NOT automatically re-mounted into the new parent tree —
90
+ `Component` only handles a single component's own re-render cycle, not tree
91
+ reconciliation. Compose independent components into stable slots (see `Router`'s own
92
+ pattern of keeping page instances alive) to avoid this rather than nesting components
93
+ that both re-render.
94
+
95
+ ## `Router` (`router.ks`)
96
+
97
+ Real URLs via the History API (`pushState`/`popstate`), not hash routing.
98
+
99
+ ```ks
100
+ class NotFoundPage : Component {
101
+ public override Element Render() { /* ... */ }
102
+ }
103
+ class HomePage : Component {
104
+ private Router Nav;
105
+ constructor(Router nav) : base() { this.Nav = nav; }
106
+ public override Element Render() {
107
+ Element btn = document.createElement("button");
108
+ btn.addEventListener("click", (Event e) => { this.Nav.Navigate("/about"); });
109
+ return btn;
110
+ }
111
+ }
112
+
113
+ Router nav = new Router(new NotFoundPage()); // NotFoundPage REQUIRED up front — no null route
114
+ nav.AddRoute("/", new HomePage(nav)); // pages built once, kept alive for Router's lifetime
115
+ nav.AddRoute("/about", new AboutPage(nav));
116
+ nav.Mount(document.body);
117
+ nav.Navigate("/about"); // pushState + immediate re-render
118
+ ```
119
+
120
+ - Constructor takes the fallback page (`Component`), required — there's no nullable
121
+ "no match" representation.
122
+ - `AddRoute(path, page)` takes an **already-constructed `Component` instance**, not a
123
+ factory — every registered page is built once and stays alive for the Router's whole
124
+ lifetime, so a page's own `state<T>` fields survive navigating away and back.
125
+ - `Navigate(path)` calls `history.pushState` then re-renders immediately. A browser
126
+ back/forward triggers re-render via a `popstate` listener registered in the
127
+ constructor — `Navigate()` itself doesn't rely on that event.
128
+ - Needs a server that falls back to the app shell for any unrecognized path (a plain
129
+ static server has nothing to serve at `/about` on direct load/refresh).
130
+
131
+ ## `If` (`directives.ks`) — the `*ngIf` equivalent
132
+
133
+ ```ks
134
+ Element If(bool condition, () => Element whenTrue, () => Element whenFalse)
135
+ ```
136
+
137
+ ```ks
138
+ root.appendChild(If(this.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLogin()));
139
+ ```
140
+
141
+ Both branches always required (no null "nothing" value); only the branch actually taken
142
+ is called — the other lambda never runs. `*ngFor` and `*ngSwitch` need no Kopular helper
143
+ at all:
144
+
145
+ ```ks
146
+ // *ngFor — plain array method
147
+ this.Items.ForEach((Item item) => { list.appendChild(this.BuildItemRow(item)); });
148
+
149
+ // *ngSwitch — plain KopScript `match` expression (exhaustiveness-checked, unlike *ngSwitch)
150
+ root.appendChild(match this.Status {
151
+ "loading" => this.BuildSpinner(),
152
+ "error" => this.BuildError(),
153
+ _ => this.BuildContent()
154
+ });
155
+ ```
156
+
157
+ No keyed/reuse-existing-DOM-nodes diffing (the performance angle of `*ngFor trackBy`) —
158
+ that needs comparing old/new data by a caller key, generic over item type, and KopScript
159
+ has no generics. Not planned as a workaround; would need real language-level generics
160
+ first.
161
+
162
+ ## `Http` (`http.ks`) — thin wrapper over `fetch`
163
+
164
+ ```ks
165
+ Response r = await Http.Get(url); // task<Response>
166
+ Response r = await Http.Post(url, jsonBody); // string body, Content-Type: application/json
167
+ Response r = await Http.Put(url, jsonBody);
168
+ Response r = await Http.Patch(url, jsonBody);
169
+ Response r = await Http.Delete(url); // no body param — DELETE has none
170
+
171
+ r.ok // bool
172
+ r.status // number
173
+ await r.text(); // task<string> — the raw body, nothing more
174
+ ```
175
+
176
+ **No typed JSON deserialization** — no generics means no safe `Get<T>(url): task<T>`.
177
+ Get a typed response by describing its shape as its own `extern class` and parsing with
178
+ a per-shape `extern ... as "JSON.parse"` (unchecked, same trust model as every other
179
+ `extern`):
180
+
181
+ ```ks
182
+ extern class DogDto { string name { get; } };
183
+ extern DogDto ParseDog(string json) as "JSON.parse";
184
+
185
+ DogDto dog = ParseDog(await (await Http.Get(url)).text());
186
+ ```
187
+
188
+ `Get`/`Delete` need no request body, so they bind straight to the real global `fetch` —
189
+ no object literal involved (KopScript has none). `Post`/`Put`/`Patch` (and a
190
+ hypothetical `Delete`-with-a-body) need one for `{ method, headers, body }`, which
191
+ KopScript categorically cannot construct — Kopular ships one small hand-written JS
192
+ function (`http_runtime.js`, not compiled from `.ks`) that does, for exactly that reason.
193
+
194
+ ## Dependency injection — no container, no decorators
195
+
196
+ There is no injector, no `@Injectable`, no provider tokens. "Injecting" a service is
197
+ passing it as a constructor argument — the compiler enforces it (a missing/mistyped
198
+ dependency is a build error). For an app with more than a couple of services, use one
199
+ plain "composition root" class (not a `Component`, never touches the DOM) that builds
200
+ the whole service/page graph exactly once and hands the finished pieces to whatever
201
+ needs them:
202
+
203
+ ```ks
204
+ class AppContainer {
205
+ public Router Nav;
206
+ constructor() {
207
+ CounterService counter = new CounterService(); // shared singleton: pass the same
208
+ this.Nav = new Router(new NotFoundPage()); // instance wherever it should be
209
+ this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
210
+ }
211
+ }
212
+
213
+ class RoutedApp : Component {
214
+ private Router Nav;
215
+ constructor(AppContainer services) : base() { this.Nav = services.Nav; }
216
+ public override Element Render() {
217
+ Element el = document.createElement("div");
218
+ this.Nav.Mount(el);
219
+ return el;
220
+ }
221
+ }
222
+
223
+ RoutedApp app = new RoutedApp(new AppContainer());
224
+ app.Mount(document.body);
225
+ ```
226
+
227
+ A service needing its own state/logic is a plain class — no base class, no
228
+ decorators, nothing framework-specific:
229
+
230
+ ```ks
231
+ class CounterService {
232
+ public state<number> Count;
233
+ constructor() { this.Count = state(0); }
234
+ public void Increment() { this.Count.Value = this.Count.Value + 1; }
235
+ }
236
+ ```
237
+
238
+ ## Does not exist
239
+
240
+ DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a template
241
+ language/DSL — everything is imperative `Render()` code against plain DOM bindings ·
242
+ vdom diffing / reconciliation beyond a single component's own re-render · pipes ·
243
+ animations · forms/validation module · typed/generic HTTP responses (`Http` returns raw
244
+ text — see above) · a CLI/scaffolding tool (`ng generate`-equivalent) · SSR.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <img src="https://cdn.jsdelivr.net/npm/kopular@latest/assets/logo.png" width="96" height="96" alt="Kopular logo">
2
+
1
3
  # Kopular
2
4
 
3
5
  Kopular is a small component framework for [KopScript](https://dev.azure.com/koppinator/Koppindependence/_git/Kop),
@@ -5,6 +7,9 @@ built to give Angular's separation of concerns — components own UI, services o
5
7
  a router owns navigation — without Angular's steepest learning-curve pieces: no RxJS, no
6
8
  dependency-injection container, no template DSL.
7
9
 
10
+ Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./LLM.md)**
11
+ — a dense, complete reference designed to be loaded straight into an LLM's context.
12
+
8
13
  ## Highlights
9
14
 
10
15
  - **`Component`**: a base class with `virtual Render()` (builds a fresh DOM subtree from
@@ -24,16 +29,211 @@ dependency-injection container, no template DSL.
24
29
  DSL. Needs a server that falls back to the app shell for unrecognized paths — see
25
30
  [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)'s
26
31
  `scripts/serve.mjs`.
32
+ - **Structural directives, no template DSL**: `*ngIf`/`*ngFor`/`*ngSwitch`'s job — build
33
+ a subtree conditionally, repeat one per item, pick one of several cases — done as plain
34
+ function calls (`If(...)`) and existing KopScript expressions (`array.ForEach(...)`,
35
+ `match`), not special template syntax. See "Structural directives" below.
36
+ - **`Http`**: a thin, static wrapper over the real Fetch API (`Http.Get(url)`,
37
+ `Http.Post(url, jsonBody)`, ...) — no HttpClient injection tokens, no RxJS
38
+ observables/operators. See "HTTP" below.
27
39
 
28
40
  ## What's here
29
41
 
30
42
  - `src/dom.ks` — ambient DOM bindings (`document`, `Element`, `Event`, `window`,
31
- `location`) that `component.ks`/`router.ks` are built on.
43
+ `location`) that `component.ks`/`router.ks`/`directives.ks` are built on.
32
44
  - `src/component.ks` — the `Component` base class.
33
45
  - `src/router.ks` — the `Router`.
46
+ - `src/directives.ks` — `If()`, the structural-directive equivalents' one genuinely new
47
+ piece (see below).
48
+ - `src/http.ks` — `Http`, a thin wrapper over `fetch` (see below). `src/http_runtime.js`
49
+ is its one companion file — the single hand-written (not compiled from `.ks`) file in
50
+ Kopular, and why is explained in its own header comment.
51
+
52
+ That's the whole framework — six files. Everything else (a real app built on top of it)
53
+ lives in a separate consumer repo, [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo).
54
+
55
+ ## Dependency injection: the composition root pattern
56
+
57
+ Kopular has no injector because KopScript has nothing for one to hook into — no
58
+ decorators, no reflection, no generics for a type-safe `Resolve<T>()`. Instead, the
59
+ whole app's service/page graph gets built exactly once, by hand, in one place: a plain
60
+ class with no `Component` base and no framework code in it at all, sometimes called an
61
+ **app container** or (in the wider DI literature) a **composition root**. Everything
62
+ else just takes what it needs as constructor arguments and never constructs its own
63
+ dependencies.
64
+
65
+ ```ks
66
+ // app_container.ks — the one place that decides what's shared and builds
67
+ // the graph, in dependency order.
68
+ class AppContainer {
69
+ public Router Nav;
70
+
71
+ constructor() {
72
+ // Built once, passed to every page that needs it below — that's the
73
+ // whole mechanism for a shared singleton. A page that constructed its
74
+ // own `new CounterService()` instead would get an independent one; the
75
+ // difference is which variable gets passed in, not a config flag.
76
+ CounterService counter = new CounterService();
77
+
78
+ this.Nav = new Router(new NotFoundPage());
79
+ this.Nav.AddRoute("/", new HomePage(this.Nav, counter));
80
+ this.Nav.AddRoute("/about", new AboutPage(this.Nav));
81
+ }
82
+ }
83
+
84
+ // routed_app.ks — the root Component. Takes the already-built graph; never
85
+ // builds one of its own.
86
+ class RoutedApp : Component {
87
+ private Router Nav;
88
+
89
+ constructor(AppContainer services) : base() {
90
+ this.Nav = services.Nav;
91
+ }
92
+
93
+ public override Element Render() {
94
+ Element container = document.createElement("div");
95
+ this.Nav.Mount(container);
96
+ return container;
97
+ }
98
+ }
99
+
100
+ RoutedApp app = new RoutedApp(new AppContainer());
101
+ app.Mount(document.body);
102
+ ```
103
+
104
+ This is sometimes called "Pure DI" — the same benefits a container gives you (nothing
105
+ hardcodes its own dependencies, everything is swappable in a test) with none of a
106
+ container's cost:
107
+
108
+ - **Compile-time checked.** A missing or mistyped dependency is `expected N arguments,
109
+ got M` from the KopScript compiler, not a `NullInjectorError` your users hit at
110
+ runtime after a container fails to resolve something.
111
+ - **Fully legible.** The entire dependency graph is ordinary, readable code in one file
112
+ — grep for `new` in the composition root and you've read the whole wiring diagram.
113
+ Nothing is constructed by a framework inspecting metadata behind the scenes.
114
+ - **No new concepts.** If you already know how to call a constructor, you already know
115
+ Kopular's DI story — there's no separate injector API, provider syntax, or
116
+ injection-token vocabulary to learn.
117
+
118
+ See [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo)'s
119
+ `src/app_container.ks` and `src/routed_app.ks` for the real, working version this
120
+ example is drawn from.
121
+
122
+ ## Structural directives
123
+
124
+ Angular's `*ngIf`/`*ngFor`/`*ngSwitch` are template syntax that expands, at compile time,
125
+ into imperative view-container calls. Kopular has no template compiler to expand
126
+ anything into (`Render()` is already imperative — see `component.ks`), so there's no
127
+ special syntax here either: each one maps onto a plain expression, and two of the three
128
+ need nothing new at all.
129
+
130
+ | Angular | Kopular | New code? |
131
+ | ----------------- | ------------------------------------------- | :-------: |
132
+ | `*ngFor` | `array.ForEach((item) => ...)` | none — already a KopScript array method |
133
+ | `*ngSwitch` | `match value { ... }` | none — already a KopScript expression, and exhaustiveness-checked (`*ngSwitch` isn't) |
134
+ | `*ngIf` / `*ngIf-else` | `If(condition, () => ..., () => ...)` | `directives.ks` |
135
+
136
+ `*ngIf` is the one case that needs something new: `if` is a *statement* in KopScript, so
137
+ without a helper you'd need a throwaway mutable local just to get a conditional value out
138
+ of it. `If()` is that helper — nothing more than:
139
+
140
+ ```ks
141
+ Element If(bool condition, () => Element whenTrue, () => Element whenFalse) {
142
+ if (condition) {
143
+ return whenTrue();
144
+ }
145
+ return whenFalse();
146
+ }
147
+ ```
148
+
149
+ Both branches are required (same reasoning `Router` uses for requiring a `NotFoundPage`
150
+ up front — see `router.ks`): v1 has no nullable types, so "render nothing" has no value
151
+ to hand back. Only the branch actually taken runs — the other lambda is never called, so
152
+ an explicit empty branch (`() => document.createElement("span")`) costs nothing when
153
+ there's genuinely nothing to show.
154
+
155
+ All three read the same way, right inside `Render()` — no template file, no directive
156
+ registration, nothing to import beyond the function itself:
157
+
158
+ ```ks
159
+ public override Element Render() {
160
+ Element root = document.createElement("div");
161
+
162
+ // *ngIf
163
+ root.appendChild(If(this.User.IsLoggedIn, () => this.BuildProfile(), () => this.BuildLoginButton()));
164
+
165
+ // *ngFor
166
+ Element list = document.createElement("ul");
167
+ this.Items.ForEach((Item item) => { list.appendChild(this.BuildItemRow(item)); });
168
+ root.appendChild(list);
169
+
170
+ // *ngSwitch
171
+ root.appendChild(match this.Status {
172
+ "loading" => this.BuildSpinner(),
173
+ "error" => this.BuildError(),
174
+ _ => this.BuildContent()
175
+ });
176
+
177
+ return root;
178
+ }
179
+ ```
180
+
181
+ Why not a general reuse-existing-DOM-nodes diffing layer, the way `*ngFor trackBy`
182
+ avoids rebuilding unchanged rows? That needs comparing old and new *data* items by a
183
+ caller-supplied key, generic over the item type — and KopScript has no generics (no
184
+ `class Foo<T>`, no `T Resolve<T>()`). A one-off keyed-diff helper could be hand-written
185
+ per list, but that's real vdom-diffing work — already called out as out of scope in
186
+ "Status" below, and not something these three lines take on.
187
+
188
+ ## HTTP
189
+
190
+ ```ks
191
+ using "./http";
192
+
193
+ Response r = await Http.Get("/api/dogs");
194
+ if (r.ok) {
195
+ string body = await r.text();
196
+ print(body);
197
+ }
198
+
199
+ await Http.Post("/api/dogs", "{\"name\":\"Rex\"}");
200
+ await Http.Put("/api/dogs/1", "{\"name\":\"Rexy\"}");
201
+ await Http.Patch("/api/dogs/1", "{\"name\":\"Max\"}");
202
+ await Http.Delete("/api/dogs/1");
203
+ ```
204
+
205
+ `Http` is a thin, static wrapper over the real Fetch API — `Get`/`Post`/`Put`/`Patch`/
206
+ `Delete`, each returning `task<Response>` (`.ok`, `.status`, `async text()`). No
207
+ `HttpClient` to inject, no RxJS `Observable`/operators, no interceptors — call it from
208
+ anywhere, including straight out of a service's own methods.
209
+
210
+ **No typed JSON deserialization** — `Response.text()` gets you the raw body, nothing
211
+ more. This isn't a corner cut for v1; it's a direct consequence of two things KopScript
212
+ doesn't have: generics (so there's no safe way to write a general `Get<T>(url):
213
+ task<T>`) and object-literal syntax (`{ ... }` as a value — see below). If you want a
214
+ typed response, describe its shape as its own `extern class` and parse it yourself with
215
+ a per-shape `extern ... as "JSON.parse"` declaration — the same trust-based approach
216
+ `extern` already uses for everything else, not a new mechanism:
217
+
218
+ ```ks
219
+ extern class DogDto {
220
+ string name { get; }
221
+ };
222
+ extern DogDto ParseDog(string json) as "JSON.parse";
223
+
224
+ string body = await (await Http.Get("/api/dogs/1")).text();
225
+ DogDto dog = ParseDog(body); // unchecked, like a TypeScript `as DogDto` cast
226
+ ```
34
227
 
35
- That's the whole framework three files. Everything else (a real app built on top of
36
- it) lives in a separate consumer repo, [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo).
228
+ **Why `Post`/`Put`/`Patch`/`Delete` aren't just `extern` bindings straight to `fetch`,
229
+ the way `Get` is**: setting a request method/body/headers means passing `fetch` a second
230
+ argument that's a plain JS object literal (`{ method, headers, body }`) — and KopScript
231
+ has no object-literal syntax at all, so it can't construct one. `src/http_runtime.js` is
232
+ one small hand-written function that does, and `Get`/`Delete`-with-no-body skip it
233
+ entirely (`fetch(url)` alone needs no options object, so `Get` binds straight to the
234
+ real global). It's the one file in this package not compiled from `.ks` — everywhere
235
+ else avoids the problem by only wrapping JS APIs that take plain positional arguments
236
+ (see `dom.ks`'s `addEventListener(string, handler)`, never an options-object-taking API).
37
237
 
38
238
  ## Using Kopular from another KopScript project
39
239
 
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "0.2.0",
4
- "description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, and routing, with no template DSL and no DI container",
3
+ "version": "0.4.0",
4
+ "description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, structural directives, and HTTP, with no template DSL and no DI container",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Joe Koppin <koppinjo@gmail.com>",
@@ -20,19 +20,23 @@
20
20
  ".": "./src/component.js",
21
21
  "./component": "./src/component.js",
22
22
  "./router": "./src/router.js",
23
- "./dom": "./src/dom.js"
23
+ "./dom": "./src/dom.js",
24
+ "./directives": "./src/directives.js",
25
+ "./http": "./src/http.js"
24
26
  },
25
27
  "files": [
26
- "src"
28
+ "src",
29
+ "assets",
30
+ "LLM.md"
27
31
  ],
28
32
  "scripts": {
29
- "build": "ks build src/router.ks",
33
+ "build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks",
30
34
  "prepublishOnly": "npm run build",
31
35
  "test": "vitest run",
32
36
  "test:watch": "vitest"
33
37
  },
34
38
  "devDependencies": {
35
- "kopscript": "^0.1.0",
39
+ "kopscript": "^0.3.0",
36
40
  "@types/jsdom": "^30.0.0",
37
41
  "@types/node": "^20.14.0",
38
42
  "jsdom": "^25.0.1",
@@ -0,0 +1,8 @@
1
+ import { Event, Element, Document, Location, History, Window, document, location, history, window } from "./dom.js";
2
+
3
+ export function If(condition, whenTrue, whenFalse) {
4
+ if (condition) {
5
+ return whenTrue();
6
+ }
7
+ return whenFalse();
8
+ }
@@ -0,0 +1,30 @@
1
+ using "./dom";
2
+
3
+ // Kopular's structural-directive equivalents — see the README's
4
+ // "Structural directives" section for the full *ngIf/*ngFor/*ngSwitch
5
+ // mapping. There's no template language here (Kopular doesn't have one, by
6
+ // design — see component.ks), so these are just plain functions: call them
7
+ // like any other expression from inside Render(), the same way you'd call
8
+ // document.createElement.
9
+ //
10
+ // *ngFor and *ngSwitch need nothing new — `array.ForEach(...)` and
11
+ // KopScript's own `match` expression already cover them (and `match` is
12
+ // exhaustiveness-checked, which *ngSwitch isn't). `If` below is the one
13
+ // piece the language doesn't already give you as an expression: `if` is a
14
+ // statement in KopScript, so without this you'd need a throwaway mutable
15
+ // local to get a conditional value.
16
+
17
+ // The *ngIf equivalent — conditionally build one of two subtrees, as an
18
+ // expression. Both branches are required: v1 has no nullable types, so
19
+ // "render nothing" has no value to return — the same reasoning Router uses
20
+ // for requiring a NotFoundPage up front (see router.ks) rather than letting
21
+ // "no match" be null. Only the branch actually taken runs; the other
22
+ // lambda is never called, so an explicit empty branch (e.g.
23
+ // `() => document.createElement("span")`) costs nothing when there's
24
+ // genuinely nothing to show.
25
+ Element If(bool condition, () => Element whenTrue, () => Element whenFalse) {
26
+ if (condition) {
27
+ return whenTrue();
28
+ }
29
+ return whenFalse();
30
+ }
package/src/http.js ADDED
@@ -0,0 +1,25 @@
1
+ export const Response = globalThis.Response;
2
+ export const FetchUrl = globalThis.fetch;
3
+ import { requestWithBody as RequestWithBody } from "./http_runtime.js";
4
+ export { RequestWithBody };
5
+ export class Http {
6
+ static async Get(url) {
7
+ return await FetchUrl(url);
8
+ }
9
+
10
+ static async Delete(url) {
11
+ return await RequestWithBody(url, "DELETE", null, "application/json");
12
+ }
13
+
14
+ static async Post(url, jsonBody) {
15
+ return await RequestWithBody(url, "POST", jsonBody, "application/json");
16
+ }
17
+
18
+ static async Put(url, jsonBody) {
19
+ return await RequestWithBody(url, "PUT", jsonBody, "application/json");
20
+ }
21
+
22
+ static async Patch(url, jsonBody) {
23
+ return await RequestWithBody(url, "PATCH", jsonBody, "application/json");
24
+ }
25
+ }
package/src/http.ks ADDED
@@ -0,0 +1,52 @@
1
+ // A thin wrapper over the real Fetch API — no object literals (KopScript
2
+ // has no syntax for one), no generics, no automatic JSON deserialization.
3
+ // `Response.Text()` gets you the raw body; for a typed JSON response,
4
+ // describe the shape as its own `extern class` and parse it with a
5
+ // per-shape `extern ... as "JSON.parse"` declaration (see Kopular's README)
6
+ // — the same trust-based approach `extern` already uses for everything
7
+ // else, not a new mechanism.
8
+
9
+ extern class Response {
10
+ bool ok { get; }
11
+ number status { get; }
12
+ task<string> text();
13
+ };
14
+
15
+ // GET/HEAD/DELETE-without-a-body need no options object at all, so they
16
+ // bind straight to the real global `fetch` — no runtime helper involved.
17
+ extern task<Response> FetchUrl(string url) as "fetch";
18
+
19
+ // POST/PUT/PATCH (and DELETE-with-a-body) need to set a method/body/
20
+ // headers, which does need an options object — the one thing in this file
21
+ // that isn't a direct, unassisted binding to a real JS global. See
22
+ // http_runtime.js for why, and for the only hand-written JS in this
23
+ // package. A relative path, not a package-name one: this is Kopular
24
+ // referencing its own sibling file (which isn't part of Kopular's public
25
+ // API — only Http's static methods below are), not a consumer reaching
26
+ // into Kopular from outside.
27
+ extern task<Response> RequestWithBody(string url, string method, string? body, string contentType) from "./http_runtime.js" as "requestWithBody";
28
+
29
+ // Static methods, not free functions, purely so call sites read as
30
+ // `Http.Get(url)` / `Http.Post(url, body)` — there's no instance state here
31
+ // to justify a real object.
32
+ class Http {
33
+ public static async task<Response> Get(string url) {
34
+ return await FetchUrl(url);
35
+ }
36
+
37
+ public static async task<Response> Delete(string url) {
38
+ return await RequestWithBody(url, "DELETE", null, "application/json");
39
+ }
40
+
41
+ public static async task<Response> Post(string url, string jsonBody) {
42
+ return await RequestWithBody(url, "POST", jsonBody, "application/json");
43
+ }
44
+
45
+ public static async task<Response> Put(string url, string jsonBody) {
46
+ return await RequestWithBody(url, "PUT", jsonBody, "application/json");
47
+ }
48
+
49
+ public static async task<Response> Patch(string url, string jsonBody) {
50
+ return await RequestWithBody(url, "PATCH", jsonBody, "application/json");
51
+ }
52
+ }
@@ -0,0 +1,15 @@
1
+ // The one hand-written file in Kopular — every other .js file here is
2
+ // compiled from a same-named .ks source. KopScript has no object-literal
3
+ // syntax, so it can't construct `fetch`'s second (options) argument itself;
4
+ // every other Kopular binding avoids this by only wrapping JS APIs that take
5
+ // plain positional arguments (see dom.ks). `fetch(url)` alone needs no
6
+ // options object at all (that's a plain `extern` in http.ks), but a request
7
+ // with a body/headers does — this function exists so http.ks has something
8
+ // with a real, callable, options-object-free signature to bind to.
9
+ export function requestWithBody(url, method, body, contentType) {
10
+ return fetch(url, {
11
+ method,
12
+ headers: body === null ? undefined : { "Content-Type": contentType },
13
+ body: body === null ? undefined : body,
14
+ });
15
+ }