@uniweb/build 1.8.0 → 1.8.1

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 (2) hide show
  1. package/package.json +6 -6
  2. package/src/uwx/zip.js +39 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,11 +56,11 @@
56
56
  "js-yaml": "^4.1.0",
57
57
  "sharp": "^0.35.3",
58
58
  "yaml": "^2.5.0",
59
- "@uniweb/content-writer": "~1.3.0",
60
59
  "@uniweb/schemas": "~1.6.5",
61
- "@uniweb/content-reader": "~1.5.4",
62
- "@uniweb/projections": "~1.1.6",
63
60
  "@uniweb/semantic-parser": "~1.12.2",
61
+ "@uniweb/projections": "~1.1.6",
62
+ "@uniweb/content-writer": "~1.3.0",
63
+ "@uniweb/content-reader": "~1.5.4",
64
64
  "@uniweb/theming": "~1.2.1"
65
65
  },
66
66
  "optionalDependencies": {
@@ -73,8 +73,8 @@
73
73
  "@tailwindcss/vite": "^4.0.0",
74
74
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
75
75
  "vite-plugin-svgr": "^4.0.0",
76
- "@uniweb/runtime": "~1.1.15",
77
- "@uniweb/core": "~1.4.0"
76
+ "@uniweb/core": "~1.4.0",
77
+ "@uniweb/runtime": "~1.1.15"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
package/src/uwx/zip.js CHANGED
@@ -1,29 +1,40 @@
1
1
  // Minimal, zero-dependency ZIP writer/reader for .uwx containers.
2
2
  //
3
- // DESIGN DECISION: our WRITER (`createZip`) emits Stored only (compression
4
- // method 0), no Deflate. A .uwx container is a ZIP; compression is an
5
- // optimization, not part of the contract, and every standard ZIP reader
6
- // handles Stored entries. Staying Stored-only on write removes a class of
7
- // cross-tool byte asymmetry. The READER (`readZip`) additionally inflates
8
- // Deflate (method 8) entries — the backend's pull `.uwx` Deflates larger
9
- // entities, and the framework must read what the backend produces.
3
+ // DESIGN DECISION: our WRITER (`createZip`) Deflates (compression method 8)
4
+ // every entry that Deflate makes smaller, and Stores (method 0) the rest — an
5
+ // empty or tiny one. A .uwx travels as a request body, and a backend measures
6
+ // a request by its size AS SENT, so the container's own compression is what
7
+ // carries a large site: a 3.6 MB page tree Deflates to about 0.5 MB. It is the
8
+ // container's job and not the transport's — the backend reads the ZIP and
9
+ // decodes no HTTP `Content-Encoding` (backend, 2026-10-23). ⛔ Until 2026-10-23
10
+ // this writer emitted Stored only ("compression is an optimization, not part of
11
+ // the contract"), and a documentation site's push was refused `413` for its
12
+ // size. The READER (`readZip`) inflates both methods, mixed freely in one
13
+ // archive — the backend's pull `.uwx` Deflates its entities and Stores its
14
+ // `manifest.json`, and the framework must read what the backend produces.
10
15
  //
11
16
  // No Zip64: per-record JSON files are far below 4 GiB and entry counts far
12
17
  // below 65535. The format is otherwise the classic APPNOTE layout, all
13
18
  // multi-byte fields little-endian.
14
19
  //
15
- // Determinism: mod time/date are fixed to 0. The .uwx content key is
16
- // `manifest.package_sha256` (computed over the manifest, NOT the ZIP bytes),
17
- // so ZIP framing never participates in dedupe; fixing the timestamp just
18
- // makes byte output reproducible for a given input.
20
+ // Determinism: mod time/date are fixed to 0 and Deflate runs at one fixed
21
+ // level, so a given input gives the same bytes under the same zlib. The .uwx
22
+ // content key is `manifest.package_sha256` (computed over the manifest, NOT the
23
+ // ZIP bytes), so ZIP framing — compression included — never participates in
24
+ // dedupe.
19
25
 
20
26
  import { crc32 } from './crc32.js'
21
- import { inflateRawSync } from 'node:zlib'
27
+ import { deflateRawSync, inflateRawSync } from 'node:zlib'
22
28
 
23
29
  const LOCAL_SIG = 0x04034b50
24
30
  const CENTRAL_SIG = 0x02014b50
25
31
  const EOCD_SIG = 0x06054b50
26
- const VERSION = 20 // 2.0 — the minimum that supports Stored with a CRC
32
+ const VERSION = 20 // 2.0 — the minimum that supports Deflate, and Stored with a CRC
33
+ const STORED = 0
34
+ const DEFLATE = 8
35
+ // Measured 2026-10-23 on a 3.6 MB page tree: level 9 → 533 KB in 38 ms, the
36
+ // default 6 → 540 KB in 25 ms. A push is measured compressed, so take the bytes.
37
+ const DEFLATE_LEVEL = 9
27
38
 
28
39
  /**
29
40
  * @param {{name: string, data: Buffer}[]} files
@@ -38,31 +49,34 @@ export function createZip(files) {
38
49
  const nameBuf = Buffer.from(file.name, 'utf8')
39
50
  const data = file.data
40
51
  const crc = crc32(data)
52
+ const deflated = deflateRawSync(data, { level: DEFLATE_LEVEL })
53
+ const method = deflated.length < data.length ? DEFLATE : STORED
54
+ const body = method === DEFLATE ? deflated : data
41
55
 
42
56
  const local = Buffer.alloc(30)
43
57
  local.writeUInt32LE(LOCAL_SIG, 0)
44
58
  local.writeUInt16LE(VERSION, 4)
45
59
  local.writeUInt16LE(0, 6) // general purpose bit flag
46
- local.writeUInt16LE(0, 8) // compression method: 0 = Stored
60
+ local.writeUInt16LE(method, 8) // compression method: 8 = Deflate, 0 = Stored
47
61
  local.writeUInt16LE(0, 10) // mod time (fixed)
48
62
  local.writeUInt16LE(0, 12) // mod date (fixed)
49
- local.writeUInt32LE(crc, 14)
50
- local.writeUInt32LE(data.length, 18) // compressed size == uncompressed
51
- local.writeUInt32LE(data.length, 22)
63
+ local.writeUInt32LE(crc, 14) // over the uncompressed data
64
+ local.writeUInt32LE(body.length, 18) // compressed size
65
+ local.writeUInt32LE(data.length, 22) // uncompressed size
52
66
  local.writeUInt16LE(nameBuf.length, 26)
53
67
  local.writeUInt16LE(0, 28) // extra field length
54
- localParts.push(local, nameBuf, data)
68
+ localParts.push(local, nameBuf, body)
55
69
 
56
70
  const central = Buffer.alloc(46)
57
71
  central.writeUInt32LE(CENTRAL_SIG, 0)
58
72
  central.writeUInt16LE(VERSION, 4) // version made by
59
73
  central.writeUInt16LE(VERSION, 6) // version needed
60
74
  central.writeUInt16LE(0, 8)
61
- central.writeUInt16LE(0, 10)
75
+ central.writeUInt16LE(method, 10)
62
76
  central.writeUInt16LE(0, 12)
63
77
  central.writeUInt16LE(0, 14)
64
78
  central.writeUInt32LE(crc, 16)
65
- central.writeUInt32LE(data.length, 20)
79
+ central.writeUInt32LE(body.length, 20)
66
80
  central.writeUInt32LE(data.length, 24)
67
81
  central.writeUInt16LE(nameBuf.length, 28)
68
82
  central.writeUInt16LE(0, 30) // extra
@@ -73,7 +87,7 @@ export function createZip(files) {
73
87
  central.writeUInt32LE(offset, 42) // local header offset
74
88
  centralParts.push(central, nameBuf)
75
89
 
76
- offset += local.length + nameBuf.length + data.length
90
+ offset += local.length + nameBuf.length + body.length
77
91
  }
78
92
 
79
93
  const centralDir = Buffer.concat(centralParts)
@@ -91,9 +105,10 @@ export function createZip(files) {
91
105
  }
92
106
 
93
107
  /**
94
- * Reader for `.uwx` containers. Handles Stored (method 0 — what our writer emits)
95
- * and Deflate (method 8 — what the backend's pull `.uwx` uses); any other method
96
- * throws. Not otherwise a general-purpose unzip (no Zip64, no encryption).
108
+ * Reader for `.uwx` containers. Handles Stored (method 0) and Deflate (method 8),
109
+ * mixed freely — both are what our writer and the backend's pull `.uwx` emit;
110
+ * any other method throws. Not otherwise a general-purpose unzip (no Zip64, no
111
+ * encryption).
97
112
  *
98
113
  * @param {Buffer} buf
99
114
  * @returns {Map<string, Buffer>} name -> uncompressed data