@blac/react 2.0.0-rc-16 → 2.0.0-rc.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,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = useExternalBlocStore;
4
+ const core_1 = require("@blac/core");
5
+ const react_1 = require("react");
6
+ /**
7
+ * Hook for using an external bloc store
8
+ * Provides low-level access to bloc instances for use with React's useSyncExternalStore
9
+ */
10
+ function useExternalBlocStore(blocConstructor, options = {}) {
11
+ const ridRef = (0, react_1.useRef)(`external-${(0, core_1.generateUUID)()}`);
12
+ const usedKeysRef = (0, react_1.useRef)(new Set());
13
+ const usedClassPropKeysRef = (0, react_1.useRef)(new Set());
14
+ const instanceRef = (0, react_1.useRef)(null);
15
+ // Get or create bloc instance
16
+ const bloc = (0, react_1.useMemo)(() => {
17
+ const blac = core_1.Blac.getInstance();
18
+ const base = blocConstructor;
19
+ // For isolated blocs, always create a new instance
20
+ if (base.constructor.isolated ||
21
+ blocConstructor.isolated) {
22
+ const newBloc = new blocConstructor(options?.staticProps);
23
+ const uniqueId = options?.id || `${blocConstructor.name}_${(0, core_1.generateUUID)()}`;
24
+ newBloc._id = uniqueId;
25
+ blac.activateBloc(newBloc);
26
+ return newBloc;
27
+ }
28
+ // For shared blocs, use the existing getBloc logic
29
+ return blac.getBloc(blocConstructor, {
30
+ id: options?.id,
31
+ constructorParams: options?.staticProps,
32
+ });
33
+ }, [blocConstructor, options?.id]);
34
+ // Create external store interface
35
+ const externalStore = (0, react_1.useMemo)(() => ({
36
+ getSnapshot: () => {
37
+ const currentInstance = instanceRef.current;
38
+ return currentInstance ? currentInstance.state : undefined;
39
+ },
40
+ subscribe: (listener) => {
41
+ const currentInstance = instanceRef.current;
42
+ if (!currentInstance) {
43
+ // Return no-op unsubscribe if no instance
44
+ return () => { };
45
+ }
46
+ // Wrap listener to handle errors gracefully
47
+ const safeListener = (newState, oldState) => {
48
+ try {
49
+ // Reset tracking keys on each listener call
50
+ usedKeysRef.current.clear();
51
+ usedClassPropKeysRef.current.clear();
52
+ // Call selector if provided
53
+ if (options.selector && currentInstance) {
54
+ options.selector(newState, oldState, currentInstance);
55
+ }
56
+ // Call listener with state if it expects it
57
+ if (listener.length > 0) {
58
+ listener(newState);
59
+ }
60
+ else {
61
+ listener();
62
+ }
63
+ }
64
+ catch (error) {
65
+ console.error('Listener error in useExternalBlocStore:', error);
66
+ // Don't rethrow to prevent breaking other listeners
67
+ }
68
+ };
69
+ // Store the previous state for the listener
70
+ let previousState = currentInstance.state;
71
+ const unsubscribe = currentInstance.subscribe((state) => {
72
+ const oldState = previousState;
73
+ previousState = state;
74
+ safeListener(state, oldState);
75
+ });
76
+ return unsubscribe;
77
+ },
78
+ getServerSnapshot: () => {
79
+ const currentInstance = instanceRef.current;
80
+ return currentInstance ? currentInstance.state : undefined;
81
+ },
82
+ }), []);
83
+ // Update instance ref
84
+ instanceRef.current = bloc;
85
+ return {
86
+ externalStore,
87
+ instance: instanceRef,
88
+ usedKeys: usedKeysRef,
89
+ usedClassPropKeys: usedClassPropKeysRef,
90
+ rid: ridRef.current,
91
+ };
92
+ }
93
+ //# sourceMappingURL=useExternalBlocStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExternalBlocStore.js","sourceRoot":"","sources":["../src/useExternalBlocStore.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@blac/react",
3
- "version": "2.0.0-rc-16",
3
+ "version": "2.0.0-rc.1",
4
4
  "license": "MIT",
5
5
  "author": "Brendan Mullins <jsnanigans@gmail.com>",
6
- "main": "src/index.ts",
7
- "module": "src/index.ts",
8
- "typings": "src/index.ts",
9
- "types": "src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": {
13
+ "development": "./src/index.ts",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "require": "./dist/index.cjs",
17
+ "source": "./src/index.ts"
18
+ }
19
+ },
10
20
  "files": [
11
- "src",
21
+ "dist",
12
22
  "README.md",
13
23
  "LICENSE"
14
24
  ],
@@ -26,10 +36,8 @@
26
36
  "dependencies": {},
27
37
  "peerDependencies": {
28
38
  "@types/react": "^18.0.0 || ^19.0.0",
29
- "@types/react-dom": "^18.0.0 || ^19.0.0",
30
39
  "react": "^18.0.0 || ^19.0.0",
31
- "react-dom": "^18.0.0 || ^19.0.0",
32
- "@blac/core": "2.0.0-rc-15"
40
+ "@blac/core": "2.0.0-rc.1"
33
41
  },
34
42
  "peerDependenciesMeta": {
35
43
  "@types/react": {
@@ -55,10 +63,18 @@
55
63
  "react-dom": "^19.1.1",
56
64
  "typescript": "^5.9.2",
57
65
  "vitest": "^3.2.4",
58
- "@blac/plugin-render-logging": "2.0.0-rc-15"
66
+ "@blac/plugin-render-logging": "2.0.0-rc.1",
67
+ "@blac/core": "2.0.0-rc.1"
59
68
  },
60
69
  "scripts": {
70
+ "dev": "tsc --watch --project tsconfig.build.json",
71
+ "build": "pnpm run clean && pnpm run build:esm && pnpm run build:cjs",
72
+ "build:esm": "tsc --project tsconfig.build.json",
73
+ "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir dist --declaration false --declarationMap false && mv dist/index.js dist/index.cjs",
74
+ "clean": "rm -rf dist",
61
75
  "format": "prettier --write \".\"",
76
+ "lint": "eslint src --ext .ts,.tsx",
77
+ "lint:fix": "eslint src --ext .ts,.tsx --fix",
62
78
  "test": "vitest run --config vitest.config.ts",
63
79
  "test:watch": "vitest --watch --config vitest.config.ts",
64
80
  "typecheck": "tsc --noEmit",
@@ -1,257 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { renderHook, act } from '@testing-library/react';
3
- import { Blac, Cubit, RerenderLogger } from '@blac/core';
4
- import { RenderLoggingPlugin } from '@blac/plugin-render-logging';
5
- import useBloc from '../useBloc';
6
-
7
- class TestCubit extends Cubit<{ count: number; name: string }> {
8
- constructor() {
9
- super({ count: 0, name: 'test' });
10
- }
11
-
12
- increment = () => this.emit({ ...this.state, count: this.state.count + 1 });
13
- changeName = (name: string) => this.emit({ ...this.state, name });
14
- changeAll = () => this.emit({ count: this.state.count + 1, name: 'changed' });
15
- }
16
-
17
- describe('Rerender Logging', () => {
18
- let consoleSpy: any;
19
- let plugin: RenderLoggingPlugin;
20
-
21
- beforeEach(() => {
22
- Blac.resetInstance();
23
- RerenderLogger.clear();
24
- consoleSpy = {
25
- log: vi.spyOn(console, 'log').mockImplementation(() => {}),
26
- group: vi.spyOn(console, 'group').mockImplementation(() => {}),
27
- groupEnd: vi.spyOn(console, 'groupEnd').mockImplementation(() => {}),
28
- table: vi.spyOn(console, 'table').mockImplementation(() => {}),
29
- };
30
- });
31
-
32
- afterEach(() => {
33
- consoleSpy.log.mockRestore();
34
- consoleSpy.group.mockRestore();
35
- consoleSpy.groupEnd.mockRestore();
36
- consoleSpy.table.mockRestore();
37
- // Remove plugin if it was added
38
- if (plugin) {
39
- Blac.getInstance().plugins.remove(plugin.name);
40
- }
41
- });
42
-
43
- it('should not log when rerender logging is disabled', () => {
44
- // No plugin registered
45
- const { result } = renderHook(() => useBloc(TestCubit));
46
-
47
- act(() => {
48
- result.current[1].increment();
49
- });
50
-
51
- expect(consoleSpy.log).not.toHaveBeenCalled();
52
- });
53
-
54
- it('should log rerenders when enabled with minimal level', () => {
55
- plugin = new RenderLoggingPlugin({ enabled: true, level: 'minimal' });
56
- Blac.getInstance().plugins.add(plugin);
57
-
58
- const { result } = renderHook(() => useBloc(TestCubit));
59
-
60
- // Clear logs from mount
61
- consoleSpy.log.mockClear();
62
-
63
- act(() => {
64
- result.current[1].increment();
65
- });
66
-
67
- expect(consoleSpy.log).toHaveBeenCalled();
68
- const logCall = consoleSpy.log.mock.calls[0][0];
69
- expect(logCall).toContain('#2'); // Second render (first is mount)
70
- });
71
-
72
- it('should log state changes with normal level', () => {
73
- plugin = new RenderLoggingPlugin({ enabled: true, level: 'normal' });
74
- Blac.getInstance().plugins.add(plugin);
75
-
76
- const { result } = renderHook(() => useBloc(TestCubit));
77
-
78
- // Clear logs from mount
79
- consoleSpy.log.mockClear();
80
-
81
- act(() => {
82
- result.current[1].increment();
83
- });
84
-
85
- expect(consoleSpy.log).toHaveBeenCalled();
86
- const logCall = consoleSpy.log.mock.calls[0];
87
- expect(logCall.join(' ')).toContain('State changed');
88
- expect(logCall.join(' ')).toContain('(entire state)'); // Without proxy tracking individual properties
89
- });
90
-
91
- it('should log detailed information with detailed level', () => {
92
- plugin = new RenderLoggingPlugin({ enabled: true, level: 'detailed' });
93
- Blac.getInstance().plugins.add(plugin);
94
-
95
- const { result } = renderHook(() => useBloc(TestCubit));
96
-
97
- // Clear logs from mount
98
- consoleSpy.log.mockClear();
99
- consoleSpy.group.mockClear();
100
- consoleSpy.table.mockClear();
101
-
102
- act(() => {
103
- result.current[1].increment();
104
- });
105
-
106
- expect(consoleSpy.group).toHaveBeenCalled();
107
- expect(consoleSpy.table).toHaveBeenCalled();
108
-
109
- const tableData = consoleSpy.table.mock.calls[0][0];
110
- // Since we haven't accessed specific properties, it shows '(entire state)'
111
- expect(tableData).toHaveProperty('(entire state)');
112
- expect(tableData['(entire state)']).toHaveProperty('old');
113
- expect(tableData['(entire state)']).toHaveProperty('new');
114
- });
115
-
116
- it('should track dependency changes when using manual dependencies', () => {
117
- plugin = new RenderLoggingPlugin({ enabled: true, level: 'normal' });
118
- Blac.getInstance().plugins.add(plugin);
119
-
120
- let depValue = 1;
121
- const { result, rerender } = renderHook(() =>
122
- useBloc(TestCubit, {
123
- dependencies: () => [depValue],
124
- }),
125
- );
126
-
127
- // Clear logs from mount
128
- consoleSpy.log.mockClear();
129
-
130
- // Change dependency
131
- depValue = 2;
132
- rerender();
133
-
134
- expect(consoleSpy.log).toHaveBeenCalled();
135
- const logCall = consoleSpy.log.mock.calls[0];
136
- const logMessage = logCall.join(' ');
137
-
138
- // The plugin logs "Manual dependencies changed" in the description
139
- expect(logMessage).toContain('Manual dependencies changed');
140
- // Verify it's logged as a dependency change (check for the emoji)
141
- expect(logMessage).toContain('🔗');
142
- });
143
-
144
- it('should respect filter function', () => {
145
- plugin = new RenderLoggingPlugin({
146
- enabled: true,
147
- filter: ({ blocName }) => blocName !== 'TestCubit',
148
- });
149
- Blac.getInstance().plugins.add(plugin);
150
-
151
- const { result } = renderHook(() => useBloc(TestCubit));
152
-
153
- act(() => {
154
- result.current[1].increment();
155
- });
156
-
157
- expect(consoleSpy.log).not.toHaveBeenCalled();
158
- });
159
-
160
- it('should group consecutive rerenders when groupRerenders is enabled', async () => {
161
- plugin = new RenderLoggingPlugin({
162
- enabled: true,
163
- groupRerenders: true,
164
- });
165
- Blac.getInstance().plugins.add(plugin);
166
-
167
- const { result } = renderHook(() => useBloc(TestCubit));
168
-
169
- // Clear logs from mount
170
- consoleSpy.log.mockClear();
171
- consoleSpy.group.mockClear();
172
-
173
- // Multiple rapid state changes - React may batch these
174
- act(() => {
175
- result.current[1].increment();
176
- });
177
-
178
- act(() => {
179
- result.current[1].increment();
180
- });
181
-
182
- act(() => {
183
- result.current[1].increment();
184
- });
185
-
186
- // Wait for debounce
187
- await new Promise((resolve) => setTimeout(resolve, 100));
188
-
189
- expect(consoleSpy.group).toHaveBeenCalled();
190
- const groupCall = consoleSpy.group.mock.calls[0][0];
191
- // Should have at least 2 renders (React may batch some updates)
192
- expect(groupCall).toMatch(/rendered \d+ times/);
193
- // Verify it grouped multiple renders
194
- expect(
195
- parseInt(groupCall.match(/rendered (\d+) times/)?.[1] || '0'),
196
- ).toBeGreaterThanOrEqual(2);
197
- });
198
-
199
- it('should track multiple state properties changes', () => {
200
- plugin = new RenderLoggingPlugin({ enabled: true, level: 'normal' });
201
- Blac.getInstance().plugins.add(plugin);
202
-
203
- const { result } = renderHook(() => useBloc(TestCubit));
204
-
205
- // Clear logs from mount
206
- consoleSpy.log.mockClear();
207
-
208
- // Change all properties
209
- act(() => {
210
- result.current[1].changeAll();
211
- });
212
-
213
- expect(consoleSpy.log).toHaveBeenCalled();
214
- const logCall = consoleSpy.log.mock.calls[0].join(' ');
215
- expect(logCall).toContain('(entire state)'); // Without proxy tracking, all properties changed
216
- });
217
-
218
- it('should show mount reason for first render', () => {
219
- plugin = new RenderLoggingPlugin({ enabled: true });
220
- Blac.getInstance().plugins.add(plugin);
221
-
222
- renderHook(() => useBloc(TestCubit));
223
-
224
- expect(consoleSpy.log).toHaveBeenCalled();
225
- const logCall = consoleSpy.log.mock.calls[0];
226
- expect(logCall.join(' ')).toContain('mount');
227
- expect(logCall.join(' ')).toContain('Component mounted');
228
- });
229
-
230
- it('should include stack trace in detailed mode when enabled', () => {
231
- plugin = new RenderLoggingPlugin({
232
- enabled: true,
233
- level: 'detailed',
234
- includeStackTrace: true,
235
- });
236
- Blac.getInstance().plugins.add(plugin);
237
-
238
- const { result } = renderHook(() => useBloc(TestCubit));
239
-
240
- // Clear logs from mount
241
- consoleSpy.log.mockClear();
242
- consoleSpy.group.mockClear();
243
-
244
- act(() => {
245
- result.current[1].increment();
246
- });
247
-
248
- // In detailed mode with stack trace, it logs the stack
249
- const logCalls = consoleSpy.log.mock.calls;
250
- const hasStackTrace = logCalls.some((call: any[]) =>
251
- call.some(
252
- (arg: any) => typeof arg === 'string' && arg.includes('Stack trace:'),
253
- ),
254
- );
255
- expect(hasStackTrace).toBe(true);
256
- });
257
- });