@nisli/core 0.54.1 → 0.56.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.
Files changed (47) hide show
  1. package/README.md +119 -4
  2. package/dist/component.js +146 -5
  3. package/dist/component.js.map +1 -1
  4. package/dist/diagnostics.d.ts +48 -0
  5. package/dist/diagnostics.js +118 -0
  6. package/dist/diagnostics.js.map +1 -0
  7. package/dist/disposable.d.ts +14 -0
  8. package/dist/disposable.js +8 -0
  9. package/dist/disposable.js.map +1 -0
  10. package/dist/emitter.d.ts +2 -1
  11. package/dist/emitter.js +2 -1
  12. package/dist/emitter.js.map +1 -1
  13. package/dist/hmr/registry.js +8 -1
  14. package/dist/hmr/registry.js.map +1 -1
  15. package/dist/index.d.ts +6 -3
  16. package/dist/index.js +9 -2
  17. package/dist/index.js.map +1 -1
  18. package/dist/injector.d.ts +9 -2
  19. package/dist/injector.js +18 -0
  20. package/dist/injector.js.map +1 -1
  21. package/dist/lifecycle.d.ts +9 -0
  22. package/dist/lifecycle.js +17 -7
  23. package/dist/lifecycle.js.map +1 -1
  24. package/dist/projection.js +24 -5
  25. package/dist/projection.js.map +1 -1
  26. package/dist/query.d.ts +134 -55
  27. package/dist/query.js +379 -188
  28. package/dist/query.js.map +1 -1
  29. package/dist/resource.d.ts +2 -1
  30. package/dist/resource.js +21 -4
  31. package/dist/resource.js.map +1 -1
  32. package/dist/settle.d.ts +61 -0
  33. package/dist/settle.js +106 -0
  34. package/dist/settle.js.map +1 -0
  35. package/dist/signal.d.ts +42 -7
  36. package/dist/signal.js +248 -68
  37. package/dist/signal.js.map +1 -1
  38. package/dist/template-audit.d.ts +64 -0
  39. package/dist/template-audit.js +246 -0
  40. package/dist/template-audit.js.map +1 -0
  41. package/dist/template.d.ts +69 -12
  42. package/dist/template.js +349 -66
  43. package/dist/template.js.map +1 -1
  44. package/dist/view-transition.d.ts +70 -0
  45. package/dist/view-transition.js +98 -0
  46. package/dist/view-transition.js.map +1 -0
  47. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ /**
2
+ * template-audit.ts — First-parse static template audit (ADR 0030.2 T4).
3
+ *
4
+ * Attached to the parse-once cache in template.ts: runs ONCE per `html`
5
+ * callsite (keyed by TemplateStringsArray identity) at the first parse, and
6
+ * walks the parsed-but-never-mounted template element checking its STATIC
7
+ * parts. The silent-static-typo class — the no-build equivalent of a compile
8
+ * error — dies in the engine, amortized free.
9
+ *
10
+ * Checks (per ADR 0030.2 §2 T4 as amended by §8 "T4 (template)"):
11
+ * - N101 — a dash-tag with no customElements definition. Races
12
+ * `customElements.whenDefined` against a microtask + macrotask grace so
13
+ * ordinary lazy definition (module import order) never warns.
14
+ * - N102 — an `@event` name that is neither a known native event nor an
15
+ * allowlisted component-dispatched event (`ui-*`, `nisli-*`).
16
+ * - N103 — an unknown `@event.modifier`.
17
+ * - N104 — an attribute on a component tag that declares attributes
18
+ * (non-empty `static observedAttributes`) which is neither declared nor a
19
+ * global HTML attribute. Components WITHOUT declared attributes get NO
20
+ * attribute audit: pre-schema (T3) there is no truth source — auto-resolved
21
+ * props are deliberately not attributes (§8).
22
+ *
23
+ * Suppression escape (designed for lazy islands, §8): a comment directive as
24
+ * the template's FIRST node (leading whitespace allowed):
25
+ *
26
+ * html`<!-- nisli-audit off --><my-island>…</my-island>`
27
+ * — disables the whole audit for this template.
28
+ *
29
+ * html`<!-- nisli-audit allow-undefined: my-island other-tag --><…>`
30
+ * — suppresses only N101 for the named tags (other checks stay live).
31
+ *
32
+ * The directive comment is static content: it survives into the mounted DOM
33
+ * (and SSG output) as an inert HTML comment. Documented, accepted for the
34
+ * prototype; the design gate may choose to strip it at parse.
35
+ *
36
+ * Dev gating: core has no build-time dev/prod define today — that mechanism
37
+ * is a named Wave-1 deliverable (§8). Until it exists this module is gated on
38
+ * the `devMode` flag below ONLY. SSG/happy-dom detection is deliberately NOT
39
+ * attempted (unreliable); an SSG build that wants silence must call
40
+ * `setTemplateAuditEnabled(false)` before rendering.
41
+ */
42
+ // Reporting goes through the diagnostics leaf. The audit keeps its own
43
+ // enable override (tests, SSG environments) and otherwise follows the
44
+ // leaf's dev gate — under NODE_ENV=production builds it is silent.
45
+ import { diag as emitDiag, isDev } from './diagnostics.js';
46
+ let enabledOverride = null;
47
+ /**
48
+ * Enable/disable the first-parse audit (and its callsite capture);
49
+ * `null` restores the dev-gate default.
50
+ */
51
+ export function setTemplateAuditEnabled(enabled) {
52
+ enabledOverride = enabled;
53
+ }
54
+ /** Current audit gate state (exposed for template.ts and tests). */
55
+ export function isTemplateAuditEnabled() {
56
+ return enabledOverride ?? isDev();
57
+ }
58
+ /** Emit a coded audit diagnostic through the diagnostics leaf (audit-gated). */
59
+ export function diag(code, message, detail) {
60
+ if (!isTemplateAuditEnabled())
61
+ return;
62
+ emitDiag(code, message, detail === undefined ? undefined : { detail });
63
+ }
64
+ // ── Callsite capture ────────────────────────────────────────────────
65
+ /** First-seen authoring callsite per template identity (dev only). */
66
+ const callsites = new WeakMap();
67
+ /**
68
+ * Remember the authoring callsite of an `html` template the first time its
69
+ * strings identity is seen. Must be called from `html()` itself — by the
70
+ * time the first parse happens (first mount), the authoring frame is gone
71
+ * from the stack. No-op when the audit is disabled or already captured.
72
+ */
73
+ export function captureCallsite(strings) {
74
+ if (!isTemplateAuditEnabled() || callsites.has(strings))
75
+ return;
76
+ const stack = new Error().stack ?? '';
77
+ const frame = stack
78
+ .split('\n')
79
+ .slice(1)
80
+ .find(line => line.includes('at ') && !/template(-audit)?\.(ts|js|mjs|cjs)/.test(line));
81
+ callsites.set(strings, frame ? frame.trim() : '(unknown callsite)');
82
+ }
83
+ // ── Audit ───────────────────────────────────────────────────────────
84
+ const audited = new WeakSet();
85
+ /** Modifiers understood by bindEvent (template.ts). */
86
+ const KNOWN_MODIFIERS = new Set(['stop', 'prevent', 'once', 'enter', 'escape', 'space', 'tab']);
87
+ /** Component-dispatched event prefixes exempt from the native-event check (§8). */
88
+ const COMPONENT_EVENT_PREFIXES = ['ui-', 'nisli-'];
89
+ /** Real native events with no `on*` IDL attribute (the `in`-check misses them). */
90
+ const NATIVE_EVENTS_WITHOUT_IDL = new Set(['focusin', 'focusout']);
91
+ /**
92
+ * Global HTML attributes legal on ANY component tag. Mirrors (and must stay a
93
+ * superset of) template.ts's auto-resolution exclusion list — these reach the
94
+ * element as plain attributes, never `_setProp`.
95
+ */
96
+ const GLOBAL_ATTRS = new Set([
97
+ 'id', 'class', 'style', 'slot', 'title', 'role', 'tabindex', 'name',
98
+ 'part', 'hidden', 'dir', 'lang', 'is',
99
+ ]);
100
+ /** Parse the leading `<!-- nisli-audit … -->` directive comment, if any. */
101
+ function parseDirectives(content) {
102
+ const directives = { off: false, allowUndefined: new Set() };
103
+ for (const node of content.childNodes) {
104
+ if (node.nodeType === Node.TEXT_NODE) {
105
+ if ((node.textContent ?? '').trim() === '')
106
+ continue; // leading whitespace
107
+ break;
108
+ }
109
+ if (node.nodeType === Node.COMMENT_NODE) {
110
+ const body = (node.textContent ?? '').trim();
111
+ if (!body.startsWith('nisli-audit'))
112
+ break;
113
+ const rest = body.slice('nisli-audit'.length).trim();
114
+ if (rest === 'off') {
115
+ directives.off = true;
116
+ }
117
+ else if (rest.startsWith('allow-undefined')) {
118
+ const tags = rest
119
+ .slice('allow-undefined'.length)
120
+ .replace(/^[:\s]+/, '')
121
+ .split(/[\s,]+/)
122
+ .filter(Boolean);
123
+ for (const tag of tags)
124
+ directives.allowUndefined.add(tag.toLowerCase());
125
+ }
126
+ }
127
+ break; // only the FIRST non-whitespace node may carry the directive
128
+ }
129
+ return directives;
130
+ }
131
+ /**
132
+ * N101 — dash-tag with no definition. Rather than warning synchronously (the
133
+ * defining module may simply import later in the same task), race
134
+ * `customElements.whenDefined(tag)` against a microtask + macrotask grace;
135
+ * warn only if the tag is still undefined after the grace elapses.
136
+ */
137
+ function checkDashTag(tag, site) {
138
+ let registry;
139
+ try {
140
+ registry = customElements;
141
+ }
142
+ catch {
143
+ return; // no registry in this environment — nothing to check
144
+ }
145
+ if (!registry || registry.get(tag))
146
+ return;
147
+ let defined = false;
148
+ try {
149
+ registry.whenDefined(tag).then(() => { defined = true; }, () => { });
150
+ }
151
+ catch {
152
+ diag('N101', `<${tag}> is not a valid custom element name — at ${site}`);
153
+ return;
154
+ }
155
+ // Grace: one microtask hop (whenDefined settlements for definitions made
156
+ // earlier in this task) + one macrotask hop (definitions made later in the
157
+ // same task, e.g. module evaluation order). Under fake timers the macrotask
158
+ // may never fire — the warning is simply skipped, never wrong.
159
+ queueMicrotask(() => {
160
+ if (defined)
161
+ return;
162
+ setTimeout(() => {
163
+ try {
164
+ if (!isTemplateAuditEnabled() || defined || registry.get(tag))
165
+ return;
166
+ diag('N101', `<${tag}> is used in a template but never defined (customElements.get returned ` +
167
+ `undefined after grace) — at ${site}. If this tag is a lazily-loaded island, ` +
168
+ `suppress with a leading <!-- nisli-audit allow-undefined: ${tag} --> comment.`);
169
+ }
170
+ catch { /* environment torn down during grace — nothing to report */ }
171
+ }, 0);
172
+ });
173
+ }
174
+ /** N102/N103 — event-name and modifier checks for one `@event.mods` attribute. */
175
+ function checkEvent(el, attrName, site) {
176
+ const parts = attrName.slice(1).split('.');
177
+ const eventName = parts[0] ?? '';
178
+ const modifiers = parts.slice(1);
179
+ const isNative = `on${eventName}` in el || NATIVE_EVENTS_WITHOUT_IDL.has(eventName);
180
+ const isComponentEvent = COMPONENT_EVENT_PREFIXES.some(p => eventName.startsWith(p));
181
+ if (eventName && !isNative && !isComponentEvent) {
182
+ diag('N102', `@${eventName} on <${el.tagName.toLowerCase()}> is not a known native event ` +
183
+ `(and not an allowlisted ${COMPONENT_EVENT_PREFIXES.join('/')} component event) — at ${site}`);
184
+ }
185
+ for (const mod of modifiers) {
186
+ if (!KNOWN_MODIFIERS.has(mod)) {
187
+ diag('N103', `unknown event modifier ".${mod}" in "${attrName}" — known: ` +
188
+ `${[...KNOWN_MODIFIERS].join(', ')} — at ${site}`);
189
+ }
190
+ }
191
+ }
192
+ /** N104 — undeclared attribute on a component that DECLARES attributes. */
193
+ function checkComponentAttr(tag, name, observed, site) {
194
+ if (name.startsWith('data-') || name.startsWith('aria-'))
195
+ return;
196
+ if (GLOBAL_ATTRS.has(name) || observed.includes(name))
197
+ return;
198
+ diag('N104', `<${tag}> declares attributes [${observed.join(', ')}] but the template sets ` +
199
+ `unknown "${name}" — at ${site}. Undeclared names route through _setProp and are ` +
200
+ `silently dead as attributes.`);
201
+ }
202
+ /**
203
+ * Run the one-time static audit over a freshly parsed template. Called by
204
+ * template.ts's parse-once cache on cache miss; idempotent per template
205
+ * identity, no-op when the audit is disabled. Read-only over the cached
206
+ * template element — mounting state is never touched.
207
+ */
208
+ export function auditTemplate(strings, template) {
209
+ if (!isTemplateAuditEnabled() || audited.has(strings))
210
+ return;
211
+ audited.add(strings);
212
+ const site = callsites.get(strings) ?? '(unknown callsite)';
213
+ const content = template.content;
214
+ const directives = parseDirectives(content);
215
+ if (directives.off)
216
+ return;
217
+ for (const el of content.querySelectorAll('*')) {
218
+ const tag = el.tagName.toLowerCase();
219
+ const isDashTag = tag.includes('-');
220
+ if (isDashTag && !directives.allowUndefined.has(tag)) {
221
+ checkDashTag(tag, site);
222
+ }
223
+ // Attribute audit ONLY for components with declared attrs (§8): a
224
+ // non-empty static observedAttributes is the single pre-T3 truth source.
225
+ let observed;
226
+ if (isDashTag) {
227
+ const ctor = customElements.get(tag);
228
+ const list = ctor?.observedAttributes;
229
+ if (Array.isArray(list) && list.length > 0)
230
+ observed = list;
231
+ }
232
+ for (const attr of [...el.attributes]) {
233
+ const name = attr.name;
234
+ if (name.startsWith('@')) {
235
+ checkEvent(el, name, site);
236
+ continue;
237
+ }
238
+ // Directive positions with their own semantics — never attributes.
239
+ if (name.startsWith('class:') || name === 'ref' || name === 'html:inner')
240
+ continue;
241
+ if (observed)
242
+ checkComponentAttr(tag, name, observed, site);
243
+ }
244
+ }
245
+ }
246
+ //# sourceMappingURL=template-audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-audit.js","sourceRoot":"","sources":["../src/template-audit.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,59 @@ export interface TemplateResult {
21
21
  }
22
22
  /** A DOM event handler whose event type is inferred from its native event name. */
23
23
  export type TypedEventHandler<K extends keyof HTMLElementEventMap> = (event: HTMLElementEventMap[K]) => void;
24
+ /**
25
+ * Branded trusted-HTML value accepted by `html:inner` (and by @nisli/ssg's
26
+ * static renderer, which re-exports this brand — core owns it, ADR 0030.2 §8).
27
+ * Structural, not nominal: cross-package/cross-realm values with the same
28
+ * shape stay valid.
29
+ */
30
+ export interface RawHtml {
31
+ value: string;
32
+ __raw: true;
33
+ }
34
+ /**
35
+ * Mark a string as trusted raw HTML. `html:inner` accepts ONLY branded
36
+ * values — a bare string throws N106 (the accidental-XSS-sink class closes,
37
+ * ADR 0030.2 §3). NEVER wrap user-generated input.
38
+ */
39
+ export declare function raw(value: string): RawHtml;
40
+ /**
41
+ * Branded UNTRUSTED-HTML value accepted by `html:inner`. The mirror image of
42
+ * {@link RawHtml}: where `raw()` is the author asserting markup is already
43
+ * trustworthy, this brand asserts the opposite — the string is untrusted and
44
+ * MUST pass a sanitizer before it reaches the DOM (ADR 0019:124–128, executed
45
+ * by bet-09(c)). Structural, not nominal, on the same rule as RawHtml.
46
+ */
47
+ export interface SanitizedHtml {
48
+ value: string;
49
+ __sanitize: true;
50
+ }
51
+ /**
52
+ * Mark a string as untrusted markup to be sanitized at the sink — the brand
53
+ * for user-generated HTML, which must NEVER go through `raw()`.
54
+ *
55
+ * `html:inner` writes it with the platform's `Element.setHTML()` (the spec's
56
+ * XSS-safe default sanitizer) when the engine has one, otherwise with the hook
57
+ * registered by {@link setSanitizerFallback}. With NEITHER available the
58
+ * binding throws N107: it never falls back to `innerHTML`. nisli bundles no
59
+ * sanitizer — the trust decision, and its bytes, stay in the app.
60
+ */
61
+ export declare function sanitized(value: string): SanitizedHtml;
62
+ /** A sanitizing writer: sanitize `html`, then put the result inside `el`. */
63
+ export type SanitizerFallback = (el: Element, html: string) => void;
64
+ /**
65
+ * Register the sanitizing writer used for `sanitized()` values on engines
66
+ * WITHOUT native `Element.setHTML()` (Safari today, older Chromium/Firefox),
67
+ * or `null` to unregister. Native `setHTML()` always wins where it exists, so
68
+ * modern engines never pay the hook. nisli ships no sanitizer; wire one:
69
+ *
70
+ * setSanitizerFallback((el, html) => {
71
+ * el.innerHTML = DOMPurify.sanitize(html);
72
+ * });
73
+ */
74
+ export declare function setSanitizerFallback(fn: SanitizerFallback | null): void;
75
+ /** @internal Test-only: number of actual `<template>` parses performed. */
76
+ export declare function __templateParseCount(): number;
24
77
  /**
25
78
  * Tagged template literal for creating reactive DOM templates.
26
79
  *
@@ -34,22 +87,26 @@ export type TypedEventHandler<K extends keyof HTMLElementEventMap> = (event: HTM
34
87
  * the specific text node or attribute updates — no diffing needed.
35
88
  */
36
89
  export declare function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
90
+ export declare function each<T>(items: ReadonlySignal<T[]>, keyFn: (item: T, index: number) => string | number, templateFn: (item: ReadonlySignal<T>, index: ReadonlySignal<number>) => TemplateResult | {
91
+ __type: 'factory';
92
+ }): TemplateResult;
37
93
  /**
38
- * Reactive list rendering with keyed reconciliation.
94
+ * Conditional rendering helper (ADR 0030.2 §3, boolean-gated with `else`).
39
95
  *
40
- * Renders a list of items from a signal, tracking each item by key.
41
- * When the array changes, only affected DOM nodes are added, removed,
42
- * or reordered — existing items update in-place via their signals.
96
+ * Shows `template` when the condition is truthy, `elseTemplate` (optional)
97
+ * when falsy. Both arms accept a TemplateResult or a lazy callback
98
+ * `() => TemplateResult`; callbacks are only evaluated for the active arm.
43
99
  *
44
- * ```ts
45
- * const tasks = signal([{ id: '1', title: 'A' }, { id: '2', title: 'B' }]);
46
- * html`<ul>${each(tasks, t => t.id, (task, index) =>
47
- * html`<li>${computed(() => task.value.title)}</li>`
48
- * )}</ul>`
49
- * ```
100
+ * Semantics for signal conditions:
101
+ * - The gate is `!!condition.value` — a truthy→truthy (or falsy→falsy)
102
+ * transition returns the memoized previous result, so the live branch is
103
+ * NEVER rebuilt (no state/scroll/focus loss) and callbacks never re-run.
104
+ * - Branch callbacks evaluate UNTRACKED: a signal read during branch
105
+ * construction does not subscribe the when() computed, so it cannot
106
+ * trigger a rebuild. Reactive parts inside a branch must be interpolated
107
+ * as signals/computeds (values), not read raw at construction time.
50
108
  */
51
- export declare function each<T>(items: ReadonlySignal<T[]>, keyFn: (item: T, index: number) => string | number, templateFn: (item: ReadonlySignal<T>, index: ReadonlySignal<number>) => TemplateResult): TemplateResult;
52
- export declare function when(condition: unknown, template: TemplateResult | (() => TemplateResult)): TemplateResult | ReadonlySignal<TemplateResult | null> | null;
109
+ export declare function when(condition: unknown, template: TemplateResult | (() => TemplateResult), elseTemplate?: TemplateResult | (() => TemplateResult)): TemplateResult | ReadonlySignal<TemplateResult | null> | null;
53
110
  /** A value bindable to an `el()` attribute — static or reactive. */
54
111
  type ElAttrValue = string | number | boolean | null | undefined | ReadonlySignal<unknown>;
55
112
  export interface ElProps {