kopular 0.3.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 CHANGED
@@ -1,13 +1,14 @@
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 4 files total; this covers all of
4
+ see `README.md` for narrative/rationale. Kopular is 6 files total; this covers all of
5
5
  them. For the host language, see KopScript's own `LLM.md` in the `Kop` repo (or its
6
6
  published `LLM.md` on the `kopscript` npm package) — that reference is a prerequisite,
7
7
  not repeated here.
8
8
 
9
9
  Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Component),
10
- `kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives` (If).
10
+ `kopular/router` (Router), `kopular/dom` (ambient DOM bindings), `kopular/directives`
11
+ (If), `kopular/http` (Http).
11
12
 
12
13
  ## Consuming Kopular from your own KopScript project
13
14
 
@@ -30,6 +31,19 @@ extern class Router {
30
31
  } from "kopular/router";
31
32
 
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";
33
47
  ```
34
48
 
35
49
  You also need your own ambient DOM `extern` block (`document`, `Element`, `Event`, ...) —
@@ -145,6 +159,38 @@ that needs comparing old/new data by a caller key, generic over item type, and K
145
159
  has no generics. Not planned as a workaround; would need real language-level generics
146
160
  first.
147
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
+
148
194
  ## Dependency injection — no container, no decorators
149
195
 
150
196
  There is no injector, no `@Injectable`, no provider tokens. "Injecting" a service is
@@ -194,5 +240,5 @@ class CounterService {
194
240
  DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a template
195
241
  language/DSL — everything is imperative `Render()` code against plain DOM bindings ·
196
242
  vdom diffing / reconciliation beyond a single component's own re-render · pipes ·
197
- animations · forms/validation module · HTTP client · a CLI/scaffolding tool (`ng
198
- generate`-equivalent) · SSR.
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
@@ -33,6 +33,9 @@ Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./
33
33
  a subtree conditionally, repeat one per item, pick one of several cases — done as plain
34
34
  function calls (`If(...)`) and existing KopScript expressions (`array.ForEach(...)`,
35
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.
36
39
 
37
40
  ## What's here
38
41
 
@@ -42,8 +45,11 @@ Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./
42
45
  - `src/router.ks` — the `Router`.
43
46
  - `src/directives.ks` — `If()`, the structural-directive equivalents' one genuinely new
44
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.
45
51
 
46
- That's the whole framework — four files. Everything else (a real app built on top of it)
52
+ That's the whole framework — six files. Everything else (a real app built on top of it)
47
53
  lives in a separate consumer repo, [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo).
48
54
 
49
55
  ## Dependency injection: the composition root pattern
@@ -179,6 +185,56 @@ caller-supplied key, generic over the item type — and KopScript has no generic
179
185
  per list, but that's real vdom-diffing work — already called out as out of scope in
180
186
  "Status" below, and not something these three lines take on.
181
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
+ ```
227
+
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).
237
+
182
238
  ## Using Kopular from another KopScript project
183
239
 
184
240
  KopScript's own `using "./path";` only resolves relative paths within a project — it has
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "0.3.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>",
@@ -21,7 +21,8 @@
21
21
  "./component": "./src/component.js",
22
22
  "./router": "./src/router.js",
23
23
  "./dom": "./src/dom.js",
24
- "./directives": "./src/directives.js"
24
+ "./directives": "./src/directives.js",
25
+ "./http": "./src/http.js"
25
26
  },
26
27
  "files": [
27
28
  "src",
@@ -29,13 +30,13 @@
29
30
  "LLM.md"
30
31
  ],
31
32
  "scripts": {
32
- "build": "ks build src/router.ks && ks build src/directives.ks",
33
+ "build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks",
33
34
  "prepublishOnly": "npm run build",
34
35
  "test": "vitest run",
35
36
  "test:watch": "vitest"
36
37
  },
37
38
  "devDependencies": {
38
- "kopscript": "^0.2.0",
39
+ "kopscript": "^0.3.0",
39
40
  "@types/jsdom": "^30.0.0",
40
41
  "@types/node": "^20.14.0",
41
42
  "jsdom": "^25.0.1",
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
+ }