Open itpropro opened 4 years ago
this being a web-component based solution, there isn't any good tooling AFAIK that can render web-components on server
That being said keep an eye on this space, We do have it planned out in our roadmap, but need to wait for lit-ssr to be in some usable form
anther approach would be to factor out the core-engine and create a CLI, it can be done using lit-html-server. I am open for a PR in this direction too
If you need this now, it's possible to do with jsdom. Load up the index.html in there, and mock window.URL.createObjectURL
in the DOM and you'll get a blob back with the PDF content.
I'm afraid I don't have a code example but this is what I've been using for the past couple of days. It's just a pain to get the blob output to a file afterwards, but things like this should work so long as you're getting globals from the jsdom window.
For what it's worth, we were able to use RapiPdf in Node.js with something like this:
const fs = require('fs');
const jsdom = require('jsdom');
const virtualConsole = new jsdom.VirtualConsole();
const {JSDOM} = jsdom;
const script = fs.readFileSync('./node_modules/rapipdf/dist/rapipdf-min.js', 'utf-8');
const json = fs.readFileSync('api.json', 'utf-8');
const obj = JSON.parse(json);
const dom = new JSDOM(`
<!doctype html>
<script>${script}</script>
<rapi-pdf id="thedoc"></rapi-pdf>
`, {
virtualConsole,
runScripts: 'dangerously'
});
const {document} = dom.window;
dom.window.onopen = function () {};
dom.window.URL.createObjectURL = (blob) => {
const reader = new dom.window.FileReader();
reader.addEventListener('loadend', () => {
fs.writeFileSync('out.pdf', Buffer.from(reader.result));
});
reader.readAsArrayBuffer(blob);
};
document.addEventListener('DOMContentLoaded', async () => {
const rapiPdf = document.querySelector('rapi-pdf');
rapiPdf.generatePdf(obj);
});
Unfortunately, it stopped working with Node.js 18. Further inspection revealed that the custom element type rapi-pdf
failed to register due to a JavaScript error when loaded by jsdom.
JS DOM error: Error: Uncaught [TypeError: Cannot redefine property: onmessage]
at reportException (...\docs\node_modules\jsdom\lib\jsdom\living\helpers\runtime-script-errors.js:66:24)
at processJavaScript (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\HTMLScriptElement-impl.js:240:7)
at HTMLScriptElementImpl._innerEval (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\HTMLScriptElement-impl.js:173:5)
at ...\docs\node_modules\jsdom\lib\jsdom\living\nodes\HTMLScriptElement-impl.js:114:12
at ResourceQueue.push (...\docs\node_modules\jsdom\lib\jsdom\browser\resources\resource-queue.js:53:16)
at HTMLScriptElementImpl._fetchInternalScript (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\HTMLScriptElement-impl.js:113:21)
at HTMLScriptElementImpl._eval (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\HTMLScriptElement-impl.js:167:12)
at HTMLScriptElementImpl._poppedOffStackOfOpenElements (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\HTMLScriptElement-impl.js:130:10)
at JSDOMParse5Adapter.onItemPop (...\docs\node_modules\jsdom\lib\jsdom\browser\parser\html.js:175:43)
at Parser.onItemPop (...\docs\node_modules\parse5\dist\cjs\parser\index.js:158:90) {
detail: TypeError: Cannot redefine property: onmessage
at about:blank:55:149568
at about:blank:55:149574
at Object.<anonymous> (about:blank:55:150731)
at Object.<anonymous> (about:blank:55:150783)
at D (about:blank:6:7214)
at r (about:blank:6:568)
at Object.<anonymous> (about:blank:55:54348)
at Object.<anonymous> (about:blank:55:54589)
at D (about:blank:6:7214)
at r (about:blank:6:568),
type: 'unhandled exception'
}
...\docs\use_rapipdf.js:43
rapiPdf.generatePdf({});
^
TypeError: rapiPdf.generatePdf is not a function
at Document.<anonymous> (...\docs\use_rapipdf.js:43:13)
at Document.callTheUserObjectsOperation (...\docs\node_modules\jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
at innerInvokeEventListeners (...\docs\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:350:25)
at invokeEventListeners (...\docs\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:286:3)
at DocumentImpl._dispatch (...\docs\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:233:9)
at fireAnEvent (...\docs\node_modules\jsdom\lib\jsdom\living\helpers\events.js:18:36)
at dispatchEvent (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\Document-impl.js:452:9)
at ...\docs\node_modules\jsdom\lib\jsdom\living\nodes\Document-impl.js:457:11
at new Promise (<anonymous>)
at onDOMContentLoad (...\docs\node_modules\jsdom\lib\jsdom\living\nodes\Document-impl.js:455:14)
Node.js v18.16.0
There might be a way to fix this particular problem, but I wouldn't know where to look at the moment.
Update: See jsdom/jsdom#3546
Enet4, Thanks for the example above. I used it and enhanced it. Here's a full node.js pdf document generator which I used on a WSL Ubuntu linux.
`"use strict";
/eslint-disable no-console/
const fs = require("fs"); const path = require("path"); const util = require("util"); const yaml = require("js-yaml"); const jsdom = require("jsdom"); const virtualConsole = new jsdom.VirtualConsole(); const { JSDOM } = jsdom; const script = fs.readFileSync(path.join(__dirname, "rapipdf-min.js"), "utf-8"); const dom = new JSDOM( ` <!doctype html>
<rapi-pdf id="thedoc"></rapi-pdf>
`,
{ virtualConsole, runScripts: "dangerously", } ); const { document } = dom.window; var verbose = false; var warn = false;
// REPLACEREFS // Explanation: // Function Definition:
// replaceRefs(json) is the main function that takes the JSON object and replaces all $ref references with their corresponding content.
// getRefContent(ref, root) is a helper function that takes a $ref string and the root of the JSON structure. It returns the content at the path specified by the $ref.
//
// Get Reference Content:
// If the $ref starts with #, it is treated as an internal reference, and the function proceeds to traverse the path within the JSON structure.
// If the $ref does not start with #, it is treated as a file path. The function checks the file extension:
// For yaml and yml extensions, it reads and parses the file using yaml.load.
// For json extensions, it reads and parses the file using JSON.parse.
// If the file extension is unsupported, an error is logged.
// The parsed content of the file is then processed with replaceRefs.
// Logging statements print each step of the path traversal and file loading process.
//
// Recursive Traversal:
// If the current object (obj) is an array, it iterates over each element and calls recurse on it, passing the array as the parent and the index as the key.
// If the current object (obj) is a plain object, it iterates over its keys.
// If the key is $ref, it retrieves the new content from the JSON structure or external file using the getRefContent function.
// The retrieved new content replaces the entire object entry (parent[key]), and a deep copy of the new content is created to avoid reference issues.
// The function then recursively scans the new content for further $ref replacements.
// If the key is not $ref, it recursively calls recurse on the value of the key, passing the current object as the parent and the current key.
//
function replaceRefs(json) {
function getRefContent(ref, root) {
if (ref.startsWith("#")) {
// Internal reference
const path = ref.replace(/^#\/?/, "").split("/");
if (verbose) console.log(Resolving $ref: ${ref}
);
let acc = root;
for (const part of path) {
if (acc && acc[part] !== undefined) {
//console.log(Accessing: ${part} ->
, acc[part]);
acc = acc[part];
} else {
if (warn || verbose)
console.warn(
Path not found: ${part}, possibly forward referenced
);
return undefined;
}
}
return acc;
} else {
// External file reference
try {
const fileExtension = ref.split(".").pop();
let fileContent;
if (fileExtension === "yaml" || fileExtension === "yml") {
fileContent = yaml.load(fs.readFileSync(ref, "utf8"));
if (verbose) console.log(`Resolving external $ref YAML file: ${ref}`);
} else if (fileExtension === "json") {
fileContent = JSON.parse(fs.readFileSync(ref, "utf8"));
if (verbose) console.log(`Resolving external $ref JSON file: ${ref}`);
} else {
console.error(`Unsupported file extension: ${fileExtension}`);
return undefined;
}
fileContent = replaceRefs(fileContent); // pass 1: Resolve file includes and non forward references
fileContent = replaceRefs(fileContent); // pass 2: Resolve forward references
return fileContent;
} catch (error) {
console.error(`Error reading or parsing file: ${ref}`, error);
return undefined;
}
}
}
function recurse(obj, parent, key) { if (typeof obj === "object" && obj !== null) { if (Array.isArray(obj)) { obj.forEach((item, index) => recurse(item, obj, index)); } else { for (let k in obj) { if (obj.hasOwnProperty(k)) { if (k === "$ref") { const newContent = getRefContent(obj[k], json); if (newContent !== undefined) { parent[key] = JSON.parse(JSON.stringify(newContent)); // Deep copy to avoid reference issues recurse(parent[key], parent, key); // Recurse into the new content return; // Return early since the current object has been replaced } } else { recurse(obj[k], obj, k); } } } } } }
recurse(json, null, null); return json; }
// Main function to handle input and output filenames function main() { const args = process.argv.slice(2); const inputFile = args.find((arg) => !arg.startsWith("-")); let outputFile = args.find((arg) => arg.startsWith("-"))?.replace(/^-/, "") || ""; verbose = args.includes("-v"); warn = args.includes("-w");
if (!inputFile) {
console.error("Usage: node gendoc.js [-v] [-w]
if (!outputFile) {
// Default output file to input filename with .pdf extension
const ext = path.extname(inputFile);
const base = path.basename(inputFile, ext);
outputFile = ${base}.pdf
;
}
try { const filename = path.join(__dirname, inputFile); const fileExtension = path.extname(inputFile).toLowerCase(); const contents = fs.readFileSync(filename, "utf8"); var json;
// Read the input file
if (fileExtension === ".yaml" || fileExtension === ".yml") {
json = yaml.load(contents);
} else if (fileExtension === ".json") {
json = JSON.parse(contents);
} else {
console.error(`Unsupported input file extension: ${fileExtension}`);
process.exit(1);
}
// Process the JSON content
json = replaceRefs(json); // pass 1: Resolve file includes and non forward references
json = replaceRefs(json); // pass 2: Resolve forward references
if (verbose) console.log(JSON.parse(JSON.stringify(json)));
} catch (error) {
console.error(Error processing file: ${error.message}
);
process.exit(1);
}
// Generate the PDF output
dom.window.onopen = function () {};
dom.window.URL.createObjectURL = (blob) => {
const reader = new dom.window.FileReader();
reader.addEventListener("loadend", () => {
fs.writeFileSync(outputFile, Buffer.from(reader.result));
});
reader.readAsArrayBuffer(blob);
};
document.addEventListener("DOMContentLoaded", async () => {
const rapiPdf = document.querySelector("rapi-pdf");
rapiPdf.generatePdf(json);
});
}
gendoc.zip
// Execute the main function main(); ` Maybe someone can add this to a git repository as a tool.
Hello, Is there an example on how we could use this in node js? It would be a great functionality for microservice and serverless applications.