@pyreon/runtime-server 0.24.4 → 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.
@@ -1,83 +0,0 @@
1
- import { h, Suspense } from '@pyreon/core'
2
- import type { ComponentFn } from '@pyreon/core'
3
- import { renderToStream } from '../index'
4
-
5
- async function collectStream(stream: ReadableStream<string>): Promise<string> {
6
- const reader = stream.getReader()
7
- let out = ''
8
- while (true) {
9
- const { value, done } = await reader.read()
10
- if (done) break
11
- out += value
12
- }
13
- return out
14
- }
15
-
16
- // PR #233 follow-up: when an async child inside a Suspense boundary
17
- // rejects mid-stream, what happens? The fallback should stay visible,
18
- // the swap must NOT be emitted, and the stream must close (not hang
19
- // waiting for a resolution that will never come).
20
-
21
- describe('renderToStream — Suspense boundary rejection', () => {
22
- test('keeps fallback visible and closes stream when async child rejects', async () => {
23
- async function Rejects(): Promise<ReturnType<typeof h>> {
24
- await new Promise<void>((r) => setTimeout(r, 5))
25
- throw new Error('deliberate test failure')
26
- }
27
-
28
- const vnode = h(Suspense, {
29
- fallback: h('p', { id: 'fallback' }, 'loading...'),
30
- children: h(Rejects as unknown as ComponentFn, null),
31
- })
32
-
33
- // If the stream hangs, this test will time out. Passing means it
34
- // closed cleanly via controller.close().
35
- const html = await collectStream(renderToStream(vnode))
36
-
37
- // Fallback placeholder + content are present
38
- expect(html).toContain('id="pyreon-s-0"')
39
- expect(html).toContain('loading...')
40
-
41
- // NO swap template or __NS invocation for this boundary — those only
42
- // emit on successful resolution. The __NS helper FUNCTION is always
43
- // inlined once per stream; distinguish definition vs call.
44
- expect(html).not.toContain('id="pyreon-t-0"')
45
- expect(html).not.toContain('__NS("pyreon-s-0"')
46
- })
47
-
48
- test('one rejecting boundary does not abort siblings — other content still streams', async () => {
49
- async function Rejects(): Promise<ReturnType<typeof h>> {
50
- await new Promise<void>((r) => setTimeout(r, 5))
51
- throw new Error('sibling rejection')
52
- }
53
- async function Resolves(): Promise<ReturnType<typeof h>> {
54
- await new Promise<void>((r) => setTimeout(r, 10))
55
- return h('span', { id: 'ok' }, 'ok')
56
- }
57
-
58
- const vnode = h(
59
- 'div',
60
- null,
61
- h(Suspense, {
62
- fallback: h('span', { id: 'fb-a' }, 'fb-a'),
63
- children: h(Rejects as unknown as ComponentFn, null),
64
- }),
65
- h(Suspense, {
66
- fallback: h('span', { id: 'fb-b' }, 'fb-b'),
67
- children: h(Resolves as unknown as ComponentFn, null),
68
- }),
69
- )
70
-
71
- const html = await collectStream(renderToStream(vnode))
72
-
73
- // Both fallbacks shipped
74
- expect(html).toContain('id="fb-a"')
75
- expect(html).toContain('id="fb-b"')
76
- // The resolving sibling's swap still went through. The rejecting
77
- // sibling (boundary id 0) must NOT swap; the resolving one (id 1)
78
- // must. Assert the specific invocations.
79
- expect(html).toContain('id="ok"')
80
- expect(html).not.toContain('__NS("pyreon-s-0"')
81
- expect(html).toContain('__NS("pyreon-s-1"')
82
- })
83
- })
@@ -1,55 +0,0 @@
1
- import { h } from '@pyreon/core'
2
- import { renderToString } from '../index'
3
-
4
- // Security sweep follow-up to #233/#235. `vnode.type` is interpolated
5
- // into `<TAG>` and `</TAG>` unescaped (matching React/Vue/Solid — the
6
- // framework trusts callers not to feed user-controlled strings as tag
7
- // names). Defense-in-depth: dev-mode warning when the tag contains
8
- // characters that would break HTML structure, so the mistake surfaces
9
- // before it ships.
10
-
11
- describe('SSR — dev warning for unsafe tag names', () => {
12
- let originalNodeEnv: string | undefined
13
- let warnSpy: ReturnType<typeof vi.spyOn>
14
-
15
- beforeEach(() => {
16
- originalNodeEnv = process.env.NODE_ENV
17
- // Ensure __DEV__ is true for the duration of these tests
18
- process.env.NODE_ENV = 'development'
19
- warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
20
- })
21
-
22
- afterEach(() => {
23
- if (originalNodeEnv === undefined) delete process.env.NODE_ENV
24
- else process.env.NODE_ENV = originalNodeEnv
25
- warnSpy.mockRestore()
26
- })
27
-
28
- it('does not warn for a normal HTML tag', async () => {
29
- await renderToString(h('div', null, 'ok'))
30
- expect(warnSpy).not.toHaveBeenCalled()
31
- })
32
-
33
- it('does not warn for custom elements with hyphens', async () => {
34
- await renderToString(h('my-element', null, 'ok'))
35
- expect(warnSpy).not.toHaveBeenCalled()
36
- })
37
-
38
- it('warns when the tag contains > (HTML breakout attempt)', async () => {
39
- await renderToString(h('div><script>alert(1)</script><div', null, 'x'))
40
- expect(warnSpy).toHaveBeenCalled()
41
- const msg = warnSpy.mock.calls[0]?.[0] as string
42
- expect(msg).toContain('[Pyreon SSR]')
43
- expect(msg).toContain('break HTML structure')
44
- })
45
-
46
- it('warns when the tag contains a space (attribute-smuggling attempt)', async () => {
47
- await renderToString(h('div onerror=alert(1)', null, 'x'))
48
- expect(warnSpy).toHaveBeenCalled()
49
- })
50
-
51
- it('warns when the tag starts with a non-letter', async () => {
52
- await renderToString(h('123-bad', null, 'x'))
53
- expect(warnSpy).toHaveBeenCalled()
54
- })
55
- })
@@ -1,40 +0,0 @@
1
- import { h } from '@pyreon/core'
2
- import { renderToString } from '../index'
3
-
4
- // Lock-in regression suite for attribute-value escaping. Flagged as
5
- // PR #233 follow-up: would a user-supplied string with embedded
6
- // HTML-significant characters break out of an attribute value and
7
- // inject arbitrary markup? Verified: escapeHtml covers all five
8
- // critical characters (& < > " '); this suite locks the invariant.
9
-
10
- describe('renderToString — attribute value escaping', () => {
11
- it('escapes double quotes in attribute values (breakout prevention)', async () => {
12
- const html = await renderToString(h('div', { 'data-x': 'he said "hi"' }))
13
- expect(html).toContain('data-x="he said &quot;hi&quot;"')
14
- })
15
-
16
- it('escapes single quotes in attribute values', async () => {
17
- const html = await renderToString(h('div', { 'data-x': "it's fine" }))
18
- expect(html).toContain('data-x="it&#39;s fine"')
19
- })
20
-
21
- it('escapes ampersands (prevent entity confusion)', async () => {
22
- const html = await renderToString(h('a', { href: 'https://x.test?a=1&b=2' }))
23
- expect(html).toContain('href="https://x.test?a=1&amp;b=2"')
24
- })
25
-
26
- it('escapes < and > (prevent tag injection via attribute)', async () => {
27
- const html = await renderToString(h('div', { 'data-x': '<script>bad</script>' }))
28
- expect(html).toContain('data-x="&lt;script&gt;bad&lt;/script&gt;"')
29
- })
30
-
31
- it('escapes all five critical characters in a single value', async () => {
32
- const html = await renderToString(h('div', { title: `&<>"'` }))
33
- expect(html).toContain('title="&amp;&lt;&gt;&quot;&#39;"')
34
- })
35
-
36
- it('escapes text children containing HTML-significant chars', async () => {
37
- const html = await renderToString(h('p', null, 'a & b < c > "d"'))
38
- expect(html).toContain('a &amp; b &lt; c &gt; &quot;d&quot;')
39
- })
40
- })