@pyreon/document-primitives 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,366 +0,0 @@
1
- import type { DocumentMarker } from '@pyreon/connector-document'
2
- import { h } from '@pyreon/core'
3
- import { describe, expect, it } from 'vitest'
4
- import { createDocumentExport, extractDocNode } from '../useDocumentExport'
5
-
6
- // Helper: build a real VNode via @pyreon/core's h(). The third arg
7
- // is an array (kept for parity with the prior mock helper) and
8
- // spreads into h()'s varargs. Tests run real-h() VNodes through
9
- // the export pipeline — the mock-only path masked PR #197's silent
10
- // metadata drop bug.
11
- const node = (
12
- type: string | ((...args: any[]) => any),
13
- props: Record<string, any> = {},
14
- children: unknown[] = [],
15
- ) => h(type as any, props, ...(children as any[])) as any
16
-
17
- // Document-marked component
18
- const docComponent = (docType: string) => {
19
- const fn = (props: any) => node('div', props, props.children ? [props.children] : [])
20
- ;(fn as any)._documentType = docType
21
- return fn as ((...args: any[]) => any) & DocumentMarker
22
- }
23
-
24
- const DocDocument = docComponent('document')
25
- const DocHeading = docComponent('heading')
26
- const DocText = docComponent('text')
27
-
28
- describe('createDocumentExport', () => {
29
- it('extracts a document tree from template function', () => {
30
- const doc = createDocumentExport(() =>
31
- node(DocDocument, { _documentProps: { title: 'Test' } }, [
32
- node(
33
- DocHeading,
34
- {
35
- $rocketstyle: { fontSize: 24, fontWeight: 'bold' },
36
- _documentProps: { level: 1 },
37
- },
38
- ['Hello'],
39
- ),
40
- node(
41
- DocText,
42
- {
43
- $rocketstyle: { fontSize: 14, color: '#333' },
44
- },
45
- ['World'],
46
- ),
47
- ]),
48
- )
49
-
50
- const tree = doc.getDocNode()
51
-
52
- expect(tree.type).toBe('document')
53
- expect(tree.props.title).toBe('Test')
54
- expect(tree.children).toHaveLength(2)
55
-
56
- const heading = tree.children[0] as any
57
- expect(heading.type).toBe('heading')
58
- expect(heading.props.level).toBe(1)
59
- expect(heading.styles?.fontSize).toBe(24)
60
- expect(heading.children).toEqual(['Hello'])
61
-
62
- const text = tree.children[1] as any
63
- expect(text.type).toBe('text')
64
- expect(text.styles?.fontSize).toBe(14)
65
- expect(text.styles?.color).toBe('#333')
66
- })
67
-
68
- it('can be called multiple times', () => {
69
- const doc = createDocumentExport(() =>
70
- node(DocText, { $rocketstyle: { fontSize: 14 } }, ['Static']),
71
- )
72
-
73
- const tree1 = doc.getDocNode()
74
- const tree2 = doc.getDocNode()
75
-
76
- expect(tree1.type).toBe('text')
77
- expect(tree2.type).toBe('text')
78
- })
79
-
80
- it('respects includeStyles option', () => {
81
- const doc = createDocumentExport(
82
- () => node(DocHeading, { $rocketstyle: { fontSize: 24 } }, ['Hello']),
83
- { includeStyles: false },
84
- )
85
-
86
- const tree = doc.getDocNode()
87
- expect(tree.styles).toBeUndefined()
88
- })
89
-
90
- it('handles empty template', () => {
91
- const doc = createDocumentExport(() => null)
92
-
93
- const tree = doc.getDocNode()
94
- expect(tree.type).toBe('document')
95
- expect(tree.children).toEqual([])
96
- })
97
- })
98
-
99
- describe('extractDocNode (one-step alias)', () => {
100
- // The one-step form. Equivalent to
101
- // `createDocumentExport(templateFn).getDocNode()` but without
102
- // the wrapper-object indirection. This is the form most
103
- // consumers should use.
104
-
105
- it('extracts a tree from a template function in one call', () => {
106
- const tree = extractDocNode(() =>
107
- node(DocDocument, { _documentProps: { title: 'Test' } }, [
108
- node(DocHeading, { _documentProps: { level: 1 } }, ['Hello']),
109
- ]),
110
- )
111
-
112
- expect(tree.type).toBe('document')
113
- expect(tree.props.title).toBe('Test')
114
- expect(tree.children).toHaveLength(1)
115
- const heading = tree.children[0] as { type: string; props: Record<string, unknown> }
116
- expect(heading.type).toBe('heading')
117
- expect(heading.props.level).toBe(1)
118
- })
119
-
120
- it('respects extraction options (includeStyles: false)', () => {
121
- const tree = extractDocNode(
122
- () => node(DocHeading, { $rocketstyle: { fontSize: 24 } }, ['Hello']),
123
- { includeStyles: false },
124
- )
125
- expect(tree.styles).toBeUndefined()
126
- })
127
-
128
- it('returns the same tree shape as createDocumentExport().getDocNode()', () => {
129
- // Equivalence guarantee — the two-step form delegates to the
130
- // one-step form internally, so they MUST produce identical
131
- // output for identical input.
132
- const template = () =>
133
- node(DocText, { $rocketstyle: { fontSize: 14 } }, ['Hello'])
134
-
135
- const oneStep = extractDocNode(template)
136
- const twoStep = createDocumentExport(template).getDocNode()
137
-
138
- expect(oneStep).toEqual(twoStep)
139
- })
140
-
141
- it('is idempotent — calling extractDocNode twice on the same template produces equivalent results', () => {
142
- // The framework fix in PR #197 changed extractDocumentTree to
143
- // CALL the component function for documentType vnodes when
144
- // _documentProps is not directly on the JSX vnode. The fix is
145
- // correct because rocketstyle's attrs HOC is meant to be pure
146
- // setup with no observable side effects on the second call.
147
- // This test locks in that purity assumption: extracting the
148
- // same template twice produces structurally equivalent doc
149
- // nodes.
150
- //
151
- // If a future change to a primitive accidentally introduces a
152
- // side effect in its attrs callback (e.g. a counter, a log
153
- // line, a stateful import), the second extraction would still
154
- // SUCCEED but produce different output — and this test would
155
- // catch the regression. The three extractions should be deeply
156
- // equal under any change to the primitive's setup path.
157
- const template = () =>
158
- node(
159
- DocDocument,
160
- { _documentProps: { title: 'Idempotent', author: 'Test' } },
161
- [
162
- node(DocHeading, { _documentProps: { level: 1 } }, ['Hello']),
163
- node(DocText, { $rocketstyle: { fontSize: 14 } }, ['World']),
164
- ],
165
- )
166
-
167
- const first = extractDocNode(template)
168
- const second = extractDocNode(template)
169
- const third = extractDocNode(template)
170
-
171
- expect(first).toEqual(second)
172
- expect(second).toEqual(third)
173
- })
174
-
175
- it('resolves function values in _documentProps each call (D1+D2 integration)', () => {
176
- // The combined contract: extractDocNode is the one-step form,
177
- // and it benefits from the same function-value resolution
178
- // that extractDocumentTree does. Each call sees the live
179
- // value of any accessor in _documentProps.
180
- let counter = 0
181
- const template = () =>
182
- node(
183
- DocDocument,
184
- {
185
- _documentProps: {
186
- title: () => `Export ${++counter}`,
187
- author: 'Plain string still works',
188
- },
189
- },
190
- [],
191
- )
192
-
193
- const first = extractDocNode(template)
194
- const second = extractDocNode(template)
195
-
196
- expect(first.props.title).toBe('Export 1')
197
- expect(first.props.author).toBe('Plain string still works')
198
- expect(second.props.title).toBe('Export 2')
199
- })
200
- })
201
-
202
- describe('DocDocument reactive metadata (D1 integration)', () => {
203
- // The end-to-end contract: a real DocDocument primitive (NOT a
204
- // mock vnode) accepts accessor functions for title/author/subject,
205
- // stores them in _documentProps, and the export pipeline reads
206
- // the LIVE values at extraction time.
207
- //
208
- // This test mounts DocDocument as if from JSX (via h()) and
209
- // extracts the resulting tree, then mutates the closure-captured
210
- // state and re-extracts to prove the second extraction sees the
211
- // updated value. The previous tests use mock vnodes — this one
212
- // uses the real primitive.
213
-
214
- // Long timeout: this test dynamic-imports @pyreon/test-utils,
215
- // @pyreon/core, and DocDocument inside the test body. Each
216
- // dynamic import triggers Vite's transform pipeline (JSX
217
- // compilation, rocketstyle wrapping, etc.) which takes 5+
218
- // seconds on slow CI runners on first hit. The default 5000ms
219
- // timeout fails reliably on CI.
220
- it('DocDocument with accessor title produces live values across multiple extractions', { timeout: 60_000 }, async () => {
221
- // Use happy-dom + initTestConfig like the rest of the test suite
222
- const { initTestConfig } = await import('@pyreon/test-utils')
223
- const { h } = await import('@pyreon/core')
224
- const cleanup = initTestConfig()
225
- try {
226
- // Real DocDocument from the package (not the mock above)
227
- const RealDocDocument = (await import('../primitives/DocDocument')).default
228
-
229
- // Closure state that the accessor reads from. Mutating it
230
- // between extractions simulates a signal change.
231
- let currentName = 'Aisha'
232
- const titleAccessor = () => `${currentName} — Resume`
233
- const authorAccessor = () => currentName
234
-
235
- // Build the template via h() so DocDocument's attrs callback
236
- // runs (storing the accessor functions in _documentProps).
237
- const template = () =>
238
- h(
239
- RealDocDocument as never,
240
- { title: titleAccessor, author: authorAccessor } as never,
241
- )
242
-
243
- const first = extractDocNode(template)
244
- expect(first.type).toBe('document')
245
- expect(first.props.title).toBe('Aisha — Resume')
246
- expect(first.props.author).toBe('Aisha')
247
-
248
- // Mutate the closure-captured name. The next extraction
249
- // should see the new value because extractDocumentTree calls
250
- // the function fresh.
251
- currentName = 'Marcus'
252
- const second = extractDocNode(template)
253
- expect(second.props.title).toBe('Marcus — Resume')
254
- expect(second.props.author).toBe('Marcus')
255
-
256
- // And once more for good measure
257
- currentName = 'Priya'
258
- const third = extractDocNode(template)
259
- expect(third.props.title).toBe('Priya — Resume')
260
- expect(third.props.author).toBe('Priya')
261
- } finally {
262
- cleanup()
263
- }
264
- })
265
-
266
- it('DocDocument with plain string title still works (backward compat)', { timeout: 30_000 }, async () => {
267
- const { initTestConfig } = await import('@pyreon/test-utils')
268
- const { h } = await import('@pyreon/core')
269
- const cleanup = initTestConfig()
270
- try {
271
- const RealDocDocument = (await import('../primitives/DocDocument')).default
272
-
273
- const tree = extractDocNode(() =>
274
- h(
275
- RealDocDocument as never,
276
- { title: 'Static title', author: 'Static author' } as never,
277
- ),
278
- )
279
-
280
- expect(tree.props.title).toBe('Static title')
281
- expect(tree.props.author).toBe('Static author')
282
- } finally {
283
- cleanup()
284
- }
285
- })
286
-
287
- it('DocDocument subject prop also accepts both string and accessor (full prop coverage)', { timeout: 30_000 }, async () => {
288
- // The widening covered all three metadata props (title, author,
289
- // subject). The previous tests only exercise title and author —
290
- // this test fills the coverage gap so a typo in the subject
291
- // type widening would be caught.
292
- const { initTestConfig } = await import('@pyreon/test-utils')
293
- const { h } = await import('@pyreon/core')
294
- const cleanup = initTestConfig()
295
- try {
296
- const RealDocDocument = (await import('../primitives/DocDocument')).default
297
-
298
- // Plain string subject
299
- const plainTree = extractDocNode(() =>
300
- h(RealDocDocument as never, { subject: 'Q4 Report' } as never),
301
- )
302
- expect(plainTree.props.subject).toBe('Q4 Report')
303
-
304
- // Accessor subject — value resolved at extraction time
305
- let topic = 'Initial topic'
306
- const accessorTree1 = extractDocNode(() =>
307
- h(RealDocDocument as never, { subject: () => topic } as never),
308
- )
309
- expect(accessorTree1.props.subject).toBe('Initial topic')
310
-
311
- topic = 'Updated topic'
312
- const accessorTree2 = extractDocNode(() =>
313
- h(RealDocDocument as never, { subject: () => topic } as never),
314
- )
315
- expect(accessorTree2.props.subject).toBe('Updated topic')
316
- } finally {
317
- cleanup()
318
- }
319
- })
320
-
321
- it('extractDocNode does NOT invoke a real DocDocument component (T3.1 Path C)', { timeout: 30_000 }, async () => {
322
- // The architectural invariant locked in by T3.1 (PR #321):
323
- // `extractDocumentTree` consumes a real rocketstyle primitive's
324
- // `__rs_attrs` chain directly, never invoking the wrapped component
325
- // function. The previous Path B workaround had to call the full
326
- // styled wrapper per export to read post-attrs `_documentProps`.
327
- //
328
- // Spy mechanism: wrap the imported component in a Proxy whose
329
- // `apply` trap counts function-call invocations. Property reads
330
- // (IS_ROCKETSTYLE, __rs_attrs, _documentType, .meta, .displayName,
331
- // etc.) flow through to the original via the default `get` handler,
332
- // so extractDocumentTree's contract still works — but any code
333
- // path that CALLS the spied component bumps the counter.
334
- //
335
- // The connector-document tests already pin this with a
336
- // `FakeRocketDoc` fixture; this test pins it for a real
337
- // rocketstyle primitive end-to-end, closing the abstract /
338
- // concrete coverage gap.
339
- const { initTestConfig } = await import('@pyreon/test-utils')
340
- const { h } = await import('@pyreon/core')
341
- const cleanup = initTestConfig()
342
- try {
343
- const RealDocDocument = (await import('../primitives/DocDocument')).default
344
-
345
- let callCount = 0
346
- const SpiedDoc = new Proxy(RealDocDocument, {
347
- apply(target, thisArg, args) {
348
- callCount++
349
- return Reflect.apply(target as (...a: unknown[]) => unknown, thisArg, args as unknown[])
350
- },
351
- })
352
-
353
- const tree = extractDocNode(() =>
354
- h(SpiedDoc as never, { title: 'Hoisted', author: 'Aisha' } as never),
355
- )
356
-
357
- expect(tree.type).toBe('document')
358
- expect(tree.props.title).toBe('Hoisted')
359
- expect(tree.props.author).toBe('Aisha')
360
- // The architectural assertion — the styled wrapper must NOT run.
361
- expect(callCount).toBe(0)
362
- } finally {
363
- cleanup()
364
- }
365
- })
366
- })
package/src/index.ts DELETED
@@ -1,37 +0,0 @@
1
- // Re-export connector utilities
2
-
3
- export type {
4
- DocChild,
5
- DocNode,
6
- ExtractOptions,
7
- NodeType,
8
- ResolvedStyles,
9
- } from '@pyreon/connector-document'
10
- export { extractDocumentTree, resolveStyles } from '@pyreon/connector-document'
11
- // Preview
12
- export { default as DocumentPreview } from './DocumentPreview'
13
- // Primitives
14
- export { default as DocButton } from './primitives/DocButton'
15
- export { default as DocCode } from './primitives/DocCode'
16
- export { default as DocColumn } from './primitives/DocColumn'
17
- export { default as DocDivider } from './primitives/DocDivider'
18
- export { default as DocDocument } from './primitives/DocDocument'
19
- export { default as DocHeading } from './primitives/DocHeading'
20
- export { default as DocImage } from './primitives/DocImage'
21
- export { default as DocLink } from './primitives/DocLink'
22
- export { default as DocList } from './primitives/DocList'
23
- export { default as DocListItem } from './primitives/DocListItem'
24
- export { default as DocPage } from './primitives/DocPage'
25
- export { default as DocPageBreak } from './primitives/DocPageBreak'
26
- export { default as DocQuote } from './primitives/DocQuote'
27
- export { default as DocRow } from './primitives/DocRow'
28
- export { default as DocSection } from './primitives/DocSection'
29
- export { default as DocSpacer } from './primitives/DocSpacer'
30
- export { default as DocTable } from './primitives/DocTable'
31
- export { default as DocText } from './primitives/DocText'
32
- // Theme
33
- export type { DocumentTheme } from './theme'
34
- export { documentTheme } from './theme'
35
- // Export helper
36
- export type { DocumentExport, DocumentExportOptions } from './useDocumentExport'
37
- export { createDocumentExport, extractDocNode } from './useDocumentExport'