@zoijs/core 1.0.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.
@@ -0,0 +1,7 @@
1
+ // effect() + untrack() — reactive side effects (used internally by the renderer
2
+ // to wire bindings). An effect re-runs when something it read changes; errors it
3
+ // throws are contained so other bindings keep working.
4
+ //
5
+ // Implemented by the shared reactive core (see core.js).
6
+
7
+ export { effect, untrack } from "./core.js";
@@ -0,0 +1,24 @@
1
+ // env.js — development / production mode (Task 4).
2
+ //
3
+ // Development (the default) shows helpful warnings: duplicate `each` keys,
4
+ // self-triggering effects, runaway loops. Production silences them for less
5
+ // noise and a touch less work. No build step required — just call configure().
6
+ //
7
+ // import { configure } from "@zoijs/core";
8
+ // configure({ dev: false }); // production
9
+
10
+ let dev = true;
11
+
12
+ /**
13
+ * @param {{ dev?: boolean }} options
14
+ */
15
+ export function configure(options) {
16
+ if (options && typeof options.dev === "boolean") {
17
+ dev = options.dev;
18
+ }
19
+ }
20
+
21
+ /** @returns {boolean} true in development mode */
22
+ export function isDev() {
23
+ return dev;
24
+ }
@@ -0,0 +1,61 @@
1
+ // owner.js — ownership scopes for deterministic cleanup (Task 2).
2
+ //
3
+ // An owner collects "disposers" — functions that tear down whatever was created
4
+ // inside its scope (effects, computeds, event listeners, list-item subtrees).
5
+ // Owners nest: disposing a parent disposes its children first. This is how
6
+ // unmount() and removed list items free their reactive subscriptions instead of
7
+ // leaving them attached to long-lived state.
8
+ //
9
+ // These are internal helpers (not part of the public API).
10
+
11
+ let currentOwner = null;
12
+
13
+ /** The active owner, or null outside any scope. */
14
+ export function getOwner() {
15
+ return currentOwner;
16
+ }
17
+
18
+ /** Create a scope, nested under the currently active owner. */
19
+ export function createOwner() {
20
+ const owner = {
21
+ disposers: [],
22
+ children: new Set(),
23
+ parent: currentOwner,
24
+ disposed: false,
25
+ };
26
+ if (currentOwner) currentOwner.children.add(owner);
27
+ return owner;
28
+ }
29
+
30
+ /** Run `fn` with `owner` active, so things it creates register into `owner`. */
31
+ export function runWithOwner(owner, fn) {
32
+ const previous = currentOwner;
33
+ currentOwner = owner;
34
+ try {
35
+ return fn();
36
+ } finally {
37
+ currentOwner = previous;
38
+ }
39
+ }
40
+
41
+ /** Register a cleanup function in the active owner (no-op outside a scope). */
42
+ export function onCleanup(fn) {
43
+ if (currentOwner) currentOwner.disposers.push(fn);
44
+ }
45
+
46
+ /** Dispose a scope: tear down child scopes first, then run its disposers. */
47
+ export function disposeOwner(owner) {
48
+ if (owner.disposed) return;
49
+ owner.disposed = true;
50
+ for (const child of [...owner.children]) disposeOwner(child);
51
+ owner.children.clear();
52
+ for (let i = owner.disposers.length - 1; i >= 0; i--) {
53
+ try {
54
+ owner.disposers[i]();
55
+ } catch (err) {
56
+ console.error("Zoijs: a cleanup handler threw:", err);
57
+ }
58
+ }
59
+ owner.disposers.length = 0;
60
+ if (owner.parent) owner.parent.children.delete(owner);
61
+ }
@@ -0,0 +1,7 @@
1
+ // flush() — force the batched effect queue to run now (used by tests and by code
2
+ // that needs a synchronous DOM update before measuring). Updates normally flush
3
+ // automatically on a microtask.
4
+ //
5
+ // Implemented by the shared reactive core (see core.js).
6
+
7
+ export { flush } from "./core.js";
@@ -0,0 +1,10 @@
1
+ // createState() — the reactive value primitive.
2
+ //
3
+ // const count = createState(0);
4
+ // count.get(); // read (subscribes the running effect/computed)
5
+ // count.set(1); // write (wakes subscribers, only if the value changed)
6
+ // count.peek(); // read without subscribing
7
+ //
8
+ // Implemented by the shared reactive core (see core.js).
9
+
10
+ export { createState } from "./core.js";
@@ -0,0 +1,51 @@
1
+ // dom.js — small helpers over native DOM APIs.
2
+ //
3
+ // Intentionally thin: the framework prefers using the platform directly.
4
+
5
+ /**
6
+ * Resolve a target that may be an element or a CSS selector string.
7
+ * @param {Element|string} target
8
+ * @returns {Element}
9
+ */
10
+ export function resolveTarget(target) {
11
+ const el = typeof target === "string" ? document.querySelector(target) : target;
12
+ if (!el) {
13
+ throw new Error(`Zoijs: mount target not found: ${String(target)}`);
14
+ }
15
+ return el;
16
+ }
17
+
18
+ // Trusted Types support. `template.innerHTML = string` is a Trusted-Types sink,
19
+ // so under a strict `require-trusted-types-for 'script'` CSP it would throw. The
20
+ // htmlText here is ALWAYS framework-generated from the author's static template
21
+ // strings + markers — dynamic values never reach it (the scanner forbids that) —
22
+ // so a pass-through policy is safe by construction. Pages enforcing Trusted Types
23
+ // must allow the `zoijs` policy (e.g. `trusted-types zoijs`).
24
+ let ttPolicy;
25
+ let ttChecked = false;
26
+ function trustedHTML(htmlText) {
27
+ if (!ttChecked) {
28
+ ttChecked = true;
29
+ try {
30
+ const tt = typeof window !== "undefined" ? window.trustedTypes : undefined;
31
+ if (tt && tt.createPolicy) {
32
+ ttPolicy = tt.createPolicy("zoijs", { createHTML: (s) => s });
33
+ }
34
+ } catch {
35
+ ttPolicy = undefined; // policy name unavailable; fall back
36
+ }
37
+ }
38
+ return ttPolicy ? ttPolicy.createHTML(htmlText) : htmlText;
39
+ }
40
+
41
+ /**
42
+ * Build an inert <template> from an HTML string and return the element.
43
+ * Parsing happens inside <template>, so no scripts run and no resources load.
44
+ * @param {string} htmlText
45
+ * @returns {HTMLTemplateElement}
46
+ */
47
+ export function createTemplate(htmlText) {
48
+ const template = document.createElement("template");
49
+ template.innerHTML = trustedHTML(htmlText);
50
+ return template;
51
+ }
@@ -0,0 +1,55 @@
1
+ // security.js — secure-by-default helpers.
2
+ //
3
+ // These are NOT optional add-ons; the renderer routes every dynamic value
4
+ // through them so the safe path is the default path. See docs/security.md.
5
+
6
+ /**
7
+ * Coerce a value to a string for safe insertion as TEXT.
8
+ * Text slots are written via a Text node (inert), so this is just predictable
9
+ * coercion: null/undefined become "".
10
+ * @param {any} value
11
+ * @returns {string}
12
+ */
13
+ export function toText(value) {
14
+ return value === null || value === undefined ? "" : String(value);
15
+ }
16
+
17
+ // Allowlisted URL schemes for URL-bearing attributes (href, src, ...).
18
+ const SAFE_SCHEMES = new Set(["http", "https", "mailto", "tel"]);
19
+ // data: is allowed only for raster image MIME types (never text/html, never SVG,
20
+ // which can carry script when navigated to).
21
+ const SAFE_DATA_IMAGE = /^data:image\/(png|jpe?g|gif|webp|avif|bmp|x-icon)[;,]/;
22
+
23
+ /**
24
+ * Is this URL safe for a URL-bearing attribute (href, src, action, ...)?
25
+ * Relative URLs (no scheme) are allowed; otherwise only an allowlist of schemes.
26
+ * @param {string} url
27
+ * @returns {boolean}
28
+ */
29
+ export function isSafeUrl(url) {
30
+ // Browsers strip ASCII control characters (incl. TAB/CR/LF) before parsing a
31
+ // URL, so "java\tscript:alert(1)" becomes "javascript:". Strip them first, or
32
+ // the scheme check can be bypassed.
33
+ const cleaned = String(url).replace(/[\x00-\x1F]/g, "").trim();
34
+ const match = cleaned.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):/);
35
+ if (!match) return true; // relative URL (no scheme) — safe
36
+ const scheme = match[1].toLowerCase();
37
+ if (scheme === "data") return SAFE_DATA_IMAGE.test(cleaned.toLowerCase());
38
+ return SAFE_SCHEMES.has(scheme);
39
+ }
40
+
41
+ // Attribute names that must never be bound from data.
42
+ const DANGEROUS_ATTRS = new Set(["srcdoc"]); // iframe srcdoc = raw-HTML sink
43
+
44
+ /**
45
+ * Reject attribute names that should never be bound from data: inline event
46
+ * handlers (`on*`, which have their own safe path) and raw-HTML sinks (`srcdoc`).
47
+ * @param {string} name
48
+ * @returns {boolean} true if the attribute name is allowed
49
+ */
50
+ export function isSafeAttributeName(name) {
51
+ const lower = name.toLowerCase();
52
+ if (lower.startsWith("on")) return false;
53
+ if (DANGEROUS_ATTRS.has(lower)) return false;
54
+ return true;
55
+ }