From 63eb646e4171b7bc867c298983b1e41c6fc8ad62 Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Tue, 2 Sep 2025 01:55:16 +0200 Subject: [PATCH 1/5] Add a zlib-based compress script Based on `compress.py` from #170, with some modifications: - Can be run as `npm run compress` or simply `node compress.mjs` - Uses best zlib compression setting. - Takes arbitrary glob patterns for input files, defaulting to all .z files for decompression. - Copies the file mode over to avoid spurious git diffs. --- compress.mjs | 107 ++++++++++ package-lock.json | 497 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 4 +- 3 files changed, 595 insertions(+), 13 deletions(-) create mode 100644 compress.mjs diff --git a/compress.mjs b/compress.mjs new file mode 100644 index 00000000..660da4c1 --- /dev/null +++ b/compress.mjs @@ -0,0 +1,107 @@ +import { globSync } from 'glob'; +import zlib from 'zlib'; +import fs from 'fs'; + +const isNPM = process.env.npm_config_user_agent !== undefined; +const command = isNPM ? 'npm run compress --' : 'node compress.mjs'; + +const usage = `Usage: ${command} [options] ... + +Options: + -d, --decompress Decompress files (default: compress). + -k, --keep Keep input files after processing (default: delete).`; + +const args = process.argv.slice(2); +if (args.length === 0) { + console.log(usage); + process.exit(1); +} + +const keepInputFiles = args.some(arg => arg === '-k' || arg === '--keep'); +const decompressMode = args.some(arg => arg === '-d' || arg === '--decompress'); + +let globs = args.filter(arg => !arg.startsWith('-')); +if (globs.length === 0) { + if (decompressMode) { + const defaultGlob = '**/*.z'; + console.log(`No input glob pattern given, using default: ${defaultGlob}`); + globs = [defaultGlob]; + } else { + // To prevent accidental compression, require explicit input file patterns. + console.log(usage); + process.exit(1); + } +} + +let files = new Set(); +if (globs.length > 0) { + for (const glob of globs) { + try { + const matches = globSync(glob, { nodir: true }); + for (const match of matches) { + files.add(match); + } + } catch (err) { + console.error(`Error processing glob: ${glob}`, err); + } + } +} +files = Array.from(files).sort(); +if (files.length === 0) { + console.log(`No files found to process with the given globs: ${globs.join(', ')}`); + process.exit(0); +} + +function compress(inputData) { + const compressedData = zlib.deflateSync(inputData, { level: zlib.constants.Z_BEST_COMPRESSION }); + + const originalSize = inputData.length; + const compressedSize = compressedData.length; + const compressionRatio = (1 - compressedSize / originalSize) * 100; + console.log(` Original size: ${String(originalSize).padStart(8)} bytes`); + console.log(` Compressed size: ${String(compressedSize).padStart(8)} bytes`); + console.log(` Compression ratio: ${compressionRatio.toFixed(2).padStart(8)}%`); + + return compressedData; +} + +function decompress(inputData) { + const decompressedData = zlib.inflateSync(inputData); + + const compressedSize = inputData.length; + const decompressedSize = decompressedData.length; + const expansionRatio = (decompressedSize / compressedSize - 1) * 100; + console.log(` Compressed size: ${String(compressedSize).padStart(8)} bytes`); + console.log(` Decompressed size: ${String(decompressedSize).padStart(8)} bytes`); + console.log(` Expansion ratio: ${expansionRatio.toFixed(2).padStart(8)}%`); + + return decompressedData; +} + +const verb = decompressMode ? 'decompress' : 'compress'; +console.log(`Found ${files.length} files to ${verb}...`); + +for (const inputFile of files) { + try { + console.log(inputFile); + + // Copy the mode over to avoid git status entries after a roundtrip. + const { mode } = fs.statSync(inputFile); + const inputData = fs.readFileSync(inputFile); + const outputData = decompressMode ? decompress(inputData) : compress(inputData); + let outputFile; + if (decompressMode) { + outputFile = inputFile.endsWith('.z') ? inputFile.slice(0, -2) : `${inputFile}.decompressed`; + } else { + outputFile = `${inputFile}.z`; + } + fs.writeFileSync(outputFile, outputData, { mode }); + + if (!keepInputFiles) { + fs.unlinkSync(inputFile); + console.log(` Deleted input file.`); + } + } catch (err) { + console.error(`Error ${verb}ing ${inputFile}:`, err); + } +} diff --git a/package-lock.json b/package-lock.json index 86b557e2..0d5639c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "command-line-usage": "^6.1.3", "es-main": "^1.3.0", "eslint": "^8.38.0", + "glob": "^11.0.3", "http-server": "^14.1.1", "jsvu": "^2.5.1", "local-web-server": "^5.4.0", @@ -607,6 +608,101 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1845,6 +1941,13 @@ "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", "dev": true }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2473,6 +2576,36 @@ } } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -2509,7 +2642,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.2", @@ -2582,21 +2716,24 @@ } }, "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": "*" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -2614,6 +2751,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/global-dirs": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", @@ -3041,6 +3204,7 @@ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3290,6 +3454,22 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3418,6 +3598,28 @@ "node": ">=18.0.0" } }, + "node_modules/jsvu/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -4544,6 +4746,13 @@ "node": ">=8" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -4604,6 +4813,43 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", @@ -5028,6 +5274,28 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -5413,6 +5681,32 @@ "node": ">=4" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", @@ -5446,6 +5740,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -6099,6 +6407,171 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index bebd87c1..2705b286 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "test:shell": "npm run test:v8 && npm run test:jsc && npm run test:spidermonkey", "test:v8": "node tests/run-shell.mjs --shell v8", "test:jsc": "node tests/run-shell.mjs --shell jsc", - "test:spidermonkey": "node tests/run-shell.mjs --shell spidermonkey" + "test:spidermonkey": "node tests/run-shell.mjs --shell spidermonkey", + "compress": "node compress.mjs" }, "devDependencies": { "@actions/core": "^1.11.1", @@ -37,6 +38,7 @@ "command-line-usage": "^6.1.3", "es-main": "^1.3.0", "eslint": "^8.38.0", + "glob": "^11.0.3", "http-server": "^14.1.1", "jsvu": "^2.5.1", "local-web-server": "^5.4.0", From a6c88278c69773d17c858057eaeb1de09ba7bef6 Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Tue, 2 Sep 2025 13:34:35 +0200 Subject: [PATCH 2/5] address first round comments --- compress.mjs | 134 +++++++++++++++++++++++++++------------------------ 1 file changed, 71 insertions(+), 63 deletions(-) diff --git a/compress.mjs b/compress.mjs index 660da4c1..feffa3b6 100644 --- a/compress.mjs +++ b/compress.mjs @@ -1,55 +1,49 @@ +import commandLineArgs from 'command-line-args'; +import commandLineUsage from 'command-line-usage'; import { globSync } from 'glob'; import zlib from 'zlib'; import fs from 'fs'; -const isNPM = process.env.npm_config_user_agent !== undefined; -const command = isNPM ? 'npm run compress --' : 'node compress.mjs'; - -const usage = `Usage: ${command} [options] ... - -Options: - -d, --decompress Decompress files (default: compress). - -k, --keep Keep input files after processing (default: delete).`; - -const args = process.argv.slice(2); -if (args.length === 0) { - console.log(usage); - process.exit(1); -} +function parseCommandLineArgs() { + const optionDefinitions = [ + { name: 'decompress', alias: 'd', type: Boolean, description: 'Decompress files (default: compress).' }, + { name: 'keep', alias: 'k', type: Boolean, description: 'Keep input files after processing (default: delete).' }, + { name: 'help', alias: 'h', type: Boolean, description: 'Print this usage guide.' }, + { name: 'globs', type: String, multiple: true, defaultOption: true, description: 'Glob patterns of files to process.' }, + ]; + const options = commandLineArgs(optionDefinitions); + + const isNPM = process.env.npm_config_user_agent !== undefined; + const command = isNPM ? 'npm run compress --' : 'node compress.mjs'; + const usage = commandLineUsage([ + { + header: 'Usage', + content: `${command} [options] ...` + }, + { + header: 'Options', + optionList: optionDefinitions + } + ]); -const keepInputFiles = args.some(arg => arg === '-k' || arg === '--keep'); -const decompressMode = args.some(arg => arg === '-d' || arg === '--decompress'); - -let globs = args.filter(arg => !arg.startsWith('-')); -if (globs.length === 0) { - if (decompressMode) { - const defaultGlob = '**/*.z'; - console.log(`No input glob pattern given, using default: ${defaultGlob}`); - globs = [defaultGlob]; - } else { - // To prevent accidental compression, require explicit input file patterns. + if (options.help) { console.log(usage); - process.exit(1); + process.exit(0); } -} -let files = new Set(); -if (globs.length > 0) { - for (const glob of globs) { - try { - const matches = globSync(glob, { nodir: true }); - for (const match of matches) { - files.add(match); - } - } catch (err) { - console.error(`Error processing glob: ${glob}`, err); + if (options.globs === undefined) { + if (options.decompress) { + const defaultGlob = '**/*.z'; + console.log(`No input glob pattern given, using default: ${defaultGlob}`); + options.globs = [defaultGlob]; + } else { + // For compression, require the user to specify explicit input file patterns. + console.error('No input glob pattern given.'); + console.log(usage); + process.exit(1); } } -} -files = Array.from(files).sort(); -if (files.length === 0) { - console.log(`No files found to process with the given globs: ${globs.join(', ')}`); - process.exit(0); + return options; } function compress(inputData) { @@ -78,30 +72,44 @@ function decompress(inputData) { return decompressedData; } -const verb = decompressMode ? 'decompress' : 'compress'; -console.log(`Found ${files.length} files to ${verb}...`); +function processFiles(options) { + let files = []; + console.assert(options.globs.length > 0); + for (const glob of options.globs) { + const matches = globSync(glob, { nodir: true }); + files = files.concat(matches); + } + files = Array.from(new Set(files)).sort(); -for (const inputFile of files) { - try { - console.log(inputFile); + const verb = options.decompress ? 'decompress' : 'compress'; + console.log(`Found ${files.length} files to ${verb}` + (files.length ? ':' : '.')); - // Copy the mode over to avoid git status entries after a roundtrip. - const { mode } = fs.statSync(inputFile); - const inputData = fs.readFileSync(inputFile); - const outputData = decompressMode ? decompress(inputData) : compress(inputData); - let outputFile; - if (decompressMode) { - outputFile = inputFile.endsWith('.z') ? inputFile.slice(0, -2) : `${inputFile}.decompressed`; - } else { - outputFile = `${inputFile}.z`; - } - fs.writeFileSync(outputFile, outputData, { mode }); + for (const inputFile of files) { + try { + console.log(inputFile); + + // Copy the mode over to avoid git status entries after a roundtrip. + const { mode } = fs.statSync(inputFile); + const inputData = fs.readFileSync(inputFile); + const outputData = options.decompress ? decompress(inputData) : compress(inputData); + let outputFile; + if (options.decompress) { + outputFile = inputFile.endsWith('.z') ? inputFile.slice(0, -2) : `${inputFile}.decompressed`; + } else { + outputFile = `${inputFile}.z`; + } + fs.writeFileSync(outputFile, outputData, { mode }); - if (!keepInputFiles) { - fs.unlinkSync(inputFile); - console.log(` Deleted input file.`); + if (!options.keep) { + fs.unlinkSync(inputFile); + console.log(` Deleted input file.`); + } + } catch (err) { + console.error(`Error ${verb}ing ${inputFile}:`, err); } - } catch (err) { - console.error(`Error ${verb}ing ${inputFile}:`, err); } } + +const options = parseCommandLineArgs(); +processFiles(options); + From f8c64d5215e72ce7bf4ff8f09cbc16e2dc02472c Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Tue, 2 Sep 2025 13:38:53 +0200 Subject: [PATCH 3/5] move to new utils subdir --- package.json | 4 ++-- compress.mjs => utils/compress.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename compress.mjs => utils/compress.mjs (98%) diff --git a/package.json b/package.json index 2705b286..7ce95718 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "license": "SEE LICENSE IN LICENSE", "scripts": { "server": "node tests/server.mjs", + "compress": "node utils/compress.mjs", "lint:check": "eslint **/*.{js,mjs,jsx,ts,tsx}", "pretty:check": "prettier --check ./", "format:check": "npm run pretty:check && npm run lint:check", @@ -26,8 +27,7 @@ "test:shell": "npm run test:v8 && npm run test:jsc && npm run test:spidermonkey", "test:v8": "node tests/run-shell.mjs --shell v8", "test:jsc": "node tests/run-shell.mjs --shell jsc", - "test:spidermonkey": "node tests/run-shell.mjs --shell spidermonkey", - "compress": "node compress.mjs" + "test:spidermonkey": "node tests/run-shell.mjs --shell spidermonkey" }, "devDependencies": { "@actions/core": "^1.11.1", diff --git a/compress.mjs b/utils/compress.mjs similarity index 98% rename from compress.mjs rename to utils/compress.mjs index feffa3b6..c7b9d6de 100644 --- a/compress.mjs +++ b/utils/compress.mjs @@ -14,7 +14,7 @@ function parseCommandLineArgs() { const options = commandLineArgs(optionDefinitions); const isNPM = process.env.npm_config_user_agent !== undefined; - const command = isNPM ? 'npm run compress --' : 'node compress.mjs'; + const command = isNPM ? 'npm run compress --' : 'node utils/compress.mjs'; const usage = commandLineUsage([ { header: 'Usage', From c336666423fbe74dbfde2551a735ad5440b8ee2b Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Tue, 2 Sep 2025 14:37:21 +0200 Subject: [PATCH 4/5] address more comments, add total sizes stats at end --- utils/compress.mjs | 85 +++++++++++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/utils/compress.mjs b/utils/compress.mjs index c7b9d6de..f517b58e 100644 --- a/utils/compress.mjs +++ b/utils/compress.mjs @@ -3,6 +3,7 @@ import commandLineUsage from 'command-line-usage'; import { globSync } from 'glob'; import zlib from 'zlib'; import fs from 'fs'; +import path from 'path'; function parseCommandLineArgs() { const optionDefinitions = [ @@ -46,12 +47,20 @@ function parseCommandLineArgs() { return options; } +function calculateCompressionRatio(originalSize, compressedSize) { + return (1 - compressedSize / originalSize) * 100; +} + +function calculateExpansionRatio(compressedSize, decompressedSize) { + return (decompressedSize / compressedSize - 1) * 100; +} + function compress(inputData) { const compressedData = zlib.deflateSync(inputData, { level: zlib.constants.Z_BEST_COMPRESSION }); const originalSize = inputData.length; const compressedSize = compressedData.length; - const compressionRatio = (1 - compressedSize / originalSize) * 100; + const compressionRatio = calculateCompressionRatio(originalSize, compressedSize); console.log(` Original size: ${String(originalSize).padStart(8)} bytes`); console.log(` Compressed size: ${String(compressedSize).padStart(8)} bytes`); console.log(` Compression ratio: ${compressionRatio.toFixed(2).padStart(8)}%`); @@ -64,7 +73,7 @@ function decompress(inputData) { const compressedSize = inputData.length; const decompressedSize = decompressedData.length; - const expansionRatio = (decompressedSize / compressedSize - 1) * 100; + const expansionRatio = calculateExpansionRatio(compressedSize, decompressedSize); console.log(` Compressed size: ${String(compressedSize).padStart(8)} bytes`); console.log(` Decompressed size: ${String(decompressedSize).padStart(8)} bytes`); console.log(` Expansion ratio: ${expansionRatio.toFixed(2).padStart(8)}%`); @@ -72,44 +81,74 @@ function decompress(inputData) { return decompressedData; } -function processFiles(options) { +function globsToFiles(globs) { let files = []; - console.assert(options.globs.length > 0); - for (const glob of options.globs) { + console.assert(globs.length > 0); + for (const glob of globs) { const matches = globSync(glob, { nodir: true }); files = files.concat(matches); } files = Array.from(new Set(files)).sort(); + return files; +} - const verb = options.decompress ? 'decompress' : 'compress'; +function processFiles(files, isDecompress, keep) { + const verb = isDecompress ? 'decompress' : 'compress'; console.log(`Found ${files.length} files to ${verb}` + (files.length ? ':' : '.')); - for (const inputFile of files) { - try { - console.log(inputFile); + // For printing overall statistics at the end. + let totalInputSize = 0; + let totalOutputSize = 0; - // Copy the mode over to avoid git status entries after a roundtrip. - const { mode } = fs.statSync(inputFile); - const inputData = fs.readFileSync(inputFile); - const outputData = options.decompress ? decompress(inputData) : compress(inputData); - let outputFile; - if (options.decompress) { - outputFile = inputFile.endsWith('.z') ? inputFile.slice(0, -2) : `${inputFile}.decompressed`; + for (const inputFilename of files) { + try { + console.log(inputFilename); + let outputFilename; + if (isDecompress) { + if (path.extname(inputFilename) !== '.z') { + console.warn(` Warning: Input file does not have a .z extension.`); + outputFilename = `${inputFilename}.decompressed`; + } else { + outputFilename = inputFilename.slice(0, -2); + } + console.log(` Decompressing to: ${outputFilename}`); } else { - outputFile = `${inputFile}.z`; + outputFilename = `${inputFilename}.z`; } - fs.writeFileSync(outputFile, outputData, { mode }); - if (!options.keep) { - fs.unlinkSync(inputFile); + // Copy the mode over to avoid git status entries after a roundtrip. + const { mode } = fs.statSync(inputFilename); + const inputData = fs.readFileSync(inputFilename); + const outputData = isDecompress ? decompress(inputData) : compress(inputData); + fs.writeFileSync(outputFilename, outputData, { mode }); + + totalInputSize += inputData.length; + totalOutputSize += outputData.length; + + if (!keep) { + fs.unlinkSync(inputFilename); console.log(` Deleted input file.`); } } catch (err) { - console.error(`Error ${verb}ing ${inputFile}:`, err); + console.error(`Error ${verb}ing ${inputFilename}:`, err); + } + } + + if (files.length > 1) { + if (isDecompress) { + const totalExpansionRatio = calculateExpansionRatio(totalInputSize, totalOutputSize); + console.log(`Total compressed sizes: ${String(totalInputSize).padStart(9)} bytes`); + console.log(`Total decompressed sizes: ${String(totalOutputSize).padStart(9)} bytes`); + console.log(`Average expansion ratio: ${totalExpansionRatio.toFixed(2).padStart(9)}%`); + } else { + const totalCompressionRatio = calculateCompressionRatio(totalInputSize, totalOutputSize); + console.log(`Total original sizes: ${String(totalInputSize).padStart(9)} bytes`); + console.log(`Total compressed sizes: ${String(totalOutputSize).padStart(9)} bytes`); + console.log(`Average compression ratio: ${totalCompressionRatio.toFixed(2).padStart(9)}%`); } } } const options = parseCommandLineArgs(); -processFiles(options); - +const files = globsToFiles(options.globs); +processFiles(files, options.decompress, options.keep); From 8090c8a0f661106c9264359a8f8848e0e973d1cf Mon Sep 17 00:00:00 2001 From: Daniel Lehmann Date: Tue, 2 Sep 2025 14:39:51 +0200 Subject: [PATCH 5/5] warn against compressing .z files again --- utils/compress.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils/compress.mjs b/utils/compress.mjs index f517b58e..fb91d7bc 100644 --- a/utils/compress.mjs +++ b/utils/compress.mjs @@ -113,6 +113,9 @@ function processFiles(files, isDecompress, keep) { } console.log(` Decompressing to: ${outputFilename}`); } else { + if (path.extname(inputFilename) === '.z') { + console.warn(` Warning: Input file already has a .z extension.`); + } outputFilename = `${inputFilename}.z`; }