@potok-web-framework/core 0.1.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/bun.lock +25 -0
  3. package/package.json +39 -0
  4. package/src/block.ts +102 -0
  5. package/src/bootstrap-app.ts +115 -0
  6. package/src/client-only.ts +17 -0
  7. package/src/constants.ts +27 -0
  8. package/src/context.ts +85 -0
  9. package/src/detect-child.ts +21 -0
  10. package/src/error-boundary.ts +51 -0
  11. package/src/exports/client.ts +1 -0
  12. package/src/exports/hmr.ts +1 -0
  13. package/src/exports/index.ts +21 -0
  14. package/src/exports/jsx-runtime.ts +4 -0
  15. package/src/exports/server.ts +1 -0
  16. package/src/fragment.ts +28 -0
  17. package/src/global.dev.d.ts +12 -0
  18. package/src/hmr/hmr-dev.ts +10 -0
  19. package/src/hmr/register-component.ts +109 -0
  20. package/src/hmr/registered-component.ts +59 -0
  21. package/src/hmr/registry.ts +78 -0
  22. package/src/hmr/types.ts +6 -0
  23. package/src/hmr/utils.ts +20 -0
  24. package/src/html-element.ts +95 -0
  25. package/src/jsx-types.ts +13 -0
  26. package/src/lazy.ts +44 -0
  27. package/src/lib-context-reader.ts +33 -0
  28. package/src/lib-scripts.ts +8 -0
  29. package/src/lifecycle.ts +44 -0
  30. package/src/list.ts +175 -0
  31. package/src/portal.ts +101 -0
  32. package/src/prop-types.ts +1165 -0
  33. package/src/ref.ts +11 -0
  34. package/src/render-to-dom.ts +325 -0
  35. package/src/render-to-string.ts +65 -0
  36. package/src/server-node.ts +98 -0
  37. package/src/show.ts +46 -0
  38. package/src/signals.ts +323 -0
  39. package/src/store.ts +68 -0
  40. package/src/text.ts +35 -0
  41. package/src/types.ts +69 -0
  42. package/src/utils.ts +118 -0
  43. package/tests/signals.test.ts +403 -0
  44. package/tsconfig.json +17 -0
  45. package/vite.config.ts +21 -0
@@ -0,0 +1,403 @@
1
+ import { test, expect, mock } from 'bun:test'
2
+ import {
3
+ batch,
4
+ createDisposer,
5
+ createEffect,
6
+ createSignal,
7
+ deepTrack,
8
+ untrack,
9
+ } from '@/signals'
10
+
11
+ test('сигнал сохраняет значение и вызывает эффекты при изменении', () => {
12
+ const signal = createSignal({ count: 1 })
13
+
14
+ // Проверка начального значения
15
+ expect(signal.count).toBe(1)
16
+
17
+ const spyFn = mock(() => {
18
+ signal.count
19
+ })
20
+
21
+ // Создаём эффект, подписанный на signal.count
22
+ createEffect(spyFn)
23
+
24
+ // Эффект вызывается при инициализации
25
+ expect(spyFn).toHaveBeenCalledTimes(1)
26
+
27
+ // Изменение значения вызывает эффект
28
+ signal.count = 2
29
+ expect(signal.count).toBe(2)
30
+ expect(spyFn).toHaveBeenCalledTimes(2)
31
+
32
+ // Установка нового значения снова вызывает эффект
33
+ signal.count = 3
34
+ expect(signal.count).toBe(3)
35
+ expect(spyFn).toHaveBeenCalledTimes(3)
36
+ })
37
+
38
+ test('эффект вызывается даже при установке сигнала в то же значение', () => {
39
+ const signal = createSignal({ count: 1 })
40
+
41
+ const spyFn = mock(() => {
42
+ signal.count
43
+ })
44
+
45
+ createEffect(spyFn)
46
+
47
+ // Эффект должен вызваться при инициализации
48
+ expect(spyFn).toHaveBeenCalledTimes(1)
49
+
50
+ // Установка нового значения вызывает эффект
51
+ signal.count = 2
52
+ expect(spyFn).toHaveBeenCalledTimes(2)
53
+
54
+ // Установка снова того же значения вызывает эффект
55
+ signal.count = 2
56
+ expect(spyFn).toHaveBeenCalledTimes(3)
57
+ })
58
+
59
+ test('эффект реагирует на изменения всех сигналов, на которые он подписан', () => {
60
+ const firstSignal = createSignal({ count: 1 })
61
+ const secondSignal = createSignal({ count: 1 })
62
+
63
+ const spyFn = mock(() => {
64
+ firstSignal.count
65
+ secondSignal.count
66
+ })
67
+
68
+ createEffect(spyFn)
69
+
70
+ // Эффект вызывается сразу при инициализации
71
+ expect(spyFn).toHaveBeenCalledTimes(1)
72
+
73
+ // Изменение первого сигнала
74
+ firstSignal.count++
75
+ expect(spyFn).toHaveBeenCalledTimes(2)
76
+
77
+ // Изменение второго сигнала
78
+ secondSignal.count++
79
+ expect(spyFn).toHaveBeenCalledTimes(3)
80
+ })
81
+
82
+ test('все эффекты, подписанные на один сигнал, вызываются при изменении', () => {
83
+ const signal = createSignal({ count: 1 })
84
+
85
+ const firstSpyFn = mock(() => {
86
+ signal.count
87
+ })
88
+
89
+ const secondSpyFn = mock(() => {
90
+ signal.count
91
+ })
92
+
93
+ // Создаём два эффекта, подписанных на signal.count
94
+ createEffect(firstSpyFn)
95
+ createEffect(secondSpyFn)
96
+
97
+ // Оба эффекта вызываются один раз при создании
98
+ expect(firstSpyFn).toHaveBeenCalledTimes(1)
99
+ expect(secondSpyFn).toHaveBeenCalledTimes(1)
100
+
101
+ // Изменение сигнала вызывает оба эффекта
102
+ signal.count++
103
+
104
+ expect(signal.count).toBe(2)
105
+ expect(firstSpyFn).toHaveBeenCalledTimes(2)
106
+ expect(secondSpyFn).toHaveBeenCalledTimes(2)
107
+ })
108
+
109
+ test('вызывает эффект при изменении значения сигнала по ключу', () => {
110
+ const signal = createSignal({ count: 1 })
111
+
112
+ const spyFn = mock(() => {
113
+ signal.count // Чтение для подписки
114
+ })
115
+
116
+ createEffect(spyFn)
117
+
118
+ expect(spyFn).toHaveBeenCalledTimes(1)
119
+
120
+ signal.count++ // Изменение значения
121
+ expect(spyFn).toHaveBeenCalledTimes(2)
122
+
123
+ signal.count = signal.count // Установка того же значения — всё равно должно вызвать, т.к. мы не проверяем на "одинаковость"
124
+ expect(spyFn).toHaveBeenCalledTimes(3)
125
+
126
+ signal.count = 100 // Новое значение
127
+ expect(spyFn).toHaveBeenCalledTimes(4)
128
+ })
129
+
130
+ test('условная подписка: эффект не вызывается, если сигнал не читался', () => {
131
+ const firstSignal = createSignal({ count: 1 })
132
+ const secondSignal = createSignal({ count: 1 })
133
+
134
+ const spyFn = mock(() => {
135
+ // Подписка на secondSignal происходит только при выполнении условия
136
+ if (firstSignal.count < 2) {
137
+ secondSignal.count
138
+ }
139
+ })
140
+
141
+ createEffect(spyFn)
142
+
143
+ // Первый вызов эффекта
144
+ expect(spyFn).toHaveBeenCalledTimes(1)
145
+
146
+ // secondSignal.count был прочитан (т.к. условие выполнилось), значит подписка есть
147
+ secondSignal.count++
148
+ expect(spyFn).toHaveBeenCalledTimes(2)
149
+
150
+ // Меняем firstSignal так, чтобы условие не выполнялось => secondSignal больше не читается
151
+ firstSignal.count = 2
152
+ expect(spyFn).toHaveBeenCalledTimes(3)
153
+
154
+ // Изменение secondSignal — эффекта не будет, т.к. подписка снята
155
+ secondSignal.count++
156
+ expect(spyFn).toHaveBeenCalledTimes(3)
157
+
158
+ // Возвращаем условие обратно: подписка снова появится
159
+ firstSignal.count = 1
160
+ expect(spyFn).toHaveBeenCalledTimes(4)
161
+
162
+ // Подписка на secondSignal снова активна
163
+ secondSignal.count++
164
+ expect(spyFn).toHaveBeenCalledTimes(5)
165
+ })
166
+
167
+ test('условная подписка: флаг игнорирования версионирования сохраняет подписку вне зависимости от чтения', () => {
168
+ const firstSignal = createSignal({ count: 1 })
169
+ const secondSignal = createSignal({ count: 1 })
170
+
171
+ const spyFn = mock(() => {
172
+ // При `firstSignal.count < 2` читаем secondSignal
173
+ if (firstSignal.count < 2) {
174
+ secondSignal.count
175
+ }
176
+ })
177
+
178
+ // Второй аргумент `true` — включаем режим игнорирования версионирования
179
+ createEffect(spyFn, true)
180
+
181
+ // Изначальный запуск
182
+ expect(spyFn).toHaveBeenCalledTimes(1)
183
+
184
+ // Подписка активна — изменение secondSignal вызывает эффект
185
+ secondSignal.count++
186
+ expect(spyFn).toHaveBeenCalledTimes(2)
187
+
188
+ // Условие теперь не выполняется — secondSignal НЕ читается
189
+ firstSignal.count = 2
190
+ expect(spyFn).toHaveBeenCalledTimes(3)
191
+
192
+ // Несмотря на то, что secondSignal не был прочитан, эффект всё равно вызывается
193
+ secondSignal.count++
194
+ expect(spyFn).toHaveBeenCalledTimes(4)
195
+
196
+ // Возвращаем обратно выполнение условия
197
+ firstSignal.count = 1
198
+ expect(spyFn).toHaveBeenCalledTimes(5)
199
+
200
+ // Подписка всё ещё живая
201
+ secondSignal.count++
202
+ expect(spyFn).toHaveBeenCalledTimes(6)
203
+ })
204
+
205
+ test('эффект вызывается один раз при батчинге нескольких обновлений', () => {
206
+ const firstSignal = createSignal({ count: 1 })
207
+ const secondSignal = createSignal({ count: 1 })
208
+
209
+ const spyFn = mock(() => {
210
+ firstSignal.count
211
+ secondSignal.count
212
+ })
213
+
214
+ createEffect(spyFn)
215
+
216
+ // Изначально вызывается один раз
217
+ expect(spyFn).toHaveBeenCalledTimes(1)
218
+
219
+ // Обновление firstSignal вызывает эффект
220
+ firstSignal.count++
221
+ expect(spyFn).toHaveBeenCalledTimes(2)
222
+
223
+ // Обновление secondSignal вызывает эффект
224
+ secondSignal.count++
225
+ expect(spyFn).toHaveBeenCalledTimes(3)
226
+
227
+ // Два последовательных обновления вызывают эффект дважды
228
+ firstSignal.count++
229
+ secondSignal.count++
230
+ expect(spyFn).toHaveBeenCalledTimes(5)
231
+
232
+ // Обновления внутри batch вызывают эффект только один раз
233
+ batch(() => {
234
+ firstSignal.count++
235
+ secondSignal.count++
236
+ })
237
+ expect(spyFn).toHaveBeenCalledTimes(6)
238
+ })
239
+
240
+ test('untrack исключает сигналы из подписки внутри эффекта', () => {
241
+ const firstSignal = createSignal({ count: 1 })
242
+ const secondSignal = createSignal({ count: 1 })
243
+
244
+ const spyFn = mock(() => {
245
+ // Читаем firstSignal.count — устанавливаем реактивную подписку на firstSignal
246
+ firstSignal.count
247
+
248
+ // Читаем secondSignal.count внутри untrack — исключаем secondSignal из подписки эффекта
249
+ return untrack(() => secondSignal.count)
250
+ })
251
+
252
+ createEffect(spyFn)
253
+
254
+ // Эффект вызывается один раз при создании — начальная инициализация
255
+ expect(spyFn).toHaveBeenCalledTimes(1)
256
+
257
+ // Обновляем firstSignal — т.к. есть подписка, эффект должен вызваться заново
258
+ firstSignal.count++
259
+ expect(spyFn).toHaveBeenCalledTimes(2)
260
+
261
+ // Обновляем secondSignal — эффект НЕ вызывается, потому что secondSignal был прочитан в untrack
262
+ secondSignal.count++
263
+ expect(spyFn).toHaveBeenCalledTimes(2)
264
+
265
+ // Ещё раз обновляем firstSignal — эффект вызывается
266
+ firstSignal.count++
267
+ expect(spyFn).toHaveBeenCalledTimes(3)
268
+
269
+ // Проверяем значение, возвращённое первым вызовом эффекта — secondSignal.count равен 1
270
+ expect(spyFn.mock.results[0]?.value).toBe(1)
271
+
272
+ // Проверяем значение, возвращённое вторым вызовом эффекта — secondSignal.count всё ещё 1,
273
+ // т.к. мы не подписаны на его изменения
274
+ expect(spyFn.mock.results[1]?.value).toBe(1)
275
+
276
+ // Проверяем значение, возвращённое третьим вызовом эффекта — secondSignal.count равно 2,
277
+ // т.к. в последнем вызове эффект прочитал значение после обновления firstSignal
278
+ expect(spyFn.mock.results[2]?.value).toBe(2)
279
+ })
280
+
281
+ test('deepTrack: реактивность на глубину отслеживания и разные уровни вложенности', () => {
282
+ const firstSignal = createSignal({
283
+ object: {
284
+ nestedObject: {
285
+ count: 1,
286
+ },
287
+ },
288
+ })
289
+ const secondSignal = createSignal({
290
+ object: {
291
+ nestedObject: {
292
+ count: 1,
293
+ },
294
+ },
295
+ count: 1,
296
+ })
297
+ const thirdSignal = createSignal({
298
+ object: {
299
+ nestedObject: {
300
+ count: 1,
301
+ },
302
+ },
303
+ })
304
+
305
+ const spyFn = mock(() => {
306
+ // Подписка на верхний уровень объекта firstSignal (без глубокой подписки)
307
+ firstSignal.object
308
+
309
+ // Подписка с глубиной 1 — отслеживает изменения только первого уровня в secondSignal
310
+ deepTrack(secondSignal, 1)
311
+
312
+ // Подписка с глубиной по умолчанию (глубокая) — отслеживает изменения на всех уровнях thirdSignal
313
+ deepTrack(thirdSignal)
314
+ })
315
+
316
+ createEffect(spyFn)
317
+
318
+ // Изначальный вызов эффекта
319
+ expect(spyFn).toHaveBeenCalledTimes(1)
320
+
321
+ // Изменение вложенного поля первого сигнала не вызывает эффект (глубина подписки 0)
322
+ firstSignal.object.nestedObject.count++
323
+ expect(spyFn).toHaveBeenCalledTimes(1)
324
+
325
+ // Изменение самого объекта первого сигнала (ссылка) вызывает эффект
326
+ firstSignal.object = {
327
+ nestedObject: {
328
+ count: 2,
329
+ },
330
+ }
331
+ expect(spyFn).toHaveBeenCalledTimes(2)
332
+
333
+ // Изменение вложенного поля в secondSignal не вызывает эффект (глубина подписки 1)
334
+ secondSignal.object.nestedObject.count++
335
+ expect(spyFn).toHaveBeenCalledTimes(2)
336
+
337
+ // Изменение поля второго уровня secondSignal (count) вызывает эффект
338
+ secondSignal.count++
339
+ expect(spyFn).toHaveBeenCalledTimes(3)
340
+
341
+ // Изменение ссылки на объект верхнего уровня secondSignal вызывает эффект
342
+ secondSignal.object = {
343
+ nestedObject: {
344
+ count: 1,
345
+ },
346
+ }
347
+ expect(spyFn).toHaveBeenCalledTimes(4)
348
+
349
+ // Изменение вложенного объекта внутри secondSignal.object не вызывает эффект (глубина 1)
350
+ secondSignal.object.nestedObject = {
351
+ count: 1,
352
+ }
353
+ expect(spyFn).toHaveBeenCalledTimes(4)
354
+
355
+ // Изменение вложенного свойства thirdSignal вызывает эффект (глубина подписки глубокая)
356
+ thirdSignal.object.nestedObject.count++
357
+ expect(spyFn).toHaveBeenCalledTimes(5)
358
+
359
+ // Изменение ссылки на объект верхнего уровня thirdSignal вызывает эффект
360
+ thirdSignal.object = {
361
+ nestedObject: {
362
+ count: 1,
363
+ },
364
+ }
365
+ expect(spyFn).toHaveBeenCalledTimes(6)
366
+ })
367
+
368
+ test('createDisposer: удаляет все эффекты, созданные внутри области действия', () => {
369
+ const signal = createSignal({
370
+ count: 1,
371
+ })
372
+
373
+ const firstSpyFn = mock(() => {
374
+ signal.count
375
+ })
376
+
377
+ const secondSpyFn = mock(() => {
378
+ signal.count
379
+ })
380
+
381
+ // Создаём disposer, который регистрирует два эффекта
382
+ const disposer = createDisposer(() => {
383
+ createEffect(firstSpyFn)
384
+ createEffect(secondSpyFn)
385
+ })
386
+
387
+ // Эффекты сразу выполняются один раз при создании
388
+ expect(firstSpyFn).toHaveBeenCalledTimes(1)
389
+ expect(secondSpyFn).toHaveBeenCalledTimes(1)
390
+
391
+ // Изменение сигнала вызывает оба эффекта повторно
392
+ signal.count++
393
+ expect(firstSpyFn).toHaveBeenCalledTimes(2)
394
+ expect(secondSpyFn).toHaveBeenCalledTimes(2)
395
+
396
+ // Отключаем все эффекты, созданные внутри disposer'а
397
+ disposer.dispose()
398
+
399
+ // Изменение сигнала больше не вызывает эффекты
400
+ signal.count++
401
+ expect(firstSpyFn).toHaveBeenCalledTimes(2)
402
+ expect(secondSpyFn).toHaveBeenCalledTimes(2)
403
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "declarationMap": false,
5
+ "emitDeclarationOnly": true,
6
+ "jsx": "preserve",
7
+ "strict": true,
8
+ "types": ["bun-types"],
9
+ "outDir": "dist",
10
+ "skipLibCheck": true,
11
+ "esModuleInterop": true,
12
+ "moduleResolution": "bundler",
13
+ "target": "ESNext",
14
+ "module": "ESNext"
15
+ },
16
+ "include": ["./src/**/*"]
17
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vite'
2
+ import dtsPlugin from 'vite-plugin-dts'
3
+
4
+ export default defineConfig({
5
+ build: {
6
+ lib: {
7
+ entry: {
8
+ index: 'src/exports/index.ts',
9
+ server: 'src/exports/server.ts',
10
+ client: 'src/exports/client.ts',
11
+ 'jsx-runtime': 'src/exports/jsx-runtime.ts',
12
+ hmr: 'src/exports/hmr.ts',
13
+ },
14
+ formats: ['es'],
15
+ },
16
+ rolldownOptions: {
17
+ external: ['path', /node:.*/],
18
+ },
19
+ },
20
+ plugins: [dtsPlugin()],
21
+ })