kopular 0.10.0 → 0.11.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
@@ -12,7 +12,9 @@ Published as npm `kopular`. Entry points: `kopular` / `kopular/component` (Compo
12
12
  (runKopularApp, runKopularFixture — see below). Also ships a bin, `kp` — `npx kp new
13
13
  <dir>` scaffolds a new project (the `extern` bindings below, plus the vendor/serve
14
14
  scripts needed to run in a browser) rather than requiring it be reconstructed by hand;
15
- prefer it over hand-writing the section below for a new project.
15
+ prefer it over hand-writing the section below for a new project. Templates (`template
16
+ from "./x.html";`, see below) are a KopScript language feature, not a Kopular export —
17
+ there is no `kopular/template` entry point to import.
16
18
 
17
19
  ## Consuming Kopular from your own KopScript project
18
20
 
@@ -112,8 +114,10 @@ w.Bump(); // re-render
112
114
  ```
113
115
 
114
116
  - `Render()`: `virtual`, override it to build a fresh DOM subtree from current state.
115
- Called by both `Mount()` and `Update()`. **No template language, no diffing** — every
116
- call rebuilds the whole subtree from scratch.
117
+ Called by both `Mount()` and `Update()`. **No diffing either way** — every call
118
+ rebuilds the whole subtree from scratch. Provide it as a real markup file via
119
+ KopScript's `template from "./x.html";` (see "Templates" below) instead of hand-writing
120
+ it — both produce the exact same method; Kopular needed no code changes to support this.
117
121
  - `Mount(parent)`: calls `Render()` once, appends the result to `parent`, remembers both
118
122
  for `Update()` to use later.
119
123
  - `Update()` (protected — called from within the component, not externally): calls
@@ -141,6 +145,38 @@ w.Bump(); // re-render
141
145
  into the component) renders normally again — there's no separate "broken" state to
142
146
  reset, `SafeRender()` just tries `Render()` again like any other `Update()`.
143
147
 
148
+ ## Templates — `template from` (see KopScript's own `LLM.md` for the full syntax)
149
+
150
+ ```ks
151
+ class Counter : Component {
152
+ public state<number> Count;
153
+ constructor() : base() { this.Count = state(0); }
154
+ public void Increment() { this.Count.Value = this.Count.Value + 1; }
155
+ template from "./counter.html"; // replaces Render() entirely — cannot coexist with a hand-written one
156
+ }
157
+ ```
158
+ ```html
159
+ <!-- counter.html -->
160
+ <button (click)="Increment()">Count: {{ Count.Value }}</button>
161
+ ```
162
+
163
+ - `{{ expr }}` interpolation, `(event)="stmt"`, `[prop]="expr"`, `*if="expr"`,
164
+ `*for="Type varName of expr"` — all real KopScript, checked at compile time, desugared
165
+ to the exact same `document.createElement`/`.appendChild`/`.textContent`/
166
+ `.addEventListener` calls a hand-written `Render()` would use.
167
+ - A `state<T>` field declared directly on the class and referenced directly in the
168
+ template (`Count` above) gets `Subscribe((v) => this.Update())` wired automatically —
169
+ no manual `Subscribe` in the constructor for that field. State reached indirectly
170
+ (through a method, or `this.SomeService.Count`) still needs a manual `Subscribe`, same
171
+ as a hand-written `Render()` always has.
172
+ - One top-level element per template (hard error otherwise); no mixing text and element
173
+ children under one element (no text-node type in `dom.ks`, only `.textContent`); no
174
+ two-way binding, no pipes, at most one structural directive per element.
175
+ - This is entirely a KopScript compiler feature (parsed/desugared before type-checking
176
+ runs) — Kopular's own framework code (`component.ks`, `dom.ks`) is unmodified and
177
+ unaware templates exist; a template-generated `Render()` is indistinguishable from a
178
+ hand-written one to every other part of the framework.
179
+
144
180
  ## `Router` (`router.ks`)
145
181
 
146
182
  Real URLs via the History API (`pushState`/`popstate`), not hash routing.
@@ -217,7 +253,12 @@ nav.Navigate("/about"); // pushState + immediate re-ren
217
253
  "SPA fallback" / "custom 404 → index.html" option — look for that host's own docs on
218
254
  single-page-application routing, the terminology is standard across all of them.
219
255
 
220
- ## `If` (`directives.ks`) — the `*ngIf` equivalent
256
+ ## `If` (`directives.ks`) — the `*ngIf` equivalent for a hand-written `Render()`
257
+
258
+ In a **template**, `*if="expr"`/`*for="Type v of expr"` are the direct equivalents (real
259
+ `if`/`for` under the hood — see "Templates" above), no helper needed. This section is for
260
+ a **hand-written** `Render()`, where `if` being a statement (not an expression) means a
261
+ conditional value needs a helper to get one out of it:
221
262
 
222
263
  ```ks
223
264
  Element If(bool condition, () => Element whenTrue, () => Element whenFalse)
@@ -229,7 +270,7 @@ root.appendChild(If(this.IsLoggedIn, () => this.BuildProfile(), () => this.Build
229
270
 
230
271
  Both branches always required (no null "nothing" value); only the branch actually taken
231
272
  is called — the other lambda never runs. `*ngFor` and `*ngSwitch` need no Kopular helper
232
- at all:
273
+ at all in hand-written `Render()` either:
233
274
 
234
275
  ```ks
235
276
  // *ngFor — plain array method
@@ -386,8 +427,10 @@ class CounterService {
386
427
 
387
428
  ## Does not exist
388
429
 
389
- DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a template
390
- language/DSL everything is imperative `Render()` code against plain DOM bindings ·
430
+ DI container/injector · decorators (`@Injectable`, `@Component`, ...) · a runtime
431
+ template engine or interpreted expression language templates compile to the same
432
+ imperative `Render()` code as the hand-written form, checked at compile time, not
433
+ interpreted at runtime (see "Templates" above) · two-way binding (`[(ngModel)]`) ·
391
434
  vdom diffing / reconciliation beyond a single component's own re-render · pipes ·
392
435
  animations · forms/validation module · typed/generic HTTP responses (`Http` returns raw
393
436
  text — see above) · a CLI/scaffolding tool (`ng generate`-equivalent) · SSR.
package/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  Kopular is a small component framework for [KopScript](https://dev.azure.com/koppinator/Koppindependence/_git/Kop),
6
6
  built to give Angular's separation of concerns — components own UI, services own logic,
7
7
  a router owns navigation — without Angular's steepest learning-curve pieces: no RxJS, no
8
- dependency-injection container, no template DSL.
8
+ dependency-injection container, and templates that are real, compiled, type-checked
9
+ KopScript rather than a separate interpreted template language.
9
10
 
10
11
  Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./LLM.md)**
11
12
  — a dense, complete reference designed to be loaded straight into an LLM's context.
@@ -13,11 +14,19 @@ Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./
13
14
  ## Highlights
14
15
 
15
16
  - **`Component`**: a base class with `virtual Render()` (builds a fresh DOM subtree from
16
- current state) and `Update()` (swaps the old subtree for the new one). No template
17
- language, no diffing components build/update the DOM imperatively against plain DOM
18
- bindings, the way you'd write careful vanilla-JS UI code. An optional `virtual
17
+ current state) and `Update()` (swaps the old subtree for the new one). No diffing either
18
+ way `Render()` is provided as a real, separate markup file compiled by KopScript's
19
+ `template from` (see "Templates" below), or written by hand as plain imperative DOM
20
+ code against plain DOM bindings, the way you'd write careful vanilla-JS UI code — your
21
+ choice, and both compile to the exact same thing. An optional `virtual
19
22
  RenderError(message)` renders a fallback instead of a hard crash if `Render()` throws —
20
23
  purely additive; not overriding it keeps today's exact (uncaught) behavior.
24
+ - **Templates, compiled and type-checked, not interpreted**: markup lives in its own
25
+ `.html` file — interpolation (`{{ }}`), event/property bindings (`(click)="..."`,
26
+ `[prop]="..."`), and `*if`/`*for` structural directives — desugared by the KopScript
27
+ compiler into the exact same code a hand-written `Render()` would produce, with
28
+ automatic `Subscribe`/`Update()` wiring for `state<T>` fields referenced directly in the
29
+ markup. See "Templates" below.
21
30
  - **Reactive state, no RxJS**: components hold `state<number>`/`state<string>`/... (a
22
31
  KopScript language feature — see the [Kop](https://dev.azure.com/koppinator/Koppindependence/_git/Kop)
23
32
  repo) and subscribe once, in their constructor, to call `Update()` on change. No
@@ -30,10 +39,12 @@ Generating Kopular code with an AI coding assistant? Point it at **[`LLM.md`](./
30
39
  (`pushState`/`popstate`), with route registration as plain method calls, not a config
31
40
  DSL. See "Router, and deploying it" below — every deployment target needs its own
32
41
  SPA-fallback config, not just local dev.
33
- - **Structural directives, no template DSL**: `*ngIf`/`*ngFor`/`*ngSwitch`'s job — build
34
- a subtree conditionally, repeat one per item, pick one of several cases done as plain
35
- function calls (`If(...)`) and existing KopScript expressions (`array.ForEach(...)`,
36
- `match`), not special template syntax. See "Structural directives" below.
42
+ - **Structural directives**: `*ngIf`/`*ngFor`/`*ngSwitch`'s job — build a subtree
43
+ conditionally, repeat one per item, pick one of several cases. In a template, that's
44
+ `*if`/`*for` (real `if`/`for` under the hood see "Templates"); in a hand-written
45
+ `Render()`, the same job is a plain function call (`If(...)`) or existing KopScript
46
+ expression (`array.ForEach(...)`, `match`) — no special syntax needed there either way.
47
+ See "Structural directives" below.
37
48
  - **`Http`**: a thin, static wrapper over the real Fetch API (`Http.Get(url)`,
38
49
  `Http.Post(url, jsonBody)`, ...) — no HttpClient injection tokens, no RxJS
39
50
  observables/operators. See "HTTP" below.
@@ -62,6 +73,66 @@ That's the whole framework — eight files, plus the scaffolding CLI. Everything
62
73
  real app built on top of it)
63
74
  lives in a separate consumer repo, [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo).
64
75
 
76
+ ## Templates
77
+
78
+ A component's `Render()` can be a real markup file instead of hand-written imperative DOM
79
+ code — `template from "./x.html";` in the class body, a KopScript language feature (see
80
+ [Kop](https://dev.azure.com/koppinator/Koppindependence/_git/Kop)'s own README/LLM.md for
81
+ the full syntax reference). Kopular itself needed **zero framework code changes** for
82
+ this — the compiler desugars a template straight into calls against the same
83
+ `document.createElement`/`.appendChild`/`.textContent`/`.addEventListener` surface
84
+ `dom.ks` already declares, so a template-generated `Render()` is indistinguishable from
85
+ one you'd write by hand:
86
+
87
+ ```ks
88
+ // counter.ks
89
+ class Counter : Component {
90
+ public state<number> Count;
91
+ constructor() : base() { this.Count = state(0); }
92
+ public void Increment() { this.Count.Value = this.Count.Value + 1; }
93
+ template from "./counter.html";
94
+ }
95
+ ```
96
+
97
+ ```html
98
+ <!-- counter.html -->
99
+ <button (click)="Increment()">Count: {{ Count.Value }}</button>
100
+ ```
101
+
102
+ Note there's no `this.Count.Subscribe(...)` anywhere — a `state<T>` field referenced
103
+ directly in the template (`Count.Value` above) gets it wired automatically. The exact
104
+ same component, hand-written, needs that `Subscribe` call itself:
105
+
106
+ ```ks
107
+ class Counter : Component {
108
+ private state<number> Count;
109
+
110
+ constructor() : base() {
111
+ this.Count = state(0);
112
+ this.Count.Subscribe((number v) => this.Update());
113
+ }
114
+
115
+ public override Element Render() {
116
+ Element button = document.createElement("button");
117
+ button.textContent = "Count: " + this.Count.Value;
118
+ button.addEventListener("click", (Event e) => {
119
+ this.Count.Value = this.Count.Value + 1;
120
+ });
121
+ return button;
122
+ }
123
+ }
124
+ ```
125
+
126
+ Both produce the same `Render()`, and can be mixed freely across a codebase — nothing
127
+ about `Component`, `Update()`, or any other Kopular API differs between them. The manual
128
+ `Subscribe` call is still exactly what you need the moment state is reached *indirectly*
129
+ — through a method call, or through an injected service's own state
130
+ (`this.Service.Count`, say) — which is why the real, production `Counter` on the
131
+ [KopularDemo](https://dev.azure.com/koppinator/Koppindependence/_git/KopularDemo) site
132
+ (it injects a `CounterService` rather than holding `Count` itself) uses a template but
133
+ still has one manual `Subscribe`. `*if`/`*for` in a template are covered under
134
+ "Structural directives" below, alongside their hand-written-`Render()` equivalents.
135
+
65
136
  ## Dependency injection: the composition root pattern
66
137
 
67
138
  Kopular has no injector because KopScript has nothing for one to hook into — no
@@ -202,20 +273,23 @@ standard. For local dev, see KopularDemo's `scripts/serve.mjs`.
202
273
  ## Structural directives
203
274
 
204
275
  Angular's `*ngIf`/`*ngFor`/`*ngSwitch` are template syntax that expands, at compile time,
205
- into imperative view-container calls. Kopular has no template compiler to expand
206
- anything into (`Render()` is already imperative see `component.ks`), so there's no
207
- special syntax here either: each one maps onto a plain expression, and two of the three
208
- need nothing new at all.
209
-
210
- | Angular | Kopular | New code? |
211
- | ----------------- | ------------------------------------------- | :-------: |
212
- | `*ngFor` | `array.ForEach((item) => ...)` | none already a KopScript array method |
213
- | `*ngSwitch` | `match value { ... }` | none — already a KopScript expression, and exhaustiveness-checked (`*ngSwitch` isn't) |
214
- | `*ngIf` / `*ngIf-else` | `If(condition, () => ..., () => ...)` | `directives.ks` |
215
-
216
- `*ngIf` is the one case that needs something new: `if` is a *statement* in KopScript, so
217
- without a helper you'd need a throwaway mutable local just to get a conditional value out
218
- of it. `If()` is that helper nothing more than:
276
+ into imperative view-container calls. In a Kopular **template**, `*if`/`*for` are exactly
277
+ that real KopScript `if`/`for` statements underneath (see "Templates" above), compiled
278
+ by KopScript itself, not interpreted by Kopular at runtime. In a **hand-written**
279
+ `Render()`, there's no separate directive syntax to reach for: each job maps onto a plain
280
+ expression, and two of the three need nothing new at all.
281
+
282
+ | Angular | Kopular template | Kopular hand-written `Render()` | New code? |
283
+ | ----------------- | ---------------- | -------------------------------------------- | :-------: |
284
+ | `*ngFor` | `*for="Type v of expr"` | `array.ForEach((item) => ...)` | none — already a KopScript array method |
285
+ | `*ngSwitch` | *(not supported — use `*if`, or switch in the backing class)* | `match value { ... }` | none — already a KopScript expression, and exhaustiveness-checked (`*ngSwitch` isn't) |
286
+ | `*ngIf` / `*ngIf-else` | `*if="expr"` | `If(condition, () => ..., () => ...)` | `directives.ks` (hand-written form only — a template's `*if` needs no helper, it's a real `if`) |
287
+
288
+ The rest of this section is about the **hand-written `Render()`** column above a
289
+ template's `*if`/`*for` need no further explanation, they're covered under "Templates".
290
+ `*ngIf` is the one case in hand-written `Render()` that needs something new: `if` is a
291
+ *statement* in KopScript, so without a helper you'd need a throwaway mutable local just
292
+ to get a conditional value out of it. `If()` is that helper — nothing more than:
219
293
 
220
294
  ```ks
221
295
  Element If(bool condition, () => Element whenTrue, () => Element whenFalse) {
@@ -232,7 +306,7 @@ to hand back. Only the branch actually taken runs — the other lambda is never
232
306
  an explicit empty branch (`() => document.createElement("span")`) costs nothing when
233
307
  there's genuinely nothing to show.
234
308
 
235
- All three read the same way, right inside `Render()` — no template file, no directive
309
+ All three read the same way, right inside a hand-written `Render()` — no directive
236
310
  registration, nothing to import beyond the function itself:
237
311
 
238
312
  ```ks
package/bin/kp.mjs CHANGED
@@ -97,7 +97,7 @@ A [KopScript](https://www.npmjs.com/package/kopscript) + [Kopular](https://www.n
97
97
 
98
98
  ## Where to go next
99
99
 
100
- - \`src/counter.ks\` is a real, working Kopular \`Component\` — start there.
100
+ - \`src/counter.ks\` + \`src/counter.html\` is a real, working Kopular \`Component\` — start there. Its markup lives in \`counter.html\` (interpolation, \`(click)="..."\`) via KopScript's \`template from\`; \`counter.ks\` holds only state and logic. See \`node_modules/kopscript/LLM.md\`'s "Templates" section for the full binding/directive syntax, and Kopular's own README/LLM.md for the hand-written-\`Render()\` alternative.
101
101
  - \`src/kopular_bindings.ks\` declares the ambient DOM and Kopular types this project builds on (see the comments inside it for why these have to be redeclared per-project rather than imported).
102
102
  - For routing, dependency injection ("Pure DI" / a composition root), structural directives, or the \`Http\` client, see Kopular's own README and \`node_modules/kopular/LLM.md\`.
103
103
  - For the full language reference, see \`node_modules/kopscript/LLM.md\`.
@@ -242,28 +242,31 @@ extern class Router {
242
242
  `;
243
243
 
244
244
  // The same Counter shown in Kopular's own README/LLM.md — real, verified
245
- // example code, not a placeholder.
245
+ // example code, not a placeholder. Uses a template (see counter.html below)
246
+ // rather than a hand-written Render(), since that's the more common style
247
+ // today; Kopular's/KopScript's own docs show the equivalent hand-written
248
+ // form too, for the case a component needs one (e.g. Subscribe-ing to state
249
+ // reached indirectly, through an injected service).
246
250
  const COUNTER_KS_TEMPLATE = `using "./kopular_bindings";
247
251
 
248
252
  class Counter : Component {
249
- private state<number> Count;
253
+ public state<number> Count;
250
254
 
251
255
  constructor() : base() {
252
256
  this.Count = state(0);
253
- this.Count.Subscribe((number v) => this.Update());
254
257
  }
255
258
 
256
- public override Element Render() {
257
- Element button = document.createElement("button");
258
- button.textContent = "Count: " + this.Count.Value;
259
- button.addEventListener("click", (Event e) => {
260
- this.Count.Value = this.Count.Value + 1;
261
- });
262
- return button;
259
+ public void Increment() {
260
+ this.Count.Value = this.Count.Value + 1;
263
261
  }
262
+
263
+ template from "./counter.html";
264
264
  }
265
265
  `;
266
266
 
267
+ const COUNTER_HTML_TEMPLATE = `<button (click)="Increment()">Count: {{ Count.Value }}</button>
268
+ `;
269
+
267
270
  const APP_KS_TEMPLATE = `using "./kopular_bindings";
268
271
  using "./counter";
269
272
 
@@ -293,6 +296,7 @@ function scaffoldProject(dirPath) {
293
296
  writeFileSync(join(dirPath, "scripts", "vendor-kopular.mjs"), VENDOR_KOPULAR_MJS_TEMPLATE, "utf-8");
294
297
  writeFileSync(join(dirPath, "src", "kopular_bindings.ks"), KOPULAR_BINDINGS_KS_TEMPLATE, "utf-8");
295
298
  writeFileSync(join(dirPath, "src", "counter.ks"), COUNTER_KS_TEMPLATE, "utf-8");
299
+ writeFileSync(join(dirPath, "src", "counter.html"), COUNTER_HTML_TEMPLATE, "utf-8");
296
300
  writeFileSync(join(dirPath, "src", "app.ks"), APP_KS_TEMPLATE, "utf-8");
297
301
 
298
302
  console.log(`Created ${name} in ${dirPath}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kopular",
3
- "version": "0.10.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",
3
+ "version": "0.11.0",
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",
7
7
  "author": "Joe Koppin <koppinjo@gmail.com>",
@@ -38,6 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "ks build src/router.ks && ks build src/directives.ks && ks build src/http.ks && ks build src/forms.ks",
40
40
  "prepublishOnly": "npm run build",
41
+ "pretest": "npm run build",
41
42
  "test": "vitest run",
42
43
  "test:watch": "vitest"
43
44
  },
@@ -50,7 +51,7 @@
50
51
  }
51
52
  },
52
53
  "devDependencies": {
53
- "kopscript": "^0.5.0",
54
+ "kopscript": "^0.7.0",
54
55
  "@types/jsdom": "^30.0.0",
55
56
  "@types/node": "^20.14.0",
56
57
  "jsdom": "^25.0.1",
package/src/component.js CHANGED
@@ -18,14 +18,14 @@ export class Component {
18
18
  }
19
19
 
20
20
  Mount(parent) {
21
- (this.ParentElement = parent);
22
- (this.Root = this.SafeRender());
21
+ this.ParentElement = parent;
22
+ this.Root = this.SafeRender();
23
23
  parent.appendChild(this.Root);
24
24
  }
25
25
 
26
26
  Update() {
27
27
  let newRoot = this.SafeRender();
28
28
  this.ParentElement.replaceChild(newRoot, this.Root);
29
- (this.Root = newRoot);
29
+ this.Root = newRoot;
30
30
  }
31
31
  }
package/src/forms.js CHANGED
@@ -15,16 +15,16 @@ class __KopState {
15
15
 
16
16
  export class FormField {
17
17
  constructor(initial, validate) {
18
- (this.Value = new __KopState(initial));
19
- (this.Error = new __KopState(validate(initial)));
20
- (this.Touched = new __KopState(false));
18
+ this.Value = new __KopState(initial);
19
+ this.Error = new __KopState(validate(initial));
20
+ this.Touched = new __KopState(false);
21
21
  this.Value.Subscribe((v) => {
22
- (this.Error.Value = validate(v));
22
+ this.Error.Value = validate(v);
23
23
  });
24
24
  }
25
25
 
26
26
  Touch() {
27
- (this.Touched.Value = true);
27
+ this.Touched.Value = true;
28
28
  }
29
29
 
30
30
  Valid() {
package/src/router.js CHANGED
@@ -4,23 +4,23 @@ import { Component } from "./component.js";
4
4
  export class Router extends Component {
5
5
  constructor(notFoundPage) {
6
6
  super();
7
- (this.Paths = []);
8
- (this.Pages = []);
9
- (this.NotFoundPage = notFoundPage);
10
- (this.Param = "");
11
- (this.Guard = (path) => (true));
12
- (this.RedirectPath = "");
7
+ this.Paths = [];
8
+ this.Pages = [];
9
+ this.NotFoundPage = notFoundPage;
10
+ this.Param = "";
11
+ this.Guard = (path) => (true);
12
+ this.RedirectPath = "";
13
13
  window.addEventListener("popstate", (e) => (this.Update()));
14
14
  }
15
15
 
16
16
  AddRoute(path, page) {
17
- (this.Paths = [...this.Paths, path]);
18
- (this.Pages = [...this.Pages, page]);
17
+ this.Paths = [...this.Paths, path];
18
+ this.Pages = [...this.Pages, page];
19
19
  }
20
20
 
21
21
  SetGuard(redirectPath, guard) {
22
- (this.RedirectPath = redirectPath);
23
- (this.Guard = guard);
22
+ this.RedirectPath = redirectPath;
23
+ this.Guard = guard;
24
24
  }
25
25
 
26
26
  Navigate(path) {
@@ -32,39 +32,39 @@ export class Router extends Component {
32
32
  let effectivePath = path;
33
33
  if (!this.Guard(path)) {
34
34
  history.pushState("", "", this.RedirectPath);
35
- (effectivePath = this.RedirectPath);
35
+ effectivePath = this.RedirectPath;
36
36
  }
37
37
  let pathSegments = effectivePath.split("/");
38
38
  let found = this.NotFoundPage;
39
39
  let param = "";
40
- for (let i = 0; (i < this.Paths.length); (i = (i + 1))) {
40
+ for (let i = 0; (i < this.Paths.length); i = (i + 1)) {
41
41
  let patternSegments = this.Paths[i].split("/");
42
42
  if ((patternSegments.length !== pathSegments.length)) {
43
43
  continue;
44
44
  }
45
45
  let matched = true;
46
46
  let capturedParam = "";
47
- for (let j = 0; (j < patternSegments.length); (j = (j + 1))) {
47
+ for (let j = 0; (j < patternSegments.length); j = (j + 1)) {
48
48
  if (patternSegments[j].startsWith(":")) {
49
- (capturedParam = pathSegments[j]);
49
+ capturedParam = pathSegments[j];
50
50
  } else if ((patternSegments[j] !== pathSegments[j])) {
51
- (matched = false);
51
+ matched = false;
52
52
  break;
53
53
  }
54
54
  }
55
55
  if (matched) {
56
- (found = this.Pages[i]);
57
- (param = capturedParam);
56
+ found = this.Pages[i];
57
+ param = capturedParam;
58
58
  break;
59
59
  }
60
60
  }
61
- (this.Param = param);
61
+ this.Param = param;
62
62
  return found;
63
63
  }
64
64
 
65
65
  Render() {
66
66
  let outlet = document.createElement("div");
67
- (outlet.className = "router-outlet");
67
+ outlet.className = "router-outlet";
68
68
  let path = location.pathname;
69
69
  let page = this.Match(path);
70
70
  page.Mount(outlet);
package/src/testing.js CHANGED
@@ -18,13 +18,26 @@
18
18
  // who actually calls this, not a weight every `kopular` install pays.
19
19
  import { JSDOM } from "jsdom";
20
20
  import { compileGraph } from "kopscript";
21
- import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
22
- import { tmpdir } from "node:os";
21
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
23
22
  import { dirname, join } from "node:path";
24
23
  import { fileURLToPath } from "node:url";
25
24
 
26
25
  const GLOBAL_KEYS = ["document", "Element", "Event", "location", "history", "window", "fetch"];
27
26
  const OWN_SRC_DIR = dirname(fileURLToPath(import.meta.url));
27
+ // A hidden folder at this package's own root — for an external consumer,
28
+ // that's inside their node_modules/kopular/, i.e. still within their
29
+ // project tree. Deliberately NOT os.tmpdir(): Vitest/Vite's own module
30
+ // resolution (used to dynamically `import()` the compiled fixture below)
31
+ // restricts filesystem access to the project's workspace root by default,
32
+ // and the OS temp directory sits outside that boundary entirely — on top
33
+ // of macOS's os.tmpdir() itself being a symlink (/var/folders/... ->
34
+ // /private/var/folders/...), a second, independent reason the same
35
+ // resolution can fail. This bit a real CI run with "Failed to load url
36
+ // kopular/component ... Does the file exist?" — a resolution failure, not
37
+ // a transform error — while passing intermittently elsewhere, which is
38
+ // exactly the signature of a filesystem-access-boundary problem rather
39
+ // than anything wrong with the copied files themselves.
40
+ const TEMP_ROOT = join(OWN_SRC_DIR, "..", ".kopular-testing-tmp");
28
41
 
29
42
  // Copies every .ks/.html file from `srcDir` into `dir`, plus any filename
30
43
  // listed in `extraFiles` (e.g. http_runtime.js — a hand-written sibling a
@@ -40,13 +53,14 @@ function copySources(srcDir, dir, extraFiles) {
40
53
 
41
54
  // kopscript's compiled output is genuine ESM (`export class ...`), which
42
55
  // Node only interprets correctly given a ".mjs" extension or an ancestor
43
- // package.json declaring "type": "module" — a bare os.tmpdir() has neither,
56
+ // package.json declaring "type": "module" — a fresh directory has neither,
44
57
  // so the dynamic `import()` below would otherwise fail with "Cannot use
45
58
  // import statement outside a module" the moment anything outside this
46
59
  // file's own module graph (a real npm package, not code vitest/vite-node
47
60
  // itself transforms) tries to load it.
48
61
  function mkTempDir() {
49
- const dir = mkdtempSync(join(tmpdir(), "kopular-testing-"));
62
+ mkdirSync(TEMP_ROOT, { recursive: true });
63
+ const dir = mkdtempSync(join(TEMP_ROOT, "run-"));
50
64
  writeFileSync(join(dir, "package.json"), JSON.stringify({ type: "module" }), "utf-8");
51
65
  return dir;
52
66
  }
@@ -57,9 +71,26 @@ function mkTempDir() {
57
71
  // not via relative `using`. Resolved off this very file's own location, so
58
72
  // it's always the real package as installed in the *caller's*
59
73
  // node_modules, not a guess at a path.
74
+ //
75
+ // Copies only package.json plus its own "files" allowlist (src, bin,
76
+ // assets, LLM.md) — i.e. exactly what a real `npm install` would put there
77
+ // — not the whole package root. Copying the root wholesale would also drag
78
+ // along Kopular's *own* node_modules/.git/test, producing a nested
79
+ // node_modules/kopular/node_modules/... no real consumer's install ever
80
+ // has; that mismatch is what caused this to resolve fine in some
81
+ // environments and fail with "Does the file exist?" in CI, since a
82
+ // bundler's dependency resolution isn't obligated to behave the same in
83
+ // the presence of a duplicate, unexpected nested node_modules.
60
84
  function copyKopularPackage(dir) {
61
- mkdirSync(join(dir, "node_modules"), { recursive: true });
62
- cpSync(join(OWN_SRC_DIR, ".."), join(dir, "node_modules", "kopular"), { recursive: true });
85
+ const kopularRoot = join(OWN_SRC_DIR, "..");
86
+ const pkg = JSON.parse(readFileSync(join(kopularRoot, "package.json"), "utf-8"));
87
+ const destRoot = join(dir, "node_modules", "kopular");
88
+ mkdirSync(destRoot, { recursive: true });
89
+ cpSync(join(kopularRoot, "package.json"), join(destRoot, "package.json"));
90
+ for (const entry of pkg.files ?? []) {
91
+ const src = join(kopularRoot, entry);
92
+ if (existsSync(src)) cpSync(src, join(destRoot, entry), { recursive: true });
93
+ }
63
94
  }
64
95
 
65
96
  // Shared by both exports below: compiles `entryFileName` (already written