sitevision-cli 0.4.0-beta.2 → 0.6.0-beta.0

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.
package/dist/utils/zip.js CHANGED
@@ -6,16 +6,23 @@
6
6
  */
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
- import { spawn } from 'child_process';
9
+ import zlib from 'zlib';
10
10
  import { ensureDistDir } from './project-detection.js';
11
11
  // =============================================================================
12
12
  // ZIP CREATION
13
13
  // =============================================================================
14
14
  /**
15
- * Create a zip archive of a directory
15
+ * Create a zip archive of a directory.
16
16
  *
17
- * Uses the system `zip` command for cross-platform compatibility.
18
- * Falls back to a basic implementation if zip is not available.
17
+ * In-house, dependency-free implementation: walks the directory, deflates each
18
+ * file with Node's built-in zlib, and assembles a standard ZIP container (local
19
+ * file headers + central directory + end-of-central-directory record). This
20
+ * removes the previous reliance on the external `zip`/`tar`/PowerShell binaries
21
+ * and behaves identically across macOS, Linux, and Windows.
22
+ *
23
+ * Mirrors `zip -r <out> .` run from inside `sourceDir`: archive paths are
24
+ * relative to `sourceDir`, use forward slashes, and directory entries are
25
+ * emitted so empty directories are preserved.
19
26
  *
20
27
  * @param sourceDir - Directory to zip
21
28
  * @param outputPath - Path for the output zip file
@@ -31,77 +38,182 @@ export async function createZip(sourceDir, outputPath) {
31
38
  if (fs.existsSync(outputPath)) {
32
39
  fs.unlinkSync(outputPath);
33
40
  }
34
- return new Promise((resolve, reject) => {
35
- // Try using system zip command (works on macOS, Linux, and Windows with Git Bash)
36
- const zipProcess = spawn('zip', ['-r', outputPath, '.'], {
37
- cwd: sourceDir,
38
- stdio: ['ignore', 'pipe', 'pipe'],
39
- });
40
- let stderr = '';
41
- zipProcess.stderr?.on('data', (data) => {
42
- stderr += data.toString();
43
- });
44
- zipProcess.on('error', error => {
45
- // If zip command not found, try alternative methods
46
- if (error.code === 'ENOENT') {
47
- // Fall back to tar on systems without zip
48
- createZipWithTar(sourceDir, outputPath).then(resolve).catch(reject);
49
- }
50
- else {
51
- reject(new Error(`Zip process error: ${error.message}`));
52
- }
53
- });
54
- zipProcess.on('close', code => {
55
- if (code === 0) {
56
- resolve(outputPath);
41
+ const entries = collectZipEntries(sourceDir);
42
+ const buffer = await buildZipBuffer(entries);
43
+ fs.writeFileSync(outputPath, buffer);
44
+ return outputPath;
45
+ }
46
+ /**
47
+ * Recursively collect file and directory entries for the archive.
48
+ * Directories are emitted before their contents, matching `zip -r`.
49
+ */
50
+ function collectZipEntries(sourceDir) {
51
+ const entries = [];
52
+ const walk = (dir, prefix) => {
53
+ const dirEntries = fs.readdirSync(dir, { withFileTypes: true });
54
+ for (const entry of dirEntries) {
55
+ const absolutePath = path.join(dir, entry.name);
56
+ const archiveName = prefix + entry.name;
57
+ if (entry.isDirectory()) {
58
+ const stat = fs.statSync(absolutePath);
59
+ entries.push({
60
+ name: archiveName + '/',
61
+ isDirectory: true,
62
+ mtime: stat.mtime,
63
+ });
64
+ walk(absolutePath, archiveName + '/');
57
65
  }
58
- else {
59
- reject(new Error(`Zip failed with code ${code}: ${stderr}`));
66
+ else if (entry.isFile()) {
67
+ const stat = fs.statSync(absolutePath);
68
+ entries.push({
69
+ name: archiveName,
70
+ isDirectory: false,
71
+ absolutePath,
72
+ mtime: stat.mtime,
73
+ });
60
74
  }
61
- });
62
- });
75
+ // Symlinks and special files are skipped (matches prior `zip` defaults
76
+ // closely enough for Sitevision build output, which has neither).
77
+ }
78
+ };
79
+ walk(sourceDir, '');
80
+ return entries;
63
81
  }
64
82
  /**
65
- * Fallback: Create zip using tar (converts to zip format)
66
- * This is a fallback for systems without the zip command.
83
+ * Assemble the full ZIP byte buffer from collected entries.
67
84
  */
68
- async function createZipWithTar(sourceDir, outputPath) {
69
- // On Windows without zip, we might need to use PowerShell
70
- const isWindows = process.platform === 'win32';
71
- if (isWindows) {
72
- return createZipWithPowerShell(sourceDir, outputPath);
85
+ async function buildZipBuffer(entries) {
86
+ const localChunks = [];
87
+ const centralChunks = [];
88
+ let offset = 0;
89
+ for (const entry of entries) {
90
+ const nameBuffer = Buffer.from(entry.name, 'utf8');
91
+ const { dosTime, dosDate } = toDosDateTime(entry.mtime);
92
+ let rawData;
93
+ let compressed;
94
+ let method;
95
+ if (entry.isDirectory) {
96
+ rawData = Buffer.alloc(0);
97
+ compressed = Buffer.alloc(0);
98
+ method = 0; // stored
99
+ }
100
+ else {
101
+ rawData = fs.readFileSync(entry.absolutePath);
102
+ if (rawData.length === 0) {
103
+ compressed = Buffer.alloc(0);
104
+ method = 0; // stored (deflating empty data is wasteful)
105
+ }
106
+ else {
107
+ compressed = await deflateRaw(rawData);
108
+ method = 8; // deflate
109
+ }
110
+ }
111
+ const crc = crc32(rawData);
112
+ const localHeaderOffset = offset;
113
+ // Local file header (signature 0x04034b50)
114
+ const localHeader = Buffer.alloc(30);
115
+ localHeader.writeUInt32LE(0x04034b50, 0);
116
+ localHeader.writeUInt16LE(20, 4); // version needed to extract
117
+ localHeader.writeUInt16LE(0, 6); // general purpose flag
118
+ localHeader.writeUInt16LE(method, 8);
119
+ localHeader.writeUInt16LE(dosTime, 10);
120
+ localHeader.writeUInt16LE(dosDate, 12);
121
+ localHeader.writeUInt32LE(crc, 14);
122
+ localHeader.writeUInt32LE(compressed.length, 18);
123
+ localHeader.writeUInt32LE(rawData.length, 22);
124
+ localHeader.writeUInt16LE(nameBuffer.length, 26);
125
+ localHeader.writeUInt16LE(0, 28); // extra field length
126
+ localChunks.push(localHeader, nameBuffer, compressed);
127
+ offset += localHeader.length + nameBuffer.length + compressed.length;
128
+ // Central directory header (signature 0x02014b50)
129
+ const centralHeader = Buffer.alloc(46);
130
+ centralHeader.writeUInt32LE(0x02014b50, 0);
131
+ centralHeader.writeUInt16LE(20, 4); // version made by
132
+ centralHeader.writeUInt16LE(20, 6); // version needed
133
+ centralHeader.writeUInt16LE(0, 8); // general purpose flag
134
+ centralHeader.writeUInt16LE(method, 10);
135
+ centralHeader.writeUInt16LE(dosTime, 12);
136
+ centralHeader.writeUInt16LE(dosDate, 14);
137
+ centralHeader.writeUInt32LE(crc, 16);
138
+ centralHeader.writeUInt32LE(compressed.length, 20);
139
+ centralHeader.writeUInt32LE(rawData.length, 24);
140
+ centralHeader.writeUInt16LE(nameBuffer.length, 28);
141
+ centralHeader.writeUInt16LE(0, 30); // extra field length
142
+ centralHeader.writeUInt16LE(0, 32); // comment length
143
+ centralHeader.writeUInt16LE(0, 34); // disk number start
144
+ centralHeader.writeUInt16LE(0, 36); // internal attributes
145
+ // External attributes: directory vs file unix-ish mode in high bytes.
146
+ centralHeader.writeUInt32LE(entry.isDirectory ? 0x41ed0010 : 0x81a40000, 38);
147
+ centralHeader.writeUInt32LE(localHeaderOffset, 42);
148
+ centralChunks.push(centralHeader, nameBuffer);
73
149
  }
74
- // On Unix without zip, this is unlikely but we'll throw an error
75
- throw new Error('zip command not found. Please install zip: apt-get install zip (Linux) or brew install zip (macOS)');
150
+ const centralDirectory = Buffer.concat(centralChunks);
151
+ const centralDirectoryOffset = offset;
152
+ // End of central directory record (signature 0x06054b50)
153
+ const eocd = Buffer.alloc(22);
154
+ eocd.writeUInt32LE(0x06054b50, 0);
155
+ eocd.writeUInt16LE(0, 4); // disk number
156
+ eocd.writeUInt16LE(0, 6); // disk with central directory
157
+ eocd.writeUInt16LE(entries.length, 8); // entries on this disk
158
+ eocd.writeUInt16LE(entries.length, 10); // total entries
159
+ eocd.writeUInt32LE(centralDirectory.length, 12);
160
+ eocd.writeUInt32LE(centralDirectoryOffset, 16);
161
+ eocd.writeUInt16LE(0, 20); // comment length
162
+ return Buffer.concat([...localChunks, centralDirectory, eocd]);
76
163
  }
77
164
  /**
78
- * Create zip using PowerShell on Windows
165
+ * Deflate (raw, no zlib header) a buffer.
79
166
  */
80
- async function createZipWithPowerShell(sourceDir, outputPath) {
167
+ async function deflateRaw(data) {
81
168
  return new Promise((resolve, reject) => {
82
- const absoluteSourceDir = path.resolve(sourceDir);
83
- const absoluteOutputPath = path.resolve(outputPath);
84
- const command = `Compress-Archive -Path "${absoluteSourceDir}\\*" -DestinationPath "${absoluteOutputPath}" -Force`;
85
- const psProcess = spawn('powershell', ['-Command', command], {
86
- stdio: ['ignore', 'pipe', 'pipe'],
87
- });
88
- let stderr = '';
89
- psProcess.stderr?.on('data', (data) => {
90
- stderr += data.toString();
91
- });
92
- psProcess.on('error', error => {
93
- reject(new Error(`PowerShell error: ${error.message}`));
94
- });
95
- psProcess.on('close', code => {
96
- if (code === 0) {
97
- resolve(absoluteOutputPath);
169
+ zlib.deflateRaw(data, (error, result) => {
170
+ if (error) {
171
+ reject(error);
98
172
  }
99
173
  else {
100
- reject(new Error(`PowerShell zip failed with code ${code}: ${stderr}`));
174
+ resolve(result);
101
175
  }
102
176
  });
103
177
  });
104
178
  }
179
+ /**
180
+ * Convert a Date to DOS date/time fields used by the ZIP format.
181
+ * ZIP timestamps only span 1980–2107 with 2-second resolution.
182
+ */
183
+ function toDosDateTime(date) {
184
+ const year = date.getFullYear();
185
+ if (year < 1980) {
186
+ // Clamp to the ZIP epoch (1980-01-01 00:00:00).
187
+ return { dosTime: 0, dosDate: (1 << 5) | 1 };
188
+ }
189
+ const dosTime = (date.getHours() << 11) |
190
+ (date.getMinutes() << 5) |
191
+ Math.floor(date.getSeconds() / 2);
192
+ const dosDate = ((year - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
193
+ return { dosTime, dosDate };
194
+ }
195
+ // CRC-32 table (IEEE polynomial 0xEDB88320), built once and reused.
196
+ const crc32Table = (() => {
197
+ const table = new Uint32Array(256);
198
+ for (let n = 0; n < 256; n++) {
199
+ let c = n;
200
+ for (let k = 0; k < 8; k++) {
201
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
202
+ }
203
+ table[n] = c >>> 0;
204
+ }
205
+ return table;
206
+ })();
207
+ /**
208
+ * Compute the CRC-32 checksum of a buffer.
209
+ */
210
+ function crc32(data) {
211
+ let crc = 0xffffffff;
212
+ for (const byte of data) {
213
+ crc = crc32Table[(crc ^ byte) & 0xff] ^ (crc >>> 8);
214
+ }
215
+ return (crc ^ 0xffffffff) >>> 0;
216
+ }
105
217
  /**
106
218
  * Create a zip of the build directory for deployment
107
219
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sitevision-cli",
3
- "version": "0.4.0-beta.2",
3
+ "version": "0.6.0-beta.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "svc": "dist/cli.js"