Skip to content

Commit 712a84f

Browse files
authored
Imaplement inject json data (#5)
* replace image of sample code in landing page * feat(*): support async events and inject json data in core * chore(*): bump versions to 0.0.7
1 parent c80ed9f commit 712a84f

36 files changed

+554
-240
lines changed

examples/zeroReport/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ <h1>End of Section</h1>
2323
<h4>Page <span data-pz-v-page-number></span> of <span data-pz-v-total-pages></span> Footer </h4>
2424
</div>
2525
<div data-pz-page-content>
26+
<p>Hello <span data-pz-v-json-data-key="info.name"></span>!</p>
2627
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the
2728
industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and
2829
scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap

examples/zeroReport/ssg.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ try {
2020

2121
const pdf = await reportToPdf(
2222
page,
23-
new URL(`file://${import.meta.dirname}/index.html`)
23+
new URL(`file://${import.meta.dirname}/index.html`),
24+
{
25+
info: {
26+
name: 'Mike',
27+
lastName: 'Ross',
28+
age: 24,
29+
},
30+
}
2431
);
2532

2633
await fs.writeFile('index.pdf', pdf);

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@paprize/core",
3-
"version": "0.0.6",
3+
"version": "0.0.7",
44
"description": "Paginate DOM elements for professional, print-ready reports",
55
"type": "module",
66
"files": [

packages/core/src/report/EventDispatcher.test.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,38 @@ describe('EventDispatcher', () => {
1212
dispatcher = new EventDispatcher<TestEvents>();
1313
});
1414

15-
it('should add and call event listeners', () => {
15+
it('should add and call event listeners', async () => {
1616
const handler = vi.fn();
1717
dispatcher.addEventListener('foo', handler);
18-
dispatcher.dispatch('foo', 1, 'x');
18+
19+
await dispatcher.dispatch('foo', 1, 'x');
20+
1921
expect(handler).toHaveBeenCalledWith(1, 'x');
2022
});
2123

22-
it('should remove event listeners', () => {
24+
it('should remove event listeners', async () => {
2325
const handler = vi.fn();
2426
const remove = dispatcher.addEventListener('foo', handler);
2527
remove();
26-
dispatcher.dispatch('foo', 2, 'y');
28+
29+
await dispatcher.dispatch('foo', 2, 'y');
30+
2731
expect(handler).not.toHaveBeenCalled();
2832
});
2933

30-
it('should support multiple listeners for the same event', () => {
34+
it('should support multiple listeners for the same event', async () => {
3135
const handler1 = vi.fn();
3236
const handler2 = vi.fn();
3337
dispatcher.addEventListener('foo', handler1);
3438
dispatcher.addEventListener('foo', handler2);
35-
dispatcher.dispatch('foo', 3, 'z');
39+
40+
await dispatcher.dispatch('foo', 3, 'z');
41+
3642
expect(handler1).toHaveBeenCalledWith(3, 'z');
3743
expect(handler2).toHaveBeenCalledWith(3, 'z');
3844
});
3945

40-
it('should do nothing if dispatching an event with no listeners', () => {
41-
expect(() => dispatcher.dispatch('bar')).not.toThrow();
46+
it('should do nothing if dispatching an event with no listeners', async () => {
47+
await expect(dispatcher.dispatch('bar')).resolves.toBe(undefined);
4248
});
4349
});

packages/core/src/report/EventDispatcher.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type EventHandler<TKey extends keyof TEvents, TEvents> = TEvents[TKey] extends (
77
export class EventDispatcher<TEvents> {
88
private registry = new Map<
99
keyof TEvents,
10-
Set<(...args: unknown[]) => void>
10+
Set<(...args: unknown[]) => unknown | Promise<unknown>>
1111
>();
1212

1313
public addEventListener<T extends keyof TEvents>(
@@ -34,7 +34,7 @@ export class EventDispatcher<TEvents> {
3434
registry.set(name, listeners);
3535
}
3636

37-
public dispatch<T extends keyof TEvents>(
37+
public async dispatch<T extends keyof TEvents>(
3838
name: T,
3939
...args: Parameters<EventHandler<T, TEvents>>
4040
) {
@@ -46,7 +46,7 @@ export class EventDispatcher<TEvents> {
4646
}
4747

4848
for (const listener of listeners) {
49-
listener(...args);
49+
await listener(...args);
5050
}
5151
}
5252
}

packages/core/src/report/ReportBuilder.test.ts

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Paginator } from '../paginate/Paginator';
44
import { paprize_isInitialized, paprize_isReady } from '../window';
55
import type { SectionComponents } from './sectionComponents';
66
import { globalStyleId } from '../constants';
7+
import { jsonDataReader } from './utils';
78

89
vi.mock('../paginate/Paginator', () => {
910
const paginate = vi.fn();
@@ -22,6 +23,7 @@ vi.mock('./utils', async () => {
2223
sectionFooterHeight: 5,
2324
}),
2425
createSectionPageHeightPlugin: vi.fn().mockReturnValue({}),
26+
jsonDataReader: vi.fn(),
2527
};
2628
});
2729

@@ -53,7 +55,7 @@ describe('ReportBuilder', () => {
5355

5456
it.for([true, false])(
5557
'tryAddSection should add a section and dispatch sectionCreated',
56-
(withsuspense) => {
58+
async (withsuspense) => {
5759
const sectionCreated = vi.fn();
5860
rb.monitor.addEventListener('sectionCreated', sectionCreated);
5961

@@ -62,7 +64,11 @@ describe('ReportBuilder', () => {
6264
suspense: withsuspense ? [Promise.resolve()] : [],
6365
};
6466

65-
const added = rb.tryAddSection(testOptions, components, () => {});
67+
const added = await rb.tryAddSection(
68+
testOptions,
69+
components,
70+
() => {}
71+
);
6672
expect(added).toBe(true);
6773

6874
expect(sectionCreated).toHaveBeenCalled();
@@ -74,9 +80,9 @@ describe('ReportBuilder', () => {
7480
}
7581
);
7682

77-
it('tryAddSection should return false if section id already exists', () => {
78-
rb.tryAddSection(options, components, () => {});
79-
const result = rb.tryAddSection(options, components, () => {});
83+
it('tryAddSection should return false if section id already exists', async () => {
84+
await rb.tryAddSection(options, components, () => {});
85+
const result = await rb.tryAddSection(options, components, () => {});
8086
expect(result).toBe(false);
8187
});
8288

@@ -115,7 +121,7 @@ describe('ReportBuilder', () => {
115121

116122
const onPaginationCompleted = vi.fn();
117123

118-
const added = rb.tryAddSection(
124+
const added = await rb.tryAddSection(
119125
options,
120126
testComponents,
121127
onPaginationCompleted
@@ -160,7 +166,7 @@ describe('ReportBuilder', () => {
160166
const section1 = 'se1';
161167
const section2 = 'se2';
162168

163-
rb.tryAddSection(
169+
await rb.tryAddSection(
164170
{
165171
...options,
166172
id: section1,
@@ -169,7 +175,7 @@ describe('ReportBuilder', () => {
169175
components,
170176
onPaginationCompleted
171177
);
172-
const added = rb.tryAddSection(
178+
const added = await rb.tryAddSection(
173179
{
174180
...options,
175181
id: section2,
@@ -209,7 +215,7 @@ describe('ReportBuilder', () => {
209215
const paginateMock = vi.mocked(Paginator.paginate);
210216
paginateMock.mockReturnValue(['<div>page1</div>']);
211217

212-
rb.tryAddSection(options, components, vi.fn());
218+
await rb.tryAddSection(options, components, vi.fn());
213219

214220
const section1Promise = rb.schedulePagination();
215221
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -228,7 +234,7 @@ describe('ReportBuilder', () => {
228234
const paginateMock = vi.mocked(Paginator.paginate);
229235
paginateMock.mockReturnValue(['<div>page1</div>']);
230236

231-
rb.tryAddSection(options, components, vi.fn());
237+
await rb.tryAddSection(options, components, vi.fn());
232238

233239
const section1Promise = rb.schedulePagination();
234240
const section2Promise = rb.schedulePagination();
@@ -246,7 +252,7 @@ describe('ReportBuilder', () => {
246252
const sectionId = 'test-section';
247253
const onPaginationCompleted = vi.fn();
248254

249-
rb.tryAddSection(
255+
await rb.tryAddSection(
250256
{
251257
id: sectionId,
252258
size: { width: '100px', height: '200px' },
@@ -260,6 +266,39 @@ describe('ReportBuilder', () => {
260266

261267
expect(result.sections.length).toBe(0);
262268
});
269+
270+
it('getJsonData should call jsonDataReader once', async () => {
271+
const data = {
272+
a: 'a',
273+
};
274+
vi.mocked(jsonDataReader).mockResolvedValue(data);
275+
276+
const result1 = await rb.getJsonData({ a: 'b' });
277+
const result2 = await rb.getJsonData({ a: 'b' });
278+
279+
expect(result1).toMatchObject(data);
280+
expect(result2).toMatchObject(data);
281+
expect(vi.mocked(jsonDataReader)).toHaveBeenCalledOnce();
282+
});
283+
284+
it('getJsonData should return default value when jsonDataReader is not available', async () => {
285+
const defaultData = {
286+
a: 'a',
287+
};
288+
vi.mocked(jsonDataReader).mockResolvedValue(undefined);
289+
290+
const result = await rb.getJsonData(defaultData);
291+
292+
expect(result).toMatchObject(defaultData);
293+
});
294+
295+
it('getJsonData should return null without default value and jsonDataReader', async () => {
296+
vi.mocked(jsonDataReader).mockResolvedValue(null);
297+
298+
const result = await rb.getJsonData();
299+
300+
expect(result).toBeNull();
301+
});
263302
});
264303

265304
describe('ReportBuilder constructor', () => {

packages/core/src/report/ReportBuilder.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import type {
1212
} from './ReportBuilderEvents';
1313
import { reportStyles } from './reportStyles';
1414
import { cloneComponents, type SectionComponents } from './sectionComponents';
15-
import { calculatePageSizes, createSectionPageHeightPlugin } from './utils';
15+
import {
16+
calculatePageSizes,
17+
createSectionPageHeightPlugin,
18+
jsonDataReader,
19+
lazyPromise,
20+
} from './utils';
1621
import { paprize_isInitialized, paprize_isReady } from '../window';
1722
import { globalStyleId } from '../constants';
1823
import { PromiseTracker } from './PromiseTracker';
@@ -123,11 +128,11 @@ export class ReportBuilder {
123128
* @param onPaginationCompleted - Callback invoked when pagination for the section is completed.
124129
* @returns `true` if the section was added to the report’s section list, or `false` if it already exists.
125130
*/
126-
public tryAddSection(
131+
public async tryAddSection(
127132
options: SectionOptions,
128133
components: SectionComponents,
129134
onPaginationCompleted: (pages: PageContext[]) => void
130-
): boolean {
135+
): Promise<boolean> {
131136
if (this._sections.has(options.id)) {
132137
return false;
133138
}
@@ -155,7 +160,8 @@ export class ReportBuilder {
155160
this._injectStyle(
156161
reportStyles.sectionPageMedia(options.id, options.size)
157162
);
158-
this._monitor.dispatch('sectionCreated', context);
163+
164+
await this._monitor.dispatch('sectionCreated', context);
159165

160166
return true;
161167
}
@@ -204,6 +210,27 @@ export class ReportBuilder {
204210
return this._executePagination();
205211
}
206212

213+
/**
214+
* Retrieves JSON data injected by **@paprize/puppeteer** during server-side rendering (SSR).
215+
*
216+
* If no injected data is available, the function returns the provided `defaultData`, or `null` if none is given.
217+
*
218+
* ⚠️ **Important Notes:**
219+
* - This function is **not type-safe** — it performs **no runtime type validation** on the returned data.
220+
* - It is available **only during server-side rendering** when using **@paprize/puppeteer**.
221+
* - When used in **client-side rendering** or **development** mode, you should provide a `defaultData` value for testing purposes.
222+
*
223+
* @template T - The expected type of the injected JSON data.
224+
* @param defaultData - Optional fallback value to return if no injected data is found.
225+
* @returns A promise resolving to the injected JSON data if available, otherwise the provided default value or `null`.
226+
*/
227+
public async getJsonData<T>(defaultData?: T): Promise<T | null> {
228+
const json = await this._lazyJsonDataReader().catch(() => defaultData);
229+
return (json as T) ?? defaultData ?? null;
230+
}
231+
232+
private _lazyJsonDataReader = lazyPromise(jsonDataReader);
233+
207234
private async _executePagination(): Promise<ScheduleResult> {
208235
this._paginationInProgress = true;
209236
this._currentAbortController = new AbortController();
@@ -249,9 +276,9 @@ export class ReportBuilder {
249276
}
250277

251278
const reportTracker = new PromiseTracker();
252-
reportTracker.monitor.addEventListener('onChange', () => {
279+
reportTracker.monitor.addEventListener('onChange', async () => {
253280
logger.debug(logPrefix, 'Report pagination completed.');
254-
this._monitor.dispatch('paginationCycleCompleted', {
281+
await this._monitor.dispatch('paginationCycleCompleted', {
255282
sections: [...this._sections.values()].map(
256283
(s) => s.context
257284
),
@@ -395,7 +422,7 @@ export class ReportBuilder {
395422
state.onPaginationCompleted(pageContexts);
396423

397424
for (const pageContext of pageContexts) {
398-
this._monitor.dispatch('pageCompleted', pageContext);
425+
await this._monitor.dispatch('pageCompleted', pageContext);
399426
}
400427

401428
const sectionContext: SectionContext = {
@@ -409,6 +436,6 @@ export class ReportBuilder {
409436
context: sectionContext,
410437
});
411438

412-
this._monitor.dispatch('sectionCompleted', sectionContext);
439+
await this._monitor.dispatch('sectionCompleted', sectionContext);
413440
}
414441
}

0 commit comments

Comments
 (0)