kopular 0.23.2 → 1.0.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/LLM.md +46 -5
- package/README.md +24 -7
- package/package.json +2 -2
- package/src/testing.js +45 -7
package/LLM.md
CHANGED
|
@@ -109,6 +109,14 @@ extern class VElement {
|
|
|
109
109
|
static VElement Mount(Component component);
|
|
110
110
|
} from "kopular/velement";
|
|
111
111
|
|
|
112
|
+
// Only needed if you use `styles from "./x.css";` in a class body (see
|
|
113
|
+
// "Scoped styles" below) — the compiler splices a call to this into the
|
|
114
|
+
// constructor automatically, but (like every other Kopular export) it
|
|
115
|
+
// still needs its own `extern` declaration; it is NOT auto-imported.
|
|
116
|
+
extern class ScopedStyles {
|
|
117
|
+
static void Inject(string id, string css);
|
|
118
|
+
} from "kopular/vdom";
|
|
119
|
+
|
|
112
120
|
extern class Component {
|
|
113
121
|
constructor();
|
|
114
122
|
virtual VElement Render(); // `virtual` here is what lets your subclass `override` it
|
|
@@ -133,6 +141,11 @@ extern class Router {
|
|
|
133
141
|
// Every registered path (AddRoute + AddLazyRoute), in registration
|
|
134
142
|
// order — see "Router" below.
|
|
135
143
|
string[] AllPaths();
|
|
144
|
+
// Overridable outlet content shown while an AddLazyRoute page's loader
|
|
145
|
+
// is in flight — default: `<div class="router-loading">Loading...</div>`.
|
|
146
|
+
// Only declare this if you actually override it (a class extending
|
|
147
|
+
// Router) — see "Lazy routes" below.
|
|
148
|
+
protected virtual VElement BuildLoadingPlaceholder();
|
|
136
149
|
void Navigate(string path);
|
|
137
150
|
void Mount(Element parent);
|
|
138
151
|
// One guard for the whole Router, not per-route — see "Router" below.
|
|
@@ -317,6 +330,11 @@ w.Bump(); // re-render + diff + patch
|
|
|
317
330
|
return ul;
|
|
318
331
|
}
|
|
319
332
|
```
|
|
333
|
+
`item.Id` above means exactly what it looks like: for a keyed list of mounted children,
|
|
334
|
+
the mounted `Component` itself needs its own public `Id` (or similarly-named) field/
|
|
335
|
+
property to copy onto `slot.Id` — `VElement.Id` lives on the wrapper slot, not on the
|
|
336
|
+
component, so there's nothing to key by without one. Set it however suits the type
|
|
337
|
+
(constructor param, or a plain field assigned right after construction).
|
|
320
338
|
The SAME `Mountable` instance still in a slot across a re-render is patched in place
|
|
321
339
|
(`Update()` inside that child re-renders just its own subtree, siblings untouched); a
|
|
322
340
|
DIFFERENT instance (or the slot disappearing) tears the old one down first — calling its
|
|
@@ -434,7 +452,11 @@ class Widget : Component {
|
|
|
434
452
|
- Independent of `template from` — works with a hand-written `Render()` too. The compiler
|
|
435
453
|
rewrites the referenced `.css` so every selector requires a per-class
|
|
436
454
|
`data-kop-scope="<id>"` attribute, then splices one `ScopedStyles.Inject(id, css);` call
|
|
437
|
-
into the constructor. **
|
|
455
|
+
into the constructor. **This means `ScopedStyles` must be `extern`-declared in your own
|
|
456
|
+
project the same as `Component`/`VElement`/etc. (see the copy-paste block above) — it is
|
|
457
|
+
NOT auto-imported just because you wrote `styles from`.** Forgetting it is a real `KS4048
|
|
458
|
+
Undefined identifier 'ScopedStyles'` at the constructor the compiler spliced the call
|
|
459
|
+
into. **Unlike `template from`, this needed real framework code**:
|
|
438
460
|
`ScopedStyles` (`vdom.ks`, new) — idempotent, static-array-registry dedup shape same as
|
|
439
461
|
`Batching`, injects one real `<style>` per component *type* (not per instance) into
|
|
440
462
|
`document.head` (`dom.ks`'s `head { get; }`, new) the first time any instance is
|
|
@@ -532,10 +554,29 @@ nav.Navigate("/about"); // pushState + immediate re-ren
|
|
|
532
554
|
extern task<Component> LoadDogsPage() from "./dogs_page_loader";
|
|
533
555
|
nav.AddLazyRoute("/dogs", LoadDogsPage);
|
|
534
556
|
```
|
|
535
|
-
The outlet shows a plain loading placeholder (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
the same
|
|
557
|
+
The outlet shows a plain loading placeholder (`<div class="router-loading">Loading...</div>`
|
|
558
|
+
by default; override `protected virtual VElement BuildLoadingPlaceholder()` on a class
|
|
559
|
+
extending `Router` to customize) while the fetch is in flight; the loaded page is cached
|
|
560
|
+
after the first fetch, same as an eager page — navigating away and back reuses it, no
|
|
561
|
+
re-fetch. Mixes freely with `AddRoute` in the same `Router`.
|
|
562
|
+
- **Building it for real**: the whole point of a lazy route is that `dogs_page.ks` is
|
|
563
|
+
deliberately *not* `using`'d from your app's own entry file — that's what keeps its
|
|
564
|
+
code out of the eager bundle. That also means your normal build command (`ks build
|
|
565
|
+
src/app.ks`) never compiles it, since `ks build`/`compileGraph` only walk the `using`
|
|
566
|
+
graph reachable from the entry you give them. Build the lazy page as its own separate
|
|
567
|
+
entry too, e.g. `ks build src/app.ks && ks build src/dogs_page.ks` in your build
|
|
568
|
+
script (or one `ks build` invocation per lazy route, if you have several) — each
|
|
569
|
+
already-compiled shared dependency just gets written again with identical output, so
|
|
570
|
+
this is safe to add without restructuring anything else.
|
|
571
|
+
- **Testing it**: `kopular/testing`'s `runKopularApp` (kopular 0.24.0+) compiles any
|
|
572
|
+
other real `.ks` file present in your app's own `srcDir` that the entry doesn't
|
|
573
|
+
reach — a lazy-route target is exactly that — so a test exercising `AddLazyRoute`
|
|
574
|
+
needs no special setup: write `dogs_page.ks`/`dogs_page_loader.js` into the same
|
|
575
|
+
directory as your entry file, same as any other page, and the loader's
|
|
576
|
+
`import("./dogs_page.js")` finds a real, freshly-compiled file. (`runKopularFixture`,
|
|
577
|
+
used only by Kopular's own internal test suite, does not do this — it copies
|
|
578
|
+
Kopular's entire framework source tree alongside a fixture, where the same behavior
|
|
579
|
+
would mean recompiling most of the framework on every test.)
|
|
539
580
|
- **`AllPaths(): string[]`** — every registered path (`AddRoute` + `AddLazyRoute`), in
|
|
540
581
|
registration order. For enumerating real routes (a build-time prerender step, most
|
|
541
582
|
likely) without a second, hand-maintained list.
|
package/README.md
CHANGED
|
@@ -166,8 +166,13 @@ class Widget : Component {
|
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
Unlike `template from`, this **did** need real framework code — `ScopedStyles.Inject`
|
|
169
|
-
(`vdom.ks`) is the runtime half
|
|
170
|
-
|
|
169
|
+
(`vdom.ks`) is the runtime half, and (like every other Kopular export) it needs its own
|
|
170
|
+
`extern` declaration in your project — `extern class ScopedStyles { static void
|
|
171
|
+
Inject(string id, string css); } from "kopular/vdom";` — it is not auto-imported just
|
|
172
|
+
because you wrote `styles from`; omitting it is a real `KS4048 Undefined identifier
|
|
173
|
+
'ScopedStyles'` at the constructor the compiler spliced the call into. It's idempotent,
|
|
174
|
+
injecting one real `<style>` per component *type* into `document.head` the first time any
|
|
175
|
+
instance of that type is constructed
|
|
171
176
|
(dedup is per-type, not per-instance — every instance's constructor calls `Inject` with
|
|
172
177
|
the same compile-time `id`/rewritten-`css`, so only the first actually creates a tag).
|
|
173
178
|
Never removed once injected — a scoped stylesheet is global infrastructure for as long as
|
|
@@ -368,11 +373,23 @@ export async function LoadDogsPage() {
|
|
|
368
373
|
extern task<Component> LoadDogsPage() from "./dogs_page_loader";
|
|
369
374
|
```
|
|
370
375
|
|
|
371
|
-
The outlet shows a plain loading placeholder (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
+
The outlet shows a plain loading placeholder (`<div class="router-loading">Loading...</div>`
|
|
377
|
+
by default; override `protected virtual VElement BuildLoadingPlaceholder()` on a class
|
|
378
|
+
extending `Router` to customize it) while the fetch is in flight, then the real page once
|
|
379
|
+
it resolves — and, like an eager page, it's only ever fetched once: navigating away and
|
|
380
|
+
back reuses the same already-loaded instance, keeping whatever state it built up. A lazy
|
|
381
|
+
and an eager route mix freely in the same `Router`; nothing about `AddRoute`'s own existing
|
|
382
|
+
signature changes.
|
|
383
|
+
|
|
384
|
+
**Building and testing a lazy route**: `dogs_page.ks` is deliberately *not* `using`'d from
|
|
385
|
+
your app's own entry — that's what keeps it out of the eager bundle — which also means your
|
|
386
|
+
normal `ks build src/app.ks` never compiles it, since `ks build` only walks the `using`
|
|
387
|
+
graph reachable from the entry you give it. Build it as its own separate entry too:
|
|
388
|
+
`ks build src/app.ks && ks build src/dogs_page.ks` (already-compiled shared dependencies
|
|
389
|
+
just get written again with identical output, so this is safe to add). For tests,
|
|
390
|
+
`kopular/testing`'s `runKopularApp` (0.24.0+) compiles any other real `.ks` file present in
|
|
391
|
+
your `srcDir` that the entry doesn't reach — a lazy-route target is exactly that — so
|
|
392
|
+
`AddLazyRoute` needs no special test setup at all.
|
|
376
393
|
|
|
377
394
|
**`AllPaths()`** returns every registered path (both `AddRoute` and `AddLazyRoute`), in
|
|
378
395
|
registration order — for a caller that needs to enumerate real routes (a build-time static
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kopular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Kopular: a small component framework for KopScript — components, reactive state, constructor-injected services, routing, real compiled templates, and HTTP, with no DI container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@types/jsdom": "^30.0.0",
|
|
64
64
|
"@types/node": "^20.14.0",
|
|
65
65
|
"jsdom": "^25.0.1",
|
|
66
|
-
"kopscript": "^0.
|
|
66
|
+
"kopscript": "^1.0.0",
|
|
67
67
|
"typescript": "^5.5.0",
|
|
68
68
|
"vitest": "^4.1.11"
|
|
69
69
|
},
|
package/src/testing.js
CHANGED
|
@@ -111,19 +111,52 @@ function copyKopularPackage(dir) {
|
|
|
111
111
|
// Shared by both exports below: compiles `entryFileName` (already written
|
|
112
112
|
// into `dir`, alongside everything it `using`s) and runs it against a fresh
|
|
113
113
|
// jsdom instance.
|
|
114
|
+
//
|
|
115
|
+
// `compileOrphanEntries` additionally compiles any other real `.ks` file
|
|
116
|
+
// present in `dir` that the entry's own `using` graph never reaches. A
|
|
117
|
+
// `Router.AddLazyRoute` target is *exactly* that by construction — it's
|
|
118
|
+
// deliberately kept out of the eager `using` graph so its own JS is only
|
|
119
|
+
// fetched on first navigation (see kopular's own LLM.md "Lazy routes"
|
|
120
|
+
// section) — so without this, its loader shim's `await import("./x.js")`
|
|
121
|
+
// finds nothing on disk here, even though the exact same app builds and
|
|
122
|
+
// runs fine in a real browser (there, the app's own build script is
|
|
123
|
+
// expected to invoke `ks build` on the lazy page too, as its own separate
|
|
124
|
+
// entry point — see the same docs section). Only `runKopularApp` passes
|
|
125
|
+
// this: `runKopularFixture` copies Kopular's *entire* framework source
|
|
126
|
+
// tree alongside a small fixture, where "every .ks file the fixture
|
|
127
|
+
// doesn't reach" would mean recompiling most of the framework on every
|
|
128
|
+
// test.
|
|
114
129
|
async function compileAndRun(dir, entryFileName, options) {
|
|
115
130
|
const entry = join(dir, entryFileName);
|
|
116
|
-
const
|
|
117
|
-
|
|
131
|
+
const graphs = [compileGraph(entry)];
|
|
132
|
+
|
|
133
|
+
if (options.compileOrphanEntries) {
|
|
134
|
+
const covered = new Set(graphs[0].outputs.keys());
|
|
135
|
+
for (const name of readdirSync(dir)) {
|
|
136
|
+
if (!name.endsWith(".ks")) continue;
|
|
137
|
+
const absPath = join(dir, name);
|
|
138
|
+
if (absPath === entry || covered.has(absPath)) continue;
|
|
139
|
+
const g = compileGraph(absPath);
|
|
140
|
+
for (const p of g.outputs.keys()) covered.add(p);
|
|
141
|
+
graphs.push(g);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const failed = graphs.filter((g) => !g.success);
|
|
146
|
+
if (failed.length > 0) {
|
|
118
147
|
rmSync(dir, { recursive: true, force: true });
|
|
119
|
-
const
|
|
120
|
-
|
|
148
|
+
const seen = new Set();
|
|
149
|
+
const messages = failed
|
|
150
|
+
.flatMap((g) => [...g.modules.values()])
|
|
151
|
+
.filter((m) => m.diagnostics.hasErrors && !seen.has(m.absPath) && seen.add(m.absPath))
|
|
121
152
|
.map((m) => m.diagnostics.format(m.source, m.absPath))
|
|
122
153
|
.join("\n\n");
|
|
123
154
|
throw new Error(messages || "Compilation failed");
|
|
124
155
|
}
|
|
125
|
-
for (const
|
|
126
|
-
|
|
156
|
+
for (const g of graphs) {
|
|
157
|
+
for (const [absPath, js] of g.outputs) {
|
|
158
|
+
writeFileSync(absPath.replace(/\.ks$/, ".js"), js, "utf-8");
|
|
159
|
+
}
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
const dom = new JSDOM("<!doctype html><html><body></body></html>", { url: options.url ?? "http://localhost/" });
|
|
@@ -162,6 +195,11 @@ async function compileAndRun(dir, entryFileName, options) {
|
|
|
162
195
|
* real project's own src/ directory, the way KopularDemo's test suite (or
|
|
163
196
|
* any app consuming `kopular/...` via `extern`) would use it.
|
|
164
197
|
*
|
|
198
|
+
* Also compiles any other `.ks` file in `srcDir` the entry's `using` graph
|
|
199
|
+
* doesn't reach — a `Router.AddLazyRoute` target is exactly that, by
|
|
200
|
+
* design, so its loader shim's `import("./x.js")` finds a real file here
|
|
201
|
+
* too, with no extra setup needed to test a lazy route.
|
|
202
|
+
*
|
|
165
203
|
* @param {string} srcDir - directory containing the entry file and
|
|
166
204
|
* everything it `using`s.
|
|
167
205
|
* @param {string} entryFileName - the entry .ks file's name within srcDir.
|
|
@@ -186,7 +224,7 @@ export async function runKopularApp(srcDir, entryFileName, options = {}) {
|
|
|
186
224
|
const dir = mkTempDir();
|
|
187
225
|
copySources(srcDir, dir, options.extraFiles ?? []);
|
|
188
226
|
if (options.includeKopularPackage) copyKopularPackage(dir);
|
|
189
|
-
return compileAndRun(dir, entryFileName, options);
|
|
227
|
+
return compileAndRun(dir, entryFileName, { ...options, compileOrphanEntries: true });
|
|
190
228
|
}
|
|
191
229
|
|
|
192
230
|
/**
|