rollup-plugin-websqz 1.0.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/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+
2
+
3
+ Copyright 2026 Rootkids
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # rollup-plugin-websqz
2
+ Rollup / Vite plugin for using [websqz](https://github.com/r00tkids/websqz) to compress and bundle code and assets into one HTML file. This is intented for intros in the [demoscene](https://en.wikipedia.org/wiki/Demoscene) or size restricted JS challenges.
3
+
4
+ ## Usage
5
+ ```js
6
+ // vite.config.js
7
+ import { defineConfig } from 'vite';
8
+ import websqz from 'rollup-plugin-websqz';
9
+
10
+ export default defineConfig({
11
+ plugins: [websqz()]
12
+ });
13
+ ```
14
+
15
+ See the [example](./example) for a working example with support for `vite-plugin-glsl`.
16
+
17
+ ## Example Options
18
+ ```js
19
+ websqz({
20
+ websqzPath: null, // It's resolved to the websqz executable installed when installing the npm package, else it'll try to execute based on $PATH
21
+ fileHooks: [
22
+ {
23
+ filter: /\.glsl$/,
24
+ transform: async (ctx, id, content) => {
25
+ return {
26
+ content: Buffer.from("Hello World", "utf-8"),
27
+ isCompressed: false,
28
+ isText: true,
29
+ fileExt: ".glsl" // Optional
30
+ };
31
+ }
32
+ }
33
+ ]
34
+ })
35
+ ```
Binary file
@@ -0,0 +1,39 @@
1
+ import { PluginContext, type Plugin } from "rollup";
2
+ import { FilterPattern } from "@rollup/pluginutils";
3
+ export type WebsqzFileTransformRes = {
4
+ /**
5
+ * The result of processing
6
+ */
7
+ content: Buffer;
8
+ /**
9
+ * Is already compressed and should not be compressed again by websqz
10
+ */
11
+ isCompressed: boolean;
12
+ /**
13
+ * Is it text? The plugin orders files by type for better compression ratios
14
+ */
15
+ isText: boolean;
16
+ /**
17
+ * File extension including the dot, e.g. .txt.
18
+ * This is used to order files for better compression ratios.
19
+ * Same type of content should share same file extension.
20
+ *
21
+ * By default it is extracted from the original file path.
22
+ */
23
+ fileExt?: string;
24
+ };
25
+ type WebSqzOptions = {
26
+ websqzPath?: string;
27
+ /**
28
+ * File transform hooks to process files before they are imported in code or compressed by websqz
29
+ */
30
+ fileTransforms?: [
31
+ {
32
+ include?: FilterPattern;
33
+ exclude?: FilterPattern;
34
+ transform: (ctx: PluginContext, id: string, content: Buffer) => Promise<WebsqzFileTransformRes>;
35
+ }
36
+ ];
37
+ };
38
+ export default function (options?: WebSqzOptions): Plugin;
39
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,203 @@
1
+ Object.defineProperty(exports, "__esModule", { value: true });
2
+ exports.default = default_1;
3
+ const tslib_1 = require("tslib");
4
+ const node_querystring_1 = tslib_1.__importDefault(require("node:querystring"));
5
+ const promises_1 = tslib_1.__importDefault(require("node:fs/promises"));
6
+ const node_fs_1 = tslib_1.__importDefault(require("node:fs"));
7
+ const node_path_1 = tslib_1.__importDefault(require("node:path"));
8
+ const node_child_process_1 = tslib_1.__importDefault(require("node:child_process"));
9
+ const pluginutils_1 = require("@rollup/pluginutils");
10
+ class WebSqzExe {
11
+ websqzPath;
12
+ constructor(websqzPath) {
13
+ this.websqzPath = websqzPath;
14
+ }
15
+ async run(cliOptions) {
16
+ const args = [];
17
+ args.push("--js-main", cliOptions.jsMain);
18
+ for (const file of cliOptions.files) {
19
+ args.push("--files", file);
20
+ }
21
+ for (const preCompressedFile of cliOptions.preCompressedFiles) {
22
+ args.push("--pre-compressed-files", preCompressedFile);
23
+ }
24
+ args.push("--output-directory", cliOptions.output);
25
+ const spawn = await node_child_process_1.default.spawn(this.websqzPath, args, {
26
+ stdio: "inherit",
27
+ });
28
+ return new Promise((resolve, reject) => {
29
+ spawn.once("error", (err) => {
30
+ reject(err);
31
+ });
32
+ spawn.once("close", (code) => {
33
+ if (code !== 0) {
34
+ reject(new Error(`websqz process exited with code ${code}`));
35
+ }
36
+ else {
37
+ resolve();
38
+ }
39
+ });
40
+ });
41
+ }
42
+ }
43
+ function websqzExecutablePath(executablePath) {
44
+ if (!executablePath) {
45
+ const path = __dirname + "/bin/websqz";
46
+ const extension = process.platform == "win32" ? ".exe" : "";
47
+ if (node_fs_1.default.existsSync(path + extension)) {
48
+ return path + extension;
49
+ }
50
+ return "websqz" + extension;
51
+ }
52
+ return executablePath;
53
+ }
54
+ function default_1(options = {}) {
55
+ const isBuild = process.env.NODE_ENV === "production";
56
+ const websqzExePath = websqzExecutablePath(options.websqzPath);
57
+ const websqzExe = new WebSqzExe(websqzExePath);
58
+ const fileTransforms = options.fileTransforms?.map(transform => {
59
+ const include = transform.include ? (Array.isArray(transform.include) ? transform.include : [transform.include])
60
+ : undefined;
61
+ const exclude = transform.exclude ? (Array.isArray(transform.exclude) ? transform.exclude : [transform.exclude])
62
+ : undefined;
63
+ const filter = (0, pluginutils_1.createFilter)(include, exclude);
64
+ return {
65
+ transform: transform.transform,
66
+ filter,
67
+ };
68
+ });
69
+ const files = new Map();
70
+ let fileNameIdx = 0;
71
+ const findNextAvailableFileName = () => {
72
+ const startChar = 97; // a
73
+ const endChar = 122; // z
74
+ const alphabetSize = endChar - startChar;
75
+ let numChars = fileNameIdx === 0 ? 1 : Math.floor(Math.log(fileNameIdx) / Math.log(alphabetSize)) + 1;
76
+ let candidateName = "";
77
+ for (let i = 0; i < numChars; i++) {
78
+ const charCode = startChar + ((Math.floor(fileNameIdx / Math.pow(alphabetSize, i))) % alphabetSize);
79
+ candidateName = String.fromCharCode(charCode) + candidateName;
80
+ }
81
+ fileNameIdx++;
82
+ return candidateName;
83
+ };
84
+ const loadAndTransform = async function (id, hookRes) {
85
+ if (isBuild) {
86
+ const fileName = findNextAvailableFileName();
87
+ files.set(id, {
88
+ fileName,
89
+ content: hookRes.content,
90
+ isCompressed: hookRes.isCompressed,
91
+ fileExt: hookRes.fileExt ?? node_path_1.default.extname(id),
92
+ isText: hookRes.isText,
93
+ });
94
+ return {
95
+ code: hookRes.isText
96
+ ? `export default new TextDecoder().decode(wsqz.files["${fileName}"]);`
97
+ : `export default wsqz.files["${fileName}"];`,
98
+ moduleSideEffects: false,
99
+ moduleType: 'js',
100
+ };
101
+ }
102
+ else {
103
+ return {
104
+ code: hookRes.isText
105
+ ? `export default ${JSON.stringify(hookRes.content.toString("utf-8"))};`
106
+ : `export default Uint8Array.fromBase64("${hookRes.content.toString("base64")}");`,
107
+ moduleSideEffects: false,
108
+ moduleType: 'js',
109
+ };
110
+ }
111
+ };
112
+ return {
113
+ name: "rollup-plugin-websqz",
114
+ load: {
115
+ order: "pre",
116
+ async handler(id) {
117
+ const qIdx = id.indexOf("?");
118
+ const beforeParams = id.slice(0, qIdx === -1 ? id.length : qIdx);
119
+ const afterParams = id.slice(id.indexOf("?") + 1);
120
+ const parsed = node_querystring_1.default.parse(afterParams);
121
+ const cleanedUpId = beforeParams;
122
+ let cachedFile = null;
123
+ const loadFromDisk = async () => {
124
+ if (cachedFile != null) {
125
+ return cachedFile;
126
+ }
127
+ cachedFile = await promises_1.default.readFile(cleanedUpId);
128
+ return cachedFile;
129
+ };
130
+ if (fileTransforms) {
131
+ for (const transform of fileTransforms) {
132
+ if (transform.filter(id)) {
133
+ let hookRes = await transform.transform(this, id, await loadFromDisk());
134
+ if (hookRes == null) {
135
+ continue;
136
+ }
137
+ return await loadAndTransform(id, hookRes);
138
+ }
139
+ }
140
+ }
141
+ const isWebSqzTxt = parsed["websqz-txt"] != null;
142
+ const isWebSqzBin = parsed["websqz-bin"] != null;
143
+ if (isWebSqzTxt && isWebSqzBin) {
144
+ throw new Error(`Cannot use both websqz-txt and websqz-bin on the same import: ${id}`);
145
+ }
146
+ const isCompressed = parsed["compressed"] != null;
147
+ if (isWebSqzTxt || isWebSqzBin) {
148
+ const content = await loadFromDisk();
149
+ let hookRes = {
150
+ content,
151
+ isCompressed,
152
+ isText: isWebSqzTxt,
153
+ fileExt: node_path_1.default.extname(cleanedUpId),
154
+ };
155
+ return await loadAndTransform(id, hookRes);
156
+ }
157
+ }
158
+ },
159
+ writeBundle: async function (outputOptions, bundle) {
160
+ const jsFiles = Object.entries(bundle)
161
+ .filter(([fileName, asset]) => fileName.endsWith('.js') && asset.type === 'chunk');
162
+ if (jsFiles.length === 0) {
163
+ throw new Error('No JavaScript file found in the bundle.');
164
+ }
165
+ if (jsFiles.length > 1) {
166
+ throw new Error('Multiple JavaScript files found in the bundle. Make sure to bundle into a single file.\nTry setting "build.rollupOptions.output.inlineDynamicImports" to true in Vite config.');
167
+ }
168
+ const [jsFileName, jsChunk] = jsFiles[0];
169
+ if (isBuild) {
170
+ const outDir = node_path_1.default.resolve(outputOptions.dir || "", "websqz-tmp");
171
+ if (await promises_1.default.stat(outDir).catch(() => false)) {
172
+ await promises_1.default.rmdir(outDir, { recursive: true });
173
+ }
174
+ const filesToCompress = [];
175
+ const preCompressedFiles = [];
176
+ for (const [id, file] of files) {
177
+ this.debug(`Copying '${id}' for websqz...`);
178
+ const outPath = node_path_1.default.resolve(outputOptions.dir || "", "websqz-tmp", file.fileName);
179
+ await promises_1.default.mkdir(node_path_1.default.dirname(outPath), { recursive: true });
180
+ await promises_1.default.writeFile(outPath, file.content);
181
+ if (file.isCompressed) {
182
+ preCompressedFiles.push(node_path_1.default.relative(".", outPath));
183
+ }
184
+ else {
185
+ filesToCompress.push({ isText: file.isText, fileExt: file.fileExt, path: node_path_1.default.relative(".", outPath) });
186
+ }
187
+ }
188
+ // Sort by file extension for better compression ratios in websqz
189
+ filesToCompress.sort((a, b) => Math.sign(a.fileExt.localeCompare(b.fileExt)) + 2 * (a.isText === b.isText ? 0 : a.isText ? -1 : 1));
190
+ this.info(`Using websqz executable at '${websqzExe.websqzPath}'`);
191
+ await websqzExe.run({
192
+ jsMain: node_path_1.default.resolve(outputOptions.dir || "", jsFileName),
193
+ files: filesToCompress.map(f => f.path),
194
+ preCompressedFiles: preCompressedFiles,
195
+ output: node_path_1.default.resolve(outputOptions.dir || "", "websqz-output"),
196
+ });
197
+ const relOutPath = node_path_1.default.relative(".", node_path_1.default.resolve(outputOptions.dir || "", "websqz-output"));
198
+ this.info(`Websqz completed, output at '${relOutPath}'.\nRun 'python -m http.server -d ${relOutPath}' to serve the output.`);
199
+ }
200
+ },
201
+ };
202
+ }
203
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { PluginContext, rollup, type OutputAsset, type OutputChunk, type OutputOptions, type Plugin } from \"rollup\";\nimport querystring from \"node:querystring\";\nimport fs from \"node:fs/promises\";\nimport fsSync from \"node:fs\";\nimport path from \"node:path\";\nimport child_process from \"node:child_process\";\nimport { createFilter, FilterPattern } from \"@rollup/pluginutils\";\n\ntype WebSqzFile = {\n fileName: string;\n content: Buffer;\n isCompressed: boolean;\n fileExt: string;\n isText: boolean;\n};\n\nexport type WebsqzFileTransformRes = {\n /**\n * The result of processing\n */\n content: Buffer;\n\n /**\n * Is already compressed and should not be compressed again by websqz\n */\n isCompressed: boolean;\n\n /**\n * Is it text? The plugin orders files by type for better compression ratios\n */\n isText: boolean;\n\n /**\n * File extension including the dot, e.g. .txt.\n * This is used to order files for better compression ratios.\n * Same type of content should share same file extension.\n * \n * By default it is extracted from the original file path.\n */\n fileExt?: string;\n};\n\ntype WebSqzOptions = {\n websqzPath?: string;\n\n /**\n * File transform hooks to process files before they are imported in code or compressed by websqz\n */\n fileTransforms?: [\n {\n include?: FilterPattern,\n exclude?: FilterPattern,\n transform: (ctx: PluginContext, id: string, content: Buffer) => Promise<WebsqzFileTransformRes>;\n }\n ]\n};\n\ntype WebSqzCliOptions = {\n jsMain: string;\n files: string[];\n preCompressedFiles: string[];\n output: string;\n}\n\nclass WebSqzExe {\n websqzPath: string;\n constructor(websqzPath: string) {\n this.websqzPath = websqzPath;\n }\n\n async run(cliOptions: WebSqzCliOptions): Promise<void> {\n const args: string[] = [];\n\n args.push(\"--js-main\", cliOptions.jsMain);\n\n for (const file of cliOptions.files) {\n args.push(\"--files\", file);\n }\n for (const preCompressedFile of cliOptions.preCompressedFiles) {\n args.push(\"--pre-compressed-files\", preCompressedFile);\n }\n\n args.push(\"--output-directory\", cliOptions.output);\n\n const spawn = await child_process.spawn(this.websqzPath, args, {\n stdio: \"inherit\",\n });\n\n return new Promise<void>((resolve, reject) => {\n spawn.once(\"error\", (err) => {\n reject(err);\n });\n\n spawn.once(\"close\", (code) => {\n if (code !== 0) {\n reject(new Error(`websqz process exited with code ${code}`));\n } else {\n resolve();\n }\n });\n });\n }\n}\n\nfunction websqzExecutablePath(executablePath: string | undefined): string {\n if (!executablePath) {\n const path = __dirname + \"/bin/websqz\";\n const extension = process.platform == \"win32\" ? \".exe\" : \"\";\n\n if (fsSync.existsSync(path + extension)) {\n return path + extension;\n }\n\n return \"websqz\" + extension;\n }\n\n return executablePath;\n}\n\nexport default function (options: WebSqzOptions = {}): Plugin {\n const isBuild = process.env.NODE_ENV === \"production\";\n const websqzExePath = websqzExecutablePath(options.websqzPath);\n const websqzExe = new WebSqzExe(websqzExePath);\n\n const fileTransforms = options.fileTransforms?.map(transform => {\n const include = transform.include ? (Array.isArray(transform.include) ? transform.include : [transform.include]) \n : undefined;\n const exclude = transform.exclude ? (Array.isArray(transform.exclude) ? transform.exclude : [transform.exclude]) \n : undefined;\n\n const filter = createFilter(include, exclude);\n return {\n transform: transform.transform,\n filter,\n }\n });\n\n const files = new Map<string, WebSqzFile>();\n let fileNameIdx = 0;\n\n const findNextAvailableFileName = () => {\n const startChar = 97; // a\n const endChar = 122; // z\n const alphabetSize= endChar - startChar;\n\n let numChars = fileNameIdx === 0 ? 1 : Math.floor(Math.log(fileNameIdx) / Math.log(alphabetSize)) + 1;\n let candidateName = \"\";\n \n for (let i = 0; i < numChars; i++) {\n const charCode = startChar + ((Math.floor(fileNameIdx / Math.pow(alphabetSize, i))) % alphabetSize);\n candidateName = String.fromCharCode(charCode) + candidateName;\n }\n\n fileNameIdx++;\n\n return candidateName;\n }\n\n const loadAndTransform = async function (id: string, hookRes: WebsqzFileTransformRes) {\n if (isBuild) {\n const fileName = findNextAvailableFileName();\n files.set(id, {\n fileName,\n content: hookRes.content,\n isCompressed: hookRes.isCompressed,\n fileExt: hookRes.fileExt ?? path.extname(id),\n isText: hookRes.isText,\n });\n\n return {\n code: hookRes.isText \n ? `export default new TextDecoder().decode(wsqz.files[\"${fileName}\"]);` \n : `export default wsqz.files[\"${fileName}\"];`,\n moduleSideEffects: false,\n moduleType: 'js',\n };\n } else {\n return {\n code: hookRes.isText \n ? `export default ${JSON.stringify(hookRes.content.toString(\"utf-8\"))};` \n : `export default Uint8Array.fromBase64(\"${hookRes.content.toString(\"base64\")}\");`,\n moduleSideEffects: false,\n moduleType: 'js',\n };\n }\n };\n\n return {\n name: \"rollup-plugin-websqz\",\n\n load: {\n order: \"pre\",\n async handler(id: string) {\n const qIdx = id.indexOf(\"?\");\n const beforeParams = id.slice(0, qIdx === -1 ? id.length : qIdx);\n const afterParams = id.slice(id.indexOf(\"?\") + 1);\n const parsed = querystring.parse(afterParams);\n const cleanedUpId = beforeParams;\n\n let cachedFile: Buffer | null = null;\n const loadFromDisk = async () => {\n if (cachedFile != null) {\n return cachedFile;\n }\n cachedFile = await fs.readFile(cleanedUpId);\n return cachedFile;\n };\n\n if (fileTransforms) {\n for (const transform of fileTransforms) {\n if (transform.filter(id)) {\n let hookRes = await transform.transform(this, id, await loadFromDisk());\n if (hookRes == null) {\n continue;\n }\n return await loadAndTransform(id, hookRes);\n }\n }\n }\n\n const isWebSqzTxt = parsed[\"websqz-txt\"] != null;\n const isWebSqzBin = parsed[\"websqz-bin\"] != null;\n\n if (isWebSqzTxt && isWebSqzBin) {\n throw new Error(\n `Cannot use both websqz-txt and websqz-bin on the same import: ${id}`,\n );\n }\n\n const isCompressed = parsed[\"compressed\"] != null;\n\n if (isWebSqzTxt || isWebSqzBin) {\n const content = await loadFromDisk();\n \n let hookRes: WebsqzFileTransformRes = {\n content,\n isCompressed,\n isText: isWebSqzTxt,\n fileExt: path.extname(cleanedUpId),\n };\n return await loadAndTransform(id, hookRes);\n }\n }\n },\n\n writeBundle: async function (outputOptions: OutputOptions, bundle: { [fileName: string]: OutputAsset | OutputChunk }) {\n const jsFiles = Object.entries(bundle)\n .filter(([fileName, asset]) => fileName.endsWith('.js') && asset.type === 'chunk');\n\n if (jsFiles.length === 0) {\n throw new Error('No JavaScript file found in the bundle.');\n }\n if (jsFiles.length > 1) {\n throw new Error('Multiple JavaScript files found in the bundle. Make sure to bundle into a single file.\\nTry setting \"build.rollupOptions.output.inlineDynamicImports\" to true in Vite config.');\n }\n\n const [jsFileName, jsChunk] = jsFiles[0];\n\n if (isBuild) {\n const outDir = path.resolve(\n outputOptions.dir || \"\",\n \"websqz-tmp\");\n if (await fs.stat(outDir).catch(() => false)) {\n await fs.rmdir(outDir, { recursive: true });\n }\n\n const filesToCompress = [];\n const preCompressedFiles = [];\n\n for (const [id, file] of files) {\n this.debug(`Copying '${id}' for websqz...`);\n const outPath = path.resolve(\n outputOptions.dir || \"\",\n \"websqz-tmp\",\n file.fileName,\n );\n\n await fs.mkdir(path.dirname(outPath), { recursive: true });\n await fs.writeFile(outPath, file.content);\n\n if (file.isCompressed) {\n preCompressedFiles.push(path.relative(\".\", outPath));\n } else {\n filesToCompress.push({ isText: file.isText, fileExt: file.fileExt, path: path.relative(\".\", outPath) });\n }\n }\n\n // Sort by file extension for better compression ratios in websqz\n filesToCompress.sort((a, b) => Math.sign(a.fileExt.localeCompare(b.fileExt)) + 2 * (a.isText === b.isText ? 0 : a.isText ? -1 : 1));\n\n this.info(`Using websqz executable at '${websqzExe.websqzPath}'`);\n await websqzExe.run({\n jsMain: path.resolve(\n outputOptions.dir || \"\",\n jsFileName,\n ),\n files: filesToCompress.map(f => f.path),\n preCompressedFiles: preCompressedFiles,\n output: path.resolve(\n outputOptions.dir || \"\",\n \"websqz-output\",\n ),\n });\n\n const relOutPath = path.relative(\".\", path.resolve(outputOptions.dir || \"\", \"websqz-output\"));\n this.info(`Websqz completed, output at '${relOutPath}'.\\nRun 'python -m http.server -d ${relOutPath}' to serve the output.`);\n }\n },\n };\n}\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "rollup-plugin-websqz",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": "./dist/index.js",
8
+ "files": [
9
+ "scripts/install.js",
10
+ "dist",
11
+ "LICENSE.md"
12
+ ],
13
+ "keywords": [
14
+ "rollup-plugin",
15
+ "vite-plugin",
16
+ "websqz",
17
+ "compression"
18
+ ],
19
+ "author": "",
20
+ "license": "ISC",
21
+ "dependencies": {
22
+ "@rollup/pluginutils": "^5.3.0"
23
+ },
24
+ "devDependencies": {
25
+ "@rollup/plugin-typescript": "^12.3.0",
26
+ "@tsconfig/node22": "^22.0.5",
27
+ "rollup": "^4.55.3",
28
+ "tslib": "^2.8.1",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "scripts": {
32
+ "build": "rollup -c",
33
+ "build:watch": "rollup -c -w",
34
+ "build:dts": "tsc --emitDeclarationOnly",
35
+ "postinstall": "node scripts/install.js"
36
+ }
37
+ }
@@ -0,0 +1,180 @@
1
+ const fs = require("fs");
2
+ const https = require("https");
3
+ const http = require("http");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const RELEASE_BASE_URL = "https://github.com/r00tkids/websqz/releases/download";
8
+ const VERSION = "0.3";
9
+ const TOOL_NAME = "websqz";
10
+ const BIN_DIR = path.resolve(__dirname, "../dist/bin");
11
+
12
+ function getRustTarget() {
13
+ const platform = process.platform;
14
+ const arch = process.arch;
15
+
16
+ let triple;
17
+ let ext = "";
18
+
19
+ if (platform === "win32") {
20
+ ext = ".exe";
21
+ if (arch === "x64") triple = "x86_64-pc-windows-msvc";
22
+ else if (arch === "arm64") triple = "aarch64-pc-windows-msvc";
23
+ else triple = `${arch}-pc-windows-msvc`;
24
+ } else if (platform === "darwin") {
25
+ if (arch === "x64") triple = "x86_64-apple-darwin";
26
+ else if (arch === "arm64") triple = "aarch64-apple-darwin";
27
+ else triple = `${arch}-apple-darwin`;
28
+ } else if (platform === "linux") {
29
+ if (arch === "x64") triple = "x86_64-unknown-linux-gnu";
30
+ else if (arch === "arm64") triple = "aarch64-unknown-linux-gnu";
31
+ else triple = `${arch}-unknown-linux-gnu`;
32
+ } else {
33
+ throw new Error(`Unsupported platform: ${platform}`);
34
+ }
35
+
36
+ return { triple, ext };
37
+ }
38
+
39
+ function buildAssetName(triple, ext) {
40
+ return `${TOOL_NAME}-${triple}${ext}`;
41
+ }
42
+
43
+ function getDownloadUrl(assetName) {
44
+ return `${RELEASE_BASE_URL}/v${VERSION}/${assetName}`;
45
+ }
46
+
47
+ function httpRequest(url, opts, callback) {
48
+ const client = url.startsWith("https://") ? https : http;
49
+ return client.get(url, opts, callback);
50
+ }
51
+
52
+ function downloadToFile(url, destPath) {
53
+ return new Promise((resolve, reject) => {
54
+ const maxRedirects = 5;
55
+ let redirects = 0;
56
+
57
+ function get(urlToGet) {
58
+ const req = httpRequest(urlToGet, {}, (res) => {
59
+ // Follow redirects
60
+ if (
61
+ res.statusCode >= 300 &&
62
+ res.statusCode < 400 &&
63
+ res.headers.location
64
+ ) {
65
+ if (redirects++ >= maxRedirects) {
66
+ reject(new Error("Too many redirects"));
67
+ return;
68
+ }
69
+ const next = new URL(res.headers.location, urlToGet).toString();
70
+ res.resume();
71
+ get(next);
72
+ return;
73
+ }
74
+
75
+ if (res.statusCode !== 200) {
76
+ reject(
77
+ new Error(
78
+ `Download failed: ${res.statusCode} ${res.statusMessage}`,
79
+ ),
80
+ );
81
+ res.resume();
82
+ return;
83
+ }
84
+
85
+ const total = parseInt(res.headers["content-length"] || "0", 10);
86
+ let downloaded = 0;
87
+
88
+ const fileStream = fs.createWriteStream(destPath, { mode: 0o755 });
89
+ res.on("data", (chunk) => {
90
+ downloaded += chunk.length;
91
+ if (total) {
92
+ const pct = ((downloaded / total) * 100).toFixed(1);
93
+ process.stdout.write(
94
+ `\rDownloading ${path.basename(destPath)} ${pct}% (${(downloaded / 1024).toFixed(1)} KB)`,
95
+ );
96
+ }
97
+ });
98
+
99
+ res.pipe(fileStream);
100
+
101
+ fileStream.on("finish", () => {
102
+ fileStream.close(() => {
103
+ process.stdout.write("\n");
104
+ resolve();
105
+ });
106
+ });
107
+
108
+ fileStream.on("error", (err) => {
109
+ fs.unlink(destPath, () => reject(err));
110
+ });
111
+ });
112
+
113
+ req.on("error", (err) => reject(err));
114
+ }
115
+
116
+ get(url);
117
+ });
118
+ }
119
+
120
+ async function ensureBinDir() {
121
+ await fs.promises.mkdir(BIN_DIR, { recursive: true });
122
+ }
123
+
124
+ async function makeExecutable(filePath) {
125
+ if (process.platform === "win32") {
126
+ // Windows uses .exe; no chmod needed generally
127
+ return;
128
+ }
129
+ try {
130
+ await fs.promises.chmod(filePath, 0o755);
131
+ } catch (err) {
132
+ // Best-effort, not fatal
133
+ console.warn(
134
+ `Warning: could not set executable permissions on ${filePath}: ${err.message}`,
135
+ );
136
+ }
137
+ }
138
+
139
+ async function main() {
140
+ try {
141
+ const { triple, ext } = getRustTarget();
142
+ const assetName = buildAssetName(triple, ext);
143
+ const url = getDownloadUrl(assetName);
144
+
145
+ console.log(`Detected platform: ${process.platform} ${process.arch}`);
146
+ console.log(`Downloading asset: ${assetName}`);
147
+ console.log(`From: ${url}`);
148
+
149
+ if (fs.existsSync(BIN_DIR + "/" + TOOL_NAME + ext)) {
150
+ console.log(`WebSQZ binary already exists at ${BIN_DIR}/${TOOL_NAME}${ext}, skipping download.`);
151
+ return;
152
+ }
153
+
154
+ await ensureBinDir();
155
+
156
+ const destFileName = `${TOOL_NAME}${ext}`; // place binary in bin with consistent name
157
+ const destPath = path.join(BIN_DIR, destFileName);
158
+
159
+ // Remove existing file if present (optional)
160
+ try {
161
+ await fs.promises.unlink(destPath);
162
+ } catch (err) {
163
+ // ignore if not exists
164
+ }
165
+
166
+ await downloadToFile(url, destPath);
167
+
168
+ await makeExecutable(destPath);
169
+
170
+ console.log(`Downloaded and saved to: ${destPath}`);
171
+ console.log("Done.");
172
+ } catch (err) {
173
+ console.error(`Error: ${err.message}`);
174
+ process.exitCode = 1;
175
+ }
176
+ }
177
+
178
+ if (require.main === module) {
179
+ main();
180
+ }