fix(smartpdf): harden browser lifecycle, port handling, and PDF result metadata

This commit is contained in:
2026-04-30 11:00:14 +00:00
parent 5b1615d359
commit b5ad88c33b
8 changed files with 1796 additions and 2686 deletions
+41
View File
@@ -0,0 +1,41 @@
{
"@git.zone/cli": {
"projectType": "npm",
"module": {
"githost": "code.foss.global",
"gitscope": "push.rocks",
"gitrepo": "smartpdf",
"description": "A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.",
"npmPackagename": "@push.rocks/smartpdf",
"license": "MIT",
"projectDomain": "push.rocks",
"keywords": [
"PDF generation",
"HTML to PDF",
"website to PDF",
"PDF manipulation",
"puppeteer",
"express",
"node.js",
"typescript",
"automation",
"PDF merging",
"text extraction",
"PDF management"
]
},
"release": {
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
}
},
"@git.zone/tsdoc": {
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
},
"@ship.zone/szci": {
"npmGlobalTools": []
}
}
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## 2026-04-30 - 4.2.1 - fix(smartpdf)
harden browser lifecycle, port handling, and PDF result metadata
- initialize nullable runtime state explicitly and guard browser access before PDF operations
- improve startup port selection and preserve configured port range defaults when options are partially provided
- throw explicit errors for missing render responses and failed PDF candidate security checks
- return imported PDF metadata with extracted text instead of null values
- tighten TypeScript settings and update dependencies to support stricter type safety
## 2026-03-09 - 4.2.0 - feat(smartpdf)
replace internal Express server with @push.rocks/smartserve, add PDF→WebP rendering, improve start/stop handling and bump dependencies
+15 -11
View File
@@ -10,30 +10,32 @@
"license": "MIT",
"scripts": {
"test": "(tstest test/ --verbose --timeout 120)",
"build": "(tsbuild tsfolders --allowimplicitany)",
"build": "(tsbuild tsfolders)",
"buildDocs": "tsdoc"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsdoc": "^1.12.0",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.3.0",
"@types/node": "^25.3.5"
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.3",
"@types/lodash.clonedeep": "^4.5.9",
"@types/node": "^25.6.0",
"@types/pngjs": "^6.0.5"
},
"dependencies": {
"@push.rocks/smartbuffer": "^3.0.5",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/smartfs": "^1.5.1",
"@push.rocks/smartjimp": "^1.2.0",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartnetwork": "^4.7.1",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartpuppeteer": "^2.0.5",
"@push.rocks/smartserve": "^2.0.1",
"@push.rocks/smartserve": "^2.0.4",
"@push.rocks/smartunique": "^3.0.9",
"@tsclass/tsclass": "^9.3.0",
"@tsclass/tsclass": "^9.5.1",
"pdf-lib": "^1.17.1",
"pdf2json": "^4.0.2"
"pdf2json": "^4.0.3"
},
"files": [
"ts/**/*",
@@ -44,6 +46,8 @@
"dist_ts_web/**/*",
"assets/**/*",
"cli.js",
".smartconfig.json",
"license",
"npmextra.json",
"readme.md"
],
@@ -69,5 +73,5 @@
"type": "git",
"url": "https://code.foss.global/push.rocks/smartpdf.git"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
"packageManager": "pnpm@10.28.2"
}
+1691 -2652
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -84,7 +84,8 @@ tap.test('should throw error when specific port is already in use', async () =>
await instance2.start();
} catch (error) {
errorThrown = true;
expect(error.message).toInclude('already in use');
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toInclude('already in use');
}
expect(errorThrown).toBeTrue();
@@ -94,4 +95,4 @@ tap.test('should throw error when specific port is already in use', async () =>
export default tap.start();
export default tap.start();
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartpdf',
version: '4.2.0',
version: '4.2.1',
description: 'A library for creating PDFs dynamically from HTML or websites with additional features like merging PDFs.'
}
+33 -19
View File
@@ -31,21 +31,21 @@ export class SmartPdf {
}
// INSTANCE
private smartserveInstance: plugins.smartserve.SmartServe;
serverPort: number;
headlessBrowser: plugins.smartpuppeteer.puppeteer.Browser;
private smartserveInstance: plugins.smartserve.SmartServe | null = null;
serverPort: number = 0;
headlessBrowser: plugins.smartpuppeteer.puppeteer.Browser | null = null;
externalBrowserBool: boolean = false;
private _readyDeferred: plugins.smartpromise.Deferred<void>;
private _candidates: { [key: string]: PdfCandidate } = {};
private _options: ISmartPdfOptions;
private _options: ISmartPdfOptions & { portRangeStart: number; portRangeEnd: number };
private _isRunning: boolean = false;
constructor(optionsArg?: ISmartPdfOptions) {
this._readyDeferred = new plugins.smartpromise.Deferred();
this._options = {
portRangeStart: 20000,
portRangeEnd: 30000,
...optionsArg
...optionsArg,
portRangeStart: optionsArg?.portRangeStart ?? 20000,
portRangeEnd: optionsArg?.portRangeEnd ?? 30000,
};
}
@@ -58,7 +58,8 @@ export class SmartPdf {
this._readyDeferred = new plugins.smartpromise.Deferred();
// lets set the external browser in case one is provided
this.headlessBrowser = headlessBrowserArg;
this.externalBrowserBool = !!headlessBrowserArg;
this.headlessBrowser = headlessBrowserArg ?? null;
// setup puppeteer
if (this.headlessBrowser) {
this.externalBrowserBool = true;
@@ -86,11 +87,11 @@ export class SmartPdf {
}
} else {
// Find a free port in the specified range
this.serverPort = await smartnetworkInstance.findFreePort(
const freePort = await smartnetworkInstance.findFreePort(
this._options.portRangeStart,
this._options.portRangeEnd
);
if (!this.serverPort) {
if (!freePort) {
// Clean up browser if we created one
if (!this.externalBrowserBool && this.headlessBrowser) {
await this.headlessBrowser.close();
@@ -98,6 +99,7 @@ export class SmartPdf {
}
throw new Error(`No free ports available in range ${this._options.portRangeStart}-${this._options.portRangeEnd}`);
}
this.serverPort = freePort;
}
// Now setup server using smartserve
@@ -152,6 +154,13 @@ export class SmartPdf {
this._candidates = {};
}
private getBrowser(): plugins.smartpuppeteer.puppeteer.Browser {
if (!this.headlessBrowser) {
throw new Error('SmartPdf is not running. Call start() before creating PDFs.');
}
return this.headlessBrowser;
}
/**
* Returns a PDF for a given HTML string.
*/
@@ -159,9 +168,9 @@ export class SmartPdf {
await this._readyDeferred.promise;
const pdfCandidate = new PdfCandidate(htmlStringArg);
this._candidates[pdfCandidate.pdfId] = pdfCandidate;
let page: plugins.smartpuppeteer.puppeteer.Page;
let page: plugins.smartpuppeteer.puppeteer.Page | undefined;
try {
page = await this.headlessBrowser.newPage();
page = await this.getBrowser().newPage();
await page.setViewport({
width: 794,
height: 1122,
@@ -169,10 +178,13 @@ export class SmartPdf {
const response = await page.goto(`http://localhost:${this.serverPort}/${pdfCandidate.pdfId}`, {
waitUntil: 'networkidle2',
});
if (!response) {
throw new Error('No response received while rendering PDF candidate.');
}
const headers = response.headers();
if (headers['pdf-id'] !== pdfCandidate.pdfId) {
console.log('Error! Headers do not match. For security reasons no pdf is being emitted!');
return;
throw new Error('PDF candidate security check failed.');
} else {
console.log(`id security check passed for ${pdfCandidate.pdfId}`);
}
@@ -208,7 +220,7 @@ export class SmartPdf {
}
async getPdfResultForWebsite(websiteUrl: string): Promise<plugins.tsclass.business.IPdf> {
const page = await this.headlessBrowser.newPage();
const page = await this.getBrowser().newPage();
try {
await page.setViewport({
width: 1980,
@@ -247,7 +259,7 @@ export class SmartPdf {
}
async getFullWebsiteAsSinglePdf(websiteUrl: string): Promise<plugins.tsclass.business.IPdf> {
const page = await this.headlessBrowser.newPage();
const page = await this.getBrowser().newPage();
try {
await page.setViewport({
width: 1920,
@@ -320,8 +332,10 @@ export class SmartPdf {
return {
name: parsedPath.base,
buffer,
id: null,
metadata: null,
id: parsedPath.base,
metadata: {
textExtraction: await this.extractTextFromPdfBuffer(buffer),
},
};
}
@@ -360,7 +374,7 @@ export class SmartPdf {
const scale = options.scale || 3.0;
// Create a new page using the headless browser.
const page = await this.headlessBrowser.newPage();
const page = await this.getBrowser().newPage();
try {
// Prepare PDF data as a base64 string.
@@ -471,7 +485,7 @@ export class SmartPdf {
const quality = options.quality || 85;
// Create a new page using the headless browser
const page = await this.headlessBrowser.newPage();
const page = await this.getBrowser().newPage();
try {
// Prepare PDF data as a base64 string
+3 -1
View File
@@ -5,8 +5,10 @@
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noImplicitAny": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true
"verbatimModuleSyntax": true,
"types": ["node"]
},
"exclude": [
"dist_*/**/*.d.ts"