chartjs2img 0.3.0
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 +607 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +60 -0
- package/dist/cache.js.map +1 -0
- package/dist/chromium.d.ts +10 -0
- package/dist/chromium.d.ts.map +1 -0
- package/dist/chromium.js +226 -0
- package/dist/chromium.js.map +1 -0
- package/dist/lib.d.ts +40 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +48 -0
- package/dist/lib.js.map +1 -0
- package/dist/renderer.d.ts +76 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +283 -0
- package/dist/renderer.js.map +1 -0
- package/dist/semaphore.d.ts +12 -0
- package/dist/semaphore.d.ts.map +1 -0
- package/dist/semaphore.js +42 -0
- package/dist/semaphore.js.map +1 -0
- package/dist/template.d.ts +85 -0
- package/dist/template.d.ts.map +1 -0
- package/dist/template.js +120 -0
- package/dist/template.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/package.json +75 -0
- package/src/index.ts +312 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find a Chromium/Chrome executable already installed on this machine.
|
|
3
|
+
* Checks: CHROMIUM_PATH env → Playwright cache → system Chrome → Chrome
|
|
4
|
+
* for Testing cache.
|
|
5
|
+
*/
|
|
6
|
+
export declare function findChromiumExecutable(): string | null;
|
|
7
|
+
/** Download Chrome for Testing directly via fetch — no sudo, no npm/npx needed */
|
|
8
|
+
export declare function downloadChromeForTesting(): Promise<string>;
|
|
9
|
+
export declare function ensureChromiumInstalled(): Promise<string>;
|
|
10
|
+
//# sourceMappingURL=chromium.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chromium.d.ts","sourceRoot":"","sources":["../src/chromium.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,GAAG,IAAI,CAgFtD;AAaD,kFAAkF;AAClF,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,CAAC,CAqFhE;AASD,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC,CAyB/D"}
|
package/dist/chromium.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Chromium procurement.
|
|
2
|
+
//
|
|
3
|
+
// Three functions, each with a tight responsibility:
|
|
4
|
+
// findChromiumExecutable — scan known install locations and return
|
|
5
|
+
// a path if Chrome/Chromium is already present. Pure wrt. the
|
|
6
|
+
// filesystem: it doesn't touch the network or create files.
|
|
7
|
+
// downloadChromeForTesting — install Chrome for Testing into the
|
|
8
|
+
// user-local Playwright cache. Side effects: fetch + fs write.
|
|
9
|
+
// ensureChromiumInstalled — orchestrator. Returns a cached path if
|
|
10
|
+
// called repeatedly within the same process.
|
|
11
|
+
//
|
|
12
|
+
// Split out from renderer.ts so renderer owns browser/page lifecycle,
|
|
13
|
+
// not platform-specific binary discovery.
|
|
14
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { homedir, platform, arch } from 'os';
|
|
17
|
+
/**
|
|
18
|
+
* Find a Chromium/Chrome executable already installed on this machine.
|
|
19
|
+
* Checks: CHROMIUM_PATH env → Playwright cache → system Chrome → Chrome
|
|
20
|
+
* for Testing cache.
|
|
21
|
+
*/
|
|
22
|
+
export function findChromiumExecutable() {
|
|
23
|
+
const os = platform();
|
|
24
|
+
const home = homedir();
|
|
25
|
+
// 1. Environment variable override
|
|
26
|
+
if (process.env.CHROMIUM_PATH && existsSync(process.env.CHROMIUM_PATH)) {
|
|
27
|
+
return process.env.CHROMIUM_PATH;
|
|
28
|
+
}
|
|
29
|
+
// 2. Scan Playwright's browser cache directories (from prior installs)
|
|
30
|
+
const cacheDirs = [];
|
|
31
|
+
if (process.env.PLAYWRIGHT_BROWSERS_PATH) {
|
|
32
|
+
cacheDirs.push(process.env.PLAYWRIGHT_BROWSERS_PATH);
|
|
33
|
+
}
|
|
34
|
+
if (os === 'darwin') {
|
|
35
|
+
cacheDirs.push(join(home, 'Library', 'Caches', 'ms-playwright'));
|
|
36
|
+
}
|
|
37
|
+
else if (os === 'linux') {
|
|
38
|
+
cacheDirs.push(join(home, '.cache', 'ms-playwright'));
|
|
39
|
+
}
|
|
40
|
+
else if (os === 'win32') {
|
|
41
|
+
cacheDirs.push(join(home, 'AppData', 'Local', 'ms-playwright'));
|
|
42
|
+
}
|
|
43
|
+
const playwrightExecPaths = {
|
|
44
|
+
darwin: [
|
|
45
|
+
'chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
46
|
+
'chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
|
|
47
|
+
'chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium',
|
|
48
|
+
'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
|
|
49
|
+
],
|
|
50
|
+
linux: ['chrome-linux/chrome', 'chrome-linux64/chrome', 'chrome-linux/chromium'],
|
|
51
|
+
win32: ['chrome-win64/chrome.exe', 'chrome-win/chrome.exe'],
|
|
52
|
+
};
|
|
53
|
+
const candidates = playwrightExecPaths[os] ?? playwrightExecPaths['linux'];
|
|
54
|
+
for (const cacheDir of cacheDirs) {
|
|
55
|
+
if (!existsSync(cacheDir))
|
|
56
|
+
continue;
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = readdirSync(cacheDir)
|
|
60
|
+
.filter((e) => e.startsWith('chromium'))
|
|
61
|
+
.sort()
|
|
62
|
+
.reverse();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
for (const relPath of candidates) {
|
|
69
|
+
const fullPath = join(cacheDir, entry, relPath);
|
|
70
|
+
if (existsSync(fullPath))
|
|
71
|
+
return fullPath;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// 3. System-installed Chrome/Chromium
|
|
76
|
+
const systemPaths = {
|
|
77
|
+
darwin: [
|
|
78
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
79
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
80
|
+
join(home, 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
|
|
81
|
+
],
|
|
82
|
+
linux: [
|
|
83
|
+
'/usr/bin/google-chrome-stable',
|
|
84
|
+
'/usr/bin/google-chrome',
|
|
85
|
+
'/usr/bin/chromium-browser',
|
|
86
|
+
'/usr/bin/chromium',
|
|
87
|
+
'/snap/bin/chromium',
|
|
88
|
+
],
|
|
89
|
+
win32: [
|
|
90
|
+
join(process.env.PROGRAMFILES ?? 'C:\\Program Files', 'Google\\Chrome\\Application\\chrome.exe'),
|
|
91
|
+
join(process.env['PROGRAMFILES(X86)'] ?? 'C:\\Program Files (x86)', 'Google\\Chrome\\Application\\chrome.exe'),
|
|
92
|
+
join(home, 'AppData\\Local\\Google\\Chrome\\Application\\chrome.exe'),
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
for (const p of systemPaths[os] ?? []) {
|
|
96
|
+
if (existsSync(p))
|
|
97
|
+
return p;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** Chrome-for-Testing platform string for the current OS/arch, or null if unsupported */
|
|
102
|
+
function getCftPlatform() {
|
|
103
|
+
const os = platform();
|
|
104
|
+
const a = arch();
|
|
105
|
+
if (os === 'darwin')
|
|
106
|
+
return a === 'arm64' ? 'mac-arm64' : 'mac-x64';
|
|
107
|
+
if (os === 'win32')
|
|
108
|
+
return a === 'x64' ? 'win64' : 'win32';
|
|
109
|
+
// Chrome for Testing only provides linux64 (x86_64) — no linux-arm64
|
|
110
|
+
if (a === 'arm64' || a === 'aarch64')
|
|
111
|
+
return null;
|
|
112
|
+
return 'linux64';
|
|
113
|
+
}
|
|
114
|
+
/** Download Chrome for Testing directly via fetch — no sudo, no npm/npx needed */
|
|
115
|
+
export async function downloadChromeForTesting() {
|
|
116
|
+
const os = platform();
|
|
117
|
+
const cftPlatform = getCftPlatform();
|
|
118
|
+
if (!cftPlatform) {
|
|
119
|
+
throw new Error(`Chrome for Testing is not available for ${platform()}/${arch()}.\n` +
|
|
120
|
+
'Install Chromium manually (e.g., apt install chromium) and set CHROMIUM_PATH.');
|
|
121
|
+
}
|
|
122
|
+
// Install into user-local cache directory (no sudo)
|
|
123
|
+
const home = homedir();
|
|
124
|
+
let installBase;
|
|
125
|
+
if (os === 'darwin') {
|
|
126
|
+
installBase = join(home, 'Library', 'Caches', 'ms-playwright');
|
|
127
|
+
}
|
|
128
|
+
else if (os === 'win32') {
|
|
129
|
+
installBase = join(home, 'AppData', 'Local', 'ms-playwright');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
installBase = join(home, '.cache', 'ms-playwright');
|
|
133
|
+
}
|
|
134
|
+
const installDir = join(installBase, 'chromium-cft');
|
|
135
|
+
mkdirSync(installDir, { recursive: true });
|
|
136
|
+
// Fetch latest stable download URL from Chrome for Testing API
|
|
137
|
+
console.log('[chromium] Fetching Chrome for Testing download info...');
|
|
138
|
+
const apiUrl = 'https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json';
|
|
139
|
+
const res = await fetch(apiUrl);
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
throw new Error(`Failed to fetch Chrome for Testing API: ${res.status}`);
|
|
142
|
+
const data = (await res.json());
|
|
143
|
+
const entry = data.channels.Stable.downloads.chrome.find((d) => d.platform === cftPlatform);
|
|
144
|
+
if (!entry)
|
|
145
|
+
throw new Error(`No Chrome for Testing download for platform: ${cftPlatform}`);
|
|
146
|
+
const downloadUrl = entry.url;
|
|
147
|
+
const version = data.channels.Stable.version;
|
|
148
|
+
console.log(`[chromium] Downloading Chrome ${version} for ${cftPlatform}...`);
|
|
149
|
+
const dlRes = await fetch(downloadUrl);
|
|
150
|
+
if (!dlRes.ok)
|
|
151
|
+
throw new Error(`Download failed: ${dlRes.status}`);
|
|
152
|
+
const zipBuffer = Buffer.from(await dlRes.arrayBuffer());
|
|
153
|
+
const zipPath = join(installDir, 'chrome.zip');
|
|
154
|
+
writeFileSync(zipPath, zipBuffer);
|
|
155
|
+
console.log('[chromium] Extracting...');
|
|
156
|
+
if (os === 'win32') {
|
|
157
|
+
const proc = Bun.spawn(['powershell', '-Command', `Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${installDir}'`], { stdout: 'inherit', stderr: 'inherit' });
|
|
158
|
+
if ((await proc.exited) !== 0)
|
|
159
|
+
throw new Error('Failed to extract (PowerShell Expand-Archive)');
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const proc = Bun.spawn(['unzip', '-o', '-q', zipPath, '-d', installDir], {
|
|
163
|
+
stdout: 'inherit',
|
|
164
|
+
stderr: 'inherit',
|
|
165
|
+
});
|
|
166
|
+
if ((await proc.exited) !== 0)
|
|
167
|
+
throw new Error('Failed to extract (unzip)');
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
unlinkSync(zipPath);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
/* ignore */
|
|
174
|
+
}
|
|
175
|
+
// Find the extracted executable
|
|
176
|
+
const execCandidates = {
|
|
177
|
+
darwin: [`chrome-${cftPlatform}/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`],
|
|
178
|
+
linux: [`chrome-${cftPlatform}/chrome`],
|
|
179
|
+
win32: [`chrome-${cftPlatform}\\chrome.exe`],
|
|
180
|
+
};
|
|
181
|
+
for (const rel of execCandidates[os] ?? execCandidates['linux']) {
|
|
182
|
+
const fullPath = join(installDir, rel);
|
|
183
|
+
if (existsSync(fullPath)) {
|
|
184
|
+
if (os !== 'win32') {
|
|
185
|
+
try {
|
|
186
|
+
chmodSync(fullPath, 0o755);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* ignore */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return fullPath;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw new Error('Chrome was downloaded but executable not found in extracted files');
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Return a path to a Chromium executable, installing Chrome for Testing
|
|
199
|
+
* on first call if no local Chrome/Chromium is discoverable. Caches the
|
|
200
|
+
* result for the lifetime of the process.
|
|
201
|
+
*/
|
|
202
|
+
let cachedPath = null;
|
|
203
|
+
export async function ensureChromiumInstalled() {
|
|
204
|
+
if (cachedPath)
|
|
205
|
+
return cachedPath;
|
|
206
|
+
const found = findChromiumExecutable();
|
|
207
|
+
if (found) {
|
|
208
|
+
cachedPath = found;
|
|
209
|
+
return cachedPath;
|
|
210
|
+
}
|
|
211
|
+
console.log('[chromium] Chrome/Chromium not found. Installing Chrome for Testing...');
|
|
212
|
+
try {
|
|
213
|
+
const p = await downloadChromeForTesting();
|
|
214
|
+
console.log('[chromium] Chrome for Testing installed successfully');
|
|
215
|
+
cachedPath = p;
|
|
216
|
+
return cachedPath;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
220
|
+
throw new Error(`Failed to install Chrome automatically: ${msg}\n` +
|
|
221
|
+
'You can install it manually:\n' +
|
|
222
|
+
' - Install Google Chrome, or\n' +
|
|
223
|
+
' - Set CHROMIUM_PATH env var to an existing Chrome/Chromium executable.');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=chromium.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chromium.js","sourceRoot":"","sources":["../src/chromium.ts"],"names":[],"mappings":"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"}
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chartjs2img — TypeScript / Node library entry.
|
|
3
|
+
*
|
|
4
|
+
* Import this file to render Chart.js configurations to images from
|
|
5
|
+
* any Bun / Node program:
|
|
6
|
+
*
|
|
7
|
+
* import { renderChart, closeBrowser } from 'chartjs2img'
|
|
8
|
+
*
|
|
9
|
+
* const result = await renderChart({
|
|
10
|
+
* chart: { type: 'bar', data: { labels: ['A','B'], datasets: [{ data: [1,2] }] } },
|
|
11
|
+
* width: 800, height: 600, format: 'png',
|
|
12
|
+
* })
|
|
13
|
+
* await Bun.write('chart.png', result.buffer)
|
|
14
|
+
* if (result.messages.length) console.warn(result.messages)
|
|
15
|
+
* await closeBrowser() // on process shutdown
|
|
16
|
+
*
|
|
17
|
+
* The `chartjs2img` CLI (`src/index.ts`) and the HTTP server
|
|
18
|
+
* (`src/server.ts`) share the same render pipeline but import it
|
|
19
|
+
* directly from ./renderer etc. — NOT from this file. That way the
|
|
20
|
+
* public surface exported here is a constraint for external consumers
|
|
21
|
+
* only, and internal refactoring does not have to preserve it.
|
|
22
|
+
*
|
|
23
|
+
* This module intentionally **does not** export the in-memory cache
|
|
24
|
+
* internals, the Puppeteer launch helpers, the HTML template, or the
|
|
25
|
+
* CLI argument parser. Those are implementation details; keep your
|
|
26
|
+
* dependency surface on the exports below so upgrades stay drop-in.
|
|
27
|
+
*/
|
|
28
|
+
export { renderChart, closeBrowser, rendererStats, Renderer } from './renderer';
|
|
29
|
+
export type { RenderResult, ConsoleMessage, RendererConfig, RendererStats } from './renderer';
|
|
30
|
+
export type { RenderOptions } from './template';
|
|
31
|
+
export { computeHash } from './cache';
|
|
32
|
+
export { VERSION, NAME } from './version';
|
|
33
|
+
/**
|
|
34
|
+
* The exact Chart.js + plugin versions bundled into the rendering
|
|
35
|
+
* page. Frozen as a reference table so callers can expose "what's
|
|
36
|
+
* inside this chartjs2img?" to their own users without parsing
|
|
37
|
+
* `chartjs2img llm`.
|
|
38
|
+
*/
|
|
39
|
+
export { LIBS as BUNDLED_LIBS } from './template';
|
|
40
|
+
//# sourceMappingURL=lib.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAQH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC/E,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAG7F,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAI/C,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAIrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC;;;;;GAKG;AACH,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAA"}
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chartjs2img — TypeScript / Node library entry.
|
|
3
|
+
*
|
|
4
|
+
* Import this file to render Chart.js configurations to images from
|
|
5
|
+
* any Bun / Node program:
|
|
6
|
+
*
|
|
7
|
+
* import { renderChart, closeBrowser } from 'chartjs2img'
|
|
8
|
+
*
|
|
9
|
+
* const result = await renderChart({
|
|
10
|
+
* chart: { type: 'bar', data: { labels: ['A','B'], datasets: [{ data: [1,2] }] } },
|
|
11
|
+
* width: 800, height: 600, format: 'png',
|
|
12
|
+
* })
|
|
13
|
+
* await Bun.write('chart.png', result.buffer)
|
|
14
|
+
* if (result.messages.length) console.warn(result.messages)
|
|
15
|
+
* await closeBrowser() // on process shutdown
|
|
16
|
+
*
|
|
17
|
+
* The `chartjs2img` CLI (`src/index.ts`) and the HTTP server
|
|
18
|
+
* (`src/server.ts`) share the same render pipeline but import it
|
|
19
|
+
* directly from ./renderer etc. — NOT from this file. That way the
|
|
20
|
+
* public surface exported here is a constraint for external consumers
|
|
21
|
+
* only, and internal refactoring does not have to preserve it.
|
|
22
|
+
*
|
|
23
|
+
* This module intentionally **does not** export the in-memory cache
|
|
24
|
+
* internals, the Puppeteer launch helpers, the HTML template, or the
|
|
25
|
+
* CLI argument parser. Those are implementation details; keep your
|
|
26
|
+
* dependency surface on the exports below so upgrades stay drop-in.
|
|
27
|
+
*/
|
|
28
|
+
// Core render pipeline. Most callers just need the module-level
|
|
29
|
+
// renderChart / closeBrowser helpers, which back onto a lazily-created
|
|
30
|
+
// default Renderer. Advanced callers (test harnesses, multi-tenant
|
|
31
|
+
// servers that want isolated browser pools, or programs that want to
|
|
32
|
+
// configure concurrency per-instance) can instantiate `Renderer`
|
|
33
|
+
// directly.
|
|
34
|
+
export { renderChart, closeBrowser, rendererStats, Renderer } from './renderer';
|
|
35
|
+
// Deterministic hash computation — useful for building a CDN-facing
|
|
36
|
+
// cache layer or for deduping submissions before rendering.
|
|
37
|
+
export { computeHash } from './cache';
|
|
38
|
+
// Identification. `VERSION` is the value the CLI reports and the
|
|
39
|
+
// X-Powered-By HTTP header surfaces.
|
|
40
|
+
export { VERSION, NAME } from './version';
|
|
41
|
+
/**
|
|
42
|
+
* The exact Chart.js + plugin versions bundled into the rendering
|
|
43
|
+
* page. Frozen as a reference table so callers can expose "what's
|
|
44
|
+
* inside this chartjs2img?" to their own users without parsing
|
|
45
|
+
* `chartjs2img llm`.
|
|
46
|
+
*/
|
|
47
|
+
export { LIBS as BUNDLED_LIBS } from './template';
|
|
48
|
+
//# sourceMappingURL=lib.js.map
|
package/dist/lib.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lib.js","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,gEAAgE;AAChE,uEAAuE;AACvE,mEAAmE;AACnE,qEAAqE;AACrE,iEAAiE;AACjE,YAAY;AACZ,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAM/E,oEAAoE;AACpE,4DAA4D;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAErC,iEAAiE;AACjE,qCAAqC;AACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC;;;;;GAKG;AACH,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type RenderOptions } from './template';
|
|
2
|
+
export interface ConsoleMessage {
|
|
3
|
+
level: 'error' | 'warn' | 'info' | 'log';
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RenderResult {
|
|
7
|
+
buffer: Buffer;
|
|
8
|
+
hash: string;
|
|
9
|
+
contentType: string;
|
|
10
|
+
cached: boolean;
|
|
11
|
+
/** Console messages (errors/warnings) from Chart.js during rendering */
|
|
12
|
+
messages: ConsoleMessage[];
|
|
13
|
+
}
|
|
14
|
+
export interface RendererConfig {
|
|
15
|
+
/** Max simultaneous renders. Defaults to CONCURRENCY env or 8. */
|
|
16
|
+
maxConcurrency?: number;
|
|
17
|
+
/**
|
|
18
|
+
* Hard upper bound on a single render (applied to page.goto and
|
|
19
|
+
* page.waitForFunction). Defaults to MAX_RENDER_TIME_SECONDS env or
|
|
20
|
+
* 30s. Chart rendering itself is sub-second; 30s accommodates slow
|
|
21
|
+
* CDN fetches of Chart.js + plugins on cold start.
|
|
22
|
+
*/
|
|
23
|
+
maxRenderTimeMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Safety-net timeout for force-closing a page if the render flow
|
|
26
|
+
* somehow never reaches its finally block. This is a LAST-RESORT
|
|
27
|
+
* leak guard, not a render budget — the per-render timeout above is
|
|
28
|
+
* what you want to tune for slow environments.
|
|
29
|
+
*
|
|
30
|
+
* Defaults to `maxRenderTimeMs * 2 + 10s`, which leaves room for
|
|
31
|
+
* both goto and waitForFunction to fail with their own timeout and
|
|
32
|
+
* still have the finally block clean up before this fires. Can be
|
|
33
|
+
* overridden via PAGE_TIMEOUT_SECONDS env for legacy operators.
|
|
34
|
+
*/
|
|
35
|
+
pageSafetyNetMs?: number;
|
|
36
|
+
}
|
|
37
|
+
export interface RendererStats {
|
|
38
|
+
browserConnected: boolean;
|
|
39
|
+
concurrency: {
|
|
40
|
+
max: number;
|
|
41
|
+
active: number;
|
|
42
|
+
pending: number;
|
|
43
|
+
};
|
|
44
|
+
activePages: number;
|
|
45
|
+
/** Rendering-budget timeout (goto + waitForFunction) in seconds. */
|
|
46
|
+
maxRenderTimeSeconds: number;
|
|
47
|
+
/** Last-resort page force-close timer in seconds. Always > maxRenderTimeSeconds. */
|
|
48
|
+
pageSafetyNetSeconds: number;
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated alias of pageSafetyNetSeconds for /health consumers
|
|
51
|
+
* that shipped before the field was renamed. Will be removed.
|
|
52
|
+
*/
|
|
53
|
+
pageTimeoutSeconds: number;
|
|
54
|
+
}
|
|
55
|
+
export declare class Renderer {
|
|
56
|
+
private readonly maxConcurrency;
|
|
57
|
+
private readonly maxRenderTimeMs;
|
|
58
|
+
private readonly pageSafetyNetMs;
|
|
59
|
+
private readonly semaphore;
|
|
60
|
+
private browser;
|
|
61
|
+
private launching;
|
|
62
|
+
private launchPromise;
|
|
63
|
+
private readonly activePages;
|
|
64
|
+
constructor(config?: RendererConfig);
|
|
65
|
+
private launchBrowser;
|
|
66
|
+
private ensureBrowser;
|
|
67
|
+
private schedulePageCleanup;
|
|
68
|
+
private clearPageCleanup;
|
|
69
|
+
render(options: RenderOptions): Promise<RenderResult>;
|
|
70
|
+
close(): Promise<void>;
|
|
71
|
+
stats(): RendererStats;
|
|
72
|
+
}
|
|
73
|
+
export declare function renderChart(options: RenderOptions): Promise<RenderResult>;
|
|
74
|
+
export declare function closeBrowser(): Promise<void>;
|
|
75
|
+
export declare function rendererStats(): RendererStats;
|
|
76
|
+
//# sourceMappingURL=renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAaA,OAAO,EAAa,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAS1D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;IACxC,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,OAAO,CAAA;IACf,wEAAwE;IACxE,QAAQ,EAAE,cAAc,EAAE,CAAA;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;;;;;OAUG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,OAAO,CAAA;IACzB,WAAW,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7D,WAAW,EAAE,MAAM,CAAA;IACnB,oEAAoE;IACpE,oBAAoB,EAAE,MAAM,CAAA;IAC5B,oFAAoF;IACpF,oBAAoB,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AA+BD,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAQ;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAW;IAErC,OAAO,CAAC,OAAO,CAAuB;IACtC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,aAAa,CAAgC;IAKrD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;gBAElD,MAAM,GAAE,cAAmB;YASzB,aAAa;YAmBb,aAAa;IA6B3B,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,gBAAgB;IAUlB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IA2GrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B,KAAK,IAAI,aAAa;CAWvB;AAaD,wBAAsB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CAE/E;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAKlD;AAED,wBAAgB,aAAa,IAAI,aAAa,CAa7C"}
|