@c.a.f/infrastructure-react 1.0.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.
- package/.build/DevTools/index.d.ts +3 -0
- package/.build/DevTools/index.js +3 -0
- package/.build/DevTools/useCAFDevTools.d.ts +43 -0
- package/.build/DevTools/useCAFDevTools.js +74 -0
- package/.build/DevTools/usePlocDevTools.d.ts +23 -0
- package/.build/DevTools/usePlocDevTools.js +58 -0
- package/.build/DevTools/useUseCaseDevTools.d.ts +18 -0
- package/.build/DevTools/useUseCaseDevTools.js +25 -0
- package/.build/ErrorBoundary/CAFErrorBoundary.d.ts +36 -0
- package/.build/ErrorBoundary/CAFErrorBoundary.js +99 -0
- package/.build/ErrorBoundary/ErrorContext.d.ts +18 -0
- package/.build/ErrorBoundary/ErrorContext.js +11 -0
- package/.build/ErrorBoundary/index.d.ts +2 -0
- package/.build/ErrorBoundary/index.js +2 -0
- package/.build/Ploc/index.d.ts +1 -0
- package/.build/Ploc/index.js +1 -0
- package/.build/Ploc/usePloc.d.ts +15 -0
- package/.build/Ploc/usePloc.js +26 -0
- package/.build/RouteManager/RouteHandler.d.ts +12 -0
- package/.build/RouteManager/RouteHandler.js +19 -0
- package/.build/RouteManager/RouteManger.d.ts +10 -0
- package/.build/RouteManager/RouteManger.js +16 -0
- package/.build/RouteManager/index.d.ts +1 -0
- package/.build/RouteManager/index.js +1 -0
- package/.build/RouteManager/useRouteManager.d.ts +8 -0
- package/.build/RouteManager/useRouteManager.js +15 -0
- package/.build/RouteManager/useRouteRepository.d.ts +6 -0
- package/.build/RouteManager/useRouteRepository.js +20 -0
- package/.build/UseCase/index.d.ts +1 -0
- package/.build/UseCase/index.js +1 -0
- package/.build/UseCase/useUseCase.d.ts +26 -0
- package/.build/UseCase/useUseCase.js +83 -0
- package/.build/index.d.ts +7 -0
- package/.build/index.js +7 -0
- package/.build/vitest.config.d.ts +2 -0
- package/.build/vitest.config.js +22 -0
- package/.build/vitest.setup.d.ts +1 -0
- package/.build/vitest.setup.js +1 -0
- package/README.md +319 -0
- package/package.json +57 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Ploc } from "@c.a.f/core";
|
|
2
|
+
import type { PlocDevTools } from "@c.a.f/devtools";
|
|
3
|
+
import { useUseCaseDevTools } from "./useUseCaseDevTools";
|
|
4
|
+
export interface CAFDevToolsContext {
|
|
5
|
+
plocs: Map<Ploc<any>, PlocDevTools<any>>;
|
|
6
|
+
useCases: ReturnType<typeof useUseCaseDevTools>;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
enable: () => void;
|
|
9
|
+
disable: () => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* React hook that provides centralized DevTools access for CAF applications.
|
|
13
|
+
* Tracks all Plocs and UseCases in your application and exposes them for debugging.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Global DevTools options
|
|
16
|
+
* @returns DevTools context with access to all tracked Plocs and UseCases
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```tsx
|
|
20
|
+
* const devTools = useCAFDevTools({ enabled: process.env.NODE_ENV === 'development' });
|
|
21
|
+
*
|
|
22
|
+
* // Enable/disable globally
|
|
23
|
+
* devTools.enable();
|
|
24
|
+
* devTools.disable();
|
|
25
|
+
*
|
|
26
|
+
* // Access tracked Plocs
|
|
27
|
+
* const plocTools = devTools.plocs.get(myPloc);
|
|
28
|
+
* if (plocTools) {
|
|
29
|
+
* const history = plocTools.getStateHistory();
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function useCAFDevTools(options?: {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
}): CAFDevToolsContext;
|
|
36
|
+
/**
|
|
37
|
+
* Helper hook to register a Ploc with DevTools.
|
|
38
|
+
* Use this inside components that use Plocs to automatically track them.
|
|
39
|
+
*
|
|
40
|
+
* @param ploc - Ploc instance to track
|
|
41
|
+
* @param name - Optional name for the Ploc
|
|
42
|
+
*/
|
|
43
|
+
export declare function useTrackPloc<T>(ploc: Ploc<T>, name?: string): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { usePlocDevTools } from "./usePlocDevTools";
|
|
3
|
+
import { useUseCaseDevTools } from "./useUseCaseDevTools";
|
|
4
|
+
/**
|
|
5
|
+
* React hook that provides centralized DevTools access for CAF applications.
|
|
6
|
+
* Tracks all Plocs and UseCases in your application and exposes them for debugging.
|
|
7
|
+
*
|
|
8
|
+
* @param options - Global DevTools options
|
|
9
|
+
* @returns DevTools context with access to all tracked Plocs and UseCases
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const devTools = useCAFDevTools({ enabled: process.env.NODE_ENV === 'development' });
|
|
14
|
+
*
|
|
15
|
+
* // Enable/disable globally
|
|
16
|
+
* devTools.enable();
|
|
17
|
+
* devTools.disable();
|
|
18
|
+
*
|
|
19
|
+
* // Access tracked Plocs
|
|
20
|
+
* const plocTools = devTools.plocs.get(myPloc);
|
|
21
|
+
* if (plocTools) {
|
|
22
|
+
* const history = plocTools.getStateHistory();
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function useCAFDevTools(options) {
|
|
27
|
+
const plocsRef = useRef(new Map());
|
|
28
|
+
const [enabled, setEnabled] = useState(options?.enabled ?? false);
|
|
29
|
+
const useCaseDevTools = useUseCaseDevTools();
|
|
30
|
+
// Expose devtools to React DevTools if available
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (typeof window !== "undefined") {
|
|
33
|
+
// Expose CAF DevTools to global scope for React DevTools extension
|
|
34
|
+
window.__CAF_DEVTOOLS__ = {
|
|
35
|
+
plocs: plocsRef.current,
|
|
36
|
+
useCases: useCaseDevTools,
|
|
37
|
+
enabled,
|
|
38
|
+
enable: () => setEnabled(true),
|
|
39
|
+
disable: () => setEnabled(false),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return () => {
|
|
43
|
+
if (typeof window !== "undefined") {
|
|
44
|
+
delete window.__CAF_DEVTOOLS__;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}, [enabled, useCaseDevTools]);
|
|
48
|
+
return {
|
|
49
|
+
plocs: plocsRef.current,
|
|
50
|
+
useCases: useCaseDevTools,
|
|
51
|
+
enabled,
|
|
52
|
+
enable: () => setEnabled(true),
|
|
53
|
+
disable: () => setEnabled(false),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Helper hook to register a Ploc with DevTools.
|
|
58
|
+
* Use this inside components that use Plocs to automatically track them.
|
|
59
|
+
*
|
|
60
|
+
* @param ploc - Ploc instance to track
|
|
61
|
+
* @param name - Optional name for the Ploc
|
|
62
|
+
*/
|
|
63
|
+
export function useTrackPloc(ploc, name) {
|
|
64
|
+
const devTools = useCAFDevTools();
|
|
65
|
+
const plocDevTools = usePlocDevTools(ploc, { name, enabled: devTools.enabled });
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!devTools.plocs.has(ploc)) {
|
|
68
|
+
devTools.plocs.set(ploc, plocDevTools);
|
|
69
|
+
}
|
|
70
|
+
return () => {
|
|
71
|
+
devTools.plocs.delete(ploc);
|
|
72
|
+
};
|
|
73
|
+
}, [ploc, plocDevTools, devTools]);
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Ploc } from "@c.a.f/core";
|
|
2
|
+
import { type PlocDevTools, type PlocDevToolsOptions } from "@c.a.f/devtools";
|
|
3
|
+
/**
|
|
4
|
+
* React hook that provides DevTools for a Ploc instance.
|
|
5
|
+
* Automatically cleans up on unmount.
|
|
6
|
+
*
|
|
7
|
+
* @param ploc - A Ploc instance
|
|
8
|
+
* @param options - DevTools options
|
|
9
|
+
* @returns DevTools instance and current state
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const [state, ploc] = usePloc(userPloc);
|
|
14
|
+
* const devTools = usePlocDevTools(ploc, { name: 'UserPloc', enabled: true });
|
|
15
|
+
*
|
|
16
|
+
* // Access state history
|
|
17
|
+
* const history = devTools.getStateHistory();
|
|
18
|
+
*
|
|
19
|
+
* // Time-travel debugging
|
|
20
|
+
* devTools.jumpToState(2);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function usePlocDevTools<T>(ploc: Ploc<T>, options?: PlocDevToolsOptions): PlocDevTools<T>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { createPlocDevTools, } from "@c.a.f/devtools";
|
|
3
|
+
/**
|
|
4
|
+
* React hook that provides DevTools for a Ploc instance.
|
|
5
|
+
* Automatically cleans up on unmount.
|
|
6
|
+
*
|
|
7
|
+
* @param ploc - A Ploc instance
|
|
8
|
+
* @param options - DevTools options
|
|
9
|
+
* @returns DevTools instance and current state
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const [state, ploc] = usePloc(userPloc);
|
|
14
|
+
* const devTools = usePlocDevTools(ploc, { name: 'UserPloc', enabled: true });
|
|
15
|
+
*
|
|
16
|
+
* // Access state history
|
|
17
|
+
* const history = devTools.getStateHistory();
|
|
18
|
+
*
|
|
19
|
+
* // Time-travel debugging
|
|
20
|
+
* devTools.jumpToState(2);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function usePlocDevTools(ploc, options) {
|
|
24
|
+
const devToolsRef = useRef(null);
|
|
25
|
+
const plocRef = useRef(null);
|
|
26
|
+
// Create adapter wrapper that matches PlocInstance interface
|
|
27
|
+
// PlocDevTools expects subscribe to return void (not unsubscribe function)
|
|
28
|
+
const createPlocAdapter = (p) => {
|
|
29
|
+
return {
|
|
30
|
+
state: p.state,
|
|
31
|
+
changeState: (state) => p.changeState(state),
|
|
32
|
+
subscribe: (listener) => {
|
|
33
|
+
p.subscribe(listener);
|
|
34
|
+
// Note: PlocDevTools now handles cleanup internally via MemoryLeakDetector
|
|
35
|
+
},
|
|
36
|
+
unsubscribe: (listener) => p.unsubscribe(listener),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
// Recreate devtools if ploc changed
|
|
40
|
+
if (plocRef.current !== ploc || !devToolsRef.current) {
|
|
41
|
+
plocRef.current = ploc;
|
|
42
|
+
if (devToolsRef.current) {
|
|
43
|
+
devToolsRef.current.cleanup();
|
|
44
|
+
}
|
|
45
|
+
const adapter = createPlocAdapter(ploc);
|
|
46
|
+
devToolsRef.current = createPlocDevTools(adapter, options);
|
|
47
|
+
}
|
|
48
|
+
// Cleanup on unmount
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
return () => {
|
|
51
|
+
if (devToolsRef.current) {
|
|
52
|
+
devToolsRef.current.cleanup();
|
|
53
|
+
devToolsRef.current = null;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, []);
|
|
57
|
+
return devToolsRef.current;
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type UseCaseDevTools, type UseCaseDevToolsOptions } from "@c.a.f/devtools";
|
|
2
|
+
/**
|
|
3
|
+
* React hook that provides DevTools for UseCase execution tracking.
|
|
4
|
+
* Creates a singleton DevTools instance that persists across renders.
|
|
5
|
+
*
|
|
6
|
+
* @param options - DevTools options
|
|
7
|
+
* @returns DevTools instance
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const useCaseDevTools = useUseCaseDevTools({ name: 'CreateUser', enabled: true });
|
|
12
|
+
* const wrappedUseCase = useCaseDevTools.wrap(createUserUseCase);
|
|
13
|
+
*
|
|
14
|
+
* // Get execution history
|
|
15
|
+
* const history = useCaseDevTools.getExecutionHistory();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function useUseCaseDevTools(options?: UseCaseDevToolsOptions): UseCaseDevTools;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { createUseCaseDevTools, } from "@c.a.f/devtools";
|
|
3
|
+
/**
|
|
4
|
+
* React hook that provides DevTools for UseCase execution tracking.
|
|
5
|
+
* Creates a singleton DevTools instance that persists across renders.
|
|
6
|
+
*
|
|
7
|
+
* @param options - DevTools options
|
|
8
|
+
* @returns DevTools instance
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* const useCaseDevTools = useUseCaseDevTools({ name: 'CreateUser', enabled: true });
|
|
13
|
+
* const wrappedUseCase = useCaseDevTools.wrap(createUserUseCase);
|
|
14
|
+
*
|
|
15
|
+
* // Get execution history
|
|
16
|
+
* const history = useCaseDevTools.getExecutionHistory();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function useUseCaseDevTools(options) {
|
|
20
|
+
const devToolsRef = useRef(null);
|
|
21
|
+
if (!devToolsRef.current) {
|
|
22
|
+
devToolsRef.current = createUseCaseDevTools(options);
|
|
23
|
+
}
|
|
24
|
+
return devToolsRef.current;
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, { Component, type ReactNode } from "react";
|
|
2
|
+
export interface CAFErrorBoundaryProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
fallback?: (error: Error, errorInfo: React.ErrorInfo, resetError: () => void) => ReactNode;
|
|
5
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
6
|
+
}
|
|
7
|
+
interface CAFErrorBoundaryState {
|
|
8
|
+
error: Error | null;
|
|
9
|
+
errorInfo: React.ErrorInfo | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Error Boundary component for CAF applications.
|
|
13
|
+
* Catches errors from Ploc/UseCase execution and component rendering.
|
|
14
|
+
* Provides error context via React Context and supports custom error UI and recovery.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* <CAFErrorBoundary fallback={(error, errorInfo, reset) => (
|
|
19
|
+
* <div>
|
|
20
|
+
* <h2>Something went wrong</h2>
|
|
21
|
+
* <p>{error.message}</p>
|
|
22
|
+
* <button onClick={reset}>Try again</button>
|
|
23
|
+
* </div>
|
|
24
|
+
* )}>
|
|
25
|
+
* <App />
|
|
26
|
+
* </CAFErrorBoundary>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class CAFErrorBoundary extends Component<CAFErrorBoundaryProps, CAFErrorBoundaryState> {
|
|
30
|
+
constructor(props: CAFErrorBoundaryProps);
|
|
31
|
+
static getDerivedStateFromError(error: Error): Partial<CAFErrorBoundaryState>;
|
|
32
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
33
|
+
resetError: () => void;
|
|
34
|
+
render(): ReactNode;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { Component } from "react";
|
|
2
|
+
import { CAFErrorContext } from "./ErrorContext";
|
|
3
|
+
/**
|
|
4
|
+
* Error Boundary component for CAF applications.
|
|
5
|
+
* Catches errors from Ploc/UseCase execution and component rendering.
|
|
6
|
+
* Provides error context via React Context and supports custom error UI and recovery.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <CAFErrorBoundary fallback={(error, errorInfo, reset) => (
|
|
11
|
+
* <div>
|
|
12
|
+
* <h2>Something went wrong</h2>
|
|
13
|
+
* <p>{error.message}</p>
|
|
14
|
+
* <button onClick={reset}>Try again</button>
|
|
15
|
+
* </div>
|
|
16
|
+
* )}>
|
|
17
|
+
* <App />
|
|
18
|
+
* </CAFErrorBoundary>
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export class CAFErrorBoundary extends Component {
|
|
22
|
+
constructor(props) {
|
|
23
|
+
super(props);
|
|
24
|
+
this.state = {
|
|
25
|
+
error: null,
|
|
26
|
+
errorInfo: null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
static getDerivedStateFromError(error) {
|
|
30
|
+
return {
|
|
31
|
+
error,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
componentDidCatch(error, errorInfo) {
|
|
35
|
+
this.setState({
|
|
36
|
+
error,
|
|
37
|
+
errorInfo,
|
|
38
|
+
});
|
|
39
|
+
// Call optional error handler
|
|
40
|
+
if (this.props.onError) {
|
|
41
|
+
this.props.onError(error, errorInfo);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
resetError = () => {
|
|
45
|
+
this.setState({
|
|
46
|
+
error: null,
|
|
47
|
+
errorInfo: null,
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
render() {
|
|
51
|
+
if (this.state.error) {
|
|
52
|
+
const contextValue = {
|
|
53
|
+
error: this.state.error,
|
|
54
|
+
errorInfo: this.state.errorInfo,
|
|
55
|
+
resetError: this.resetError,
|
|
56
|
+
};
|
|
57
|
+
// Use custom fallback if provided
|
|
58
|
+
if (this.props.fallback) {
|
|
59
|
+
return (React.createElement(CAFErrorContext.Provider, { value: contextValue }, this.props.fallback(this.state.error, this.state.errorInfo, this.resetError)));
|
|
60
|
+
}
|
|
61
|
+
// Default error UI
|
|
62
|
+
return (React.createElement(CAFErrorContext.Provider, { value: contextValue },
|
|
63
|
+
React.createElement("div", { style: {
|
|
64
|
+
padding: "2rem",
|
|
65
|
+
margin: "1rem",
|
|
66
|
+
border: "1px solid #ff6b6b",
|
|
67
|
+
borderRadius: "8px",
|
|
68
|
+
backgroundColor: "#fff5f5",
|
|
69
|
+
} },
|
|
70
|
+
React.createElement("h2", { style: { color: "#c92a2a", marginTop: 0 } }, "Something went wrong"),
|
|
71
|
+
React.createElement("details", { style: { marginBottom: "1rem" } },
|
|
72
|
+
React.createElement("summary", { style: { cursor: "pointer", fontWeight: "bold" } }, "Error details"),
|
|
73
|
+
React.createElement("pre", { style: {
|
|
74
|
+
marginTop: "0.5rem",
|
|
75
|
+
padding: "1rem",
|
|
76
|
+
backgroundColor: "#fff",
|
|
77
|
+
borderRadius: "4px",
|
|
78
|
+
overflow: "auto",
|
|
79
|
+
fontSize: "0.875rem",
|
|
80
|
+
} },
|
|
81
|
+
this.state.error.toString(),
|
|
82
|
+
this.state.errorInfo?.componentStack)),
|
|
83
|
+
React.createElement("button", { onClick: this.resetError, style: {
|
|
84
|
+
padding: "0.5rem 1rem",
|
|
85
|
+
backgroundColor: "#667eea",
|
|
86
|
+
color: "white",
|
|
87
|
+
border: "none",
|
|
88
|
+
borderRadius: "4px",
|
|
89
|
+
cursor: "pointer",
|
|
90
|
+
fontSize: "1rem",
|
|
91
|
+
} }, "Try again"))));
|
|
92
|
+
}
|
|
93
|
+
return (React.createElement(CAFErrorContext.Provider, { value: {
|
|
94
|
+
error: null,
|
|
95
|
+
errorInfo: null,
|
|
96
|
+
resetError: this.resetError,
|
|
97
|
+
} }, this.props.children));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
/**
|
|
3
|
+
* Context for CAF error boundary state.
|
|
4
|
+
* Provides access to the current error and recovery function.
|
|
5
|
+
*/
|
|
6
|
+
export interface CAFErrorContextValue {
|
|
7
|
+
error: Error | null;
|
|
8
|
+
errorInfo: React.ErrorInfo | null;
|
|
9
|
+
resetError: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare const CAFErrorContext: import("react").Context<CAFErrorContextValue | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Hook to access CAF error boundary context.
|
|
14
|
+
* Returns null if used outside of CAFErrorBoundary.
|
|
15
|
+
*
|
|
16
|
+
* @returns Error context value or null
|
|
17
|
+
*/
|
|
18
|
+
export declare function useCAFError(): CAFErrorContextValue | null;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
export const CAFErrorContext = createContext(null);
|
|
3
|
+
/**
|
|
4
|
+
* Hook to access CAF error boundary context.
|
|
5
|
+
* Returns null if used outside of CAFErrorBoundary.
|
|
6
|
+
*
|
|
7
|
+
* @returns Error context value or null
|
|
8
|
+
*/
|
|
9
|
+
export function useCAFError() {
|
|
10
|
+
return useContext(CAFErrorContext);
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePloc } from "./usePloc";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { usePloc } from "./usePloc";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Ploc } from "@c.a.f/core";
|
|
2
|
+
/**
|
|
3
|
+
* React hook that subscribes to a Ploc and returns the current state and the Ploc instance.
|
|
4
|
+
* Handles subscription on mount, syncs when the ploc reference changes, and unsubscribes on unmount.
|
|
5
|
+
*
|
|
6
|
+
* @param ploc - A Ploc instance (from @c.a.f/core)
|
|
7
|
+
* @returns A tuple of [currentState, ploc]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const [state, userPloc] = usePloc(userPloc);
|
|
12
|
+
* return <span>{state.name}</span>;
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function usePloc<T>(ploc: Ploc<T>): [T, Ploc<T>];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* React hook that subscribes to a Ploc and returns the current state and the Ploc instance.
|
|
4
|
+
* Handles subscription on mount, syncs when the ploc reference changes, and unsubscribes on unmount.
|
|
5
|
+
*
|
|
6
|
+
* @param ploc - A Ploc instance (from @c.a.f/core)
|
|
7
|
+
* @returns A tuple of [currentState, ploc]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const [state, userPloc] = usePloc(userPloc);
|
|
12
|
+
* return <span>{state.name}</span>;
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function usePloc(ploc) {
|
|
16
|
+
const [state, setState] = useState(() => ploc.state);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setState(ploc.state);
|
|
19
|
+
const listener = (newState) => setState(newState);
|
|
20
|
+
ploc.subscribe(listener);
|
|
21
|
+
return () => {
|
|
22
|
+
ploc.unsubscribe(listener);
|
|
23
|
+
};
|
|
24
|
+
}, [ploc]);
|
|
25
|
+
return [state, ploc];
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RouteRepository } from "@c.a.f/core";
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated This class violates React's rules of hooks by calling hooks in a constructor.
|
|
4
|
+
* Use `useRouteRepository()` hook instead, which properly calls hooks at the hook level.
|
|
5
|
+
*/
|
|
6
|
+
export declare class RouteHandler implements RouteRepository {
|
|
7
|
+
private navigate;
|
|
8
|
+
constructor();
|
|
9
|
+
currentRoute: string;
|
|
10
|
+
change(route: string): void;
|
|
11
|
+
watchCurrentRoute(): void;
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useNavigate, useLocation } from "react-router-dom";
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated This class violates React's rules of hooks by calling hooks in a constructor.
|
|
4
|
+
* Use `useRouteRepository()` hook instead, which properly calls hooks at the hook level.
|
|
5
|
+
*/
|
|
6
|
+
export class RouteHandler {
|
|
7
|
+
navigate = useNavigate();
|
|
8
|
+
constructor() {
|
|
9
|
+
this.watchCurrentRoute();
|
|
10
|
+
}
|
|
11
|
+
currentRoute = '';
|
|
12
|
+
change(route) {
|
|
13
|
+
this.navigate(route);
|
|
14
|
+
}
|
|
15
|
+
watchCurrentRoute() {
|
|
16
|
+
const { pathname } = useLocation();
|
|
17
|
+
this.currentRoute = pathname;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { RouteManager, RouteManagerAuthOptions } from "@c.a.f/core";
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated This class creates RouteHandler in constructor, which violates React's rules of hooks.
|
|
4
|
+
* Use `useRouteManager()` hook instead, which properly calls React hooks at the hook level.
|
|
5
|
+
*/
|
|
6
|
+
export declare class RouterService {
|
|
7
|
+
private routeManager;
|
|
8
|
+
constructor(authOptions?: RouteManagerAuthOptions);
|
|
9
|
+
getRouteManager(): RouteManager;
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { RouteManager } from "@c.a.f/core";
|
|
2
|
+
import { RouteHandler } from "./RouteHandler";
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated This class creates RouteHandler in constructor, which violates React's rules of hooks.
|
|
5
|
+
* Use `useRouteManager()` hook instead, which properly calls React hooks at the hook level.
|
|
6
|
+
*/
|
|
7
|
+
export class RouterService {
|
|
8
|
+
routeManager;
|
|
9
|
+
constructor(authOptions) {
|
|
10
|
+
const routeHandler = new RouteHandler();
|
|
11
|
+
this.routeManager = new RouteManager(routeHandler, authOptions);
|
|
12
|
+
}
|
|
13
|
+
getRouteManager() {
|
|
14
|
+
return this.routeManager;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './RouteManger';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './RouteManger';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RouteManager, RouteManagerAuthOptions } from "@c.a.f/core";
|
|
2
|
+
/**
|
|
3
|
+
* React hook that provides a RouteManager from @c.a.f/core.
|
|
4
|
+
* Uses useRouteRepository to get the RouteRepository implementation.
|
|
5
|
+
*
|
|
6
|
+
* @param authOptions - Optional authentication configuration. If not provided, RouteManager will work without auth checks.
|
|
7
|
+
*/
|
|
8
|
+
export declare const useRouteManager: (authOptions?: RouteManagerAuthOptions) => RouteManager;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RouteManager } from "@c.a.f/core";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { useRouteRepository } from "./useRouteRepository";
|
|
4
|
+
/**
|
|
5
|
+
* React hook that provides a RouteManager from @c.a.f/core.
|
|
6
|
+
* Uses useRouteRepository to get the RouteRepository implementation.
|
|
7
|
+
*
|
|
8
|
+
* @param authOptions - Optional authentication configuration. If not provided, RouteManager will work without auth checks.
|
|
9
|
+
*/
|
|
10
|
+
export const useRouteManager = (authOptions) => {
|
|
11
|
+
const routeRepository = useRouteRepository();
|
|
12
|
+
return useMemo(() => {
|
|
13
|
+
return new RouteManager(routeRepository, authOptions);
|
|
14
|
+
}, [routeRepository, authOptions]);
|
|
15
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useNavigate, useLocation } from "react-router-dom";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* React hook that provides a RouteRepository implementation.
|
|
5
|
+
* Calls React Router hooks at the hook level (not in a constructor).
|
|
6
|
+
*/
|
|
7
|
+
export const useRouteRepository = () => {
|
|
8
|
+
const navigate = useNavigate();
|
|
9
|
+
const location = useLocation();
|
|
10
|
+
return useMemo(() => {
|
|
11
|
+
return {
|
|
12
|
+
get currentRoute() {
|
|
13
|
+
return location.pathname;
|
|
14
|
+
},
|
|
15
|
+
change(route) {
|
|
16
|
+
navigate(route);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}, [navigate, location.pathname]);
|
|
20
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useUseCase } from "./useUseCase";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useUseCase } from "./useUseCase";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { UseCase } from "@c.a.f/core";
|
|
2
|
+
/**
|
|
3
|
+
* React hook that wraps a UseCase execution with loading/error/data state management.
|
|
4
|
+
* Handles RequestResult subscriptions automatically and provides a clean API for executing use cases.
|
|
5
|
+
*
|
|
6
|
+
* @param useCase - A UseCase instance (from @c.a.f/core)
|
|
7
|
+
* @returns An object with execute function, loading state, error state, and data
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { execute, loading, error, data } = useUseCase(createUserUseCase);
|
|
12
|
+
*
|
|
13
|
+
* const handleCreate = async () => {
|
|
14
|
+
* const result = await execute({ name: 'John', email: 'john@example.com' });
|
|
15
|
+
* if (result) {
|
|
16
|
+
* console.log('User created:', result);
|
|
17
|
+
* }
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function useUseCase<TArgs extends any[], TResult>(useCase: UseCase<TArgs, TResult>): {
|
|
22
|
+
execute: (...args: TArgs) => Promise<TResult | null>;
|
|
23
|
+
loading: boolean;
|
|
24
|
+
error: Error | null;
|
|
25
|
+
data: TResult | null;
|
|
26
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* React hook that wraps a UseCase execution with loading/error/data state management.
|
|
4
|
+
* Handles RequestResult subscriptions automatically and provides a clean API for executing use cases.
|
|
5
|
+
*
|
|
6
|
+
* @param useCase - A UseCase instance (from @c.a.f/core)
|
|
7
|
+
* @returns An object with execute function, loading state, error state, and data
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { execute, loading, error, data } = useUseCase(createUserUseCase);
|
|
12
|
+
*
|
|
13
|
+
* const handleCreate = async () => {
|
|
14
|
+
* const result = await execute({ name: 'John', email: 'john@example.com' });
|
|
15
|
+
* if (result) {
|
|
16
|
+
* console.log('User created:', result);
|
|
17
|
+
* }
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useUseCase(useCase) {
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
const [data, setData] = useState(null);
|
|
25
|
+
const requestResultRef = useRef(null);
|
|
26
|
+
const listenersRef = useRef(null);
|
|
27
|
+
// Cleanup subscriptions on unmount
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
return () => {
|
|
30
|
+
const currentResult = requestResultRef.current;
|
|
31
|
+
const listeners = listenersRef.current;
|
|
32
|
+
if (currentResult && listeners) {
|
|
33
|
+
currentResult.loading.unsubscribe(listeners.loading);
|
|
34
|
+
currentResult.data.unsubscribe(listeners.data);
|
|
35
|
+
currentResult.error.unsubscribe(listeners.error);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
const execute = useCallback(async (...args) => {
|
|
40
|
+
// Unsubscribe from previous result if exists
|
|
41
|
+
const previousResult = requestResultRef.current;
|
|
42
|
+
const previousListeners = listenersRef.current;
|
|
43
|
+
if (previousResult && previousListeners) {
|
|
44
|
+
previousResult.loading.unsubscribe(previousListeners.loading);
|
|
45
|
+
previousResult.data.unsubscribe(previousListeners.data);
|
|
46
|
+
previousResult.error.unsubscribe(previousListeners.error);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const result = await useCase.execute(...args);
|
|
50
|
+
requestResultRef.current = result;
|
|
51
|
+
// Create listeners for the new result
|
|
52
|
+
const loadingListener = (value) => setLoading(value);
|
|
53
|
+
const dataListener = (value) => setData(value);
|
|
54
|
+
const errorListener = (value) => setError(value);
|
|
55
|
+
listenersRef.current = {
|
|
56
|
+
loading: loadingListener,
|
|
57
|
+
data: dataListener,
|
|
58
|
+
error: errorListener,
|
|
59
|
+
};
|
|
60
|
+
// Subscribe to the new result
|
|
61
|
+
result.loading.subscribe(loadingListener);
|
|
62
|
+
result.data.subscribe(dataListener);
|
|
63
|
+
result.error.subscribe(errorListener);
|
|
64
|
+
// Initialize state from result
|
|
65
|
+
setLoading(result.loading.value);
|
|
66
|
+
setData(result.data.value);
|
|
67
|
+
setError(result.error.value);
|
|
68
|
+
return result.data.value;
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
72
|
+
setError(error);
|
|
73
|
+
setLoading(false);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}, [useCase]);
|
|
77
|
+
return {
|
|
78
|
+
execute,
|
|
79
|
+
loading,
|
|
80
|
+
error,
|
|
81
|
+
data,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/.build/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig } from "vitest/config";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
environment: "happy-dom",
|
|
9
|
+
include: ["**/*.spec.ts", "**/*.spec.tsx"],
|
|
10
|
+
setupFiles: [resolve(__dirname, "./vitest.setup.ts")],
|
|
11
|
+
globals: true,
|
|
12
|
+
},
|
|
13
|
+
resolve: {
|
|
14
|
+
alias: {
|
|
15
|
+
// Force resolution from workspace root node_modules
|
|
16
|
+
"@testing-library/dom": resolve(__dirname, "../../../node_modules/@testing-library/dom"),
|
|
17
|
+
// Fix ES module resolution for @c.a.f/core - resolve to the actual built file
|
|
18
|
+
"@c.a.f/core": resolve(__dirname, "../../../node_modules/@c.a.f/core/.build/src/index.js"),
|
|
19
|
+
},
|
|
20
|
+
conditions: ["import", "module", "default"],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
package/README.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# @c.a.f/infrastructure-react
|
|
2
|
+
|
|
3
|
+
React-specific infrastructure adapters for CAF.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @c.a.f/infrastructure-react react-router-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### usePloc
|
|
14
|
+
|
|
15
|
+
Hook that subscribes to a Ploc and returns the current state and the Ploc instance. Subscribes on mount, syncs when the ploc reference changes, and unsubscribes on unmount.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { usePloc } from '@c.a.f/infrastructure-react';
|
|
19
|
+
|
|
20
|
+
function UserProfile({ userPloc }: { userPloc: UserPloc }) {
|
|
21
|
+
const [state, ploc] = usePloc(userPloc);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div>
|
|
25
|
+
<span>{state.name}</span>
|
|
26
|
+
<button onClick={() => ploc.loadUser()}>Refresh</button>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The hook returns a tuple `[state, ploc]`: the current state (re-renders when the Ploc updates) and the same Ploc instance so you can call methods on it. The Ploc is typically provided via props, context, or created with `useMemo` for the component tree.
|
|
33
|
+
|
|
34
|
+
### useUseCase
|
|
35
|
+
|
|
36
|
+
Hook that wraps a UseCase execution with loading/error/data state management. Handles `RequestResult` subscriptions automatically and provides a clean API for executing use cases.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { useUseCase } from '@c.a.f/infrastructure-react';
|
|
40
|
+
import { CreateUser } from './application/User/Commands/CreateUser';
|
|
41
|
+
|
|
42
|
+
function CreateUserForm({ createUserUseCase }: { createUserUseCase: CreateUser }) {
|
|
43
|
+
const { execute, loading, error, data } = useUseCase(createUserUseCase);
|
|
44
|
+
|
|
45
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
const formData = new FormData(e.target as HTMLFormElement);
|
|
48
|
+
const result = await execute({
|
|
49
|
+
name: formData.get('name') as string,
|
|
50
|
+
email: formData.get('email') as string,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (result) {
|
|
54
|
+
console.log('User created:', result);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<form onSubmit={handleSubmit}>
|
|
60
|
+
{loading && <p>Creating user...</p>}
|
|
61
|
+
{error && <p>Error: {error.message}</p>}
|
|
62
|
+
{data && <p>User created: {data.name}</p>}
|
|
63
|
+
{/* form fields */}
|
|
64
|
+
</form>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The hook automatically subscribes to the `RequestResult`'s `loading`, `data`, and `error` pulses, so your component re-renders when these values change. The `execute` function returns the data value directly (or `null` on error), making it easy to handle results.
|
|
70
|
+
|
|
71
|
+
### CAFErrorBoundary
|
|
72
|
+
|
|
73
|
+
Error Boundary component that catches errors from Ploc/UseCase execution and component rendering. Provides error context via React Context and supports custom error UI and recovery.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { CAFErrorBoundary, useCAFError } from '@c.a.f/infrastructure-react';
|
|
77
|
+
|
|
78
|
+
function App() {
|
|
79
|
+
return (
|
|
80
|
+
<CAFErrorBoundary
|
|
81
|
+
fallback={(error, errorInfo, resetError) => (
|
|
82
|
+
<div>
|
|
83
|
+
<h2>Oops! Something went wrong</h2>
|
|
84
|
+
<p>{error.message}</p>
|
|
85
|
+
<button onClick={resetError}>Try again</button>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
onError={(error, errorInfo) => {
|
|
89
|
+
// Log to error reporting service
|
|
90
|
+
console.error('Error caught:', error, errorInfo);
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<YourApp />
|
|
94
|
+
</CAFErrorBoundary>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Access error context from anywhere within the boundary:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { useCAFError } from '@c.a.f/infrastructure-react';
|
|
103
|
+
|
|
104
|
+
function ErrorDisplay() {
|
|
105
|
+
const errorContext = useCAFError();
|
|
106
|
+
|
|
107
|
+
if (errorContext?.error) {
|
|
108
|
+
return (
|
|
109
|
+
<div>
|
|
110
|
+
<p>Error: {errorContext.error.message}</p>
|
|
111
|
+
<button onClick={errorContext.resetError}>Reset</button>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The error boundary catches:
|
|
121
|
+
- Errors during component rendering
|
|
122
|
+
- Errors in lifecycle methods
|
|
123
|
+
- Errors in constructors
|
|
124
|
+
- Errors from Ploc/UseCase execution (when not caught locally)
|
|
125
|
+
|
|
126
|
+
### DevTools Integration
|
|
127
|
+
|
|
128
|
+
React hooks for debugging and inspecting CAF applications. Integrates with `@c.a.f/devtools` and exposes data for React DevTools.
|
|
129
|
+
|
|
130
|
+
#### usePlocDevTools
|
|
131
|
+
|
|
132
|
+
Hook that provides DevTools for a Ploc instance. Enables state tracking, time-travel debugging, state history, and memory leak detection.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { usePloc, usePlocDevTools } from '@c.a.f/infrastructure-react';
|
|
136
|
+
import { createMemoryLeakDetector } from '@c.a.f/devtools';
|
|
137
|
+
|
|
138
|
+
function UserProfile({ userPloc }: { userPloc: UserPloc }) {
|
|
139
|
+
const [state, ploc] = usePloc(userPloc);
|
|
140
|
+
|
|
141
|
+
// Optional: Create memory leak detector
|
|
142
|
+
const leakDetector = createMemoryLeakDetector({
|
|
143
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
144
|
+
warnThreshold: 10000, // Warn after 10 seconds
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const devTools = usePlocDevTools(ploc, {
|
|
148
|
+
name: 'UserPloc',
|
|
149
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
150
|
+
enableLeakDetection: true,
|
|
151
|
+
leakDetector, // Optional: provide custom detector
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Access state history
|
|
155
|
+
const history = devTools.getStateHistory();
|
|
156
|
+
|
|
157
|
+
// Time-travel debugging
|
|
158
|
+
const handleUndo = () => {
|
|
159
|
+
devTools.previousState();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div>
|
|
164
|
+
<span>{state.name}</span>
|
|
165
|
+
{process.env.NODE_ENV === 'development' && (
|
|
166
|
+
<button onClick={handleUndo}>Undo</button>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
#### useUseCaseDevTools
|
|
174
|
+
|
|
175
|
+
Hook that provides DevTools for UseCase execution tracking. Tracks execution history, timing, errors, and performance profiling.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { useUseCaseDevTools } from '@c.a.f/infrastructure-react';
|
|
179
|
+
import { createPerformanceProfiler } from '@c.a.f/devtools';
|
|
180
|
+
import { CreateUser } from './application/User/Commands/CreateUser';
|
|
181
|
+
|
|
182
|
+
function CreateUserForm({ createUserUseCase }: { createUserUseCase: CreateUser }) {
|
|
183
|
+
// Optional: Create performance profiler
|
|
184
|
+
const profiler = createPerformanceProfiler({
|
|
185
|
+
enabled: process.env.NODE_ENV === 'development',
|
|
186
|
+
trackSlowOperations: true,
|
|
187
|
+
slowThreshold: 100, // ms
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const useCaseDevTools = useUseCaseDevTools({
|
|
191
|
+
name: 'CreateUser',
|
|
192
|
+
enabled: true,
|
|
193
|
+
logExecutionTime: true,
|
|
194
|
+
profiler, // Optional: provide custom profiler
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Wrap use case with DevTools tracking
|
|
198
|
+
const trackedUseCase = useCaseDevTools.wrap(createUserUseCase);
|
|
199
|
+
|
|
200
|
+
// Get execution statistics
|
|
201
|
+
const stats = useCaseDevTools.getStatistics();
|
|
202
|
+
console.log('Total executions:', stats.totalExecutions);
|
|
203
|
+
console.log('Average duration:', stats.averageDuration, 'ms');
|
|
204
|
+
|
|
205
|
+
// Use the tracked use case...
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### useCAFDevTools
|
|
210
|
+
|
|
211
|
+
Main hook that provides centralized DevTools access for your entire application. Tracks all Plocs and UseCases.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { useCAFDevTools, useTrackPloc } from '@c.a.f/infrastructure-react';
|
|
215
|
+
|
|
216
|
+
function App() {
|
|
217
|
+
const devTools = useCAFDevTools({
|
|
218
|
+
enabled: process.env.NODE_ENV === 'development'
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Enable/disable globally
|
|
222
|
+
const handleToggleDevTools = () => {
|
|
223
|
+
if (devTools.enabled) {
|
|
224
|
+
devTools.disable();
|
|
225
|
+
} else {
|
|
226
|
+
devTools.enable();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div>
|
|
232
|
+
{/* Your app */}
|
|
233
|
+
{process.env.NODE_ENV === 'development' && (
|
|
234
|
+
<button onClick={handleToggleDevTools}>
|
|
235
|
+
{devTools.enabled ? 'Disable' : 'Enable'} DevTools
|
|
236
|
+
</button>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// In components using Plocs, automatically track them:
|
|
243
|
+
function UserComponent({ userPloc }: { userPloc: UserPloc }) {
|
|
244
|
+
useTrackPloc(userPloc, 'UserPloc'); // Automatically registered with DevTools
|
|
245
|
+
const [state] = usePloc(userPloc);
|
|
246
|
+
// ...
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
The DevTools data is also exposed to `window.__CAF_DEVTOOLS__` for React DevTools extension integration.
|
|
251
|
+
|
|
252
|
+
### useRouteManager
|
|
253
|
+
|
|
254
|
+
Hook that provides a `RouteManager` from `@c.a.f/core`:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { useRouteManager } from '@c.a.f/infrastructure-react';
|
|
258
|
+
import { RouteManagerAuthOptions } from '@c.a.f/core';
|
|
259
|
+
|
|
260
|
+
function MyComponent() {
|
|
261
|
+
// Optional: provide auth configuration
|
|
262
|
+
const authOptions: RouteManagerAuthOptions = {
|
|
263
|
+
loginPath: '/login',
|
|
264
|
+
isLoggedIn: () => !!localStorage.getItem('token'),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const routeManager = useRouteManager(authOptions);
|
|
268
|
+
|
|
269
|
+
const handleLogin = async () => {
|
|
270
|
+
// ... login logic
|
|
271
|
+
routeManager.changeRoute('/dashboard');
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return <button onClick={handleLogin}>Login</button>;
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### useRouteRepository
|
|
279
|
+
|
|
280
|
+
Hook that provides a `RouteRepository` implementation:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { useRouteRepository } from '@c.a.f/infrastructure-react';
|
|
284
|
+
import { RouteManager } from '@c.a.f/core';
|
|
285
|
+
|
|
286
|
+
function MyComponent() {
|
|
287
|
+
const routeRepository = useRouteRepository();
|
|
288
|
+
const routeManager = new RouteManager(routeRepository);
|
|
289
|
+
|
|
290
|
+
// Use routeManager...
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Exports
|
|
295
|
+
|
|
296
|
+
- `usePloc` — Hook that subscribes to a Ploc and returns `[state, ploc]`; handles subscribe/unsubscribe and cleanup
|
|
297
|
+
- `useUseCase` — Hook that wraps UseCase execution with loading/error/data state management; handles RequestResult subscriptions automatically
|
|
298
|
+
- `CAFErrorBoundary` — Error Boundary component that catches errors from Ploc/UseCase execution; provides error context via React Context
|
|
299
|
+
- `useCAFError` — Hook to access error context from CAFErrorBoundary
|
|
300
|
+
- `usePlocDevTools` — Hook that provides DevTools for a Ploc instance; enables state tracking and time-travel debugging
|
|
301
|
+
- `useUseCaseDevTools` — Hook that provides DevTools for UseCase execution tracking
|
|
302
|
+
- `useCAFDevTools` — Main hook that provides centralized DevTools access; tracks all Plocs and UseCases
|
|
303
|
+
- `useTrackPloc` — Helper hook to automatically register a Ploc with DevTools
|
|
304
|
+
- `useRouteManager` — Hook returning core `RouteManager` with React Router integration
|
|
305
|
+
- `useRouteRepository` — Hook returning `RouteRepository` implementation
|
|
306
|
+
|
|
307
|
+
## Dependencies
|
|
308
|
+
|
|
309
|
+
- `@c.a.f/core` — Core primitives
|
|
310
|
+
- `@c.a.f/devtools` — DevTools utilities (for debugging)
|
|
311
|
+
- `react-router-dom` — React Router
|
|
312
|
+
|
|
313
|
+
## Peer Dependencies
|
|
314
|
+
|
|
315
|
+
- `react` >= 16.8.0
|
|
316
|
+
|
|
317
|
+
## License
|
|
318
|
+
|
|
319
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c.a.f/infrastructure-react",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Clean Architecture Frontend (CAF) — React adapters: usePloc, useUseCase, CAFErrorBoundary, DevTools, useRouteManager, useRouteRepository.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"clean-architecture-frontend",
|
|
8
|
+
"clean-architecture",
|
|
9
|
+
"caf",
|
|
10
|
+
"infrastructure",
|
|
11
|
+
"react",
|
|
12
|
+
"routing",
|
|
13
|
+
"hooks",
|
|
14
|
+
"ploc",
|
|
15
|
+
"state-management",
|
|
16
|
+
"devtools",
|
|
17
|
+
"debugging"
|
|
18
|
+
],
|
|
19
|
+
"author": "ali aslani <aliaslani.mm@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": "https://github.com/ialiaslani/caf.git",
|
|
22
|
+
"main": "./.build/index.js",
|
|
23
|
+
"module": "./.build/index.js",
|
|
24
|
+
"types": "./.build/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./.build/index.d.ts",
|
|
28
|
+
"import": "./.build/index.js",
|
|
29
|
+
"default": "./.build/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
".build"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc --build",
|
|
37
|
+
"start": "tsc --watch",
|
|
38
|
+
"prepublishOnly": "npm run build",
|
|
39
|
+
"test": "node_modules/.bin/vitest run",
|
|
40
|
+
"test:watch": "node_modules/.bin/vitest"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@c.a.f/core": "1.0.0",
|
|
44
|
+
"@c.a.f/devtools": "1.0.0",
|
|
45
|
+
"react-router-dom": "^6.23.1"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=16.8.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@testing-library/dom": "^10.4.0",
|
|
52
|
+
"@testing-library/jest-dom": "^6.1.5",
|
|
53
|
+
"@testing-library/react": "^16.0.0",
|
|
54
|
+
"happy-dom": "^15.11.7",
|
|
55
|
+
"vitest": "^2.1.0"
|
|
56
|
+
}
|
|
57
|
+
}
|