@spinajs/templates-puppeteer 2.0.481 → 2.0.482

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/README.md CHANGED
@@ -7,3 +7,38 @@ Base Puppeteer renderer for SpineJS templates. This package provides shared logi
7
7
  ``bash
8
8
  npm install @spinajs/templates-puppeteer
9
9
  ``
10
+
11
+ ## Render progress
12
+
13
+ PDF/image renders can take a long time when the HTML pulls in many external
14
+ resources (images, fonts, stylesheets). Pass an `onProgress` callback to observe
15
+ what the render is doing - especially while resources are loading.
16
+
17
+ Progress is reported through discrete phases (`starting` → `preparing` →
18
+ `loading` → `rendering` → `done`, or `failed`) with a best-effort `percent`,
19
+ live resource counters, and elapsed time. The callback is fire-and-forget: it is
20
+ never awaited and its errors are swallowed, so it can neither slow nor break a
21
+ render. Emissions are throttled, with a heartbeat during the long phases so the
22
+ value keeps moving even when a single resource is slow.
23
+
24
+ ``ts
25
+ await pdfRenderer.renderToFile('invoice.pdf', model, 'out/invoice.pdf', 'en', {
26
+ onProgress: (p) => {
27
+ // p: { phase, percent, resourcesLoaded, resourcesPending, resourcesFailed, elapsedMs, filePath, message }
28
+ console.log(`${p.phase} ${p.percent}% - ${p.message}`);
29
+ },
30
+ });
31
+ ``
32
+
33
+ The same option works on `renderHtmlToFile(html, filePath, { onProgress })` and
34
+ flows through the `Templates` facade (`render` / `renderToFile` / `compile` /
35
+ `compileToFile`). The CLI commands `render-pdf` / `render-image` already render a
36
+ live progress line via the shared `cliProgressReporter()`.
37
+
38
+ Reporting progress from a queue job is a one-liner - forward the percentage:
39
+
40
+ ``ts
41
+ await templates.renderToFile('report.pdf', model, out, lang, {
42
+ onProgress: (p) => void jobProgress(p.percent),
43
+ });
44
+ ``
@@ -0,0 +1,32 @@
1
+ import * as http from 'http';
2
+ import { Log } from '@spinajs/log';
3
+ /**
4
+ * A short-lived local static HTTP server used so headless Chromium can load
5
+ * relative asset URLs (images, css, fonts) - it refuses local files over the
6
+ * file:// protocol. Bound to loopback so the served directory is never
7
+ * network-exposed. One server is started per render and closed afterwards.
8
+ */
9
+ export declare class LocalAssetServer {
10
+ private readonly log;
11
+ private readonly portRange;
12
+ /**
13
+ * @param log - logger for lifecycle traces
14
+ * @param portRange - supplies the optional [min, max] port range; when absent
15
+ * an OS-assigned port (0) is used, which cannot collide.
16
+ */
17
+ constructor(log: Log, portRange: () => number[] | undefined);
18
+ /**
19
+ * Start a server for `basePath`. With an OS-assigned port a single attempt
20
+ * suffices; with a configured range a random pick can collide, so retry on
21
+ * EADDRINUSE with a different port.
22
+ */
23
+ serve(basePath: string): Promise<http.Server>;
24
+ /** Close a server, forcing connections closed if a graceful close times out. */
25
+ close(server: http.Server): Promise<void>;
26
+ /**
27
+ * Single attempt to start the static server on the given port (0 = OS-assigned).
28
+ * Bound to loopback so the served directory is not network-exposed.
29
+ */
30
+ private listenOnce;
31
+ }
32
+ //# sourceMappingURL=asset-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-server.d.ts","sourceRoot":"","sources":["../../src/asset-server.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAI7B,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AASnC;;;;;GAKG;AACH,qBAAa,gBAAgB;IAMf,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAO,OAAO,CAAC,QAAQ,CAAC,SAAS;IALjE;;;;OAIG;gBAC0B,GAAG,EAAE,GAAG,EAAmB,SAAS,EAAE,MAAM,MAAM,EAAE,GAAG,SAAS;IAE7F;;;;OAIG;IACU,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;IA2B1D,gFAAgF;IACnE,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BtD;;;OAGG;IACH,OAAO,CAAC,UAAU;CAgCnB"}
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LocalAssetServer = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const cors_1 = __importDefault(require("cors"));
9
+ const lodash_1 = __importDefault(require("lodash"));
10
+ /** How long to wait for the local static server to start before giving up (ms). */
11
+ const SERVER_STARTUP_TIMEOUT_MS = 10000;
12
+ /** How long to wait for the local static server to close before forcing it (ms). */
13
+ const SERVER_CLOSE_TIMEOUT_MS = 5000;
14
+ /** Attempts to find a free port when a range is configured (a random pick can collide). */
15
+ const MAX_PORT_ATTEMPTS = 10;
16
+ /**
17
+ * A short-lived local static HTTP server used so headless Chromium can load
18
+ * relative asset URLs (images, css, fonts) - it refuses local files over the
19
+ * file:// protocol. Bound to loopback so the served directory is never
20
+ * network-exposed. One server is started per render and closed afterwards.
21
+ */
22
+ class LocalAssetServer {
23
+ /**
24
+ * @param log - logger for lifecycle traces
25
+ * @param portRange - supplies the optional [min, max] port range; when absent
26
+ * an OS-assigned port (0) is used, which cannot collide.
27
+ */
28
+ constructor(log, portRange) {
29
+ this.log = log;
30
+ this.portRange = portRange;
31
+ }
32
+ /**
33
+ * Start a server for `basePath`. With an OS-assigned port a single attempt
34
+ * suffices; with a configured range a random pick can collide, so retry on
35
+ * EADDRINUSE with a different port.
36
+ */
37
+ async serve(basePath) {
38
+ const range = this.portRange();
39
+ const hasRange = !!range && range.length > 0;
40
+ const maxAttempts = hasRange ? MAX_PORT_ATTEMPTS : 1;
41
+ let lastErr;
42
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
43
+ const port = hasRange ? lodash_1.default.random(range[0], range[1]) : 0;
44
+ try {
45
+ return await this.listenOnce(basePath, port);
46
+ }
47
+ catch (err) {
48
+ lastErr = err;
49
+ if (err?.code === 'EADDRINUSE' && hasRange) {
50
+ this.log.trace(`Puppeteer asset server port ${port} in use, retrying (${attempt + 1}/${maxAttempts})`);
51
+ continue;
52
+ }
53
+ throw err;
54
+ }
55
+ }
56
+ this.log.error(lastErr, `Puppeteer asset server cannot start - no free port in range after ${maxAttempts} attempts`);
57
+ throw lastErr;
58
+ }
59
+ /** Close a server, forcing connections closed if a graceful close times out. */
60
+ async close(server) {
61
+ try {
62
+ await new Promise((resolve, reject) => {
63
+ const timeout = setTimeout(() => {
64
+ reject(new Error('Server close timeout'));
65
+ }, SERVER_CLOSE_TIMEOUT_MS);
66
+ server.close((err) => {
67
+ clearTimeout(timeout);
68
+ if (err)
69
+ reject(err);
70
+ else
71
+ resolve();
72
+ });
73
+ });
74
+ }
75
+ catch (err) {
76
+ this.log.warn(`Error closing server: ${err.message}`);
77
+ // Force close connections if available
78
+ try {
79
+ if ('closeAllConnections' in server) {
80
+ server.closeAllConnections();
81
+ }
82
+ }
83
+ catch (forceErr) {
84
+ this.log.error(`Error force closing server connections: ${forceErr.message}`);
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Single attempt to start the static server on the given port (0 = OS-assigned).
90
+ * Bound to loopback so the served directory is not network-exposed.
91
+ */
92
+ listenOnce(basePath, port) {
93
+ const log = this.log;
94
+ const app = (0, express_1.default)();
95
+ app.use((0, cors_1.default)());
96
+ app.use(express_1.default.static(basePath));
97
+ return new Promise((resolve, reject) => {
98
+ const server = app
99
+ .listen(port, '127.0.0.1')
100
+ .on('listening', function () {
101
+ log.trace(`Puppeteer asset server started on port ${this.address().port}`);
102
+ log.trace(`Puppeteer static file dir at ${basePath}`);
103
+ resolve(this);
104
+ })
105
+ .on('error', (err) => {
106
+ // Clean up the failed server, then reject so the caller can retry.
107
+ if (server) {
108
+ server.close(() => reject(err));
109
+ }
110
+ else {
111
+ reject(err);
112
+ }
113
+ });
114
+ // Set a timeout for server startup
115
+ setTimeout(() => {
116
+ if (!server.listening) {
117
+ server.close();
118
+ reject(new Error('Server startup timeout'));
119
+ }
120
+ }, SERVER_STARTUP_TIMEOUT_MS);
121
+ });
122
+ }
123
+ }
124
+ exports.LocalAssetServer = LocalAssetServer;
125
+ //# sourceMappingURL=asset-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asset-server.js","sourceRoot":"","sources":["../../src/asset-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { Browser, LaunchOptions } from 'puppeteer';
2
+ import { Log } from '@spinajs/log';
3
+ /**
4
+ * Lazily launches and pools a single Chromium browser, reused across renders and
5
+ * relaunched if it crashes/disconnects. Concurrent first-callers share one launch.
6
+ * The browser is closed only on {@link dispose}.
7
+ */
8
+ export declare class BrowserPool {
9
+ private readonly log;
10
+ private readonly launchOptions;
11
+ private pooled;
12
+ private launchPromise;
13
+ /**
14
+ * @param log - logger for lifecycle warnings
15
+ * @param launchOptions - supplies puppeteer launch options at acquire time
16
+ * (resolved lazily so DI-injected config is available).
17
+ */
18
+ constructor(log: Log, launchOptions: () => LaunchOptions);
19
+ /** The currently pooled browser, or null when none is live. */
20
+ get browser(): Browser | null;
21
+ /**
22
+ * Return the pooled browser, launching it on first use and relaunching if it
23
+ * has crashed/disconnected. Concurrent callers share a single launch.
24
+ */
25
+ acquire(): Promise<Browser>;
26
+ /** Close and drop the pooled browser. Best-effort: force-kills if a graceful close fails. */
27
+ dispose(): Promise<void>;
28
+ private safeClose;
29
+ private forceClose;
30
+ }
31
+ //# sourceMappingURL=browser-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-pool.d.ts","sourceRoot":"","sources":["../../src/browser-pool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAwB,aAAa,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEnC;;;;GAIG;AACH,qBAAa,WAAW;IASV,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAO,OAAO,CAAC,QAAQ,CAAC,aAAa;IARrE,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,aAAa,CAAiC;IAEtD;;;;OAIG;gBAC0B,GAAG,EAAE,GAAG,EAAmB,aAAa,EAAE,MAAM,aAAa;IAE1F,+DAA+D;IAC/D,IAAW,OAAO,IAAI,OAAO,GAAG,IAAI,CAEnC;IAED;;;OAGG;IACU,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAmBxC,6FAA6F;IAChF,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAUvB,SAAS;IAiBvB,OAAO,CAAC,UAAU;CAOnB"}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BrowserPool = void 0;
7
+ const puppeteer_1 = __importDefault(require("puppeteer"));
8
+ /**
9
+ * Lazily launches and pools a single Chromium browser, reused across renders and
10
+ * relaunched if it crashes/disconnects. Concurrent first-callers share one launch.
11
+ * The browser is closed only on {@link dispose}.
12
+ */
13
+ class BrowserPool {
14
+ /**
15
+ * @param log - logger for lifecycle warnings
16
+ * @param launchOptions - supplies puppeteer launch options at acquire time
17
+ * (resolved lazily so DI-injected config is available).
18
+ */
19
+ constructor(log, launchOptions) {
20
+ this.log = log;
21
+ this.launchOptions = launchOptions;
22
+ this.pooled = null;
23
+ this.launchPromise = null;
24
+ }
25
+ /** The currently pooled browser, or null when none is live. */
26
+ get browser() {
27
+ return this.pooled;
28
+ }
29
+ /**
30
+ * Return the pooled browser, launching it on first use and relaunching if it
31
+ * has crashed/disconnected. Concurrent callers share a single launch.
32
+ */
33
+ async acquire() {
34
+ if (this.pooled?.connected) {
35
+ return this.pooled;
36
+ }
37
+ // drop a disconnected/crashed browser so we relaunch a fresh one
38
+ this.pooled = null;
39
+ if (!this.launchPromise) {
40
+ this.launchPromise = puppeteer_1.default
41
+ .launch(this.launchOptions())
42
+ .then((b) => (this.pooled = b))
43
+ // reset so a failed launch is retried on the next call
44
+ .finally(() => (this.launchPromise = null));
45
+ }
46
+ return this.launchPromise;
47
+ }
48
+ /** Close and drop the pooled browser. Best-effort: force-kills if a graceful close fails. */
49
+ async dispose() {
50
+ if (!this.pooled) {
51
+ return;
52
+ }
53
+ const browser = this.pooled;
54
+ this.pooled = null;
55
+ await this.safeClose(browser);
56
+ }
57
+ async safeClose(browser) {
58
+ try {
59
+ // close all pages first, then the browser itself
60
+ const pages = await browser.pages();
61
+ await Promise.allSettled(pages.map((page) => page.close()));
62
+ await browser.close();
63
+ }
64
+ catch (err) {
65
+ this.log.warn(`Error during normal browser cleanup: ${err.message}`);
66
+ try {
67
+ this.forceClose(browser);
68
+ }
69
+ catch (killErr) {
70
+ this.log.error(`Failed to force kill browser: ${killErr.message}`);
71
+ }
72
+ }
73
+ }
74
+ forceClose(browser) {
75
+ const proc = browser.process();
76
+ if (proc) {
77
+ proc.kill('SIGKILL');
78
+ this.log.warn('Browser process force killed');
79
+ }
80
+ }
81
+ }
82
+ exports.BrowserPool = BrowserPool;
83
+ //# sourceMappingURL=browser-pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-pool.js","sourceRoot":"","sources":["../../src/browser-pool.ts"],"names":[],"mappings":";;;;;;AAAA,0DAAyE;AAGzE;;;;GAIG;AACH,MAAa,WAAW;IAItB;;;;OAIG;IACH,YAA6B,GAAQ,EAAmB,aAAkC;QAA7D,QAAG,GAAH,GAAG,CAAK;QAAmB,kBAAa,GAAb,aAAa,CAAqB;QARlF,WAAM,GAAmB,IAAI,CAAC;QAC9B,kBAAa,GAA4B,IAAI,CAAC;IAOuC,CAAC;IAE9F,+DAA+D;IAC/D,IAAW,OAAO;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;OAGG;IACI,KAAK,CAAC,OAAO;QAClB,IAAI,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,MAAM,CAAC;QACrB,CAAC;QAED,iEAAiE;QACjE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QAEnB,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACxB,IAAI,CAAC,aAAa,GAAG,mBAAS;iBAC3B,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;iBAC5B,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAC/B,uDAAuD;iBACtD,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC;QAChD,CAAC;QAED,OAAO,IAAI,CAAC,aAAa,CAAC;IAC5B,CAAC;IAED,6FAA6F;IACtF,KAAK,CAAC,OAAO;QAClB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,OAAgB;QACtC,IAAI,CAAC;YACH,iDAAiD;YACjD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;YACpC,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;YAC5D,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,wCAAwC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAErE,IAAI,CAAC;gBACH,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAC3B,CAAC;YAAC,OAAO,OAAO,EAAE,CAAC;gBACjB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,iCAAiC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;IACH,CAAC;IAEO,UAAU,CAAC,OAAgB;QACjC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;QAC/B,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACrB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QAChD,CAAC;IACH,CAAC;CACF;AA1ED,kCA0EC"}
@@ -1,8 +1,13 @@
1
- import { Browser, Page, LaunchOptions } from 'puppeteer';
2
- import { TemplateRenderer, Templates } from '@spinajs/templates';
1
+ import { Page, LaunchOptions } from 'puppeteer';
2
+ import { TemplateRenderer, Templates, IRenderOptions, RenderProgressCallback } from '@spinajs/templates';
3
+ import { IInstanceCheck } from '@spinajs/di';
3
4
  import { Log } from '@spinajs/log';
4
- import * as http from 'http';
5
5
  import '@spinajs/templates-pug';
6
+ import { LocalAssetServer } from './asset-server.js';
7
+ import { BrowserPool } from './browser-pool.js';
8
+ export * from './progress.js';
9
+ export * from './asset-server.js';
10
+ export * from './browser-pool.js';
6
11
  export interface IPuppeteerRendererOptions {
7
12
  static: {
8
13
  portRange: number[];
@@ -22,39 +27,99 @@ export interface IPuppeteerRendererOptions {
22
27
  */
23
28
  debug?: {
24
29
  /**
25
- * If true, browser will remain open after rendering for inspection
26
- * Use it with headless: false in args to see the browser window ( puppetter.launch args )
30
+ * Controls whether the browser is closed after rendering.
31
+ * Defaults to closing. Set to `false` to keep the browser open after rendering
32
+ * for inspection ( use with headless: false in args to see the browser window ).
27
33
  */
28
34
  close?: boolean;
29
35
  };
30
36
  }
31
- export declare abstract class PuppeteerRenderer extends TemplateRenderer {
37
+ export declare abstract class PuppeteerRenderer extends TemplateRenderer implements IInstanceCheck {
32
38
  protected abstract Options: IPuppeteerRendererOptions;
39
+ /**
40
+ * Per-instance options that give this renderer its identity for the DI
41
+ * @PerInstanceCheck pooling (see subclasses). Renderers that are not pooled
42
+ * per-instance inherit the `undefined` default.
43
+ */
44
+ protected get instanceOptions(): unknown;
45
+ /**
46
+ * DI @PerInstanceCheck hook: an existing instance is reused only when it was
47
+ * created with the same options.
48
+ */
49
+ __checkInstance__(creationOptions: any): boolean;
33
50
  protected Log: Log;
34
51
  protected TemplatingService: Templates;
35
- renderToFile(template: string, model: any, filePath: string, language?: string): Promise<void>;
52
+ private _assetServer?;
53
+ private _browserPool?;
54
+ /** Local static server used so Chromium can load relative asset URLs. Lazily created. */
55
+ protected get assetServer(): LocalAssetServer;
36
56
  /**
37
- * Abstract method to perform specific rendering (PDF or image)
57
+ * Pooled browser, launched lazily and reused across renders, closed on dispose().
58
+ * One pool per options-set (see @PerInstanceCheck on subclasses). Lazily created.
38
59
  */
39
- protected abstract performRender(page: Page, filePath: string): Promise<void>;
40
- render(_templateName: string, _model: unknown, _language?: string): Promise<string>;
41
- protected compile(_path: string): Promise<void>;
42
- protected runLocalServer(basePath: string): Promise<http.Server>;
60
+ protected get browserPool(): BrowserPool;
61
+ private buildLaunchOptions;
62
+ renderToFile(template: string, model: any, filePath: string, language?: string, options?: IRenderOptions): Promise<void>;
63
+ /**
64
+ * Render a raw HTML string to a file (image/pdf per the concrete renderer).
65
+ * Intended for CI (e.g. screenshot comparison) where HTML is already produced.
66
+ *
67
+ * @param html - the HTML content to render
68
+ * @param filePath - output file path
69
+ * @param options.assetBasePath - directory served over the local http server so
70
+ * relative asset URLs in the HTML resolve (defaults to the output file's dir)
71
+ * @param options.viewport - fixed viewport for deterministic captures
72
+ */
73
+ renderHtmlToFile(html: string, filePath: string, options?: {
74
+ assetBasePath?: string;
75
+ viewport?: {
76
+ width: number;
77
+ height: number;
78
+ deviceScaleFactor?: number;
79
+ };
80
+ onProgress?: RenderProgressCallback;
81
+ }): Promise<void>;
82
+ /**
83
+ * Shared render orchestration: local static server + pooled browser + page +
84
+ * timeout watchdog + cleanup. `prepare` populates the page content (template-compiled
85
+ * HTML, or raw HTML). The pooled browser survives; only the page is closed.
86
+ */
87
+ protected renderContentToFile(filePath: string, opts: {
88
+ assetBasePath?: string;
89
+ onProgress?: RenderProgressCallback;
90
+ }, prepare: (page: Page, httpPort: number, markLoading: () => void) => Promise<void>): Promise<void>;
91
+ /** Effective render timeout: configured value, else the default. */
92
+ protected get renderTimeoutMs(): number;
93
+ /** Apply the configured (or default) navigation and render timeouts to a page. */
94
+ protected configurePageTimeouts(page: Page): void;
95
+ /**
96
+ * Arm a watchdog that closes the page if a render exceeds the configured
97
+ * timeout - only the page is closed, the pooled browser may be serving other
98
+ * concurrent renders. Returns a function that cancels the watchdog.
99
+ */
100
+ protected armRenderWatchdog(page: Page): () => void;
101
+ /** Warn if a render took longer than the configured warning threshold. */
102
+ protected warnIfSlow(filePath: string, duration: number): void;
103
+ /** Close a page, downgrading any close error to a warning (best-effort cleanup). */
104
+ protected closePage(page: Page): Promise<void>;
43
105
  /**
44
- * Enhanced browser cleanup with error handling
106
+ * Insert a <base href> into the HTML <head> so relative asset URLs resolve
107
+ * against the local static server. No-op if the HTML already declares a <base>.
45
108
  */
46
- protected safeBrowserCleanup(browser: Browser): Promise<void>;
109
+ protected injectBaseHref(html: string, baseUrl: string): string;
47
110
  /**
48
- * Force close browser with process termination
111
+ * Close the pooled browser when the service is disposed (e.g. DI.dispose() at shutdown).
112
+ * Callers that render one-shot (CLI, single email) should dispose so the process can exit.
49
113
  */
50
- protected forceCloseBrowser(browser: Browser): Promise<void>;
114
+ dispose(): Promise<void>;
51
115
  /**
52
- * Enhanced server cleanup with timeout
116
+ * Abstract method to perform specific rendering (PDF or image)
53
117
  */
54
- protected safeServerCleanup(server: http.Server): Promise<void>;
118
+ protected abstract performRender(page: Page, filePath: string): Promise<void>;
119
+ render(_templateName: string, _model: unknown, _language?: string): Promise<string>;
55
120
  /**
56
121
  * Add page event listeners with cleanup function
57
122
  */
58
- protected addPageEventListeners(page: any): () => void;
123
+ protected addPageEventListeners(page: Page): () => void;
59
124
  }
60
125
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,IAAI,EAAwB,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGjE,OAAO,EAAE,GAAG,EAAU,MAAM,cAAc,CAAC;AAE3C,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,wBAAwB,CAAC;AAIhC,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE;QACN,SAAS,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;IACF,IAAI,EAAE,aAAa,CAAC;IAEpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,KAAK,CAAC,EAAE;QAEN;;;WAGG;QACH,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAA;CACF;AAED,8BAAsB,iBAAkB,SAAQ,gBAAgB;IAC9D,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAC;IAGtD,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IAGnB,SAAS,CAAC,iBAAiB,EAAE,SAAS,CAAC;IAE1B,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgH3G;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEhE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;cAKhF,OAAO,CAAC,KAAK,EAAE,MAAM;cAErB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;IA2CtE;;OAEG;cACa,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBnE;;OAEG;cACa,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAYlE;;OAEG;cACa,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BrE;;OAEG;IACH,SAAS,CAAC,qBAAqB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,IAAI;CA0BvD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"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"}