@cj-tech-master/excelts 5.0.3 → 5.0.4-canary.20260125235458.742dade

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/browser/modules/archive/unzip/zip-parser.d.ts +10 -0
  2. package/dist/browser/modules/archive/unzip/zip-parser.js +24 -0
  3. package/dist/browser/modules/archive/zip/index.d.ts +5 -0
  4. package/dist/browser/modules/archive/zip/index.js +8 -4
  5. package/dist/browser/modules/archive/zip/zip-bytes.d.ts +5 -0
  6. package/dist/browser/modules/archive/zip/zip-bytes.js +33 -11
  7. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +1 -0
  8. package/dist/browser/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +9 -5
  9. package/dist/cjs/modules/archive/unzip/zip-parser.js +24 -0
  10. package/dist/cjs/modules/archive/zip/index.js +8 -4
  11. package/dist/cjs/modules/archive/zip/zip-bytes.js +33 -11
  12. package/dist/cjs/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +9 -5
  13. package/dist/esm/modules/archive/unzip/zip-parser.js +24 -0
  14. package/dist/esm/modules/archive/zip/index.js +8 -4
  15. package/dist/esm/modules/archive/zip/zip-bytes.js +33 -11
  16. package/dist/esm/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.js +9 -5
  17. package/dist/iife/excelts.iife.js +54 -17
  18. package/dist/iife/excelts.iife.js.map +1 -1
  19. package/dist/iife/excelts.iife.min.js +5 -5
  20. package/dist/types/modules/archive/unzip/zip-parser.d.ts +10 -0
  21. package/dist/types/modules/archive/zip/index.d.ts +5 -0
  22. package/dist/types/modules/archive/zip/zip-bytes.d.ts +5 -0
  23. package/dist/types/modules/excel/xlsx/xform/pivot-table/pivot-cache-definition-xform.d.ts +1 -0
  24. package/package.json +1 -1
@@ -32,6 +32,16 @@ export declare class ZipParser {
32
32
  * Check if entry exists
33
33
  */
34
34
  hasEntry(path: string): boolean;
35
+ /**
36
+ * Get the number of child entries in a directory.
37
+ *
38
+ * Returns the count of entries whose paths start with the directory prefix,
39
+ * excluding the directory entry itself. For non-directory entries, returns 0.
40
+ *
41
+ * @param path - Directory path (with or without trailing slash)
42
+ * @returns Number of child entries
43
+ */
44
+ getChildCount(path: string): number;
35
45
  /**
36
46
  * List all file paths
37
47
  */
@@ -275,6 +275,30 @@ export class ZipParser {
275
275
  hasEntry(path) {
276
276
  return this.entryMap.has(path);
277
277
  }
278
+ /**
279
+ * Get the number of child entries in a directory.
280
+ *
281
+ * Returns the count of entries whose paths start with the directory prefix,
282
+ * excluding the directory entry itself. For non-directory entries, returns 0.
283
+ *
284
+ * @param path - Directory path (with or without trailing slash)
285
+ * @returns Number of child entries
286
+ */
287
+ getChildCount(path) {
288
+ const entry = this.entryMap.get(path);
289
+ if (!entry?.isDirectory) {
290
+ return 0;
291
+ }
292
+ // Ensure prefix ends with "/" for correct matching
293
+ const prefix = entry.path.endsWith("/") ? entry.path : entry.path + "/";
294
+ let count = 0;
295
+ for (const e of this.entries) {
296
+ if (e.path.startsWith(prefix) && e !== entry) {
297
+ count++;
298
+ }
299
+ }
300
+ return count;
301
+ }
278
302
  /**
279
303
  * List all file paths
280
304
  */
@@ -22,6 +22,11 @@ export interface ZipOptions {
22
22
  * If false, always follow `level` (DEFLATE when level > 0).
23
23
  */
24
24
  smartStore?: boolean;
25
+ /**
26
+ * If true, entries are written in their original input order.
27
+ * If false (default), entries are sorted alphabetically by name.
28
+ */
29
+ noSort?: boolean;
25
30
  }
26
31
  export interface ZipEntryOptions {
27
32
  level?: number;
@@ -15,7 +15,8 @@ export class ZipArchive {
15
15
  timestamps: options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS),
16
16
  comment: options.comment,
17
17
  modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
18
- smartStore: options.smartStore ?? true
18
+ smartStore: options.smartStore ?? true,
19
+ noSort: options.noSort ?? false
19
20
  };
20
21
  }
21
22
  add(name, source, options) {
@@ -105,7 +106,8 @@ export class ZipArchive {
105
106
  timestamps: this._options.timestamps,
106
107
  modTime: this._options.modTime,
107
108
  comment: this._options.comment,
108
- smartStore: this._options.smartStore
109
+ smartStore: this._options.smartStore,
110
+ noSort: this._options.noSort
109
111
  });
110
112
  }
111
113
  const entries = await Promise.all(this._entries.map(async (e) => ({
@@ -120,7 +122,8 @@ export class ZipArchive {
120
122
  timestamps: this._options.timestamps,
121
123
  modTime: this._options.modTime,
122
124
  comment: this._options.comment,
123
- smartStore: this._options.smartStore
125
+ smartStore: this._options.smartStore,
126
+ noSort: this._options.noSort
124
127
  });
125
128
  }
126
129
  return collect(this.stream());
@@ -145,7 +148,8 @@ export class ZipArchive {
145
148
  timestamps: this._options.timestamps,
146
149
  modTime: this._options.modTime,
147
150
  comment: this._options.comment,
148
- smartStore: this._options.smartStore
151
+ smartStore: this._options.smartStore,
152
+ noSort: this._options.noSort
149
153
  });
150
154
  }
151
155
  async pipeTo(sink) {
@@ -42,6 +42,11 @@ export interface ZipOptions extends CompressOptions {
42
42
  * - default `timestamps` becomes "dos" (no UTC extra field)
43
43
  */
44
44
  reproducible?: boolean;
45
+ /**
46
+ * If true, entries are written in their original input order.
47
+ * If false (default), entries are sorted alphabetically by name.
48
+ */
49
+ noSort?: boolean;
45
50
  /**
46
51
  * Max number of entries to compress concurrently in `createZip()`.
47
52
  * This helps avoid zlib threadpool saturation / memory spikes with many files.
@@ -93,13 +93,23 @@ function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
93
93
  offset
94
94
  };
95
95
  }
96
- function appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings) {
97
- const processedEntry = buildProcessedEntry(entry, currentOffset, settings, compressedData, deflate);
98
- processedEntries.push(processedEntry);
99
- return {
100
- processedEntry,
101
- nextOffset: currentOffset + computeLocalRecordSize(processedEntry)
102
- };
96
+ /**
97
+ * Sort entries alphabetically by name (case-insensitive).
98
+ * Uses Schwartzian transform to avoid repeated decoding during sort.
99
+ */
100
+ function sortEntriesByName(entries) {
101
+ if (entries.length <= 1) {
102
+ return;
103
+ }
104
+ const decoder = new TextDecoder();
105
+ const decorated = entries.map(entry => ({
106
+ entry,
107
+ sortKey: decoder.decode(entry.name).toLowerCase()
108
+ }));
109
+ decorated.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
110
+ for (let i = 0; i < decorated.length; i++) {
111
+ entries[i] = decorated[i].entry;
112
+ }
103
113
  }
104
114
  function finalizeZip(processedEntries, zipComment) {
105
115
  // Assemble ZIP into a single buffer to reduce allocations and copying.
@@ -161,6 +171,7 @@ function finalizeZip(processedEntries, zipComment) {
161
171
  */
162
172
  export async function createZip(entries, options = {}) {
163
173
  const reproducible = options.reproducible ?? false;
174
+ const noSort = options.noSort ?? false;
164
175
  const level = options.level ?? DEFAULT_ZIP_LEVEL;
165
176
  const smartStore = options.smartStore ?? true;
166
177
  const concurrency = options.concurrency ?? 4;
@@ -196,7 +207,10 @@ export async function createZip(entries, options = {}) {
196
207
  });
197
208
  await Promise.all(workers);
198
209
  }
199
- // Compute offsets in original order.
210
+ if (!noSort) {
211
+ sortEntriesByName(processedEntries);
212
+ }
213
+ // Compute offsets after sorting.
200
214
  let currentOffset = 0;
201
215
  for (let i = 0; i < processedEntries.length; i++) {
202
216
  const processedEntry = processedEntries[i];
@@ -212,6 +226,7 @@ export async function createZip(entries, options = {}) {
212
226
  */
213
227
  export function createZipSync(entries, options = {}) {
214
228
  const reproducible = options.reproducible ?? false;
229
+ const noSort = options.noSort ?? false;
215
230
  const level = options.level ?? DEFAULT_ZIP_LEVEL;
216
231
  const smartStore = options.smartStore ?? true;
217
232
  const timestamps = options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS);
@@ -224,7 +239,6 @@ export function createZipSync(entries, options = {}) {
224
239
  };
225
240
  const thresholdBytes = options.thresholdBytes;
226
241
  const processedEntries = [];
227
- let currentOffset = 0;
228
242
  for (const entry of entries) {
229
243
  const entryLevel = entry.level ?? level;
230
244
  const compressOptions = {
@@ -232,8 +246,16 @@ export function createZipSync(entries, options = {}) {
232
246
  thresholdBytes
233
247
  };
234
248
  const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
235
- const result = appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings);
236
- currentOffset = result.nextOffset;
249
+ processedEntries.push(buildProcessedEntry(entry, 0, settings, compressedData, deflate));
250
+ }
251
+ if (!noSort) {
252
+ sortEntriesByName(processedEntries);
253
+ }
254
+ // Compute offsets after sorting.
255
+ let currentOffset = 0;
256
+ for (const processedEntry of processedEntries) {
257
+ processedEntry.offset = currentOffset;
258
+ currentOffset += computeLocalRecordSize(processedEntry);
237
259
  }
238
260
  return finalizeZip(processedEntries, zipComment);
239
261
  }
@@ -7,6 +7,7 @@ import type { PivotTableSource } from "@excel/pivot-table";
7
7
  interface ParsedCacheDefinitionModel {
8
8
  sourceRef?: string;
9
9
  sourceSheet?: string;
10
+ sourceTableName?: string;
10
11
  cacheFields: CacheFieldModel[];
11
12
  recordCount?: number;
12
13
  rId?: string;
@@ -71,7 +71,7 @@ class PivotCacheDefinitionXform extends BaseXform {
71
71
  * Render loaded pivot cache definition (preserving original structure)
72
72
  */
73
73
  renderLoaded(xmlStream, model) {
74
- const { cacheFields, sourceRef, sourceSheet, recordCount } = model;
74
+ const { cacheFields, sourceRef, sourceSheet, sourceTableName, recordCount } = model;
75
75
  xmlStream.openXml(XmlStream.StdDocAttributes);
76
76
  xmlStream.openNode(this.tag, {
77
77
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
@@ -83,10 +83,13 @@ class PivotCacheDefinitionXform extends BaseXform {
83
83
  recordCount: recordCount || cacheFields.length + 1
84
84
  });
85
85
  xmlStream.openNode("cacheSource", { type: "worksheet" });
86
- xmlStream.leafNode("worksheetSource", {
87
- ref: sourceRef,
88
- sheet: sourceSheet
89
- });
86
+ // worksheetSource supports two reference styles:
87
+ // 1. name: references a named Table
88
+ // 2. ref + sheet: references a cell range on a worksheet
89
+ const worksheetSourceAttrs = sourceTableName
90
+ ? { name: sourceTableName }
91
+ : { ref: sourceRef, sheet: sourceSheet };
92
+ xmlStream.leafNode("worksheetSource", worksheetSourceAttrs);
90
93
  xmlStream.closeNode();
91
94
  xmlStream.openNode("cacheFields", { count: cacheFields.length });
92
95
  xmlStream.writeXml(cacheFields
@@ -124,6 +127,7 @@ class PivotCacheDefinitionXform extends BaseXform {
124
127
  if (this.inCacheSource && this.model) {
125
128
  this.model.sourceRef = attributes.ref;
126
129
  this.model.sourceSheet = attributes.sheet;
130
+ this.model.sourceTableName = attributes.name;
127
131
  }
128
132
  break;
129
133
  case "cacheFields":
@@ -278,6 +278,30 @@ class ZipParser {
278
278
  hasEntry(path) {
279
279
  return this.entryMap.has(path);
280
280
  }
281
+ /**
282
+ * Get the number of child entries in a directory.
283
+ *
284
+ * Returns the count of entries whose paths start with the directory prefix,
285
+ * excluding the directory entry itself. For non-directory entries, returns 0.
286
+ *
287
+ * @param path - Directory path (with or without trailing slash)
288
+ * @returns Number of child entries
289
+ */
290
+ getChildCount(path) {
291
+ const entry = this.entryMap.get(path);
292
+ if (!entry?.isDirectory) {
293
+ return 0;
294
+ }
295
+ // Ensure prefix ends with "/" for correct matching
296
+ const prefix = entry.path.endsWith("/") ? entry.path : entry.path + "/";
297
+ let count = 0;
298
+ for (const e of this.entries) {
299
+ if (e.path.startsWith(prefix) && e !== entry) {
300
+ count++;
301
+ }
302
+ }
303
+ return count;
304
+ }
281
305
  /**
282
306
  * List all file paths
283
307
  */
@@ -19,7 +19,8 @@ class ZipArchive {
19
19
  timestamps: options.timestamps ?? (reproducible ? "dos" : defaults_1.DEFAULT_ZIP_TIMESTAMPS),
20
20
  comment: options.comment,
21
21
  modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
22
- smartStore: options.smartStore ?? true
22
+ smartStore: options.smartStore ?? true,
23
+ noSort: options.noSort ?? false
23
24
  };
24
25
  }
25
26
  add(name, source, options) {
@@ -109,7 +110,8 @@ class ZipArchive {
109
110
  timestamps: this._options.timestamps,
110
111
  modTime: this._options.modTime,
111
112
  comment: this._options.comment,
112
- smartStore: this._options.smartStore
113
+ smartStore: this._options.smartStore,
114
+ noSort: this._options.noSort
113
115
  });
114
116
  }
115
117
  const entries = await Promise.all(this._entries.map(async (e) => ({
@@ -124,7 +126,8 @@ class ZipArchive {
124
126
  timestamps: this._options.timestamps,
125
127
  modTime: this._options.modTime,
126
128
  comment: this._options.comment,
127
- smartStore: this._options.smartStore
129
+ smartStore: this._options.smartStore,
130
+ noSort: this._options.noSort
128
131
  });
129
132
  }
130
133
  return (0, archive_sink_1.collect)(this.stream());
@@ -149,7 +152,8 @@ class ZipArchive {
149
152
  timestamps: this._options.timestamps,
150
153
  modTime: this._options.modTime,
151
154
  comment: this._options.comment,
152
- smartStore: this._options.smartStore
155
+ smartStore: this._options.smartStore,
156
+ noSort: this._options.noSort
153
157
  });
154
158
  }
155
159
  async pipeTo(sink) {
@@ -96,13 +96,23 @@ function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
96
96
  offset
97
97
  };
98
98
  }
99
- function appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings) {
100
- const processedEntry = buildProcessedEntry(entry, currentOffset, settings, compressedData, deflate);
101
- processedEntries.push(processedEntry);
102
- return {
103
- processedEntry,
104
- nextOffset: currentOffset + computeLocalRecordSize(processedEntry)
105
- };
99
+ /**
100
+ * Sort entries alphabetically by name (case-insensitive).
101
+ * Uses Schwartzian transform to avoid repeated decoding during sort.
102
+ */
103
+ function sortEntriesByName(entries) {
104
+ if (entries.length <= 1) {
105
+ return;
106
+ }
107
+ const decoder = new TextDecoder();
108
+ const decorated = entries.map(entry => ({
109
+ entry,
110
+ sortKey: decoder.decode(entry.name).toLowerCase()
111
+ }));
112
+ decorated.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
113
+ for (let i = 0; i < decorated.length; i++) {
114
+ entries[i] = decorated[i].entry;
115
+ }
106
116
  }
107
117
  function finalizeZip(processedEntries, zipComment) {
108
118
  // Assemble ZIP into a single buffer to reduce allocations and copying.
@@ -164,6 +174,7 @@ function finalizeZip(processedEntries, zipComment) {
164
174
  */
165
175
  async function createZip(entries, options = {}) {
166
176
  const reproducible = options.reproducible ?? false;
177
+ const noSort = options.noSort ?? false;
167
178
  const level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
168
179
  const smartStore = options.smartStore ?? true;
169
180
  const concurrency = options.concurrency ?? 4;
@@ -199,7 +210,10 @@ async function createZip(entries, options = {}) {
199
210
  });
200
211
  await Promise.all(workers);
201
212
  }
202
- // Compute offsets in original order.
213
+ if (!noSort) {
214
+ sortEntriesByName(processedEntries);
215
+ }
216
+ // Compute offsets after sorting.
203
217
  let currentOffset = 0;
204
218
  for (let i = 0; i < processedEntries.length; i++) {
205
219
  const processedEntry = processedEntries[i];
@@ -215,6 +229,7 @@ async function createZip(entries, options = {}) {
215
229
  */
216
230
  function createZipSync(entries, options = {}) {
217
231
  const reproducible = options.reproducible ?? false;
232
+ const noSort = options.noSort ?? false;
218
233
  const level = options.level ?? defaults_1.DEFAULT_ZIP_LEVEL;
219
234
  const smartStore = options.smartStore ?? true;
220
235
  const timestamps = options.timestamps ?? (reproducible ? "dos" : defaults_1.DEFAULT_ZIP_TIMESTAMPS);
@@ -227,7 +242,6 @@ function createZipSync(entries, options = {}) {
227
242
  };
228
243
  const thresholdBytes = options.thresholdBytes;
229
244
  const processedEntries = [];
230
- let currentOffset = 0;
231
245
  for (const entry of entries) {
232
246
  const entryLevel = entry.level ?? level;
233
247
  const compressOptions = {
@@ -235,8 +249,16 @@ function createZipSync(entries, options = {}) {
235
249
  thresholdBytes
236
250
  };
237
251
  const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
238
- const result = appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings);
239
- currentOffset = result.nextOffset;
252
+ processedEntries.push(buildProcessedEntry(entry, 0, settings, compressedData, deflate));
253
+ }
254
+ if (!noSort) {
255
+ sortEntriesByName(processedEntries);
256
+ }
257
+ // Compute offsets after sorting.
258
+ let currentOffset = 0;
259
+ for (const processedEntry of processedEntries) {
260
+ processedEntry.offset = currentOffset;
261
+ currentOffset += computeLocalRecordSize(processedEntry);
240
262
  }
241
263
  return finalizeZip(processedEntries, zipComment);
242
264
  }
@@ -74,7 +74,7 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
74
74
  * Render loaded pivot cache definition (preserving original structure)
75
75
  */
76
76
  renderLoaded(xmlStream, model) {
77
- const { cacheFields, sourceRef, sourceSheet, recordCount } = model;
77
+ const { cacheFields, sourceRef, sourceSheet, sourceTableName, recordCount } = model;
78
78
  xmlStream.openXml(xml_stream_1.XmlStream.StdDocAttributes);
79
79
  xmlStream.openNode(this.tag, {
80
80
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
@@ -86,10 +86,13 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
86
86
  recordCount: recordCount || cacheFields.length + 1
87
87
  });
88
88
  xmlStream.openNode("cacheSource", { type: "worksheet" });
89
- xmlStream.leafNode("worksheetSource", {
90
- ref: sourceRef,
91
- sheet: sourceSheet
92
- });
89
+ // worksheetSource supports two reference styles:
90
+ // 1. name: references a named Table
91
+ // 2. ref + sheet: references a cell range on a worksheet
92
+ const worksheetSourceAttrs = sourceTableName
93
+ ? { name: sourceTableName }
94
+ : { ref: sourceRef, sheet: sourceSheet };
95
+ xmlStream.leafNode("worksheetSource", worksheetSourceAttrs);
93
96
  xmlStream.closeNode();
94
97
  xmlStream.openNode("cacheFields", { count: cacheFields.length });
95
98
  xmlStream.writeXml(cacheFields
@@ -127,6 +130,7 @@ class PivotCacheDefinitionXform extends base_xform_1.BaseXform {
127
130
  if (this.inCacheSource && this.model) {
128
131
  this.model.sourceRef = attributes.ref;
129
132
  this.model.sourceSheet = attributes.sheet;
133
+ this.model.sourceTableName = attributes.name;
130
134
  }
131
135
  break;
132
136
  case "cacheFields":
@@ -275,6 +275,30 @@ export class ZipParser {
275
275
  hasEntry(path) {
276
276
  return this.entryMap.has(path);
277
277
  }
278
+ /**
279
+ * Get the number of child entries in a directory.
280
+ *
281
+ * Returns the count of entries whose paths start with the directory prefix,
282
+ * excluding the directory entry itself. For non-directory entries, returns 0.
283
+ *
284
+ * @param path - Directory path (with or without trailing slash)
285
+ * @returns Number of child entries
286
+ */
287
+ getChildCount(path) {
288
+ const entry = this.entryMap.get(path);
289
+ if (!entry?.isDirectory) {
290
+ return 0;
291
+ }
292
+ // Ensure prefix ends with "/" for correct matching
293
+ const prefix = entry.path.endsWith("/") ? entry.path : entry.path + "/";
294
+ let count = 0;
295
+ for (const e of this.entries) {
296
+ if (e.path.startsWith(prefix) && e !== entry) {
297
+ count++;
298
+ }
299
+ }
300
+ return count;
301
+ }
278
302
  /**
279
303
  * List all file paths
280
304
  */
@@ -15,7 +15,8 @@ export class ZipArchive {
15
15
  timestamps: options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS),
16
16
  comment: options.comment,
17
17
  modTime: options.modTime ?? (reproducible ? REPRODUCIBLE_ZIP_MOD_TIME : new Date()),
18
- smartStore: options.smartStore ?? true
18
+ smartStore: options.smartStore ?? true,
19
+ noSort: options.noSort ?? false
19
20
  };
20
21
  }
21
22
  add(name, source, options) {
@@ -105,7 +106,8 @@ export class ZipArchive {
105
106
  timestamps: this._options.timestamps,
106
107
  modTime: this._options.modTime,
107
108
  comment: this._options.comment,
108
- smartStore: this._options.smartStore
109
+ smartStore: this._options.smartStore,
110
+ noSort: this._options.noSort
109
111
  });
110
112
  }
111
113
  const entries = await Promise.all(this._entries.map(async (e) => ({
@@ -120,7 +122,8 @@ export class ZipArchive {
120
122
  timestamps: this._options.timestamps,
121
123
  modTime: this._options.modTime,
122
124
  comment: this._options.comment,
123
- smartStore: this._options.smartStore
125
+ smartStore: this._options.smartStore,
126
+ noSort: this._options.noSort
124
127
  });
125
128
  }
126
129
  return collect(this.stream());
@@ -145,7 +148,8 @@ export class ZipArchive {
145
148
  timestamps: this._options.timestamps,
146
149
  modTime: this._options.modTime,
147
150
  comment: this._options.comment,
148
- smartStore: this._options.smartStore
151
+ smartStore: this._options.smartStore,
152
+ noSort: this._options.noSort
149
153
  });
150
154
  }
151
155
  async pipeTo(sink) {
@@ -93,13 +93,23 @@ function buildProcessedEntry(entry, offset, settings, compressedData, deflate) {
93
93
  offset
94
94
  };
95
95
  }
96
- function appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings) {
97
- const processedEntry = buildProcessedEntry(entry, currentOffset, settings, compressedData, deflate);
98
- processedEntries.push(processedEntry);
99
- return {
100
- processedEntry,
101
- nextOffset: currentOffset + computeLocalRecordSize(processedEntry)
102
- };
96
+ /**
97
+ * Sort entries alphabetically by name (case-insensitive).
98
+ * Uses Schwartzian transform to avoid repeated decoding during sort.
99
+ */
100
+ function sortEntriesByName(entries) {
101
+ if (entries.length <= 1) {
102
+ return;
103
+ }
104
+ const decoder = new TextDecoder();
105
+ const decorated = entries.map(entry => ({
106
+ entry,
107
+ sortKey: decoder.decode(entry.name).toLowerCase()
108
+ }));
109
+ decorated.sort((a, b) => a.sortKey.localeCompare(b.sortKey));
110
+ for (let i = 0; i < decorated.length; i++) {
111
+ entries[i] = decorated[i].entry;
112
+ }
103
113
  }
104
114
  function finalizeZip(processedEntries, zipComment) {
105
115
  // Assemble ZIP into a single buffer to reduce allocations and copying.
@@ -161,6 +171,7 @@ function finalizeZip(processedEntries, zipComment) {
161
171
  */
162
172
  export async function createZip(entries, options = {}) {
163
173
  const reproducible = options.reproducible ?? false;
174
+ const noSort = options.noSort ?? false;
164
175
  const level = options.level ?? DEFAULT_ZIP_LEVEL;
165
176
  const smartStore = options.smartStore ?? true;
166
177
  const concurrency = options.concurrency ?? 4;
@@ -196,7 +207,10 @@ export async function createZip(entries, options = {}) {
196
207
  });
197
208
  await Promise.all(workers);
198
209
  }
199
- // Compute offsets in original order.
210
+ if (!noSort) {
211
+ sortEntriesByName(processedEntries);
212
+ }
213
+ // Compute offsets after sorting.
200
214
  let currentOffset = 0;
201
215
  for (let i = 0; i < processedEntries.length; i++) {
202
216
  const processedEntry = processedEntries[i];
@@ -212,6 +226,7 @@ export async function createZip(entries, options = {}) {
212
226
  */
213
227
  export function createZipSync(entries, options = {}) {
214
228
  const reproducible = options.reproducible ?? false;
229
+ const noSort = options.noSort ?? false;
215
230
  const level = options.level ?? DEFAULT_ZIP_LEVEL;
216
231
  const smartStore = options.smartStore ?? true;
217
232
  const timestamps = options.timestamps ?? (reproducible ? "dos" : DEFAULT_ZIP_TIMESTAMPS);
@@ -224,7 +239,6 @@ export function createZipSync(entries, options = {}) {
224
239
  };
225
240
  const thresholdBytes = options.thresholdBytes;
226
241
  const processedEntries = [];
227
- let currentOffset = 0;
228
242
  for (const entry of entries) {
229
243
  const entryLevel = entry.level ?? level;
230
244
  const compressOptions = {
@@ -232,8 +246,16 @@ export function createZipSync(entries, options = {}) {
232
246
  thresholdBytes
233
247
  };
234
248
  const { compressedData, deflate } = compressEntryMaybeSync(entry, entryLevel, compressOptions, smartStore);
235
- const result = appendProcessedEntry(processedEntries, entry, compressedData, deflate, currentOffset, settings);
236
- currentOffset = result.nextOffset;
249
+ processedEntries.push(buildProcessedEntry(entry, 0, settings, compressedData, deflate));
250
+ }
251
+ if (!noSort) {
252
+ sortEntriesByName(processedEntries);
253
+ }
254
+ // Compute offsets after sorting.
255
+ let currentOffset = 0;
256
+ for (const processedEntry of processedEntries) {
257
+ processedEntry.offset = currentOffset;
258
+ currentOffset += computeLocalRecordSize(processedEntry);
237
259
  }
238
260
  return finalizeZip(processedEntries, zipComment);
239
261
  }
@@ -71,7 +71,7 @@ class PivotCacheDefinitionXform extends BaseXform {
71
71
  * Render loaded pivot cache definition (preserving original structure)
72
72
  */
73
73
  renderLoaded(xmlStream, model) {
74
- const { cacheFields, sourceRef, sourceSheet, recordCount } = model;
74
+ const { cacheFields, sourceRef, sourceSheet, sourceTableName, recordCount } = model;
75
75
  xmlStream.openXml(XmlStream.StdDocAttributes);
76
76
  xmlStream.openNode(this.tag, {
77
77
  ...PivotCacheDefinitionXform.PIVOT_CACHE_DEFINITION_ATTRIBUTES,
@@ -83,10 +83,13 @@ class PivotCacheDefinitionXform extends BaseXform {
83
83
  recordCount: recordCount || cacheFields.length + 1
84
84
  });
85
85
  xmlStream.openNode("cacheSource", { type: "worksheet" });
86
- xmlStream.leafNode("worksheetSource", {
87
- ref: sourceRef,
88
- sheet: sourceSheet
89
- });
86
+ // worksheetSource supports two reference styles:
87
+ // 1. name: references a named Table
88
+ // 2. ref + sheet: references a cell range on a worksheet
89
+ const worksheetSourceAttrs = sourceTableName
90
+ ? { name: sourceTableName }
91
+ : { ref: sourceRef, sheet: sourceSheet };
92
+ xmlStream.leafNode("worksheetSource", worksheetSourceAttrs);
90
93
  xmlStream.closeNode();
91
94
  xmlStream.openNode("cacheFields", { count: cacheFields.length });
92
95
  xmlStream.writeXml(cacheFields
@@ -124,6 +127,7 @@ class PivotCacheDefinitionXform extends BaseXform {
124
127
  if (this.inCacheSource && this.model) {
125
128
  this.model.sourceRef = attributes.ref;
126
129
  this.model.sourceSheet = attributes.sheet;
130
+ this.model.sourceTableName = attributes.name;
127
131
  }
128
132
  break;
129
133
  case "cacheFields":