@ipxjs/refract 0.3.1

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,10 @@
1
+ export {
2
+ currentFiber,
3
+ pushDeletion,
4
+ renderFiber,
5
+ scheduleRender,
6
+ } from "./coreRenderer.js";
7
+ export { markPendingEffects } from "./hooksRuntime.js";
8
+ export { memo, shallowEqual } from "./memo.js";
9
+ export { setHtmlSanitizer } from "./features/security.js";
10
+ export type { HtmlSanitizer } from "./dom.js";
@@ -0,0 +1,14 @@
1
+ export { createElement, Fragment } from "./createElement.js";
2
+ export { render } from "./render.js";
3
+ export { memo } from "./memo.js";
4
+ export { setHtmlSanitizer } from "./features/security.js";
5
+ export { setDevtoolsHook, DEVTOOLS_GLOBAL_HOOK } from "./devtools.js";
6
+ export { useState, useEffect, useRef, useMemo, useCallback, useReducer, createRef, useErrorBoundary } from "./features/hooks.js";
7
+ export { createContext, useContext } from "./features/context.js";
8
+ export type { VNode, Props, Component } from "./types.js";
9
+ export type {
10
+ RefractDevtoolsHook,
11
+ RefractDevtoolsFiberSnapshot,
12
+ RefractDevtoolsRootSnapshot,
13
+ RefractDevtoolsRenderer,
14
+ } from "./devtools.js";
@@ -0,0 +1,11 @@
1
+ export {
2
+ useState,
3
+ useEffect,
4
+ useRef,
5
+ useMemo,
6
+ useCallback,
7
+ useReducer,
8
+ createRef,
9
+ useErrorBoundary,
10
+ depsChanged,
11
+ } from "./features/hooks.js";
@@ -0,0 +1,63 @@
1
+ import type { Fiber } from "./types.js";
2
+ import { reconcileChildren } from "./reconcile.js";
3
+ import {
4
+ registerAfterCommitHandler,
5
+ registerFiberCleanupHandler,
6
+ registerRenderErrorHandler,
7
+ } from "./runtimeExtensions.js";
8
+
9
+ const fibersWithPendingEffects = new Set<Fiber>();
10
+
11
+ export function markPendingEffects(fiber: Fiber): void {
12
+ fibersWithPendingEffects.add(fiber);
13
+ }
14
+
15
+ function cleanupFiberEffects(fiber: Fiber): void {
16
+ fibersWithPendingEffects.delete(fiber);
17
+ if (!fiber.hooks) return;
18
+
19
+ for (const hook of fiber.hooks) {
20
+ const state = hook.state as { cleanup?: () => void } | undefined;
21
+ if (state?.cleanup) {
22
+ state.cleanup();
23
+ state.cleanup = undefined;
24
+ }
25
+ }
26
+ }
27
+
28
+ function runPendingEffects(): void {
29
+ for (const fiber of fibersWithPendingEffects) {
30
+ if (!fiber.hooks) continue;
31
+
32
+ for (const hook of fiber.hooks) {
33
+ const state = hook.state as {
34
+ effect?: () => void | (() => void);
35
+ pending?: boolean;
36
+ cleanup?: () => void;
37
+ } | undefined;
38
+ if (state?.pending && state.effect) {
39
+ if (state.cleanup) state.cleanup();
40
+ state.cleanup = state.effect() || undefined;
41
+ state.pending = false;
42
+ }
43
+ }
44
+ }
45
+ fibersWithPendingEffects.clear();
46
+ }
47
+
48
+ function handleErrorBoundary(fiber: Fiber, error: unknown): boolean {
49
+ let current: Fiber | null = fiber.parent;
50
+ while (current) {
51
+ if (current._errorHandler) {
52
+ current._errorHandler(error);
53
+ reconcileChildren(fiber, []);
54
+ return true;
55
+ }
56
+ current = current.parent;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ registerFiberCleanupHandler(cleanupFiberEffects);
62
+ registerAfterCommitHandler(runPendingEffects);
63
+ registerRenderErrorHandler(handleErrorBoundary);
@@ -0,0 +1 @@
1
+ export * from "./full.js";
@@ -0,0 +1,27 @@
1
+ import type { Props, VNode } from "./types.js";
2
+ import { MEMO_MARKER, type MemoComponent } from "./memoMarker.js";
3
+ import { ensureMemoRuntime } from "./features/memoRuntime.js";
4
+
5
+ /** Shallow equality comparison for memo */
6
+ export function shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>): boolean {
7
+ const keysA = Object.keys(a);
8
+ const keysB = Object.keys(b);
9
+ if (keysA.length !== keysB.length) return false;
10
+ for (const key of keysA) {
11
+ if (key === "children") continue;
12
+ if (!Object.is(a[key], b[key])) return false;
13
+ }
14
+ return true;
15
+ }
16
+
17
+ export function memo(
18
+ component: (props: Props) => VNode,
19
+ compare?: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean,
20
+ ): (props: Props) => VNode {
21
+ ensureMemoRuntime();
22
+ const memoComp: MemoComponent = ((props: Props) => component(props)) as MemoComponent;
23
+ memoComp._inner = component;
24
+ memoComp._compare = compare ?? shallowEqual;
25
+ memoComp._memo = MEMO_MARKER;
26
+ return memoComp;
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { Props, VNode } from "./types.js";
2
+
3
+ export const MEMO_MARKER = Symbol.for("refract.memo");
4
+
5
+ export interface MemoComponent {
6
+ (props: Props): VNode;
7
+ _inner: (props: Props) => VNode;
8
+ _compare: (a: Record<string, unknown>, b: Record<string, unknown>) => boolean;
9
+ _memo: typeof MEMO_MARKER;
10
+ }
11
+
12
+ export function isMemoComponent(type: unknown): type is MemoComponent {
13
+ return typeof type === "function" && (type as MemoComponent)._memo === MEMO_MARKER;
14
+ }
@@ -0,0 +1,185 @@
1
+ import type { VNode, Fiber } from "./types.js";
2
+ import { PLACEMENT, UPDATE, DELETION } from "./types.js";
3
+ import { pushDeletion } from "./coreRenderer.js";
4
+
5
+ function createDeletionScheduler(): (fiber: Fiber) => void {
6
+ const scheduled = new Set<Fiber>();
7
+ return (fiber: Fiber) => {
8
+ if (scheduled.has(fiber)) return;
9
+ scheduled.add(fiber);
10
+ fiber.flags = DELETION;
11
+ pushDeletion(fiber);
12
+ };
13
+ }
14
+
15
+ function createFiber(
16
+ child: VNode,
17
+ parentFiber: Fiber,
18
+ oldFiber: Fiber | null,
19
+ ): Fiber {
20
+ if (oldFiber && oldFiber.type === child.type) {
21
+ return {
22
+ type: oldFiber.type,
23
+ props: child.props,
24
+ key: child.key,
25
+ dom: oldFiber.dom,
26
+ parentDom: parentFiber.dom ?? parentFiber.parentDom,
27
+ parent: parentFiber,
28
+ child: null,
29
+ sibling: null,
30
+ hooks: oldFiber.hooks,
31
+ alternate: oldFiber,
32
+ flags: UPDATE,
33
+ };
34
+ }
35
+ return {
36
+ type: child.type,
37
+ props: child.props,
38
+ key: child.key,
39
+ dom: null,
40
+ parentDom: parentFiber.dom ?? parentFiber.parentDom,
41
+ parent: parentFiber,
42
+ child: null,
43
+ sibling: null,
44
+ hooks: null,
45
+ alternate: null,
46
+ flags: PLACEMENT,
47
+ };
48
+ }
49
+
50
+ /** Reconcile a fiber's children against a new list of VNodes */
51
+ export function reconcileChildren(parentFiber: Fiber, children: VNode[]): void {
52
+ const oldChildren = collectOldChildren(parentFiber);
53
+ const hasKeys = children.some((c) => c.key != null);
54
+
55
+ if (hasKeys) {
56
+ reconcileKeyed(parentFiber, children, oldChildren);
57
+ } else {
58
+ reconcilePositional(parentFiber, children, oldChildren);
59
+ }
60
+ }
61
+
62
+ function collectOldChildren(parentFiber: Fiber): Fiber[] {
63
+ const result: Fiber[] = [];
64
+ let f = parentFiber.alternate?.child ?? null;
65
+ while (f) {
66
+ result.push(f);
67
+ f = f.sibling;
68
+ }
69
+ return result;
70
+ }
71
+
72
+ function reconcilePositional(
73
+ parentFiber: Fiber,
74
+ children: VNode[],
75
+ oldChildren: Fiber[],
76
+ ): void {
77
+ const scheduleDeletion = createDeletionScheduler();
78
+ let prevSibling: Fiber | null = null;
79
+ const maxLen = Math.max(children.length, oldChildren.length);
80
+
81
+ for (let i = 0; i < maxLen; i++) {
82
+ const child = i < children.length ? children[i] : null;
83
+ const oldFiber = i < oldChildren.length ? oldChildren[i] : null;
84
+
85
+ let newFiber: Fiber | null = null;
86
+
87
+ if (child) {
88
+ newFiber = createFiber(child, parentFiber, oldFiber);
89
+ }
90
+
91
+ if (oldFiber && (!child || oldFiber.type !== child.type)) {
92
+ scheduleDeletion(oldFiber);
93
+ }
94
+
95
+ if (i === 0) {
96
+ parentFiber.child = newFiber;
97
+ } else if (prevSibling && newFiber) {
98
+ prevSibling.sibling = newFiber;
99
+ }
100
+
101
+ if (newFiber) prevSibling = newFiber;
102
+ }
103
+ }
104
+
105
+ function reconcileKeyed(
106
+ parentFiber: Fiber,
107
+ children: VNode[],
108
+ oldChildren: Fiber[],
109
+ ): void {
110
+ const scheduleDeletion = createDeletionScheduler();
111
+ // Build map of keyed old fibers
112
+ const keyMap = new Map<string | number, Fiber>();
113
+ const unkeyedOld: Fiber[] = [];
114
+ const oldIndexMap = new Map<Fiber, number>();
115
+ for (const f of oldChildren) {
116
+ oldIndexMap.set(f, oldIndexMap.size);
117
+ if (f.key != null) {
118
+ keyMap.set(f.key, f);
119
+ } else {
120
+ unkeyedOld.push(f);
121
+ }
122
+ }
123
+
124
+ let prevSibling: Fiber | null = null;
125
+ let unkeyedIndex = 0;
126
+ let lastPlacedIndex = 0;
127
+ const usedOld = new Set<Fiber>();
128
+
129
+ for (let i = 0; i < children.length; i++) {
130
+ const child = children[i];
131
+ let oldFiber: Fiber | null = null;
132
+
133
+ if (child.key != null) {
134
+ oldFiber = keyMap.get(child.key) ?? null;
135
+ } else {
136
+ // Match unkeyed by position
137
+ while (unkeyedIndex < unkeyedOld.length) {
138
+ const candidate = unkeyedOld[unkeyedIndex];
139
+ unkeyedIndex++;
140
+ if (candidate.type === child.type) {
141
+ oldFiber = candidate;
142
+ break;
143
+ } else {
144
+ scheduleDeletion(candidate);
145
+ }
146
+ }
147
+ }
148
+
149
+ const newFiber = createFiber(child, parentFiber, oldFiber);
150
+
151
+ if (oldFiber) {
152
+ usedOld.add(oldFiber);
153
+ // Check if we need to move
154
+ const oldIndex = oldIndexMap.get(oldFiber)!;
155
+ if (oldIndex < lastPlacedIndex) {
156
+ newFiber.flags = PLACEMENT;
157
+ } else {
158
+ lastPlacedIndex = oldIndex;
159
+ }
160
+ }
161
+
162
+ if (i === 0) {
163
+ parentFiber.child = newFiber;
164
+ } else if (prevSibling) {
165
+ prevSibling.sibling = newFiber;
166
+ }
167
+
168
+ prevSibling = newFiber;
169
+ }
170
+
171
+ // Delete remaining old fibers that weren't matched
172
+ for (const f of oldChildren) {
173
+ if (!usedOld.has(f)) {
174
+ scheduleDeletion(f);
175
+ }
176
+ }
177
+
178
+ // Delete remaining unkeyed old fibers
179
+ while (unkeyedIndex < unkeyedOld.length) {
180
+ const f = unkeyedOld[unkeyedIndex++];
181
+ if (!usedOld.has(f)) {
182
+ scheduleDeletion(f);
183
+ }
184
+ }
185
+ }
@@ -0,0 +1,9 @@
1
+ import type { VNode } from "./types.js";
2
+ import { renderFiber } from "./coreRenderer.js";
3
+ import { ensureSecurityDefaults } from "./features/security.js";
4
+
5
+ /** Renders a VNode tree into a DOM container with security defaults */
6
+ export function render(vnode: VNode, container: HTMLElement): void {
7
+ ensureSecurityDefaults();
8
+ renderFiber(vnode, container);
9
+ }
@@ -0,0 +1,7 @@
1
+ import type { VNode } from "./types.js";
2
+ import { renderFiber } from "./coreRenderer.js";
3
+
4
+ /** Renders a VNode tree into a DOM container */
5
+ export function render(vnode: VNode, container: HTMLElement): void {
6
+ renderFiber(vnode, container);
7
+ }
@@ -0,0 +1,80 @@
1
+ import type { Fiber } from "./types.js";
2
+
3
+ type FiberCleanupHandler = (fiber: Fiber) => void;
4
+ type AfterCommitHandler = () => void;
5
+ type RenderErrorHandler = (fiber: Fiber, error: unknown) => boolean;
6
+ type CommitHandler = (rootFiber: Fiber, deletions: Fiber[]) => void;
7
+ type ComponentBailoutHandler = (fiber: Fiber) => boolean;
8
+
9
+ const fiberCleanupHandlers = new Set<FiberCleanupHandler>();
10
+ const afterCommitHandlers = new Set<AfterCommitHandler>();
11
+ const renderErrorHandlers = new Set<RenderErrorHandler>();
12
+ const commitHandlers = new Set<CommitHandler>();
13
+ const componentBailoutHandlers = new Set<ComponentBailoutHandler>();
14
+
15
+ function makeUnregister<T>(set: Set<T>, value: T): () => void {
16
+ return () => {
17
+ set.delete(value);
18
+ };
19
+ }
20
+
21
+ export function registerFiberCleanupHandler(handler: FiberCleanupHandler): () => void {
22
+ fiberCleanupHandlers.add(handler);
23
+ return makeUnregister(fiberCleanupHandlers, handler);
24
+ }
25
+
26
+ export function registerAfterCommitHandler(handler: AfterCommitHandler): () => void {
27
+ afterCommitHandlers.add(handler);
28
+ return makeUnregister(afterCommitHandlers, handler);
29
+ }
30
+
31
+ export function registerRenderErrorHandler(handler: RenderErrorHandler): () => void {
32
+ renderErrorHandlers.add(handler);
33
+ return makeUnregister(renderErrorHandlers, handler);
34
+ }
35
+
36
+ export function registerCommitHandler(handler: CommitHandler): () => void {
37
+ commitHandlers.add(handler);
38
+ return makeUnregister(commitHandlers, handler);
39
+ }
40
+
41
+ export function registerComponentBailoutHandler(handler: ComponentBailoutHandler): () => void {
42
+ componentBailoutHandlers.add(handler);
43
+ return makeUnregister(componentBailoutHandlers, handler);
44
+ }
45
+
46
+ export function runFiberCleanupHandlers(fiber: Fiber): void {
47
+ for (const handler of fiberCleanupHandlers) {
48
+ handler(fiber);
49
+ }
50
+ }
51
+
52
+ export function runAfterCommitHandlers(): void {
53
+ for (const handler of afterCommitHandlers) {
54
+ handler();
55
+ }
56
+ }
57
+
58
+ export function tryHandleRenderError(fiber: Fiber, error: unknown): boolean {
59
+ for (const handler of renderErrorHandlers) {
60
+ if (handler(fiber, error)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+
67
+ export function runCommitHandlers(rootFiber: Fiber, deletions: Fiber[]): void {
68
+ for (const handler of commitHandlers) {
69
+ handler(rootFiber, deletions);
70
+ }
71
+ }
72
+
73
+ export function shouldBailoutComponent(fiber: Fiber): boolean {
74
+ for (const handler of componentBailoutHandlers) {
75
+ if (handler(fiber)) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ }
@@ -0,0 +1,48 @@
1
+ /** A functional component */
2
+ export type Component = (props: Props) => VNode;
3
+
4
+ /** The `type` field of a VNode — any HTML tag string, fragment symbol, or component */
5
+ export type VNodeType = string | symbol | Component;
6
+
7
+ /** Props passed to elements and components */
8
+ export interface Props {
9
+ [key: string]: unknown;
10
+ children?: VNode[];
11
+ key?: string | number;
12
+ }
13
+
14
+ /** A virtual DOM node */
15
+ export interface VNode {
16
+ type: VNodeType | "TEXT";
17
+ props: Props;
18
+ key: string | number | null;
19
+ }
20
+
21
+ /** Fiber flags for commit phase */
22
+ export const PLACEMENT = 1;
23
+ export const UPDATE = 2;
24
+ export const DELETION = 4;
25
+
26
+ /** A hook state slot */
27
+ export interface Hook {
28
+ state: unknown;
29
+ queue?: unknown[];
30
+ }
31
+
32
+ /** Internal fiber node — represents a mounted VNode */
33
+ export interface Fiber {
34
+ type: VNodeType | "TEXT";
35
+ props: Props;
36
+ key: string | number | null;
37
+ dom: Node | null;
38
+ parentDom: Node;
39
+ parent: Fiber | null;
40
+ child: Fiber | null;
41
+ sibling: Fiber | null;
42
+ hooks: Hook[] | null;
43
+ _hookIndex?: number;
44
+ _contexts?: Map<number, unknown>;
45
+ _errorHandler?: (error: unknown) => void;
46
+ alternate: Fiber | null;
47
+ flags: number;
48
+ }
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { createElement } from "../src/refract/createElement.js";
3
+ import { render } from "../src/refract/render.js";
4
+ import { createContext, useContext } from "../src/refract/context.js";
5
+
6
+ describe("context", () => {
7
+ let container: HTMLDivElement;
8
+
9
+ beforeEach(() => {
10
+ container = document.createElement("div");
11
+ });
12
+
13
+ it("provides default value when no Provider", () => {
14
+ const ThemeCtx = createContext("light");
15
+ function App() {
16
+ const theme = useContext(ThemeCtx);
17
+ return createElement("span", null, theme);
18
+ }
19
+ render(createElement(App, null), container);
20
+ expect(container.querySelector("span")!.textContent).toBe("light");
21
+ });
22
+
23
+ it("provides value from Provider", () => {
24
+ const ThemeCtx = createContext("light");
25
+ function Child() {
26
+ const theme = useContext(ThemeCtx);
27
+ return createElement("span", null, theme);
28
+ }
29
+ function App() {
30
+ return createElement(ThemeCtx.Provider, { value: "dark" },
31
+ createElement(Child, null),
32
+ );
33
+ }
34
+ render(createElement(App, null), container);
35
+ expect(container.querySelector("span")!.textContent).toBe("dark");
36
+ });
37
+
38
+ it("uses nearest Provider value with nested providers", () => {
39
+ const ThemeCtx = createContext("default");
40
+ function Child() {
41
+ const theme = useContext(ThemeCtx);
42
+ return createElement("span", null, theme);
43
+ }
44
+ function App() {
45
+ return createElement(ThemeCtx.Provider, { value: "outer" },
46
+ createElement(ThemeCtx.Provider, { value: "inner" },
47
+ createElement(Child, null),
48
+ ),
49
+ );
50
+ }
51
+ render(createElement(App, null), container);
52
+ expect(container.querySelector("span")!.textContent).toBe("inner");
53
+ });
54
+
55
+ it("supports multiple contexts", () => {
56
+ const ThemeCtx = createContext("light");
57
+ const LangCtx = createContext("en");
58
+ function Child() {
59
+ const theme = useContext(ThemeCtx);
60
+ const lang = useContext(LangCtx);
61
+ return createElement("span", null, `${theme}-${lang}`);
62
+ }
63
+ function App() {
64
+ return createElement(ThemeCtx.Provider, { value: "dark" },
65
+ createElement(LangCtx.Provider, { value: "fr" },
66
+ createElement(Child, null),
67
+ ),
68
+ );
69
+ }
70
+ render(createElement(App, null), container);
71
+ expect(container.querySelector("span")!.textContent).toBe("dark-fr");
72
+ });
73
+
74
+ it("does not add an extra DOM wrapper for provider children", () => {
75
+ const ThemeCtx = createContext("light");
76
+ function App() {
77
+ return createElement("div", null,
78
+ createElement(ThemeCtx.Provider, { value: "dark" },
79
+ createElement("span", null, "one"),
80
+ createElement("span", null, "two"),
81
+ ),
82
+ );
83
+ }
84
+ render(createElement(App, null), container);
85
+ const div = container.querySelector("div")!;
86
+ expect(div.children).toHaveLength(2);
87
+ expect(div.children[0].textContent).toBe("one");
88
+ expect(div.children[1].textContent).toBe("two");
89
+ });
90
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createElement } from "../src/refract/createElement.js";
3
+
4
+ describe("createElement", () => {
5
+ it("creates a simple element with no children", () => {
6
+ const vnode = createElement("img", { src: "cat.jpg", alt: "A cat" });
7
+ expect(vnode.type).toBe("img");
8
+ expect(vnode.props.src).toBe("cat.jpg");
9
+ expect(vnode.props.alt).toBe("A cat");
10
+ expect(vnode.props.children).toBeUndefined();
11
+ expect(vnode.key).toBeNull();
12
+ });
13
+
14
+ it("creates an element with string children as text nodes", () => {
15
+ const vnode = createElement("span", null, "hello");
16
+ expect(vnode.props.children).toHaveLength(1);
17
+ expect(vnode.props.children![0].type).toBe("TEXT");
18
+ expect(vnode.props.children![0].props.nodeValue).toBe("hello");
19
+ });
20
+
21
+ it("creates nested elements", () => {
22
+ const vnode = createElement(
23
+ "div",
24
+ { className: "card" },
25
+ createElement("img", { src: "pic.jpg" }),
26
+ createElement("span", null, "Caption"),
27
+ );
28
+ expect(vnode.type).toBe("div");
29
+ expect(vnode.props.children).toHaveLength(2);
30
+ expect(vnode.props.children![0].type).toBe("img");
31
+ expect(vnode.props.children![1].type).toBe("span");
32
+ });
33
+
34
+ it("flattens array children", () => {
35
+ const items = ["a", "b", "c"];
36
+ const vnode = createElement(
37
+ "div",
38
+ null,
39
+ items.map((t) => createElement("span", null, t)),
40
+ );
41
+ expect(vnode.props.children).toHaveLength(3);
42
+ });
43
+
44
+ it("filters out null, undefined, and boolean children", () => {
45
+ const vnode = createElement("div", null, null, undefined, false, true, "text");
46
+ expect(vnode.props.children).toHaveLength(1);
47
+ expect(vnode.props.children![0].type).toBe("TEXT");
48
+ });
49
+
50
+ it("retains function type for components (deferred)", () => {
51
+ const MyImg = (props: Record<string, unknown>) =>
52
+ createElement("img", { src: props.src as string });
53
+
54
+ const vnode = createElement(MyImg, { src: "photo.jpg" });
55
+ // Components are NOT called eagerly — type stays as the function
56
+ expect(vnode.type).toBe(MyImg);
57
+ expect(vnode.props.src).toBe("photo.jpg");
58
+ });
59
+
60
+ it("converts numbers to text nodes", () => {
61
+ const vnode = createElement("span", null, 42);
62
+ expect(vnode.props.children).toHaveLength(1);
63
+ expect(vnode.props.children![0].props.nodeValue).toBe("42");
64
+ });
65
+
66
+ it("extracts key from props", () => {
67
+ const vnode = createElement("div", { key: "abc", className: "x" });
68
+ expect(vnode.key).toBe("abc");
69
+ expect(vnode.props.key).toBeUndefined();
70
+ });
71
+ });