@pyreon/core 0.24.5 → 0.24.6
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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +53 -31
- package/package.json +2 -6
- package/src/compat-marker.ts +0 -79
- package/src/compat-shared.ts +0 -80
- package/src/component.ts +0 -98
- package/src/context.ts +0 -349
- package/src/defer.ts +0 -279
- package/src/dynamic.ts +0 -32
- package/src/env.d.ts +0 -6
- package/src/error-boundary.ts +0 -90
- package/src/for.ts +0 -51
- package/src/h.ts +0 -80
- package/src/index.ts +0 -80
- package/src/jsx-dev-runtime.ts +0 -2
- package/src/jsx-runtime.ts +0 -747
- package/src/lazy.ts +0 -25
- package/src/lifecycle.ts +0 -152
- package/src/manifest.ts +0 -579
- package/src/map-array.ts +0 -42
- package/src/portal.ts +0 -39
- package/src/props.ts +0 -269
- package/src/ref.ts +0 -32
- package/src/show.ts +0 -121
- package/src/style.ts +0 -102
- package/src/suspense.ts +0 -52
- package/src/telemetry.ts +0 -120
- package/src/tests/compat-marker.test.ts +0 -96
- package/src/tests/compat-shared.test.ts +0 -99
- package/src/tests/component.test.ts +0 -281
- package/src/tests/context.test.ts +0 -629
- package/src/tests/core.test.ts +0 -1290
- package/src/tests/cx.test.ts +0 -70
- package/src/tests/defer.test.ts +0 -359
- package/src/tests/dynamic.test.ts +0 -87
- package/src/tests/error-boundary.test.ts +0 -181
- package/src/tests/extract-props-overloads.types.test.ts +0 -135
- package/src/tests/for.test.ts +0 -117
- package/src/tests/h.test.ts +0 -221
- package/src/tests/jsx-compat.test.tsx +0 -86
- package/src/tests/lazy.test.ts +0 -100
- package/src/tests/lifecycle.test.ts +0 -350
- package/src/tests/manifest-snapshot.test.ts +0 -100
- package/src/tests/map-array.test.ts +0 -313
- package/src/tests/native-marker-error-boundary.test.ts +0 -12
- package/src/tests/portal.test.ts +0 -48
- package/src/tests/props-extended.test.ts +0 -157
- package/src/tests/props.test.ts +0 -250
- package/src/tests/reactive-context.test.ts +0 -69
- package/src/tests/reactive-props.test.ts +0 -157
- package/src/tests/ref.test.ts +0 -70
- package/src/tests/show.test.ts +0 -314
- package/src/tests/style.test.ts +0 -157
- package/src/tests/suspense.test.ts +0 -139
- package/src/tests/telemetry.test.ts +0 -297
- package/src/types.ts +0 -116
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getCurrentHooks,
|
|
3
|
-
onErrorCaptured,
|
|
4
|
-
onMount,
|
|
5
|
-
onUnmount,
|
|
6
|
-
onUpdate,
|
|
7
|
-
setCurrentHooks,
|
|
8
|
-
} from '../lifecycle'
|
|
9
|
-
import type { LifecycleHooks } from '../types'
|
|
10
|
-
|
|
11
|
-
describe('setCurrentHooks / getCurrentHooks', () => {
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
setCurrentHooks(null)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
test('getCurrentHooks returns null by default', () => {
|
|
17
|
-
expect(getCurrentHooks()).toBeNull()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('setCurrentHooks sets the current hooks context', () => {
|
|
21
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
22
|
-
setCurrentHooks(hooks)
|
|
23
|
-
expect(getCurrentHooks()).toBe(hooks)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('setCurrentHooks(null) clears the context', () => {
|
|
27
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
28
|
-
setCurrentHooks(hooks)
|
|
29
|
-
expect(getCurrentHooks()).toBe(hooks)
|
|
30
|
-
setCurrentHooks(null)
|
|
31
|
-
expect(getCurrentHooks()).toBeNull()
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
describe('onMount', () => {
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
setCurrentHooks(null)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('registers callback on current hooks', () => {
|
|
41
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
42
|
-
setCurrentHooks(hooks)
|
|
43
|
-
const fn = () => undefined
|
|
44
|
-
onMount(fn)
|
|
45
|
-
expect(hooks.mount!).toHaveLength(1)
|
|
46
|
-
expect(hooks.mount![0]).toBe(fn)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('multiple onMount calls accumulate', () => {
|
|
50
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
51
|
-
setCurrentHooks(hooks)
|
|
52
|
-
onMount(() => undefined)
|
|
53
|
-
onMount(() => undefined)
|
|
54
|
-
onMount(() => undefined)
|
|
55
|
-
expect(hooks.mount!).toHaveLength(3)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('warns when called outside component setup', () => {
|
|
59
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
60
|
-
onMount(() => {})
|
|
61
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
62
|
-
expect.stringContaining('onMount() called outside component setup'),
|
|
63
|
-
)
|
|
64
|
-
warnSpy.mockRestore()
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('is a no-op outside component setup (no crash)', () => {
|
|
68
|
-
expect(() => onMount(() => {})).not.toThrow()
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('accepts callback returning cleanup function', () => {
|
|
72
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
73
|
-
setCurrentHooks(hooks)
|
|
74
|
-
const cleanup = () => {}
|
|
75
|
-
onMount(() => cleanup)
|
|
76
|
-
expect(hooks.mount!).toHaveLength(1)
|
|
77
|
-
expect(hooks.mount![0]!()).toBe(cleanup)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('accepts callback returning void', () => {
|
|
81
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
82
|
-
setCurrentHooks(hooks)
|
|
83
|
-
onMount(() => {})
|
|
84
|
-
expect(hooks.mount!).toHaveLength(1)
|
|
85
|
-
expect(hooks.mount![0]!()).toBeUndefined()
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
describe('onUnmount', () => {
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
setCurrentHooks(null)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test('registers callback on current hooks', () => {
|
|
95
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
96
|
-
setCurrentHooks(hooks)
|
|
97
|
-
const fn = () => {}
|
|
98
|
-
onUnmount(fn)
|
|
99
|
-
expect(hooks.unmount!).toHaveLength(1)
|
|
100
|
-
expect(hooks.unmount![0]).toBe(fn)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
test('warns when called outside component setup', () => {
|
|
104
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
105
|
-
onUnmount(() => {})
|
|
106
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
107
|
-
expect.stringContaining('onUnmount() called outside component setup'),
|
|
108
|
-
)
|
|
109
|
-
warnSpy.mockRestore()
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
describe('onUpdate', () => {
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
setCurrentHooks(null)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test('registers callback on current hooks', () => {
|
|
119
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
120
|
-
setCurrentHooks(hooks)
|
|
121
|
-
const fn = () => {}
|
|
122
|
-
onUpdate(fn)
|
|
123
|
-
expect(hooks.update!).toHaveLength(1)
|
|
124
|
-
expect(hooks.update![0]).toBe(fn)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test('warns when called outside component setup', () => {
|
|
128
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
129
|
-
onUpdate(() => {})
|
|
130
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
131
|
-
expect.stringContaining('onUpdate() called outside component setup'),
|
|
132
|
-
)
|
|
133
|
-
warnSpy.mockRestore()
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
describe('onErrorCaptured', () => {
|
|
138
|
-
afterEach(() => {
|
|
139
|
-
setCurrentHooks(null)
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test('registers callback on current hooks', () => {
|
|
143
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
144
|
-
setCurrentHooks(hooks)
|
|
145
|
-
const fn = () => true
|
|
146
|
-
onErrorCaptured(fn)
|
|
147
|
-
expect(hooks.error!).toHaveLength(1)
|
|
148
|
-
expect(hooks.error![0]).toBe(fn)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test('warns when called outside component setup', () => {
|
|
152
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
153
|
-
onErrorCaptured(() => true)
|
|
154
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
155
|
-
expect.stringContaining('onErrorCaptured() called outside component setup'),
|
|
156
|
-
)
|
|
157
|
-
warnSpy.mockRestore()
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test('registered handler receives the error', () => {
|
|
161
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
162
|
-
setCurrentHooks(hooks)
|
|
163
|
-
let captured: unknown = null
|
|
164
|
-
onErrorCaptured((err) => {
|
|
165
|
-
captured = err
|
|
166
|
-
return true
|
|
167
|
-
})
|
|
168
|
-
// Simulate calling the handler
|
|
169
|
-
const testError = new Error('test')
|
|
170
|
-
hooks.error![0]!(testError)
|
|
171
|
-
expect(captured).toBe(testError)
|
|
172
|
-
})
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
describe('lifecycle hooks interaction', () => {
|
|
176
|
-
afterEach(() => {
|
|
177
|
-
setCurrentHooks(null)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
test('all hook types can be registered in same context', () => {
|
|
181
|
-
const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
182
|
-
setCurrentHooks(hooks)
|
|
183
|
-
|
|
184
|
-
onMount(() => undefined)
|
|
185
|
-
onUnmount(() => {})
|
|
186
|
-
onUpdate(() => {})
|
|
187
|
-
onErrorCaptured(() => true)
|
|
188
|
-
|
|
189
|
-
expect(hooks.mount!).toHaveLength(1)
|
|
190
|
-
expect(hooks.unmount!).toHaveLength(1)
|
|
191
|
-
expect(hooks.update!).toHaveLength(1)
|
|
192
|
-
expect(hooks.error!).toHaveLength(1)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
test('hooks from different setCurrentHooks calls go to different stores', () => {
|
|
196
|
-
const hooks1: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
197
|
-
const hooks2: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
|
|
198
|
-
|
|
199
|
-
setCurrentHooks(hooks1)
|
|
200
|
-
onMount(() => undefined)
|
|
201
|
-
setCurrentHooks(hooks2)
|
|
202
|
-
onMount(() => undefined)
|
|
203
|
-
onMount(() => undefined)
|
|
204
|
-
|
|
205
|
-
expect(hooks1.mount).toHaveLength(1)
|
|
206
|
-
expect(hooks2.mount).toHaveLength(2)
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
// ─── Call-site warning enhancement (Phase 4) ───────────────────────────────
|
|
211
|
-
// Dev warnings now include the first non-framework stack frame so the
|
|
212
|
-
// developer can see WHICH of their components called the hook outside
|
|
213
|
-
// setup — previously the warning just said "lifecycle.ts:17" which was
|
|
214
|
-
// useless for debugging.
|
|
215
|
-
|
|
216
|
-
describe('warnOutsideSetup — call-site capture', () => {
|
|
217
|
-
afterEach(() => {
|
|
218
|
-
setCurrentHooks(null)
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
test('warning includes a "Called from:" frame for debugging', () => {
|
|
222
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
223
|
-
// Call onUnmount outside setup — from a user-land helper so the stack
|
|
224
|
-
// has a distinct non-framework frame we can assert against.
|
|
225
|
-
const userLandHelper = () => {
|
|
226
|
-
onUnmount(() => {})
|
|
227
|
-
}
|
|
228
|
-
userLandHelper()
|
|
229
|
-
expect(warnSpy).toHaveBeenCalled()
|
|
230
|
-
const message = warnSpy.mock.calls[0]?.[0] as string
|
|
231
|
-
expect(message).toContain('onUnmount() called outside component setup')
|
|
232
|
-
expect(message).toContain('Called from:')
|
|
233
|
-
warnSpy.mockRestore()
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
test('onUnmount warning includes provide() hint for diagnosability', () => {
|
|
237
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
238
|
-
onUnmount(() => {})
|
|
239
|
-
const message = warnSpy.mock.calls[0]?.[0] as string
|
|
240
|
-
expect(message).toContain('provide()')
|
|
241
|
-
warnSpy.mockRestore()
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
test('onMount warning does NOT include the onUnmount-specific hint', () => {
|
|
245
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
246
|
-
onMount(() => {})
|
|
247
|
-
const message = warnSpy.mock.calls[0]?.[0] as string
|
|
248
|
-
// The hint is specific to onUnmount (provide() uses it), so onMount
|
|
249
|
-
// shouldn't carry it — keeps the warning message targeted.
|
|
250
|
-
expect(message).not.toContain('provide()')
|
|
251
|
-
warnSpy.mockRestore()
|
|
252
|
-
})
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
describe('captureCallSite — skip patterns cover published-bundle paths', () => {
|
|
256
|
-
// Real bug surfaced from a 0.24.1 consumer report: the pre-fix patterns
|
|
257
|
-
// only matched source-tree paths (`/lifecycle\.ts/`, `/core\/src\//`,
|
|
258
|
-
// etc.). Published packages bundle to `node_modules/@pyreon/<name>/lib/`,
|
|
259
|
-
// so the framework's own stack frames slipped through the filter — the
|
|
260
|
-
// warning's "Called from:" line pointed at the warning emitter itself
|
|
261
|
-
// (or the framework's lib code) instead of the user's call site.
|
|
262
|
-
//
|
|
263
|
-
// We can't easily synthesise a real published-bundle stack in a test,
|
|
264
|
-
// but we CAN exercise the regex set directly. The fix added matchers
|
|
265
|
-
// for `@pyreon/<name>/lib/` paths AND function-name matches
|
|
266
|
-
// (`captureCallSite`, `warnOutsideSetup`) that survive bundling.
|
|
267
|
-
|
|
268
|
-
// Mirror the patterns from the impl. (If this list drifts, the impl
|
|
269
|
-
// and this test fall out of sync — that's the regression signal.)
|
|
270
|
-
const skipPatterns = [
|
|
271
|
-
/\/lifecycle\.[tj]s/,
|
|
272
|
-
/\/context\.[tj]s/,
|
|
273
|
-
/\/component\.[tj]s/,
|
|
274
|
-
/\bcaptureCallSite\b/,
|
|
275
|
-
/\bwarnOutsideSetup\b/,
|
|
276
|
-
/\/(core|reactivity|runtime-dom|runtime-server|router|head|ui-core|styler|unistyle|rocketstyle|attrs|elements|kinetic)\/src\//,
|
|
277
|
-
/node_modules\/@pyreon\/[^/]+\/lib\//,
|
|
278
|
-
/@pyreon\/[a-z-]+\/lib\//,
|
|
279
|
-
/node:internal/,
|
|
280
|
-
/webpack-internal/,
|
|
281
|
-
/<anonymous>/,
|
|
282
|
-
]
|
|
283
|
-
|
|
284
|
-
const isSkipped = (line: string): boolean =>
|
|
285
|
-
skipPatterns.some((p) => p.test(line))
|
|
286
|
-
|
|
287
|
-
test('skips published-bundle lib paths (`@pyreon/X/lib/`)', () => {
|
|
288
|
-
expect(
|
|
289
|
-
isSkipped(
|
|
290
|
-
'at HeadProvider (file:///app/node_modules/@pyreon/head/lib/index.js:42:7)',
|
|
291
|
-
),
|
|
292
|
-
).toBe(true)
|
|
293
|
-
expect(
|
|
294
|
-
isSkipped(
|
|
295
|
-
'at provide (file:///app/node_modules/@pyreon/core/lib/index.js:96:5)',
|
|
296
|
-
),
|
|
297
|
-
).toBe(true)
|
|
298
|
-
expect(
|
|
299
|
-
isSkipped(
|
|
300
|
-
'at ThemeProvider (file:///app/node_modules/@pyreon/styler/lib/index.js:24:3)',
|
|
301
|
-
),
|
|
302
|
-
).toBe(true)
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
test('skips workspace source paths (`bun` condition consumers)', () => {
|
|
306
|
-
expect(
|
|
307
|
-
isSkipped(
|
|
308
|
-
'at provide (/Users/me/proj/packages/core/core/src/context.ts:88:3)',
|
|
309
|
-
),
|
|
310
|
-
).toBe(true)
|
|
311
|
-
expect(
|
|
312
|
-
isSkipped('at HeadProvider (/Users/me/proj/packages/core/head/src/provider.ts:56:5)'),
|
|
313
|
-
).toBe(true)
|
|
314
|
-
expect(
|
|
315
|
-
isSkipped(
|
|
316
|
-
'at RouterProvider (/Users/me/proj/packages/core/router/src/components.tsx:30:5)',
|
|
317
|
-
),
|
|
318
|
-
).toBe(true)
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
test('skips the warning infrastructure itself (function-name match)', () => {
|
|
322
|
-
// Even if the file path is mangled (minified / bundled to a single
|
|
323
|
-
// file like `lib/index.js`), the symbol names survive when bundlers
|
|
324
|
-
// preserve exports. The function-name pattern catches both.
|
|
325
|
-
expect(isSkipped('at captureCallSite (lib/index.js:22:33)')).toBe(true)
|
|
326
|
-
expect(isSkipped('at warnOutsideSetup (lib/index.js:55:21)')).toBe(true)
|
|
327
|
-
expect(isSkipped(' at captureCallSite (mangled-bundle.js:9999:11)')).toBe(true)
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
test('skips engine / anonymous frames', () => {
|
|
331
|
-
expect(isSkipped('at <anonymous>')).toBe(true)
|
|
332
|
-
expect(isSkipped('at runMicrotasks (node:internal/process/task_queues:96:5)')).toBe(true)
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
test('does NOT skip user-code paths in src/ or pages/ etc.', () => {
|
|
336
|
-
// These should ALL fall through the skip filter so captureCallSite
|
|
337
|
-
// returns them as the "Called from:" line — the user-actionable hint.
|
|
338
|
-
expect(isSkipped('at MyComponent (/Users/me/proj/src/components/Foo.tsx:42:15)')).toBe(false)
|
|
339
|
-
expect(isSkipped('at HomePage (/Users/me/proj/src/pages/Home.tsx:10:5)')).toBe(false)
|
|
340
|
-
expect(isSkipped('at NotFound (/Users/me/proj/src/routes/_not-found.tsx:5:7)')).toBe(false)
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
test('does NOT skip user code that happens to be in node_modules but NOT @pyreon/', () => {
|
|
344
|
-
// A user-installed third-party package's component shouldn't be
|
|
345
|
-
// skipped — only `@pyreon/*` framework bundles are silenced.
|
|
346
|
-
expect(
|
|
347
|
-
isSkipped('at SomeLib (file:///app/node_modules/some-third-party/lib/index.js:42:7)'),
|
|
348
|
-
).toBe(false)
|
|
349
|
-
})
|
|
350
|
-
})
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
renderApiReferenceEntries,
|
|
3
|
-
renderLlmsFullSection,
|
|
4
|
-
renderLlmsTxtLine,
|
|
5
|
-
} from '@pyreon/manifest'
|
|
6
|
-
import coreManifest from '../manifest'
|
|
7
|
-
|
|
8
|
-
describe('gen-docs — core snapshot', () => {
|
|
9
|
-
it('renders @pyreon/core to its expected llms.txt bullet', () => {
|
|
10
|
-
expect(renderLlmsTxtLine(coreManifest)).toMatchInlineSnapshot(`"- @pyreon/core — VNode, h(), Fragment, lifecycle, context, JSX runtime, Suspense, ErrorBoundary, lazy(), Dynamic, cx(), splitProps, mergeProps, createUniqueId. Pyreon components are plain functions that execute a single time. Reactivity comes from reading signals inside reactive scopes (JSX expression thunks, \`effect()\`, \`computed()\`), not from re-running the component function. \`if (!cond()) return null\` at the top level runs once and is static — use \`return (() => { if (!cond()) return null; return <div /> })\` for reactive conditional rendering."`)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('renders @pyreon/core to its expected llms-full.txt section — full body snapshot', () => {
|
|
14
|
-
expect(renderLlmsFullSection(coreManifest)).toMatchInlineSnapshot(`
|
|
15
|
-
"## @pyreon/core — Complete API
|
|
16
|
-
|
|
17
|
-
Component model and lifecycle for Pyreon. Provides the VNode type system, \`h()\` hyperscript function, JSX automatic runtime (\`@pyreon/core/jsx-runtime\`), lifecycle hooks (\`onMount\`, \`onUnmount\`), two-tier context system (\`createContext\` for static values, \`createReactiveContext\` for signal-backed values), control-flow components (\`Show\`, \`Switch\`/\`Match\`, \`For\`, \`Suspense\`, \`ErrorBoundary\`), code-splitting via \`lazy()\`, dynamic rendering via \`Dynamic\`, and props utilities (\`splitProps\`, \`mergeProps\`, \`cx\`, \`createUniqueId\`). Components are plain functions (\`ComponentFn<P> = (props: P) => VNodeChild\`) that run ONCE — reactivity comes from reading signals inside reactive scopes, not from re-running the component.
|
|
18
|
-
|
|
19
|
-
\`\`\`typescript
|
|
20
|
-
import { h, Fragment, onMount, onUnmount, provide, createContext, createReactiveContext, useContext, Show, Switch, Match, For, Suspense, ErrorBoundary, lazy, Dynamic, cx, splitProps, mergeProps, createUniqueId, untrack } from "@pyreon/core"
|
|
21
|
-
import { signal, computed } from "@pyreon/reactivity"
|
|
22
|
-
|
|
23
|
-
// Context — static (destructure-safe) vs reactive (must call to read)
|
|
24
|
-
const ThemeCtx = createContext<"light" | "dark">("light")
|
|
25
|
-
const ModeCtx = createReactiveContext<"light" | "dark">("light")
|
|
26
|
-
|
|
27
|
-
const App = (props: { children: any }) => {
|
|
28
|
-
const mode = signal<"light" | "dark">("dark")
|
|
29
|
-
provide(ThemeCtx, "dark") // static — safe to destructure
|
|
30
|
-
provide(ModeCtx, () => mode()) // reactive — consumer must call
|
|
31
|
-
|
|
32
|
-
return <>{props.children}</>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Lifecycle
|
|
36
|
-
const Timer = () => {
|
|
37
|
-
const count = signal(0)
|
|
38
|
-
onMount(() => {
|
|
39
|
-
const id = setInterval(() => count.update(n => n + 1), 1000)
|
|
40
|
-
return () => clearInterval(id) // cleanup runs on unmount
|
|
41
|
-
})
|
|
42
|
-
return <div>{() => count()}</div>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Control flow — reactive conditional rendering
|
|
46
|
-
const Page = (props: { items: { id: number; name: string }[]; loggedIn: () => boolean }) => (
|
|
47
|
-
<div>
|
|
48
|
-
<Show when={props.loggedIn()} fallback={<p>Please log in</p>}>
|
|
49
|
-
<For each={props.items} by={item => item.id}>
|
|
50
|
-
{item => <li>{item.name}</li>}
|
|
51
|
-
</For>
|
|
52
|
-
</Show>
|
|
53
|
-
</div>
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
// Props utilities — preserve reactivity
|
|
57
|
-
const Button = (props: { class?: string; size?: string; onClick: () => void; children: any }) => {
|
|
58
|
-
const [local, rest] = splitProps(props, ["class", "size"])
|
|
59
|
-
const merged = mergeProps({ size: "md" }, local)
|
|
60
|
-
const id = createUniqueId()
|
|
61
|
-
return <button id={id} {...rest} class={cx("btn", \`btn-\${merged.size}\`, local.class)} />
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Code splitting
|
|
65
|
-
const HeavyPage = lazy(() => import("./HeavyPage"))
|
|
66
|
-
const LazyApp = () => (
|
|
67
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
68
|
-
<HeavyPage />
|
|
69
|
-
</Suspense>
|
|
70
|
-
)
|
|
71
|
-
\`\`\`
|
|
72
|
-
|
|
73
|
-
> **Components run once**: Pyreon components are plain functions that execute a single time. Reactivity comes from reading signals inside reactive scopes (JSX expression thunks, \`effect()\`, \`computed()\`), not from re-running the component function. \`if (!cond()) return null\` at the top level runs once and is static — use \`return (() => { if (!cond()) return null; return <div /> })\` for reactive conditional rendering.
|
|
74
|
-
>
|
|
75
|
-
> **Destructuring props kills reactivity**: \`const { name } = props\` captures the value at setup time — it becomes static. Use \`props.name\` inside reactive scopes, or \`splitProps(props, ["name"])\` for rest patterns. The compiler handles \`const x = props.y; return <div>{x}</div>\` by inlining \`props.y\` back at the use site, but only for \`const\` (not \`let\`/\`var\`).
|
|
76
|
-
>
|
|
77
|
-
> **Two context types**: \`createContext<T>\` returns \`T\` from \`useContext()\` — safe to destructure. \`createReactiveContext<T>\` returns \`() => T\` — must call to read. Using the wrong one is a common source of stale-value bugs (static context for dynamic values) or unnecessary ceremony (reactive context for constants).
|
|
78
|
-
>
|
|
79
|
-
> **For uses by, not key**: The \`<For>\` component uses the \`by\` prop for its key function because JSX extracts \`key\` as a special VNode reconciliation prop. Writing \`<For each={items()} key={fn}>\` silently passes the key to the VNode system instead of the list reconciler.
|
|
80
|
-
>
|
|
81
|
-
> **JSX uses standard HTML attributes**: Use \`class\` not \`className\`, \`for\` not \`htmlFor\`, \`onInput\` not \`onChange\` for per-keystroke updates. Pyreon maps to native DOM events, not the React synthetic event system.
|
|
82
|
-
"
|
|
83
|
-
`)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('renders @pyreon/core to MCP api-reference entries — one per api[] item', () => {
|
|
87
|
-
const record = renderApiReferenceEntries(coreManifest)
|
|
88
|
-
expect(Object.keys(record).length).toBe(31)
|
|
89
|
-
expect(Object.keys(record)).toContain('core/h')
|
|
90
|
-
// Compat-mode native marker — added so framework JSX components opt out
|
|
91
|
-
// of `@pyreon/{react,preact,vue,solid}-compat` wrapping.
|
|
92
|
-
expect(Object.keys(record)).toContain('core/nativeCompat')
|
|
93
|
-
expect(Object.keys(record)).toContain('core/isNativeCompat')
|
|
94
|
-
expect(Object.keys(record)).toContain('core/NATIVE_COMPAT_MARKER')
|
|
95
|
-
// Spot-check the flagship API — h() is the hyperscript function
|
|
96
|
-
const h = record['core/h']!
|
|
97
|
-
expect(h.notes).toContain('JSX')
|
|
98
|
-
expect(h.mistakes?.split('\n').length).toBeGreaterThan(2)
|
|
99
|
-
})
|
|
100
|
-
})
|