What happened?
Expected: 'pdf' is a value the shipped typings declare — dist/superdoc/src/core/types/index.d.ts:1341 has export type ExportType = 'docx' | 'pdf' | 'html';, used for ExportParams.exportType at :1363. Passing it should either produce a PDF or throw a clear "not supported" error.
Actual: it resolves successfully and hands back (or downloads) a valid but completely empty zip — 22 bytes, the End-of-Central-Directory record and nothing else. No error, no warning. 'html' behaves identically.
Mechanism
dist/chunks/create-theme-WuK2f6I4.es.js, SuperDoc.export() at lines 24998–25017. The only place exportType selects content is line 25007:
if (exportType.includes("docx")) docxFiles.forEach((blob) => {
blobsToZip.push(blob);
filenames.push(`${baseFileName}.docx`);
});
if (blobsToZip.length === 1) { ... } // 25011 — single-file fast path
const zip = await createZip(blobsToZip, filenames); // 25015
if (triggerDownload) return createDownload(zip, baseFileName, "zip"); // 25016
The function contains no pdf or html branch at all. With ['pdf'] (or ['html']) nothing is pushed, blobsToZip.length is 0 so the fast path is skipped, and createZip([], []) runs — dist/chunks/zipper-BxRAi0-5.es.js:4, a JSZip generateAsync over zero entries, which is exactly 22 bytes.
Either dropping 'pdf' and 'html' from ExportType or throwing on an exportType that contributes no files would be an improvement over emitting a corrupt archive.
Steps to reproduce
Test fixture: test_contract.docx — synthetic, no third-party content.
npm i @harbour-enterprises/superdoc@1.46.1 jsdom fflate
node repro.mjs test_contract.docx
repro.mjs:
import fs from "node:fs";
import { JSDOM } from "jsdom";
const dom = new JSDOM('<!doctype html><html><body><div id="sd"></div></body></html>', {
url: "http://localhost/",
pretendToBeVisual: true,
});
for (const k of Object.getOwnPropertyNames(dom.window)) {
if (globalThis[k] === undefined) {
try { globalThis[k] = dom.window[k]; } catch {}
}
}
// Node 24 defines its own global Blob/File; they must not shadow jsdom's.
for (const k of ["Blob", "File", "FileReader"]) globalThis[k] = dom.window[k];
const store = new Map();
globalThis.localStorage = {
getItem: (k) => store.get(k) ?? null,
setItem: (k, v) => void store.set(k, String(v)),
removeItem: (k) => void store.delete(k),
clear: () => store.clear(),
};
const { SuperDoc } = await import("@harbour-enterprises/superdoc");
const bytes = fs.readFileSync(process.argv[2] ?? "test_contract.docx");
const file = new dom.window.File([bytes], "test_contract.docx", {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
});
const superdoc = await new Promise((resolve, reject) => {
const sd = new SuperDoc({
selector: "#sd",
document: file,
documentMode: "editing",
modules: { comments: false, toolbar: false },
telemetry: { enabled: false },
onReady: () => resolve(sd),
});
setTimeout(() => reject(new Error("timeout waiting for onReady")), 60000);
});
// triggerDownload:false returns exactly the blob that would otherwise be downloaded.
for (const type of ["docx", "pdf", "html"]) {
const blob = await superdoc.export({ exportType: [type], triggerDownload: false });
const magic = [...new Uint8Array((await blob.arrayBuffer()).slice(0, 4))]
.map((b) => b.toString(16).padStart(2, "0")).join(" ");
console.log(
`exportType: ['${type}'] -> ${String(blob.size).padStart(5)} bytes ` +
`magic ${magic} mime "${blob.type}"`,
);
}
process.exit(0);
Output (stdout, verbatim except the elided docx byte count, which moves a few bytes between runs; the 22 is exact and stable):
🦋 🦸♀️ [superdoc] 🦋 [superdoc] Using SuperDoc version: 1.46.1
exportType: ['docx'] -> <varies> bytes magic 50 4b 03 04 mime "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
exportType: ['pdf'] -> 22 bytes magic 50 4b 05 06 mime "application/zip"
exportType: ['html'] -> 22 bytes magic 50 4b 05 06 mime "application/zip"
50 4b 05 06 is an empty archive's EOCD signature. triggerDownload: false only returns the blob instead of downloading it — with the default triggerDownload: true the same 22 bytes are handed to the browser as a .zip.
stderr carries one error, [PresentationEditor] Layout error Error: [incrementalLayout] Failed to get 2D context from canvas, with a multi-frame stack: jsdom has no canvas, so pagination cannot measure. It does not affect this result — the docx arm of the same run still produces a real ~21 kB document, and export() has no pdf/html branch to reach in any environment.
SuperDoc version
1.46.1 (V1) — the V1 line, now on the v1 branch. Not tested against V2 on main.
Browser
None — headless Node 24 with jsdom. No browser involved.
What happened?
Expected:
'pdf'is a value the shipped typings declare —dist/superdoc/src/core/types/index.d.ts:1341hasexport type ExportType = 'docx' | 'pdf' | 'html';, used forExportParams.exportTypeat:1363. Passing it should either produce a PDF or throw a clear "not supported" error.Actual: it resolves successfully and hands back (or downloads) a valid but completely empty zip — 22 bytes, the End-of-Central-Directory record and nothing else. No error, no warning.
'html'behaves identically.Mechanism
dist/chunks/create-theme-WuK2f6I4.es.js,SuperDoc.export()at lines 24998–25017. The only placeexportTypeselects content is line 25007:The function contains no
pdforhtmlbranch at all. With['pdf'](or['html']) nothing is pushed,blobsToZip.lengthis 0 so the fast path is skipped, andcreateZip([], [])runs —dist/chunks/zipper-BxRAi0-5.es.js:4, a JSZipgenerateAsyncover zero entries, which is exactly 22 bytes.Either dropping
'pdf'and'html'fromExportTypeor throwing on anexportTypethat contributes no files would be an improvement over emitting a corrupt archive.Steps to reproduce
Test fixture:
test_contract.docx— synthetic, no third-party content.repro.mjs:Output (stdout, verbatim except the elided
docxbyte count, which moves a few bytes between runs; the22is exact and stable):50 4b 05 06is an empty archive's EOCD signature.triggerDownload: falseonly returns the blob instead of downloading it — with the defaulttriggerDownload: truethe same 22 bytes are handed to the browser as a.zip.stderr carries one error,
[PresentationEditor] Layout error Error: [incrementalLayout] Failed to get 2D context from canvas, with a multi-frame stack: jsdom has no canvas, so pagination cannot measure. It does not affect this result — thedocxarm of the same run still produces a real ~21 kB document, andexport()has nopdf/htmlbranch to reach in any environment.SuperDoc version
1.46.1 (V1)— the V1 line, now on thev1branch. Not tested against V2 onmain.Browser
None — headless Node 24 with jsdom. No browser involved.