Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,14 @@ export namespace stream {
* Styles can add some performance overhead. Default is false
*/
useStyles: boolean;

/**
* XLS-352: an optional Date stamped as the mtime of EVERY zip entry the writer emits, so
* the output is byte-deterministic across runs (a per-entry `date` otherwise defaults to
* `new Date()`). Applies to both the streaming WorkbookWriter and the buffered
* `xlsx.write`/`writeBuffer` path. When omitted, entries keep the wall clock.
*/
zipEntryDate?: Date;
}

interface ArchiverZipOptions {
Expand Down
49 changes: 35 additions & 14 deletions lib/stream/xlsx/workbook-writer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const SharedStringsXform = require('../../xlsx/xform/strings/shared-strings-xfor

const WorksheetWriter = require('./worksheet-writer');

const theme1Xml = require('../../xlsx/xml/theme1.js');
const theme1Xml = require('../../xlsx/xml/theme1');

class WorkbookWriter {
constructor(options) {
Expand All @@ -44,6 +44,13 @@ class WorkbookWriter {

this.zipOptions = options.zip;

// XLS-352: an optional Date stamped as the mtime of EVERY zip entry this writer appends.
// `date` is a per-entry archiver option (default `new Date()`); archiver ignores any
// module-level `zip: {date}`, so byte-deterministic output requires forwarding this value to
// each `zip.append` call — which the `_append` choke point below does. Left undefined, entries
// keep the wall clock exactly as before, so callers that do not pass it see no change.
this.zipEntryDate = options.zipEntryDate;

this.media = [];
this.commentRefs = [];

Expand All @@ -65,16 +72,29 @@ class WorkbookWriter {
return this._definedNames;
}

// XLS-352 choke point: every zip entry is appended through here, so a caller-supplied
// `zipEntryDate` (see the constructor) is stamped as the entry's mtime. When it is undefined —
// or the individual call already carries its own `date` — the options pass through untouched and
// archiver's default (the wall clock) is preserved. This is the durable, in-fork replacement for
// the server's `PinnedWorkbookWriter` subclass, which wrapped `zip.append` from outside.
_append(data, options) {
if (this.zipEntryDate !== undefined && (!options || options.date === undefined)) {
return this.zip.append(data, {...options, date: this.zipEntryDate});
}
return this.zip.append(data, options);
}

_openStream(path) {
const stream = new StreamBuf({bufSize: 65536, batch: true});
this.zip.append(stream, {name: path});
this._append(stream, {name: path});
stream.on('finish', () => {
stream.emit('zipped');
});
return stream;
}

_commitWorksheets() {
// prettier-ignore
const commitWorksheet = function(worksheet) {
if (!worksheet.committed) {
return new Promise(resolve => {
Expand Down Expand Up @@ -144,6 +164,7 @@ class WorkbookWriter {
if (options.tabColor) {
// eslint-disable-next-line no-console
console.trace('tabColor option has moved to { properties: tabColor: {...} }');
// prettier-ignore
options.properties = Object.assign(
{
tabColor: options.tabColor,
Expand Down Expand Up @@ -187,14 +208,14 @@ class WorkbookWriter {

addStyles() {
return new Promise(resolve => {
this.zip.append(this.styles.xml, {name: 'xl/styles.xml'});
this._append(this.styles.xml, {name: 'xl/styles.xml'});
resolve();
});
}

addThemes() {
return new Promise(resolve => {
this.zip.append(theme1Xml, {name: 'xl/theme/theme1.xml'});
this._append(theme1Xml, {name: 'xl/theme/theme1.xml'});
resolve();
});
}
Expand All @@ -207,7 +228,7 @@ class WorkbookWriter {
{Id: 'rId2', Type: RelType.CoreProperties, Target: 'docProps/core.xml'},
{Id: 'rId3', Type: RelType.ExtenderProperties, Target: 'docProps/app.xml'},
]);
this.zip.append(xml, {name: '/_rels/.rels'});
this._append(xml, {name: '/_rels/.rels'});
resolve();
});
}
Expand All @@ -222,12 +243,13 @@ class WorkbookWriter {
};
const xform = new ContentTypesXform();
const xml = xform.toXml(model);
this.zip.append(xml, {name: '[Content_Types].xml'});
this._append(xml, {name: '[Content_Types].xml'});
resolve();
});
}

addMedia() {
// prettier-ignore
return Promise.all(
this.media.map(medium => {
if (medium.type === 'image') {
Expand All @@ -236,12 +258,12 @@ class WorkbookWriter {
return this.zip.file(medium.filename, {name: filename});
}
if (medium.buffer) {
return this.zip.append(medium.buffer, {name: filename});
return this._append(medium.buffer, {name: filename});
}
if (medium.base64) {
const dataimg64 = medium.base64;
const content = dataimg64.substring(dataimg64.indexOf(',') + 1);
return this.zip.append(content, {name: filename, base64: true});
return this._append(content, {name: filename, base64: true});
}
}
throw new Error('Unsupported media');
Expand All @@ -256,7 +278,7 @@ class WorkbookWriter {
};
const xform = new AppXform();
const xml = xform.toXml(model);
this.zip.append(xml, {name: 'docProps/app.xml'});
this._append(xml, {name: 'docProps/app.xml'});
resolve();
});
}
Expand All @@ -265,7 +287,7 @@ class WorkbookWriter {
return new Promise(resolve => {
const coreXform = new CoreXform();
const xml = coreXform.toXml(this);
this.zip.append(xml, {name: 'docProps/core.xml'});
this._append(xml, {name: 'docProps/core.xml'});
resolve();
});
}
Expand All @@ -275,7 +297,7 @@ class WorkbookWriter {
return new Promise(resolve => {
const sharedStringsXform = new SharedStringsXform();
const xml = sharedStringsXform.toXml(this.sharedStrings);
this.zip.append(xml, {name: '/xl/sharedStrings.xml'});
this._append(xml, {name: '/xl/sharedStrings.xml'});
resolve();
});
}
Expand Down Expand Up @@ -308,13 +330,12 @@ class WorkbookWriter {
return new Promise(resolve => {
const xform = new RelationshipsXform();
const xml = xform.toXml(relationships);
this.zip.append(xml, {name: '/xl/_rels/workbook.xml.rels'});
this._append(xml, {name: '/xl/_rels/workbook.xml.rels'});
resolve();
});
}

addWorkbook() {
const {zip} = this;
const model = {
worksheets: this._worksheets.filter(Boolean),
definedNames: this._definedNames.model,
Expand All @@ -326,7 +347,7 @@ class WorkbookWriter {
return new Promise(resolve => {
const xform = new WorkbookXform();
xform.prepare(model);
zip.append(xform.toXml(model), {name: '/xl/workbook.xml'});
this._append(xform.toXml(model), {name: '/xl/workbook.xml'});
resolve();
});
}
Expand Down
16 changes: 16 additions & 0 deletions lib/utils/zip-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {stringToBuffer} = require('./browser-buffer-encode');
class ZipWriter extends events.EventEmitter {
constructor(options) {
super();
// prettier-ignore
this.options = Object.assign(
{
type: 'nodebuffer',
Expand All @@ -35,7 +36,22 @@ class ZipWriter extends events.EventEmitter {
}
}

// XLS-352: stamp a caller-supplied `entryDate` as the mtime of EVERY entry before generating,
// so the buffered writer's output is byte-deterministic. Doing it here — the single generate
// choke point — rather than per-append is deliberate: JSZip AUTO-CREATES folder entries (`xl/`,
// `xl/worksheets/`, ...) that never pass through `append`, and each defaults to `new Date()`.
// Pinning at append would leave those folder entries on the wall clock (two runs then differ in
// exactly their headers). Iterating `this.zip.files` here covers the file entries and the
// auto-created folder entries alike. Undefined leaves JSZip's default (the wall clock) untouched.
_pinEntryDates() {
if (this.options.entryDate === undefined) return;
Object.keys(this.zip.files).forEach(name => {
this.zip.files[name].date = this.options.entryDate;
});
}

async finalize() {
this._pinEntryDates();
const content = await this.zip.generateAsync(this.options);
this.stream.end(content);
this.emit('finish');
Expand Down
16 changes: 12 additions & 4 deletions lib/xlsx/xlsx.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ class XLSX {
const cacheId = cacheIdMatch[1];
// Find the pivot cache definition relationship
// pivotTable.rels should have a relationship to the cache definition
// prettier-ignore
const cacheDefRel = pivotTable.rels.find(
rel => rel.Type === RelType.PivotCacheDefinition
);
Expand Down Expand Up @@ -371,6 +372,7 @@ class XLSX {
* @deprecated since version 4.0. You should use `#read` instead. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md
*/
createInputStream() {
// prettier-ignore
throw new Error(
'`XLSX#createInputStream` is deprecated. You should use `XLSX#read` instead. This method will be removed in version 5.0. Please follow upgrade instruction: https://github.com/exceljs/exceljs/blob/master/UPGRADE-4.0.md'
);
Expand Down Expand Up @@ -571,6 +573,7 @@ class XLSX {
await this._processPivotCacheDefinitionEntry(entry, model, match[1]);
break;
}
// prettier-ignore
match = entryName.match(/xl\/pivotCache\/_rels\/(pivotCacheDefinition\d+)[.]xml[.]rels/);
if (match) {
await this._processPivotCacheDefinitionRelsEntry(stream, model, match[1]);
Expand Down Expand Up @@ -617,6 +620,7 @@ class XLSX {
// Write

async addMedia(zip, model) {
// prettier-ignore
await Promise.all(
model.media.map(async medium => {
if (medium.type === 'image') {
Expand Down Expand Up @@ -702,7 +706,8 @@ class XLSX {

addPivotTables(zip, model) {
const hasProgrammaticPivots = model.pivotTables && model.pivotTables.length > 0;
const hasPreservedPivots = model.preservedPivotTables &&
const hasPreservedPivots =
model.preservedPivotTables &&
Object.keys(model.preservedPivotTables.pivotTables || {}).length > 0;

if (!hasProgrammaticPivots && !hasPreservedPivots) return;
Expand Down Expand Up @@ -840,8 +845,8 @@ class XLSX {
}

addCharts(zip, model) {
const hasPreservedCharts = model.preservedChartsXml &&
Object.keys(model.preservedChartsXml).length > 0;
const hasPreservedCharts =
model.preservedChartsXml && Object.keys(model.preservedChartsXml).length > 0;

if (!hasPreservedCharts) return;

Expand Down Expand Up @@ -1104,7 +1109,10 @@ class XLSX {
async write(stream, options) {
options = options || {};
const {model} = this.workbook;
const zip = new ZipStream.ZipWriter(options.zip);
// XLS-352: forward an optional `zipEntryDate` into the zip writer so every buffered entry's mtime
// is stamped with it (the buffered twin of the streaming writer's per-entry pin). Undefined leaves
// JSZip's default (the wall clock) untouched, so callers that do not pass it see no change.
const zip = new ZipStream.ZipWriter({...options.zip, entryDate: options.zipEntryDate});
zip.pipe(stream);

this.prepareModel(model, options);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@protobi/exceljs",
"version": "4.4.0-protobi.10",
"version": "4.4.0-protobi.11",
"description": "Excel Workbook Manager - Temporary fork with pivot table enhancements and bug fixes pending upstream merge",
"private": false,
"license": "MIT",
Expand Down
Loading
Loading