@rettangoli/fe 0.0.13 → 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,119 @@
1
+ export const FORBIDDEN_VIEW_KEYS = Object.freeze([
2
+ "elementName",
3
+ "viewDataSchema",
4
+ "propsSchema",
5
+ "events",
6
+ "methods",
7
+ "attrsSchema",
8
+ ]);
9
+
10
+ const LEGACY_PROP_BINDING_REGEX = /(^|\s)\.[A-Za-z_][A-Za-z0-9_-]*\s*=/;
11
+
12
+ const hasLegacyDotPropBinding = (node) => {
13
+ if (Array.isArray(node)) {
14
+ return node.some((item) => hasLegacyDotPropBinding(item));
15
+ }
16
+ if (!node || typeof node !== "object") {
17
+ return false;
18
+ }
19
+
20
+ return Object.entries(node).some(([key, value]) => {
21
+ if (LEGACY_PROP_BINDING_REGEX.test(key)) {
22
+ return true;
23
+ }
24
+ return hasLegacyDotPropBinding(value);
25
+ });
26
+ };
27
+
28
+ export const buildComponentContractIndex = (entries = []) => {
29
+ const index = {};
30
+
31
+ entries.forEach((entry) => {
32
+ const {
33
+ category,
34
+ component,
35
+ fileType,
36
+ filePath,
37
+ yamlObject,
38
+ } = entry || {};
39
+
40
+ if (!category || !component || !fileType || !filePath) {
41
+ return;
42
+ }
43
+
44
+ if (!index[category]) {
45
+ index[category] = {};
46
+ }
47
+
48
+ if (!index[category][component]) {
49
+ index[category][component] = {
50
+ fileTypes: new Set(),
51
+ files: [],
52
+ viewFilePath: null,
53
+ viewYaml: null,
54
+ };
55
+ }
56
+
57
+ const componentEntry = index[category][component];
58
+ componentEntry.fileTypes.add(fileType);
59
+ componentEntry.files.push(filePath);
60
+
61
+ if (fileType === "view") {
62
+ componentEntry.viewFilePath = filePath;
63
+ componentEntry.viewYaml = yamlObject;
64
+ }
65
+ });
66
+
67
+ return index;
68
+ };
69
+
70
+ export const validateComponentContractIndex = (index = {}) => {
71
+ const errors = [];
72
+
73
+ Object.entries(index).forEach(([category, components]) => {
74
+ Object.entries(components).forEach(([component, componentEntry]) => {
75
+ const componentLabel = `${category}/${component}`;
76
+ const representativeFile = componentEntry.files[0] || componentLabel;
77
+
78
+ if (!componentEntry.fileTypes.has("schema")) {
79
+ errors.push({
80
+ code: "RTGL-CONTRACT-001",
81
+ message: `${componentLabel}: missing required .schema.yaml file.`,
82
+ filePath: representativeFile,
83
+ });
84
+ }
85
+
86
+ const { viewYaml, viewFilePath } = componentEntry;
87
+ if (!viewYaml || typeof viewYaml !== "object" || Array.isArray(viewYaml)) {
88
+ return;
89
+ }
90
+
91
+ FORBIDDEN_VIEW_KEYS.forEach((forbiddenKey) => {
92
+ if (!Object.prototype.hasOwnProperty.call(viewYaml, forbiddenKey)) {
93
+ return;
94
+ }
95
+ errors.push({
96
+ code: "RTGL-CONTRACT-002",
97
+ message: `${componentLabel}: '${forbiddenKey}' is not allowed in .view.yaml. Move API metadata to .schema.yaml.`,
98
+ filePath: viewFilePath || representativeFile,
99
+ });
100
+ });
101
+
102
+ if (hasLegacyDotPropBinding(viewYaml.template)) {
103
+ errors.push({
104
+ code: "RTGL-CONTRACT-003",
105
+ message: `${componentLabel}: legacy '.prop=' binding is not supported. Use ':prop=' in .view.yaml.`,
106
+ filePath: viewFilePath || representativeFile,
107
+ });
108
+ }
109
+ });
110
+ });
111
+
112
+ return errors;
113
+ };
114
+
115
+ export const formatContractErrors = (errors = []) => {
116
+ return errors.map((error) => {
117
+ return `${error.code} ${error.message} [${error.filePath}]`;
118
+ });
119
+ };
@@ -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
+ };