@rettangoli/fe 0.0.14 → 1.0.0-rc1

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,156 @@
1
+ import { parseView } from "../../parser.js";
2
+ import { createTransformedHandlers, runAfterMount, runBeforeMount } from "./lifecycle.js";
3
+ import { attachGlobalRefListeners } from "./globalListeners.js";
4
+ import { collectRefElements } from "./refs.js";
5
+ import {
6
+ createComponentRuntimeDeps,
7
+ cleanupEventRateLimitState,
8
+ syncRefIds,
9
+ } from "./componentRuntime.js";
10
+ import { buildOnUpdateChanges } from "./lifecycle.js";
11
+ import { normalizeAttributeValue, toCamelCase } from "./props.js";
12
+
13
+ export const createRuntimeDepsForInstance = ({ instance }) => {
14
+ return createComponentRuntimeDeps({
15
+ baseDeps: instance.deps,
16
+ refs: instance.refIds,
17
+ dispatchEvent: instance.dispatchEvent.bind(instance),
18
+ store: instance.store,
19
+ render: instance.render.bind(instance),
20
+ });
21
+ };
22
+
23
+ export const runConnectedComponentLifecycle = ({
24
+ instance,
25
+ parseAndRenderFn,
26
+ renderFn,
27
+ createTransformedHandlersFn = createTransformedHandlers,
28
+ runBeforeMountFn = runBeforeMount,
29
+ attachGlobalRefListenersFn = attachGlobalRefListeners,
30
+ runAfterMountFn = runAfterMount,
31
+ }) => {
32
+ const runtimeDeps = createRuntimeDepsForInstance({ instance });
33
+
34
+ instance.transformedHandlers = createTransformedHandlersFn({
35
+ handlers: instance.handlers,
36
+ deps: runtimeDeps,
37
+ parseAndRenderFn,
38
+ });
39
+
40
+ instance._unmountCallback = runBeforeMountFn({
41
+ handlers: instance.handlers,
42
+ deps: runtimeDeps,
43
+ });
44
+
45
+ instance._globalListenersCleanup = attachGlobalRefListenersFn({
46
+ refs: instance.refs,
47
+ handlers: instance.transformedHandlers,
48
+ parseAndRenderFn,
49
+ });
50
+
51
+ renderFn();
52
+
53
+ runAfterMountFn({
54
+ handlers: instance.handlers,
55
+ deps: runtimeDeps,
56
+ });
57
+
58
+ return runtimeDeps;
59
+ };
60
+
61
+ export const runDisconnectedComponentLifecycle = ({
62
+ instance,
63
+ clearTimerFn = clearTimeout,
64
+ }) => {
65
+ if (instance._unmountCallback) {
66
+ instance._unmountCallback();
67
+ }
68
+ if (instance._globalListenersCleanup) {
69
+ instance._globalListenersCleanup();
70
+ }
71
+ return cleanupEventRateLimitState({
72
+ transformedHandlers: instance.transformedHandlers,
73
+ clearTimerFn,
74
+ });
75
+ };
76
+
77
+ export const runAttributeChangedComponentLifecycle = ({
78
+ instance,
79
+ attributeName,
80
+ oldValue,
81
+ newValue,
82
+ scheduleFrameFn,
83
+ }) => {
84
+ if (oldValue === newValue || !instance.render) {
85
+ return;
86
+ }
87
+
88
+ if (instance.handlers?.handleOnUpdate) {
89
+ const runtimeDeps = createRuntimeDepsForInstance({ instance });
90
+ const changes = buildOnUpdateChanges({
91
+ attributeName,
92
+ oldValue,
93
+ newValue,
94
+ deps: runtimeDeps,
95
+ propsSchemaKeys: instance._propsSchemaKeys,
96
+ toCamelCase,
97
+ normalizeAttributeValue,
98
+ });
99
+ instance.handlers.handleOnUpdate(runtimeDeps, changes);
100
+ return;
101
+ }
102
+
103
+ scheduleFrameFn(() => {
104
+ instance.render();
105
+ });
106
+ };
107
+
108
+ export const runRenderComponentLifecycle = ({
109
+ instance,
110
+ createComponentUpdateHookFn,
111
+ parseViewFn = parseView,
112
+ collectRefElementsFn = collectRefElements,
113
+ onError = (error) => {
114
+ console.error("Error during patching:", error);
115
+ },
116
+ }) => {
117
+ if (!instance.patch) {
118
+ console.error("Patch function is not defined!");
119
+ return null;
120
+ }
121
+
122
+ if (!instance.template) {
123
+ console.error("Template is not defined!");
124
+ return null;
125
+ }
126
+
127
+ try {
128
+ const vDom = parseViewFn({
129
+ h: instance.h,
130
+ template: instance.template,
131
+ viewData: instance.viewData,
132
+ refs: instance.refs,
133
+ handlers: instance.transformedHandlers,
134
+ createComponentUpdateHook: createComponentUpdateHookFn,
135
+ });
136
+
137
+ if (!instance._oldVNode) {
138
+ instance._oldVNode = instance.patch(instance.renderTarget, vDom);
139
+ } else {
140
+ instance._oldVNode = instance.patch(instance._oldVNode, vDom);
141
+ }
142
+
143
+ const ids = collectRefElementsFn({
144
+ rootVNode: instance._oldVNode,
145
+ refs: instance.refs,
146
+ });
147
+ syncRefIds({
148
+ refIds: instance.refIds,
149
+ nextRefIds: ids,
150
+ });
151
+ return instance._oldVNode;
152
+ } catch (error) {
153
+ onError(error);
154
+ return null;
155
+ }
156
+ };
@@ -0,0 +1,54 @@
1
+ import { createRuntimeDeps } from "./lifecycle.js";
2
+
3
+ export const buildObservedAttributes = ({ propsSchemaKeys = [], toKebabCase }) => {
4
+ const observedAttrs = new Set(["key"]);
5
+ propsSchemaKeys.forEach((propKey) => {
6
+ observedAttrs.add(propKey);
7
+ observedAttrs.add(toKebabCase(propKey));
8
+ });
9
+ return [...observedAttrs];
10
+ };
11
+
12
+ export const createComponentRuntimeDeps = ({
13
+ baseDeps,
14
+ refs,
15
+ dispatchEvent,
16
+ store,
17
+ render,
18
+ }) => {
19
+ return createRuntimeDeps({
20
+ baseDeps,
21
+ refs,
22
+ dispatchEvent,
23
+ store,
24
+ render,
25
+ });
26
+ };
27
+
28
+ export const syncRefIds = ({ refIds, nextRefIds = {} }) => {
29
+ Object.keys(refIds).forEach((key) => {
30
+ delete refIds[key];
31
+ });
32
+ Object.assign(refIds, nextRefIds);
33
+ return refIds;
34
+ };
35
+
36
+ export const cleanupEventRateLimitState = ({
37
+ transformedHandlers,
38
+ clearTimerFn = clearTimeout,
39
+ }) => {
40
+ const eventRateLimitState = transformedHandlers?.__eventRateLimitState;
41
+ if (!(eventRateLimitState instanceof Map)) {
42
+ return 0;
43
+ }
44
+
45
+ let clearedTimers = 0;
46
+ eventRateLimitState.forEach((state) => {
47
+ if (state && state.debounceTimer) {
48
+ clearTimerFn(state.debounceTimer);
49
+ clearedTimers += 1;
50
+ }
51
+ });
52
+ eventRateLimitState.clear();
53
+ return clearedTimers;
54
+ };
@@ -0,0 +1,27 @@
1
+ import { isObjectPayload } from "./payload.js";
2
+
3
+ export const deepFreeze = (value) => {
4
+ if (!isObjectPayload(value) || Object.isFrozen(value)) {
5
+ return value;
6
+ }
7
+
8
+ Object.values(value).forEach((nestedValue) => {
9
+ deepFreeze(nestedValue);
10
+ });
11
+
12
+ return Object.freeze(value);
13
+ };
14
+
15
+ export const resolveConstants = ({ setupConstants, fileConstants }) => {
16
+ const normalizedSetupConstants = isObjectPayload(setupConstants)
17
+ ? setupConstants
18
+ : {};
19
+ const normalizedFileConstants = isObjectPayload(fileConstants)
20
+ ? fileConstants
21
+ : {};
22
+
23
+ return deepFreeze({
24
+ ...normalizedSetupConstants,
25
+ ...normalizedFileConstants,
26
+ });
27
+ };
@@ -0,0 +1,191 @@
1
+ import { validateEventConfig } from "../view/refs.js";
2
+
3
+ export const getEventRateLimitState = (handlers) => {
4
+ if (!handlers.__eventRateLimitState) {
5
+ Object.defineProperty(handlers, "__eventRateLimitState", {
6
+ value: new Map(),
7
+ enumerable: false,
8
+ configurable: true,
9
+ });
10
+ }
11
+ return handlers.__eventRateLimitState;
12
+ };
13
+
14
+ export const createEventDispatchCallback = ({
15
+ eventConfig,
16
+ handlers,
17
+ onMissingHandler,
18
+ parseAndRenderFn,
19
+ }) => {
20
+ const getPayload = (event) => {
21
+ const payloadTemplate = (
22
+ eventConfig.payload
23
+ && typeof eventConfig.payload === "object"
24
+ && !Array.isArray(eventConfig.payload)
25
+ )
26
+ ? eventConfig.payload
27
+ : {};
28
+ if (typeof parseAndRenderFn !== "function") {
29
+ return payloadTemplate;
30
+ }
31
+ return parseAndRenderFn(payloadTemplate, {
32
+ _event: event,
33
+ });
34
+ };
35
+
36
+ if (eventConfig.action) {
37
+ if (typeof handlers.handleCallStoreAction !== "function") {
38
+ throw new Error(
39
+ `[Runtime] Action listener '${eventConfig.action}' requires handlers.handleCallStoreAction.`,
40
+ );
41
+ }
42
+ return (event) => {
43
+ const payload = getPayload(event);
44
+ handlers.handleCallStoreAction({
45
+ ...payload,
46
+ _event: event,
47
+ _action: eventConfig.action,
48
+ });
49
+ };
50
+ }
51
+
52
+ if (eventConfig.handler && handlers[eventConfig.handler]) {
53
+ return (event) => {
54
+ const payload = getPayload(event);
55
+ handlers[eventConfig.handler]({
56
+ ...payload,
57
+ _event: event,
58
+ });
59
+ };
60
+ }
61
+
62
+ if (eventConfig.handler) {
63
+ onMissingHandler?.(eventConfig.handler);
64
+ }
65
+
66
+ return null;
67
+ };
68
+
69
+ export const createManagedEventListener = ({
70
+ eventConfig,
71
+ callback,
72
+ hasDebounce,
73
+ hasThrottle,
74
+ stateKey,
75
+ eventRateLimitState,
76
+ fallbackCurrentTarget = null,
77
+ nowFn = Date.now,
78
+ setTimeoutFn = setTimeout,
79
+ clearTimeoutFn = clearTimeout,
80
+ }) => {
81
+ return (event) => {
82
+ const state = eventRateLimitState.get(stateKey) || {};
83
+ const currentTarget = event.currentTarget || fallbackCurrentTarget;
84
+
85
+ if (eventConfig.once) {
86
+ if (currentTarget) {
87
+ if (!state.onceTargets) {
88
+ state.onceTargets = new WeakSet();
89
+ }
90
+ if (state.onceTargets.has(currentTarget)) {
91
+ eventRateLimitState.set(stateKey, state);
92
+ return;
93
+ }
94
+ state.onceTargets.add(currentTarget);
95
+ } else if (state.onceTriggered) {
96
+ eventRateLimitState.set(stateKey, state);
97
+ return;
98
+ } else {
99
+ state.onceTriggered = true;
100
+ }
101
+ }
102
+
103
+ if (eventConfig.targetOnly && event.target !== event.currentTarget) {
104
+ eventRateLimitState.set(stateKey, state);
105
+ return;
106
+ }
107
+
108
+ if (eventConfig.preventDefault) {
109
+ event.preventDefault();
110
+ }
111
+ if (eventConfig.stopImmediatePropagation) {
112
+ event.stopImmediatePropagation();
113
+ } else if (eventConfig.stopPropagation) {
114
+ event.stopPropagation();
115
+ }
116
+
117
+ if (hasDebounce) {
118
+ if (state.debounceTimer) {
119
+ clearTimeoutFn(state.debounceTimer);
120
+ }
121
+ state.debounceTimer = setTimeoutFn(() => {
122
+ callback(event);
123
+ state.debounceTimer = null;
124
+ }, eventConfig.debounce);
125
+ eventRateLimitState.set(stateKey, state);
126
+ return;
127
+ }
128
+
129
+ if (hasThrottle) {
130
+ if (!Object.prototype.hasOwnProperty.call(state, "lastThrottleAt")) {
131
+ state.lastThrottleAt = undefined;
132
+ }
133
+ const now = nowFn();
134
+ if (state.lastThrottleAt === undefined || now - state.lastThrottleAt >= eventConfig.throttle) {
135
+ state.lastThrottleAt = now;
136
+ eventRateLimitState.set(stateKey, state);
137
+ callback(event);
138
+ return;
139
+ }
140
+ eventRateLimitState.set(stateKey, state);
141
+ return;
142
+ }
143
+
144
+ eventRateLimitState.set(stateKey, state);
145
+ callback(event);
146
+ };
147
+ };
148
+
149
+ export const createConfiguredEventListener = ({
150
+ eventType,
151
+ eventConfig,
152
+ refKey,
153
+ handlers,
154
+ eventRateLimitState,
155
+ stateKey,
156
+ fallbackCurrentTarget = null,
157
+ parseAndRenderFn,
158
+ onMissingHandler,
159
+ nowFn = Date.now,
160
+ setTimeoutFn = setTimeout,
161
+ clearTimeoutFn = clearTimeout,
162
+ }) => {
163
+ const { hasDebounce, hasThrottle } = validateEventConfig({
164
+ eventType,
165
+ eventConfig,
166
+ refKey,
167
+ });
168
+
169
+ const callback = createEventDispatchCallback({
170
+ eventConfig,
171
+ handlers,
172
+ onMissingHandler,
173
+ parseAndRenderFn,
174
+ });
175
+ if (!callback) {
176
+ return null;
177
+ }
178
+
179
+ return createManagedEventListener({
180
+ eventConfig,
181
+ callback,
182
+ hasDebounce,
183
+ hasThrottle,
184
+ stateKey,
185
+ eventRateLimitState,
186
+ fallbackCurrentTarget,
187
+ nowFn,
188
+ setTimeoutFn,
189
+ clearTimeoutFn,
190
+ });
191
+ };
@@ -0,0 +1,87 @@
1
+ import {
2
+ createConfiguredEventListener,
3
+ getEventRateLimitState,
4
+ } from "./events.js";
5
+
6
+ const resolveGlobalTarget = ({ refKey, targets }) => {
7
+ if (refKey === "window") {
8
+ return targets.window;
9
+ }
10
+ if (refKey === "document") {
11
+ return targets.document;
12
+ }
13
+ return null;
14
+ };
15
+
16
+ export const attachGlobalRefListeners = ({
17
+ refs = {},
18
+ handlers = {},
19
+ targets = {
20
+ window: globalThis.window,
21
+ document: globalThis.document,
22
+ },
23
+ parseAndRenderFn,
24
+ timing = {
25
+ nowFn: Date.now,
26
+ setTimeoutFn: setTimeout,
27
+ clearTimeoutFn: clearTimeout,
28
+ },
29
+ warnFn = console.warn,
30
+ }) => {
31
+ const cleanupCallbacks = [];
32
+ const stateKeys = new Set();
33
+ const eventRateLimitState = getEventRateLimitState(handlers);
34
+
35
+ Object.entries(refs).forEach(([refKey, refConfig]) => {
36
+ if (refKey !== "window" && refKey !== "document") {
37
+ return;
38
+ }
39
+
40
+ const target = resolveGlobalTarget({ refKey, targets });
41
+ if (!target || !refConfig?.eventListeners) {
42
+ return;
43
+ }
44
+
45
+ Object.entries(refConfig.eventListeners).forEach(([eventType, eventConfig]) => {
46
+ const stateKey = `${refKey}:${eventType}`;
47
+ stateKeys.add(stateKey);
48
+ const listener = createConfiguredEventListener({
49
+ eventType,
50
+ eventConfig,
51
+ refKey,
52
+ handlers,
53
+ eventRateLimitState,
54
+ stateKey,
55
+ fallbackCurrentTarget: target,
56
+ parseAndRenderFn,
57
+ nowFn: timing.nowFn,
58
+ setTimeoutFn: timing.setTimeoutFn,
59
+ clearTimeoutFn: timing.clearTimeoutFn,
60
+ onMissingHandler: (missingHandlerName) => {
61
+ warnFn(
62
+ `[Runtime] Handler '${missingHandlerName}' for global ref '${refKey}' is referenced but not found in available handlers.`,
63
+ );
64
+ },
65
+ });
66
+ if (!listener) {
67
+ return;
68
+ }
69
+
70
+ target.addEventListener(eventType, listener);
71
+ cleanupCallbacks.push(() => {
72
+ target.removeEventListener(eventType, listener);
73
+ });
74
+ });
75
+ });
76
+
77
+ return () => {
78
+ cleanupCallbacks.forEach((cleanup) => cleanup());
79
+ stateKeys.forEach((stateKey) => {
80
+ const state = eventRateLimitState.get(stateKey);
81
+ if (state && state.debounceTimer) {
82
+ timing.clearTimeoutFn(state.debounceTimer);
83
+ }
84
+ eventRateLimitState.delete(stateKey);
85
+ });
86
+ };
87
+ };
@@ -0,0 +1,124 @@
1
+ export const createRuntimeDeps = ({
2
+ baseDeps,
3
+ refs,
4
+ dispatchEvent,
5
+ store,
6
+ render,
7
+ }) => {
8
+ return {
9
+ ...baseDeps,
10
+ refs,
11
+ dispatchEvent,
12
+ store,
13
+ render,
14
+ };
15
+ };
16
+
17
+ export const createStoreActionDispatcher = ({
18
+ store,
19
+ render,
20
+ parseAndRenderFn,
21
+ }) => {
22
+ return (payload) => {
23
+ const { _event, _action } = payload;
24
+ const context = parseAndRenderFn(payload, {
25
+ _event,
26
+ });
27
+
28
+ if (!store[_action]) {
29
+ throw new Error(`[Store] Action 'store.${_action}' is not defined.`);
30
+ }
31
+
32
+ store[_action](context);
33
+ render();
34
+ };
35
+ };
36
+
37
+ export const createTransformedHandlers = ({
38
+ handlers,
39
+ deps,
40
+ parseAndRenderFn,
41
+ }) => {
42
+ const transformedHandlers = {
43
+ handleCallStoreAction: createStoreActionDispatcher({
44
+ store: deps.store,
45
+ render: deps.render,
46
+ parseAndRenderFn,
47
+ }),
48
+ };
49
+
50
+ Object.keys(handlers || {}).forEach((key) => {
51
+ transformedHandlers[key] = (payload) => {
52
+ return handlers[key](deps, payload);
53
+ };
54
+ });
55
+
56
+ return transformedHandlers;
57
+ };
58
+
59
+ export const ensureSyncBeforeMountResult = (beforeMountResult) => {
60
+ if (beforeMountResult && typeof beforeMountResult.then === "function") {
61
+ throw new Error("handleBeforeMount must be synchronous and cannot return a Promise.");
62
+ }
63
+ return beforeMountResult;
64
+ };
65
+
66
+ export const runBeforeMount = ({ handlers, deps }) => {
67
+ if (!handlers?.handleBeforeMount) {
68
+ return undefined;
69
+ }
70
+ const beforeMountResult = handlers.handleBeforeMount(deps);
71
+ return ensureSyncBeforeMountResult(beforeMountResult);
72
+ };
73
+
74
+ export const runAfterMount = ({ handlers, deps }) => {
75
+ if (!handlers?.handleAfterMount) {
76
+ return;
77
+ }
78
+ handlers.handleAfterMount(deps);
79
+ };
80
+
81
+ export const buildOnUpdateChanges = ({
82
+ attributeName,
83
+ oldValue,
84
+ newValue,
85
+ deps,
86
+ propsSchemaKeys,
87
+ toCamelCase,
88
+ normalizeAttributeValue,
89
+ }) => {
90
+ const changedProp = toCamelCase(attributeName);
91
+ const newProps = {};
92
+
93
+ propsSchemaKeys.forEach((propKey) => {
94
+ const propValue = deps.props[propKey];
95
+ if (propValue !== undefined) {
96
+ newProps[propKey] = propValue;
97
+ }
98
+ });
99
+
100
+ const oldProps = {
101
+ ...newProps,
102
+ };
103
+
104
+ const normalizedOldValue = normalizeAttributeValue(oldValue);
105
+ const normalizedNewValue = normalizeAttributeValue(newValue);
106
+
107
+ if (normalizedOldValue === undefined) {
108
+ delete oldProps[changedProp];
109
+ } else {
110
+ oldProps[changedProp] = normalizedOldValue;
111
+ }
112
+
113
+ if (normalizedNewValue === undefined) {
114
+ delete newProps[changedProp];
115
+ } else {
116
+ newProps[changedProp] = normalizedNewValue;
117
+ }
118
+
119
+ return {
120
+ changedProp,
121
+ oldProps,
122
+ newProps,
123
+ };
124
+ };
@@ -0,0 +1,40 @@
1
+ import { isObjectPayload } from "./payload.js";
2
+
3
+ export const bindMethods = (element, methods) => {
4
+ if (!methods || typeof methods !== "object") {
5
+ return;
6
+ }
7
+
8
+ Object.entries(methods).forEach(([methodName, methodFn]) => {
9
+ if (methodName === "default") {
10
+ throw new Error(
11
+ "[Methods] Invalid method name 'default'. Use named exports in .methods.js; default export is not supported.",
12
+ );
13
+ }
14
+
15
+ if (typeof methodFn !== "function") {
16
+ return;
17
+ }
18
+
19
+ if (methodName in element) {
20
+ throw new Error(
21
+ `[Methods] Cannot define method '${methodName}' because it already exists on the component instance.`,
22
+ );
23
+ }
24
+
25
+ Object.defineProperty(element, methodName, {
26
+ configurable: true,
27
+ enumerable: false,
28
+ writable: false,
29
+ value: (payload = {}) => {
30
+ const normalizedPayload = payload === undefined ? {} : payload;
31
+ if (!isObjectPayload(normalizedPayload)) {
32
+ throw new Error(
33
+ `[Methods] Method '${methodName}' expects payload to be an object.`,
34
+ );
35
+ }
36
+ return methodFn.call(element, normalizedPayload);
37
+ },
38
+ });
39
+ });
40
+ };
@@ -0,0 +1,3 @@
1
+ export const isObjectPayload = (value) => {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ };