dalila 1.9.24 → 1.9.25

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/README.md CHANGED
@@ -44,6 +44,8 @@ const ctx = {
44
44
  bind(document.getElementById('app')!, ctx);
45
45
  ```
46
46
 
47
+ For bundle-sensitive or no-bundler builds, prefer leaf subpaths like `dalila/core/signal` and `dalila/runtime/bind`.
48
+
47
49
  ## Docs
48
50
 
49
51
  ### Start here
@@ -102,6 +104,8 @@ bind(document.getElementById('app')!, ctx);
102
104
  ```txt
103
105
  dalila → signals, scope, persist, forms, resources, query, mutations
104
106
  dalila/runtime → bind(), mount(), configure(), components, lazy, transitions
107
+ dalila/core/* → leaf core modules for tighter bundles
108
+ dalila/runtime/* → leaf runtime modules for tighter bundles
105
109
  dalila/context → createContext(), provide(), inject()
106
110
  dalila/router → createRouter(), file-based routes, preloading
107
111
  dalila/http → createHttpClient()
@@ -1023,7 +1023,7 @@ export async function generateRoutesFile(routesDir, outputPath) {
1023
1023
  const importLines = [];
1024
1024
  importLines.push(`import type { RouteTable } from 'dalila/router';`);
1025
1025
  if (hasHtml) {
1026
- importLines.push(`import { fromHtml } from 'dalila';`);
1026
+ importLines.push(`import { fromHtml } from 'dalila/runtime/from-html';`);
1027
1027
  }
1028
1028
  if (imports.length > 0) {
1029
1029
  importLines.push(...imports);
@@ -1,4 +1,3 @@
1
- import { installDefaultSecurityObservability } from "./observability.js";
2
1
  export * from "./scope.js";
3
2
  export * from "./signal.js";
4
3
  export * from "./observability.js";
@@ -14,4 +13,3 @@ export * from "./query.js";
14
13
  export * from "./mutation.js";
15
14
  export { batch, measure, mutate, timeSlice, configureScheduler, getSchedulerConfig } from "./scheduler.js";
16
15
  export { persist, createJSONStorage, clearPersisted, createPreloadScript, createThemeScript } from "./persist.js";
17
- installDefaultSecurityObservability();
@@ -1,3 +1,4 @@
1
+ import { type EffectErrorHandler } from "./signal.js";
1
2
  export interface SecurityRuntimeEvent {
2
3
  timestamp: string;
3
4
  source: string;
@@ -6,6 +7,8 @@ export interface SecurityRuntimeEvent {
6
7
  fatal: boolean;
7
8
  }
8
9
  export declare const SECURITY_RUNTIME_EVENT_NAME = "dalila:security-error";
10
+ export declare const observabilityEffectErrorHandler: EffectErrorHandler;
11
+ export declare function reportObservedEffectError(error: Error, source: string): void;
9
12
  export declare function installDefaultSecurityObservability(): void;
10
13
  export declare function getSecurityRuntimeEvents(): readonly SecurityRuntimeEvent[];
11
14
  export declare function clearSecurityRuntimeEvents(): void;
@@ -1,11 +1,11 @@
1
- import { FatalEffectError, setDefaultEffectErrorHandler, } from "./signal.js";
1
+ import { setDefaultEffectErrorHandler } from "./signal.js";
2
2
  export const SECURITY_RUNTIME_EVENT_NAME = "dalila:security-error";
3
3
  const MAX_SECURITY_RUNTIME_EVENTS = 25;
4
4
  const SECURITY_MESSAGE_PATTERN = /\b(sanitizehtml|trusted types?|srcdoc|javascript:|data:text\/html|xss|csp)\b/i;
5
5
  const securityRuntimeEvents = [];
6
6
  let observabilityInstalled = false;
7
7
  function isSecurityRuntimeError(error) {
8
- return error instanceof FatalEffectError || (error.message.startsWith("[Dalila]")
8
+ return error.name === "FatalEffectError" || (error.message.startsWith("[Dalila]")
9
9
  && SECURITY_MESSAGE_PATTERN.test(error.message));
10
10
  }
11
11
  function storeSecurityRuntimeEvent(event) {
@@ -23,14 +23,14 @@ function dispatchSecurityRuntimeEvent(event) {
23
23
  detail: event,
24
24
  }));
25
25
  }
26
- const observabilityEffectErrorHandler = (error, source) => {
26
+ export const observabilityEffectErrorHandler = (error, source) => {
27
27
  if (isSecurityRuntimeError(error)) {
28
28
  const event = {
29
29
  timestamp: new Date().toISOString(),
30
30
  source,
31
31
  message: error.message,
32
32
  name: error.name,
33
- fatal: error instanceof FatalEffectError,
33
+ fatal: error.name === "FatalEffectError",
34
34
  };
35
35
  storeSecurityRuntimeEvent(event);
36
36
  dispatchSecurityRuntimeEvent(event);
@@ -39,6 +39,9 @@ const observabilityEffectErrorHandler = (error, source) => {
39
39
  }
40
40
  console.error(`[Dalila] Error in ${source}:`, error);
41
41
  };
42
+ export function reportObservedEffectError(error, source) {
43
+ observabilityEffectErrorHandler(error, source);
44
+ }
42
45
  export function installDefaultSecurityObservability() {
43
46
  if (observabilityInstalled)
44
47
  return;
@@ -1,5 +1,6 @@
1
1
  import { getCurrentScope, withScope } from './scope.js';
2
2
  import { scheduleMicrotask, isBatching, queueInBatch } from './scheduler.js';
3
+ import { reportObservedEffectError } from './observability.js';
3
4
  import { aliasEffectToNode, linkSubscriberSetToSignal, registerEffect, registerSignal, trackDependency, trackEffectDispose, trackComputedRunEnd, trackComputedRunStart, trackEffectRunEnd, trackEffectRunStart, trackSignalRead, trackSignalWrite, untrackDependencyBySet, } from './devtools.js';
4
5
  let effectErrorHandler = null;
5
6
  let defaultEffectErrorHandler = null;
@@ -41,7 +42,7 @@ function reportEffectErrorWithHandlers(error, source) {
41
42
  defaultEffectErrorHandler(error, source);
42
43
  return;
43
44
  }
44
- console.error(`[Dalila] Error in ${source}:`, error);
45
+ reportObservedEffectError(error, source);
45
46
  }
46
47
  /**
47
48
  * Normalize unknown throws into Error and route to the global handler (or console).
@@ -0,0 +1,18 @@
1
+ import type { BindContext, DisposeFunction, TransitionConfig } from './bind.js';
2
+ type TransitionRegistry = Map<string, TransitionConfig>;
3
+ interface TransitionController {
4
+ hasTransition: boolean;
5
+ enter: () => void;
6
+ leave: (onDone: () => void) => void;
7
+ }
8
+ interface BindIfDirectiveDeps {
9
+ qsaIncludingRoot: (root: Element, selector: string) => Element[];
10
+ normalizeBinding: (raw: string | null) => string | null;
11
+ warn: (message: string) => void;
12
+ resolve: (value: unknown) => unknown;
13
+ bindEffect: (target: Element | null | undefined, fn: () => void) => void;
14
+ createTransitionController: (el: HTMLElement, registry: TransitionRegistry, cleanups: DisposeFunction[]) => TransitionController;
15
+ syncPortalElement: (el: HTMLElement) => void;
16
+ }
17
+ export declare function bindIfDirective(root: Element, ctx: BindContext, cleanups: DisposeFunction[], transitionRegistry: TransitionRegistry, deps: BindIfDirectiveDeps): void;
18
+ export {};
@@ -0,0 +1,96 @@
1
+ export function bindIfDirective(root, ctx, cleanups, transitionRegistry, deps) {
2
+ const elements = deps.qsaIncludingRoot(root, '[d-if]');
3
+ for (const el of elements) {
4
+ const bindingName = deps.normalizeBinding(el.getAttribute('d-if'));
5
+ if (!bindingName)
6
+ continue;
7
+ const binding = ctx[bindingName];
8
+ if (binding === undefined) {
9
+ deps.warn(`d-if: "${bindingName}" not found in context`);
10
+ continue;
11
+ }
12
+ const elseEl = el.nextElementSibling?.hasAttribute('d-else') ? el.nextElementSibling : null;
13
+ const comment = document.createComment('d-if');
14
+ el.parentNode?.replaceChild(comment, el);
15
+ el.removeAttribute('d-if');
16
+ const htmlEl = el;
17
+ const transitions = deps.createTransitionController(htmlEl, transitionRegistry, cleanups);
18
+ let elseHtmlEl = null;
19
+ let elseComment = null;
20
+ let elseTransitions = null;
21
+ if (elseEl) {
22
+ elseComment = document.createComment('d-else');
23
+ elseEl.parentNode?.replaceChild(elseComment, elseEl);
24
+ elseEl.removeAttribute('d-else');
25
+ elseHtmlEl = elseEl;
26
+ elseTransitions = deps.createTransitionController(elseHtmlEl, transitionRegistry, cleanups);
27
+ }
28
+ const initialValue = !!deps.resolve(binding);
29
+ if (initialValue) {
30
+ comment.parentNode?.insertBefore(htmlEl, comment);
31
+ deps.syncPortalElement(htmlEl);
32
+ if (transitions.hasTransition) {
33
+ htmlEl.removeAttribute('data-leave');
34
+ htmlEl.setAttribute('data-enter', '');
35
+ }
36
+ }
37
+ else if (elseHtmlEl && elseComment) {
38
+ elseComment.parentNode?.insertBefore(elseHtmlEl, elseComment);
39
+ deps.syncPortalElement(elseHtmlEl);
40
+ if (elseTransitions?.hasTransition) {
41
+ elseHtmlEl.removeAttribute('data-leave');
42
+ elseHtmlEl.setAttribute('data-enter', '');
43
+ }
44
+ }
45
+ if (elseHtmlEl && elseComment) {
46
+ const capturedElseEl = elseHtmlEl;
47
+ const capturedElseComment = elseComment;
48
+ deps.bindEffect(htmlEl, () => {
49
+ const value = !!deps.resolve(binding);
50
+ if (value) {
51
+ if (!htmlEl.parentNode) {
52
+ comment.parentNode?.insertBefore(htmlEl, comment);
53
+ deps.syncPortalElement(htmlEl);
54
+ }
55
+ transitions.enter();
56
+ elseTransitions?.leave(() => {
57
+ if (capturedElseEl.parentNode) {
58
+ capturedElseEl.parentNode.removeChild(capturedElseEl);
59
+ }
60
+ });
61
+ }
62
+ else {
63
+ transitions.leave(() => {
64
+ if (htmlEl.parentNode) {
65
+ htmlEl.parentNode.removeChild(htmlEl);
66
+ }
67
+ });
68
+ if (!capturedElseEl.parentNode) {
69
+ capturedElseComment.parentNode?.insertBefore(capturedElseEl, capturedElseComment);
70
+ deps.syncPortalElement(capturedElseEl);
71
+ }
72
+ elseTransitions?.enter();
73
+ }
74
+ });
75
+ }
76
+ else {
77
+ deps.bindEffect(htmlEl, () => {
78
+ const value = !!deps.resolve(binding);
79
+ if (value) {
80
+ if (!htmlEl.parentNode) {
81
+ comment.parentNode?.insertBefore(htmlEl, comment);
82
+ deps.syncPortalElement(htmlEl);
83
+ }
84
+ transitions.enter();
85
+ }
86
+ else {
87
+ transitions.leave(() => {
88
+ if (htmlEl.parentNode) {
89
+ htmlEl.parentNode.removeChild(htmlEl);
90
+ }
91
+ });
92
+ }
93
+ });
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,13 @@
1
+ import type { BindContext, BindOptions, DisposeFunction } from './bind.js';
2
+ interface BindLazyDirectiveDeps {
3
+ qsaIncludingRoot: (root: Element, selector: string) => Element[];
4
+ normalizeBinding: (raw: string | null) => string | null;
5
+ warn: (message: string) => void;
6
+ warnRawHtmlSinkHeuristic: (sink: string, source: string, html: string) => void;
7
+ resolveSecurityOptions: (options: BindOptions) => unknown;
8
+ bind: (root: Element, ctx: BindContext, options: BindOptions) => (() => void);
9
+ bindEffect: (target: Element | null | undefined, fn: () => void) => void;
10
+ inheritNestedBindOptions: (parent: BindOptions, overrides: BindOptions) => BindOptions;
11
+ }
12
+ export declare function bindLazyDirective(root: Element, ctx: BindContext, cleanups: DisposeFunction[], refs: Map<string, Element>, events: string[], options: BindOptions, deps: BindLazyDirectiveDeps): void;
13
+ export {};
@@ -0,0 +1,139 @@
1
+ import { getLazyComponent, observeLazyElement } from './lazy.js';
2
+ import { setElementInnerHTML } from './html-sinks.js';
3
+ export function bindLazyDirective(root, ctx, cleanups, refs, events, options, deps) {
4
+ const elements = deps.qsaIncludingRoot(root, '[d-lazy]');
5
+ for (const el of elements) {
6
+ const lazyComponentName = deps.normalizeBinding(el.getAttribute('d-lazy'));
7
+ if (!lazyComponentName)
8
+ continue;
9
+ const lazyResult = getLazyComponent(lazyComponentName);
10
+ if (!lazyResult) {
11
+ deps.warn(`d-lazy: component "${lazyComponentName}" not found. Use createLazyComponent() to create it.`);
12
+ continue;
13
+ }
14
+ const { state } = lazyResult;
15
+ const htmlEl = el;
16
+ const loadingTemplate = el.getAttribute('d-lazy-loading') ?? state.loadingTemplate ?? '';
17
+ const errorTemplate = el.getAttribute('d-lazy-error') ?? state.errorTemplate ?? '';
18
+ el.removeAttribute('d-lazy');
19
+ el.removeAttribute('d-lazy-loading');
20
+ el.removeAttribute('d-lazy-error');
21
+ let currentNode = htmlEl;
22
+ let componentMounted = false;
23
+ let componentDispose = null;
24
+ let componentEl = null;
25
+ let hasIntersected = false;
26
+ const refName = deps.normalizeBinding(htmlEl.getAttribute('d-ref'));
27
+ const syncRef = (node) => {
28
+ if (!refName)
29
+ return;
30
+ if (node instanceof Element) {
31
+ refs.set(refName, node);
32
+ }
33
+ };
34
+ const replaceCurrentNode = (nextNode) => {
35
+ const parent = currentNode.parentNode;
36
+ if (!parent)
37
+ return;
38
+ parent.replaceChild(nextNode, currentNode);
39
+ currentNode = nextNode;
40
+ syncRef(nextNode);
41
+ };
42
+ const unmountComponent = () => {
43
+ if (componentDispose) {
44
+ componentDispose();
45
+ componentDispose = null;
46
+ }
47
+ componentMounted = false;
48
+ componentEl = null;
49
+ };
50
+ const renderComponent = () => {
51
+ const comp = state.component();
52
+ if (!comp)
53
+ return;
54
+ const compDef = comp.definition;
55
+ const compEl = document.createElement(compDef.tag);
56
+ for (const attr of Array.from(htmlEl.attributes)) {
57
+ if (!attr.name.startsWith('d-')) {
58
+ compEl.setAttribute(attr.name, attr.value);
59
+ }
60
+ }
61
+ if (componentMounted && componentEl === compEl)
62
+ return;
63
+ replaceCurrentNode(compEl);
64
+ componentEl = compEl;
65
+ const parentCtx = Object.create(ctx);
66
+ const parent = compEl.parentNode;
67
+ const nextSibling = compEl.nextSibling;
68
+ componentDispose = deps.bind(compEl, parentCtx, {
69
+ components: { [compDef.tag]: comp },
70
+ events,
71
+ _skipLifecycle: true,
72
+ ...deps.inheritNestedBindOptions(options, {}),
73
+ });
74
+ if (!compEl.isConnected && parent) {
75
+ const renderedNode = nextSibling ? nextSibling.previousSibling : parent.lastChild;
76
+ if (renderedNode instanceof Node) {
77
+ currentNode = renderedNode;
78
+ syncRef(renderedNode);
79
+ }
80
+ }
81
+ componentMounted = true;
82
+ };
83
+ const showLoading = () => {
84
+ if (loadingTemplate) {
85
+ if (componentMounted) {
86
+ unmountComponent();
87
+ }
88
+ const loadingEl = document.createElement('div');
89
+ deps.warnRawHtmlSinkHeuristic('d-lazy-loading', lazyComponentName, loadingTemplate);
90
+ setElementInnerHTML(loadingEl, loadingTemplate, deps.resolveSecurityOptions(options));
91
+ replaceCurrentNode(loadingEl);
92
+ }
93
+ };
94
+ const showError = (err) => {
95
+ if (componentMounted) {
96
+ unmountComponent();
97
+ }
98
+ if (errorTemplate) {
99
+ const errorEl = document.createElement('div');
100
+ deps.warnRawHtmlSinkHeuristic('d-lazy-error', lazyComponentName, errorTemplate);
101
+ setElementInnerHTML(errorEl, errorTemplate, deps.resolveSecurityOptions(options));
102
+ replaceCurrentNode(errorEl);
103
+ }
104
+ else {
105
+ deps.warn(`d-lazy: failed to load "${lazyComponentName}": ${err.message}`);
106
+ }
107
+ };
108
+ const syncFromState = () => {
109
+ const loading = state.loading();
110
+ const error = state.error();
111
+ const comp = state.component();
112
+ if (!hasIntersected)
113
+ return;
114
+ if (error) {
115
+ showError(error);
116
+ return;
117
+ }
118
+ if (loading && !comp) {
119
+ showLoading();
120
+ return;
121
+ }
122
+ if (comp && !componentMounted) {
123
+ renderComponent();
124
+ }
125
+ };
126
+ deps.bindEffect(htmlEl, () => {
127
+ syncFromState();
128
+ });
129
+ const cleanupObserver = observeLazyElement(htmlEl, () => {
130
+ hasIntersected = true;
131
+ syncFromState();
132
+ state.load();
133
+ }, 0);
134
+ cleanups.push(() => {
135
+ cleanupObserver();
136
+ unmountComponent();
137
+ });
138
+ }
139
+ }
@@ -0,0 +1,21 @@
1
+ import type { SchedulerPriority } from '../core/scheduler.js';
2
+ import type { BindContext, BindOptions, DisposeFunction, VirtualListController, VirtualScrollToIndexOptions } from './bind.js';
3
+ interface BindListDirectiveDeps {
4
+ qsaIncludingRoot: (root: Element, selector: string) => Element[];
5
+ normalizeBinding: (raw: string | null) => string | null;
6
+ warn: (message: string) => void;
7
+ resolve: (value: unknown) => unknown;
8
+ bind: (root: Element, ctx: BindContext, options: BindOptions) => (() => void);
9
+ bindEffect: (target: Element | null | undefined, fn: () => void) => void;
10
+ inheritNestedBindOptions: (parent: BindOptions, overrides: BindOptions) => BindOptions;
11
+ isSignal: (value: unknown) => value is (() => unknown) & {
12
+ set: unknown;
13
+ update: unknown;
14
+ };
15
+ resolveListRenderPriority: () => SchedulerPriority;
16
+ }
17
+ export declare function getVirtualListController(target: Element | null): VirtualListController | null;
18
+ export declare function scrollToVirtualIndex(target: Element | null, index: number, options?: VirtualScrollToIndexOptions): boolean;
19
+ export declare function bindVirtualEachDirective(root: Element, ctx: BindContext, cleanups: DisposeFunction[], options: BindOptions, deps: BindListDirectiveDeps): void;
20
+ export declare function bindEachDirective(root: Element, ctx: BindContext, cleanups: DisposeFunction[], options: BindOptions, deps: BindListDirectiveDeps): void;
21
+ export {};