@layerzerolabs/deterministic-zip 0.2.113 → 0.2.115

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.
@@ -10,6 +10,6 @@
10
10
  CLI Target: ES2023
11
11
  CLI Cleaning output folder
12
12
  ESM Build start
13
- ESM dist/index.js 2.09 KB
14
- ESM dist/index.js.map 5.65 KB
15
- ESM ⚡️ Build success in 90ms
13
+ ESM dist/index.js 2.74 KB
14
+ ESM dist/index.js.map 7.33 KB
15
+ ESM ⚡️ Build success in 86ms
@@ -2,7 +2,7 @@
2
2
  > @layerzerolabs/deterministic-zip@0.0.0 lint /home/runner/work/monorepo-internal/monorepo-internal/tools/vm-utils/common/deterministic-zip
3
3
  > eslint . --max-warnings 0 || (eslint . --fix --max-warnings 0 && false)
4
4
 
5
- (node:89679) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/monorepo-internal/monorepo-internal/eslint.config.js?mtime=1783384524622 is not specified and it doesn't parse as CommonJS.
5
+ (node:83928) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///home/runner/work/monorepo-internal/monorepo-internal/eslint.config.js?mtime=1783546211968 is not specified and it doesn't parse as CommonJS.
6
6
  Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
7
7
  To eliminate this warning, add "type": "module" to /home/runner/work/monorepo-internal/monorepo-internal/package.json.
8
8
  (Use `node --trace-warnings ...` to show where the warning was created)
@@ -7,11 +7,11 @@ Breaking changes might not follow SemVer, please pin Vitest's version when using
7
7
 
8
8
   RUN  v3.2.6 /home/runner/work/monorepo-internal/monorepo-internal/tools/vm-utils/common/deterministic-zip
9
9
 
10
- ✓ tests/deterministicZip.test.ts (3 tests) 21ms
10
+ ✓ tests/deterministicZip.test.ts (8 tests) 30ms
11
11
 
12
12
   Test Files  1 passed (1)
13
-  Tests  3 passed (3)
13
+  Tests  8 passed (8)
14
14
  Type Errors  no errors
15
-  Start at  00:39:27
16
-  Duration  443ms (transform 106ms, setup 0ms, collect 75ms, tests 21ms, environment 0ms, prepare 155ms)
15
+  Start at  21:34:01
16
+  Duration  376ms (transform 67ms, setup 0ms, collect 66ms, tests 30ms, environment 0ms, prepare 107ms)
17
17
 
package/dist/index.d.ts CHANGED
@@ -1,23 +1,41 @@
1
1
  /** fflate deflate compression levels (0 = store, 9 = max). */
2
2
  export type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
3
+ export declare enum ZipEntryType {
4
+ File = "file",
5
+ Directory = "directory"
6
+ }
7
+ export interface ZipEntryInfo {
8
+ /** POSIX-relative path from the zip root, no trailing slash. */
9
+ relPath: string;
10
+ type: ZipEntryType;
11
+ }
12
+ export type PathInclusionPredicate = (entry: ZipEntryInfo) => boolean;
3
13
  export interface ZipDirectoryOptions {
14
+ /** Deflate compression level passed to fflate. Defaults to `6`. */
4
15
  level?: DeflateLevel;
16
+ /** Modification time stamped onto every ZIP entry. */
5
17
  mtime?: Date;
18
+ /**
19
+ * Called for every file and directory. Return `false` to drop the entry;
20
+ * `false` on a directory prunes its entire subtree (children are never
21
+ * visited). Omitted = include everything.
22
+ *
23
+ * Symlinks are not supported and always throw.
24
+ */
25
+ pathInclusionPredicate?: PathInclusionPredicate;
6
26
  }
7
27
  /**
8
28
  * Fixed modification time stamped onto every archive entry.
9
29
  *
10
- * fflate derives each ZIP entry's DOS timestamp from the local-time components
11
- * of the supplied `mtime` (getFullYear/getMonth/getHours/...). Constructing the
12
- * date from local fields, rather than a UTC instant, makes those components
13
- * identical on every machine regardless of the builder's timezone.
30
+ * fflate writes ZIP timestamps from local date fields, so this value is built
31
+ * with local fields to keep the encoded timestamp stable across timezones.
14
32
  */
15
33
  export declare const DEFAULT_DETERMINISTIC_ZIP_MTIME: Date;
16
34
  /**
17
35
  * Build a byte-deterministic ZIP of `sourceDir`.
18
36
  *
19
- * Entry order, timestamps, and compression implementation are pinned so output
20
- * bytes remain stable across machines.
37
+ * Entry order and timestamps are normalized so equivalent directories produce
38
+ * stable archive bytes.
21
39
  */
22
40
  export declare const zipDirectoryToUint8Array: (sourceDir: string, options?: ZipDirectoryOptions) => Promise<Uint8Array>;
23
41
  export declare const zipDirectoryToBuffer: (sourceDir: string, options?: ZipDirectoryOptions) => Promise<Buffer>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEjE,MAAM,WAAW,mBAAmB;IAChC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,KAAK,CAAC,EAAE,IAAI,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,MAAiC,CAAC;AA+B9E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACjC,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,UAAU,CAepB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC7B,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,MAAM,CAAoE,CAAC;AAEtF,eAAO,MAAM,oBAAoB,GAC7B,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,MAAM,CAAwE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,8DAA8D;AAC9D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEjE,oBAAY,YAAY;IACpB,IAAI,SAAS;IACb,SAAS,cAAc;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC;AAEtE,MAAM,WAAW,mBAAmB;IAChC,mEAAmE;IACnE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,sDAAsD;IACtD,KAAK,CAAC,EAAE,IAAI,CAAC;IACb;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;CACnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,+BAA+B,MAAiC,CAAC;AAyC9E;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,GACjC,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,UAAU,CAiBpB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC7B,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,MAAM,CAAoE,CAAC;AAEtF,eAAO,MAAM,oBAAoB,GAC7B,WAAW,MAAM,EACjB,UAAU,mBAAmB,KAC9B,OAAO,CAAC,MAAM,CAAwE,CAAC"}
package/dist/index.js CHANGED
@@ -4,8 +4,14 @@ import path from 'path';
4
4
 
5
5
  var __defProp = Object.defineProperty;
6
6
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var ZipEntryType = /* @__PURE__ */ (function(ZipEntryType2) {
8
+ ZipEntryType2["File"] = "file";
9
+ ZipEntryType2["Directory"] = "directory";
10
+ return ZipEntryType2;
11
+ })({});
7
12
  var DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);
8
- var collectSortedEntries = /* @__PURE__ */ __name(async (root) => {
13
+ var INCLUDE_EVERYTHING = /* @__PURE__ */ __name(() => true, "INCLUDE_EVERYTHING");
14
+ var collectSortedEntries = /* @__PURE__ */ __name(async (root, includeEntry) => {
9
15
  const zipEntries = [];
10
16
  const walk = /* @__PURE__ */ __name(async (rel) => {
11
17
  const entries = await fs.readdir(path.join(root, rel), {
@@ -14,16 +20,26 @@ var collectSortedEntries = /* @__PURE__ */ __name(async (root) => {
14
20
  for (const entry of entries) {
15
21
  const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;
16
22
  if (entry.isDirectory()) {
23
+ if (!includeEntry({
24
+ relPath: childRel,
25
+ type: "directory"
26
+ })) continue;
17
27
  zipEntries.push({
18
28
  relPath: `${childRel}/`,
19
29
  isDirectory: true
20
30
  });
21
31
  await walk(childRel);
22
32
  } else if (entry.isFile()) {
33
+ if (!includeEntry({
34
+ relPath: childRel,
35
+ type: "file"
36
+ })) continue;
23
37
  zipEntries.push({
24
38
  relPath: childRel,
25
39
  isDirectory: false
26
40
  });
41
+ } else if (entry.isSymbolicLink()) {
42
+ throw new Error(`symlink entries are not supported: ${childRel}`);
27
43
  }
28
44
  }
29
45
  }, "walk");
@@ -31,7 +47,7 @@ var collectSortedEntries = /* @__PURE__ */ __name(async (root) => {
31
47
  return zipEntries.sort((a, b) => a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0);
32
48
  }, "collectSortedEntries");
33
49
  var zipDirectoryToUint8Array = /* @__PURE__ */ __name(async (sourceDir, options) => {
34
- const entries = await collectSortedEntries(sourceDir);
50
+ const entries = await collectSortedEntries(sourceDir, options?.pathInclusionPredicate ?? INCLUDE_EVERYTHING);
35
51
  const fileMap = {};
36
52
  for (const entry of entries) {
37
53
  fileMap[entry.relPath] = entry.isDirectory ? new Uint8Array(0) : new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));
@@ -44,6 +60,6 @@ var zipDirectoryToUint8Array = /* @__PURE__ */ __name(async (sourceDir, options)
44
60
  var zipDirectoryToBuffer = /* @__PURE__ */ __name(async (sourceDir, options) => Buffer.from(await zipDirectoryToUint8Array(sourceDir, options)), "zipDirectoryToBuffer");
45
61
  var zipDirectoryToBase64 = /* @__PURE__ */ __name(async (sourceDir, options) => (await zipDirectoryToBuffer(sourceDir, options)).toString("base64"), "zipDirectoryToBase64");
46
62
 
47
- export { DEFAULT_DETERMINISTIC_ZIP_MTIME, zipDirectoryToBase64, zipDirectoryToBuffer, zipDirectoryToUint8Array };
63
+ export { DEFAULT_DETERMINISTIC_ZIP_MTIME, ZipEntryType, zipDirectoryToBase64, zipDirectoryToBuffer, zipDirectoryToUint8Array };
48
64
  //# sourceMappingURL=index.js.map
49
65
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["DEFAULT_DETERMINISTIC_ZIP_MTIME","Date","collectSortedEntries","root","zipEntries","walk","rel","entries","fs","readdir","path","join","withFileTypes","entry","childRel","posix","name","isDirectory","push","relPath","isFile","sort","a","b","zipDirectoryToUint8Array","sourceDir","options","fileMap","Uint8Array","readFile","zipSync","level","mtime","zipDirectoryToBuffer","Buffer","from","zipDirectoryToBase64","toString"],"mappings":";;;;;;AAoBO,IAAMA,+BAAAA,GAAkC,IAAIC,IAAAA,CAAK,IAAA,EAAM,GAAG,EAAA,EAAI,CAAA,EAAG,GAAG,CAAA;AAW3E,IAAMC,oBAAAA,iCAA8BC,IAAAA,KAAAA;AAChC,EAAA,MAAMC,aAAsC,EAAA;AAE5C,EAAA,MAAMC,IAAAA,iCAAcC,GAAAA,KAAAA;AAChB,IAAA,MAAMC,OAAAA,GAAU,MAAMC,EAAAA,CAAGC,OAAAA,CAAQC,KAAKC,IAAAA,CAAKR,IAAAA,EAAMG,GAAAA,CAAAA,EAAM;MAAEM,aAAAA,EAAe;KAAK,CAAA;AAC7E,IAAA,KAAA,MAAWC,SAASN,OAAAA,EAAS;AACzB,MAAA,MAAMO,QAAAA,GAAWR,MAAMI,IAAAA,CAAKK,KAAAA,CAAMJ,KAAKL,GAAAA,EAAKO,KAAAA,CAAMG,IAAI,CAAA,GAAIH,KAAAA,CAAMG,IAAAA;AAChE,MAAA,IAAIH,KAAAA,CAAMI,aAAW,EAAI;AACrBb,QAAAA,UAAAA,CAAWc,IAAAA,CAAK;AAAEC,UAAAA,OAAAA,EAAS,GAAGL,QAAAA,CAAAA,CAAAA,CAAAA;UAAaG,WAAAA,EAAa;SAAK,CAAA;AAC7D,QAAA,MAAMZ,KAAKS,QAAAA,CAAAA;MACf,CAAA,MAAA,IAAWD,KAAAA,CAAMO,QAAM,EAAI;AACvBhB,QAAAA,UAAAA,CAAWc,IAAAA,CAAK;UAAEC,OAAAA,EAASL,QAAAA;UAAUG,WAAAA,EAAa;SAAM,CAAA;AAC5D,MAAA;AACJ,IAAA;EACJ,CAAA,EAXa,MAAA,CAAA;AAab,EAAA,MAAMZ,KAAK,EAAA,CAAA;AACX,EAAA,OAAOD,UAAAA,CAAWiB,IAAAA,CAAK,CAACC,CAAAA,EAAGC,MAAOD,CAAAA,CAAEH,OAAAA,GAAUI,CAAAA,CAAEJ,OAAAA,GAAU,KAAKG,CAAAA,CAAEH,OAAAA,GAAUI,CAAAA,CAAEJ,OAAAA,GAAU,IAAI,CAAA,CAAA;AAC/F,CAAA,EAlB6B,sBAAA,CAAA;AA0BtB,IAAMK,wBAAAA,mBAA2B,MAAA,CAAA,OACpCC,SAAAA,EACAC,OAAAA,KAAAA;AAEA,EAAA,MAAMnB,OAAAA,GAAU,MAAML,oBAAAA,CAAqBuB,SAAAA,CAAAA;AAC3C,EAAA,MAAME,UAAoB,EAAC;AAE3B,EAAA,KAAA,MAAWd,SAASN,OAAAA,EAAS;AAEzBoB,IAAAA,OAAAA,CAAQd,KAAAA,CAAMM,OAAO,CAAA,GAAIN,KAAAA,CAAMI,cACzB,IAAIW,UAAAA,CAAW,CAAA,CAAA,GACf,IAAIA,WAAW,MAAMpB,EAAAA,CAAGqB,SAASnB,IAAAA,CAAKC,IAAAA,CAAKc,WAAWZ,KAAAA,CAAMM,OAAO,CAAA,CAAA,CAAA;AAC7E,EAAA;AAEA,EAAA,OAAOW,QAAQH,OAAAA,EAAS;AACpBI,IAAAA,KAAAA,EAAOL,SAASK,KAAAA,IAAS,CAAA;AACzBC,IAAAA,KAAAA,EAAON,SAASM,KAAAA,IAAShC;GAC7B,CAAA;AACJ,CAAA,EAlBwC,0BAAA;AAoBjC,IAAMiC,oBAAAA,mBAAuB,MAAA,CAAA,OAChCR,SAAAA,EACAC,OAAAA,KACkBQ,MAAAA,CAAOC,IAAAA,CAAK,MAAMX,wBAAAA,CAAyBC,SAAAA,EAAWC,OAAAA,CAAAA,CAAAA,EAHxC,sBAAA;AAK7B,IAAMU,oBAAAA,mBAAuB,MAAA,CAAA,OAChCX,SAAAA,EACAC,OAAAA,KAAAA,CACmB,MAAMO,oBAAAA,CAAqBR,SAAAA,EAAWC,OAAAA,CAAAA,EAAUW,QAAAA,CAAS,QAAA,CAAA,EAH5C,sBAAA","file":"index.js","sourcesContent":["import { type Zippable, zipSync } from 'fflate';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\n/** fflate deflate compression levels (0 = store, 9 = max). */\nexport type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;\n\nexport interface ZipDirectoryOptions {\n level?: DeflateLevel;\n mtime?: Date;\n}\n\n/**\n * Fixed modification time stamped onto every archive entry.\n *\n * fflate derives each ZIP entry's DOS timestamp from the local-time components\n * of the supplied `mtime` (getFullYear/getMonth/getHours/...). Constructing the\n * date from local fields, rather than a UTC instant, makes those components\n * identical on every machine regardless of the builder's timezone.\n */\nexport const DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);\n\ntype DeterministicZipEntry = { relPath: string; isDirectory: boolean };\n\n/**\n * Recursively collect every file and directory under `root`, sorted by\n * POSIX-relative path (UTF-16 code unit order).\n *\n * The explicit sort is required for reproducibility because filesystem\n * `readdir` order is not stable across operating systems.\n */\nconst collectSortedEntries = async (root: string): Promise<DeterministicZipEntry[]> => {\n const zipEntries: DeterministicZipEntry[] = [];\n\n const walk = async (rel: string): Promise<void> => {\n const entries = await fs.readdir(path.join(root, rel), { withFileTypes: true });\n for (const entry of entries) {\n const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;\n if (entry.isDirectory()) {\n zipEntries.push({ relPath: `${childRel}/`, isDirectory: true });\n await walk(childRel);\n } else if (entry.isFile()) {\n zipEntries.push({ relPath: childRel, isDirectory: false });\n }\n }\n };\n\n await walk('');\n return zipEntries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));\n};\n\n/**\n * Build a byte-deterministic ZIP of `sourceDir`.\n *\n * Entry order, timestamps, and compression implementation are pinned so output\n * bytes remain stable across machines.\n */\nexport const zipDirectoryToUint8Array = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<Uint8Array> => {\n const entries = await collectSortedEntries(sourceDir);\n const fileMap: Zippable = {};\n\n for (const entry of entries) {\n // A directory is a zero-length entry whose name already ends in `/`.\n fileMap[entry.relPath] = entry.isDirectory\n ? new Uint8Array(0)\n : new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));\n }\n\n return zipSync(fileMap, {\n level: options?.level ?? 6,\n mtime: options?.mtime ?? DEFAULT_DETERMINISTIC_ZIP_MTIME,\n });\n};\n\nexport const zipDirectoryToBuffer = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<Buffer> => Buffer.from(await zipDirectoryToUint8Array(sourceDir, options));\n\nexport const zipDirectoryToBase64 = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<string> => (await zipDirectoryToBuffer(sourceDir, options)).toString('base64');\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["ZipEntryType","DEFAULT_DETERMINISTIC_ZIP_MTIME","Date","INCLUDE_EVERYTHING","collectSortedEntries","root","includeEntry","zipEntries","walk","rel","entries","fs","readdir","path","join","withFileTypes","entry","childRel","posix","name","isDirectory","relPath","type","push","isFile","isSymbolicLink","Error","sort","a","b","zipDirectoryToUint8Array","sourceDir","options","pathInclusionPredicate","fileMap","Uint8Array","readFile","zipSync","level","mtime","zipDirectoryToBuffer","Buffer","from","zipDirectoryToBase64","toString"],"mappings":"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","file":"index.js","sourcesContent":["import { type Zippable, zipSync } from 'fflate';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\n/** fflate deflate compression levels (0 = store, 9 = max). */\nexport type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;\n\nexport enum ZipEntryType {\n File = 'file',\n Directory = 'directory',\n}\n\nexport interface ZipEntryInfo {\n /** POSIX-relative path from the zip root, no trailing slash. */\n relPath: string;\n type: ZipEntryType;\n}\n\nexport type PathInclusionPredicate = (entry: ZipEntryInfo) => boolean;\n\nexport interface ZipDirectoryOptions {\n /** Deflate compression level passed to fflate. Defaults to `6`. */\n level?: DeflateLevel;\n /** Modification time stamped onto every ZIP entry. */\n mtime?: Date;\n /**\n * Called for every file and directory. Return `false` to drop the entry;\n * `false` on a directory prunes its entire subtree (children are never\n * visited). Omitted = include everything.\n *\n * Symlinks are not supported and always throw.\n */\n pathInclusionPredicate?: PathInclusionPredicate;\n}\n\n/**\n * Fixed modification time stamped onto every archive entry.\n *\n * fflate writes ZIP timestamps from local date fields, so this value is built\n * with local fields to keep the encoded timestamp stable across timezones.\n */\nexport const DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);\n\nconst INCLUDE_EVERYTHING: PathInclusionPredicate = () => true;\n\ntype DeterministicZipEntry = { relPath: string; isDirectory: boolean };\n\n/**\n * Recursively collect every included file and directory under `root`, sorted\n * by POSIX-relative path (UTF-16 code unit order).\n *\n * Filesystem `readdir` order varies by platform, so sorting is required for\n * reproducible archives.\n */\nconst collectSortedEntries = async (\n root: string,\n includeEntry: PathInclusionPredicate,\n): Promise<DeterministicZipEntry[]> => {\n const zipEntries: DeterministicZipEntry[] = [];\n\n const walk = async (rel: string): Promise<void> => {\n const entries = await fs.readdir(path.join(root, rel), { withFileTypes: true });\n for (const entry of entries) {\n const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;\n if (entry.isDirectory()) {\n if (!includeEntry({ relPath: childRel, type: ZipEntryType.Directory })) continue;\n // fflate treats trailing-slash zero-length entries as directories.\n zipEntries.push({ relPath: `${childRel}/`, isDirectory: true });\n await walk(childRel);\n } else if (entry.isFile()) {\n if (!includeEntry({ relPath: childRel, type: ZipEntryType.File })) continue;\n zipEntries.push({ relPath: childRel, isDirectory: false });\n } else if (entry.isSymbolicLink()) {\n throw new Error(`symlink entries are not supported: ${childRel}`);\n }\n }\n };\n\n await walk('');\n return zipEntries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));\n};\n\n/**\n * Build a byte-deterministic ZIP of `sourceDir`.\n *\n * Entry order and timestamps are normalized so equivalent directories produce\n * stable archive bytes.\n */\nexport const zipDirectoryToUint8Array = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<Uint8Array> => {\n const entries = await collectSortedEntries(\n sourceDir,\n options?.pathInclusionPredicate ?? INCLUDE_EVERYTHING,\n );\n const fileMap: Zippable = {};\n\n for (const entry of entries) {\n fileMap[entry.relPath] = entry.isDirectory\n ? new Uint8Array(0)\n : new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));\n }\n\n return zipSync(fileMap, {\n level: options?.level ?? 6,\n mtime: options?.mtime ?? DEFAULT_DETERMINISTIC_ZIP_MTIME,\n });\n};\n\nexport const zipDirectoryToBuffer = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<Buffer> => Buffer.from(await zipDirectoryToUint8Array(sourceDir, options));\n\nexport const zipDirectoryToBase64 = async (\n sourceDir: string,\n options?: ZipDirectoryOptions,\n): Promise<string> => (await zipDirectoryToBuffer(sourceDir, options)).toString('base64');\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@layerzerolabs/deterministic-zip",
3
- "version": "0.2.113",
3
+ "version": "0.2.115",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,8 +18,8 @@
18
18
  "devDependencies": {
19
19
  "tsup": "^8.4.0",
20
20
  "vitest": "^3.2.6",
21
- "@layerzerolabs/tsup-configuration": "0.2.113",
22
- "@layerzerolabs/typescript-configuration": "0.2.113"
21
+ "@layerzerolabs/tsup-configuration": "0.2.115",
22
+ "@layerzerolabs/typescript-configuration": "0.2.115"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "restricted",
package/src/index.ts CHANGED
@@ -5,31 +5,57 @@ import path from 'node:path';
5
5
  /** fflate deflate compression levels (0 = store, 9 = max). */
6
6
  export type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
7
7
 
8
+ export enum ZipEntryType {
9
+ File = 'file',
10
+ Directory = 'directory',
11
+ }
12
+
13
+ export interface ZipEntryInfo {
14
+ /** POSIX-relative path from the zip root, no trailing slash. */
15
+ relPath: string;
16
+ type: ZipEntryType;
17
+ }
18
+
19
+ export type PathInclusionPredicate = (entry: ZipEntryInfo) => boolean;
20
+
8
21
  export interface ZipDirectoryOptions {
22
+ /** Deflate compression level passed to fflate. Defaults to `6`. */
9
23
  level?: DeflateLevel;
24
+ /** Modification time stamped onto every ZIP entry. */
10
25
  mtime?: Date;
26
+ /**
27
+ * Called for every file and directory. Return `false` to drop the entry;
28
+ * `false` on a directory prunes its entire subtree (children are never
29
+ * visited). Omitted = include everything.
30
+ *
31
+ * Symlinks are not supported and always throw.
32
+ */
33
+ pathInclusionPredicate?: PathInclusionPredicate;
11
34
  }
12
35
 
13
36
  /**
14
37
  * Fixed modification time stamped onto every archive entry.
15
38
  *
16
- * fflate derives each ZIP entry's DOS timestamp from the local-time components
17
- * of the supplied `mtime` (getFullYear/getMonth/getHours/...). Constructing the
18
- * date from local fields, rather than a UTC instant, makes those components
19
- * identical on every machine regardless of the builder's timezone.
39
+ * fflate writes ZIP timestamps from local date fields, so this value is built
40
+ * with local fields to keep the encoded timestamp stable across timezones.
20
41
  */
21
42
  export const DEFAULT_DETERMINISTIC_ZIP_MTIME = new Date(2026, 5, 15, 0, 0, 0);
22
43
 
44
+ const INCLUDE_EVERYTHING: PathInclusionPredicate = () => true;
45
+
23
46
  type DeterministicZipEntry = { relPath: string; isDirectory: boolean };
24
47
 
25
48
  /**
26
- * Recursively collect every file and directory under `root`, sorted by
27
- * POSIX-relative path (UTF-16 code unit order).
49
+ * Recursively collect every included file and directory under `root`, sorted
50
+ * by POSIX-relative path (UTF-16 code unit order).
28
51
  *
29
- * The explicit sort is required for reproducibility because filesystem
30
- * `readdir` order is not stable across operating systems.
52
+ * Filesystem `readdir` order varies by platform, so sorting is required for
53
+ * reproducible archives.
31
54
  */
32
- const collectSortedEntries = async (root: string): Promise<DeterministicZipEntry[]> => {
55
+ const collectSortedEntries = async (
56
+ root: string,
57
+ includeEntry: PathInclusionPredicate,
58
+ ): Promise<DeterministicZipEntry[]> => {
33
59
  const zipEntries: DeterministicZipEntry[] = [];
34
60
 
35
61
  const walk = async (rel: string): Promise<void> => {
@@ -37,10 +63,15 @@ const collectSortedEntries = async (root: string): Promise<DeterministicZipEntry
37
63
  for (const entry of entries) {
38
64
  const childRel = rel ? path.posix.join(rel, entry.name) : entry.name;
39
65
  if (entry.isDirectory()) {
66
+ if (!includeEntry({ relPath: childRel, type: ZipEntryType.Directory })) continue;
67
+ // fflate treats trailing-slash zero-length entries as directories.
40
68
  zipEntries.push({ relPath: `${childRel}/`, isDirectory: true });
41
69
  await walk(childRel);
42
70
  } else if (entry.isFile()) {
71
+ if (!includeEntry({ relPath: childRel, type: ZipEntryType.File })) continue;
43
72
  zipEntries.push({ relPath: childRel, isDirectory: false });
73
+ } else if (entry.isSymbolicLink()) {
74
+ throw new Error(`symlink entries are not supported: ${childRel}`);
44
75
  }
45
76
  }
46
77
  };
@@ -52,18 +83,20 @@ const collectSortedEntries = async (root: string): Promise<DeterministicZipEntry
52
83
  /**
53
84
  * Build a byte-deterministic ZIP of `sourceDir`.
54
85
  *
55
- * Entry order, timestamps, and compression implementation are pinned so output
56
- * bytes remain stable across machines.
86
+ * Entry order and timestamps are normalized so equivalent directories produce
87
+ * stable archive bytes.
57
88
  */
58
89
  export const zipDirectoryToUint8Array = async (
59
90
  sourceDir: string,
60
91
  options?: ZipDirectoryOptions,
61
92
  ): Promise<Uint8Array> => {
62
- const entries = await collectSortedEntries(sourceDir);
93
+ const entries = await collectSortedEntries(
94
+ sourceDir,
95
+ options?.pathInclusionPredicate ?? INCLUDE_EVERYTHING,
96
+ );
63
97
  const fileMap: Zippable = {};
64
98
 
65
99
  for (const entry of entries) {
66
- // A directory is a zero-length entry whose name already ends in `/`.
67
100
  fileMap[entry.relPath] = entry.isDirectory
68
101
  ? new Uint8Array(0)
69
102
  : new Uint8Array(await fs.readFile(path.join(sourceDir, entry.relPath)));
@@ -4,7 +4,12 @@ import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { afterEach, describe, expect, it } from 'vitest';
6
6
 
7
- import { zipDirectoryToBase64, zipDirectoryToUint8Array } from '../src';
7
+ import {
8
+ zipDirectoryToBase64,
9
+ zipDirectoryToUint8Array,
10
+ type ZipEntryInfo,
11
+ ZipEntryType,
12
+ } from '../src';
8
13
 
9
14
  const testDirs: string[] = [];
10
15
 
@@ -56,4 +61,88 @@ describe('deterministic zip', () => {
56
61
 
57
62
  expect(Object.keys(unzipped)).toEqual(['empty/']);
58
63
  });
64
+
65
+ describe('pathInclusionPredicate', () => {
66
+ it('includes everything when no predicate is given', async () => {
67
+ const dir = await createTempDir();
68
+ await fs.writeFile(path.join(dir, '.env'), 'SECRET=1');
69
+ await fs.writeFile(path.join(dir, 'id_rsa'), 'key');
70
+
71
+ const unzipped = unzipSync(await zipDirectoryToUint8Array(dir));
72
+
73
+ expect(Object.keys(unzipped)).toEqual(['.env', 'id_rsa']);
74
+ });
75
+
76
+ it('filters files through the predicate while directories remain', async () => {
77
+ const dir = await createTempDir();
78
+ await fs.mkdir(path.join(dir, 'sources'), { recursive: true });
79
+ await fs.writeFile(path.join(dir, 'sources', 'module.move'), 'module a::m {}');
80
+ await fs.writeFile(path.join(dir, 'sources', '.env'), 'SECRET=1');
81
+ await fs.writeFile(path.join(dir, 'Move.toml'), '[package]\n');
82
+ await fs.writeFile(path.join(dir, 'notes.txt'), 'n');
83
+
84
+ const unzipped = unzipSync(
85
+ await zipDirectoryToUint8Array(dir, {
86
+ pathInclusionPredicate: ({ relPath, type }) =>
87
+ type === ZipEntryType.Directory ||
88
+ (type === ZipEntryType.File &&
89
+ /(^|\/)Move\.(toml|lock)$|\.move$/.test(relPath)),
90
+ }),
91
+ );
92
+
93
+ expect(Object.keys(unzipped)).toEqual(['Move.toml', 'sources/', 'sources/module.move']);
94
+ });
95
+
96
+ it('prunes an excluded directory subtree without visiting its children', async () => {
97
+ const dir = await createTempDir();
98
+ await fs.mkdir(path.join(dir, 'tests', 'nested'), { recursive: true });
99
+ await fs.writeFile(path.join(dir, 'tests', 't.move'), 'x');
100
+ await fs.writeFile(path.join(dir, 'tests', 'nested', 'u.move'), 'y');
101
+ await fs.writeFile(path.join(dir, 'keep.move'), 'k');
102
+
103
+ const seen: string[] = [];
104
+ const unzipped = unzipSync(
105
+ await zipDirectoryToUint8Array(dir, {
106
+ pathInclusionPredicate: (entry) => {
107
+ seen.push(entry.relPath);
108
+ return entry.relPath !== 'tests';
109
+ },
110
+ }),
111
+ );
112
+
113
+ expect(Object.keys(unzipped)).toEqual(['keep.move']);
114
+ // Children of the pruned directory are never offered to the predicate.
115
+ expect([...seen].sort()).toEqual(['keep.move', 'tests']);
116
+ });
117
+
118
+ it('passes a POSIX relPath without trailing slash and the entry type', async () => {
119
+ const dir = await createTempDir();
120
+ await fs.mkdir(path.join(dir, 'a'), { recursive: true });
121
+ await fs.writeFile(path.join(dir, 'a', 'b.txt'), 'b');
122
+
123
+ const seen: ZipEntryInfo[] = [];
124
+ await zipDirectoryToUint8Array(dir, {
125
+ pathInclusionPredicate: (entry) => {
126
+ seen.push(entry);
127
+ return true;
128
+ },
129
+ });
130
+
131
+ expect(seen).toEqual([
132
+ { relPath: 'a', type: ZipEntryType.Directory },
133
+ { relPath: 'a/b.txt', type: ZipEntryType.File },
134
+ ]);
135
+ });
136
+ });
137
+
138
+ describe('symlinks', () => {
139
+ it('throws when a symlink is encountered', async () => {
140
+ const dir = await createTempDir();
141
+ await fs.symlink('missing-target', path.join(dir, 'link.txt'));
142
+
143
+ await expect(zipDirectoryToUint8Array(dir)).rejects.toThrow(
144
+ 'symlink entries are not supported: link.txt',
145
+ );
146
+ });
147
+ });
59
148
  });