mrin9 / RapiPdf

PDF generation from OpenAPI / Swagger Spec
https://mrin9.github.io/RapiPdf
MIT License
281 stars 100 forks source link

Use with node.js #55

Open itpropro opened 4 years ago

itpropro commented 4 years ago

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.

mrin9 commented 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

andyrooger commented 4 years ago

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.

Enet4 commented 1 year ago

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

BertEzendam commented 1 month ago

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] []"); process.exit(1); }

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.