@kohi9noor/hotloop 1.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kohinoor nimes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/cli.js ADDED
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/logger.ts
13
+ var timestamp, logger;
14
+ var init_logger = __esm({
15
+ "src/logger.ts"() {
16
+ timestamp = () => (/* @__PURE__ */ new Date()).toLocaleTimeString();
17
+ logger = {
18
+ info: (msg) => {
19
+ console.log(
20
+ `\x1B[36m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
21
+ );
22
+ },
23
+ success: (msg) => {
24
+ console.log(
25
+ `\x1B[32m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
26
+ );
27
+ },
28
+ warn: (msg) => {
29
+ console.warn(
30
+ `\x1B[33m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
31
+ );
32
+ },
33
+ error: (msg) => {
34
+ console.error(
35
+ `\x1B[31m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
36
+ );
37
+ }
38
+ };
39
+ }
40
+ });
41
+
42
+ // src/server.ts
43
+ var server_exports = {};
44
+ __export(server_exports, {
45
+ HmrServer: () => HmrServer
46
+ });
47
+ import { WebSocketServer, WebSocket } from "ws";
48
+ var HmrServer;
49
+ var init_server = __esm({
50
+ "src/server.ts"() {
51
+ init_logger();
52
+ HmrServer = class {
53
+ wss;
54
+ clients = /* @__PURE__ */ new Set();
55
+ constructor(port) {
56
+ this.wss = new WebSocketServer({ port }, () => {
57
+ logger.info(`HMR Server running on ws://localhost:${port}`);
58
+ });
59
+ this.wss.on("connection", (ws) => {
60
+ this.clients.add(ws);
61
+ ws.on("close", () => {
62
+ this.clients.delete(ws);
63
+ });
64
+ });
65
+ }
66
+ broadcast(message) {
67
+ const data = JSON.stringify(message);
68
+ this.clients.forEach((client) => {
69
+ if (client.readyState === WebSocket.OPEN) {
70
+ client.send(data);
71
+ }
72
+ });
73
+ }
74
+ reload() {
75
+ this.broadcast({ type: "reload" });
76
+ }
77
+ close() {
78
+ this.wss.close();
79
+ }
80
+ };
81
+ }
82
+ });
83
+
84
+ // src/builder.ts
85
+ var builder_exports = {};
86
+ __export(builder_exports, {
87
+ ExtensionBuilder: () => ExtensionBuilder
88
+ });
89
+ import * as esbuild from "esbuild";
90
+ import * as fs from "fs/promises";
91
+ import * as path from "path";
92
+ var ExtensionBuilder;
93
+ var init_builder = __esm({
94
+ "src/builder.ts"() {
95
+ init_logger();
96
+ ExtensionBuilder = class {
97
+ constructor(cwd = process.cwd()) {
98
+ this.cwd = cwd;
99
+ }
100
+ async build(entries) {
101
+ const start = Date.now();
102
+ try {
103
+ logger.info(`Building ${entries.length} entry(ies)...`);
104
+ for (const entry of entries) {
105
+ const inputPath = path.join(this.cwd, entry.input);
106
+ const outputPath = path.join(this.cwd, entry.output);
107
+ const outputDir = path.dirname(outputPath);
108
+ await fs.mkdir(outputDir, { recursive: true });
109
+ await esbuild.build({
110
+ entryPoints: [inputPath],
111
+ bundle: true,
112
+ format: "iife",
113
+ target: "es2020",
114
+ outfile: outputPath,
115
+ sourcemap: false,
116
+ logLevel: "silent"
117
+ });
118
+ logger.success(`${entry.input} \u2192 ${entry.output}`);
119
+ }
120
+ const elapsed = Date.now() - start;
121
+ logger.success(
122
+ `Compilation complete (${entries.length} files) in ${elapsed}ms`
123
+ );
124
+ } catch (error) {
125
+ logger.error(`Build failed: ${error}`);
126
+ throw error;
127
+ }
128
+ }
129
+ async copy(files) {
130
+ for (const { src, dest } of files) {
131
+ const srcPath = path.join(this.cwd, src);
132
+ const destPath = path.join(this.cwd, dest);
133
+ const destDir = path.dirname(destPath);
134
+ await fs.mkdir(destDir, { recursive: true });
135
+ await fs.copyFile(srcPath, destPath);
136
+ }
137
+ }
138
+ async writeManifest(options) {
139
+ const srcPath = path.join(this.cwd, options.src);
140
+ const destPath = path.join(this.cwd, options.dest);
141
+ const destDir = path.dirname(destPath);
142
+ const raw = await fs.readFile(srcPath, "utf-8");
143
+ const manifest = JSON.parse(raw);
144
+ if (options.injectHmr) {
145
+ const hmrScript = options.hmrScript || "content/hmr.js";
146
+ const hmrMatches = options.hmrMatches || ["<all_urls>"];
147
+ const hmrRunAt = options.hmrRunAt || "document_start";
148
+ const existing = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
149
+ const alreadyInjected = existing.some((entry) => {
150
+ const scripts = entry.js || [];
151
+ return Array.isArray(scripts) && scripts.includes(hmrScript);
152
+ });
153
+ if (!alreadyInjected) {
154
+ existing.push({
155
+ matches: hmrMatches,
156
+ js: [hmrScript],
157
+ run_at: hmrRunAt
158
+ });
159
+ }
160
+ manifest.content_scripts = existing;
161
+ }
162
+ await fs.mkdir(destDir, { recursive: true });
163
+ await fs.writeFile(destPath, JSON.stringify(manifest, null, 2), "utf-8");
164
+ }
165
+ };
166
+ }
167
+ });
168
+
169
+ // src/watcher.ts
170
+ var watcher_exports = {};
171
+ __export(watcher_exports, {
172
+ FileWatcher: () => FileWatcher
173
+ });
174
+ import * as fs2 from "fs";
175
+ var FileWatcher;
176
+ var init_watcher = __esm({
177
+ "src/watcher.ts"() {
178
+ init_logger();
179
+ FileWatcher = class {
180
+ constructor(watchDir, onChange) {
181
+ this.watchDir = watchDir;
182
+ this.onChange = onChange;
183
+ }
184
+ watchers = [];
185
+ debounceTimer = null;
186
+ debounceDelay = 300;
187
+ start() {
188
+ const watch2 = (dir) => {
189
+ const watcher = fs2.watch(dir, { recursive: true }, async (_, file) => {
190
+ if (!file) return;
191
+ if (String(file).includes("node_modules")) return;
192
+ if (String(file).includes("dist")) return;
193
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
194
+ this.debounceTimer = setTimeout(async () => {
195
+ try {
196
+ await this.onChange();
197
+ } catch (error) {
198
+ logger.error(`Build error: ${error}`);
199
+ }
200
+ }, this.debounceDelay);
201
+ });
202
+ this.watchers.push(watcher);
203
+ };
204
+ watch2(this.watchDir);
205
+ logger.info(`Watching ${this.watchDir} for changes`);
206
+ }
207
+ stop() {
208
+ this.watchers.forEach((w) => w.close());
209
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
210
+ }
211
+ };
212
+ }
213
+ });
214
+
215
+ // src/config.ts
216
+ var config_exports = {};
217
+ __export(config_exports, {
218
+ loadConfig: () => loadConfig,
219
+ validateConfig: () => validateConfig
220
+ });
221
+ import * as path2 from "path";
222
+ import { pathToFileURL } from "url";
223
+ async function loadConfig(cwd) {
224
+ const configPath = path2.join(cwd, "hotloop.config.js");
225
+ const configUrl = pathToFileURL(configPath).href;
226
+ try {
227
+ const module = await import(configUrl);
228
+ return module.default;
229
+ } catch (error) {
230
+ throw new Error(
231
+ `Failed to load hotloop.config.js: ${error instanceof Error ? error.message : String(error)}`
232
+ );
233
+ }
234
+ }
235
+ function validateConfig(config) {
236
+ if (!config.entries || config.entries.length === 0) {
237
+ throw new Error("No entries configured. Provide at least one entry point.");
238
+ }
239
+ config.entries.forEach((entry) => {
240
+ if (!entry.input || !entry.output) {
241
+ throw new Error("Each entry must have input and output properties.");
242
+ }
243
+ });
244
+ }
245
+ var init_config = __esm({
246
+ "src/config.ts"() {
247
+ }
248
+ });
249
+
250
+ // src/index.ts
251
+ init_logger();
252
+ init_server();
253
+ init_builder();
254
+ init_watcher();
255
+ init_config();
256
+ async function runBuild(cwd, options) {
257
+ const { loadConfig: loadConfig2, validateConfig: validateConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
258
+ const { HmrServer: HmrServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
259
+ const { ExtensionBuilder: ExtensionBuilder2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
260
+ const { FileWatcher: FileWatcher2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
261
+ const config = await loadConfig2(cwd);
262
+ validateConfig2(config);
263
+ if (config.manifest && options?.injectHmr === false) {
264
+ config.manifest = { ...config.manifest, injectHmr: false };
265
+ }
266
+ const outDir = config.outDir || "dist";
267
+ const buildOutDir = config.buildOutDir || outDir;
268
+ const replaceOutDir = (target) => {
269
+ const normalized = target.replace(/\\/g, "/");
270
+ const prefix = `${outDir.replace(/\\/g, "/")}/`;
271
+ if (normalized.startsWith(prefix)) {
272
+ return normalized.replace(prefix, `${buildOutDir}/`);
273
+ }
274
+ return target;
275
+ };
276
+ if (options?.watch === false && buildOutDir !== outDir) {
277
+ config.entries = config.entries.map((entry) => ({
278
+ ...entry,
279
+ output: replaceOutDir(entry.output)
280
+ }));
281
+ if (config.manifest) {
282
+ config.manifest = {
283
+ ...config.manifest,
284
+ dest: replaceOutDir(config.manifest.dest)
285
+ };
286
+ }
287
+ if (config.assets) {
288
+ config.assets = config.assets.map((asset) => ({
289
+ ...asset,
290
+ dest: replaceOutDir(asset.dest)
291
+ }));
292
+ }
293
+ if (config.copy) {
294
+ config.copy = config.copy.map((asset) => ({
295
+ ...asset,
296
+ dest: replaceOutDir(asset.dest)
297
+ }));
298
+ }
299
+ }
300
+ const port = config.port || 3e3;
301
+ const watchDir = config.watchDir || "src";
302
+ const server = options?.watch === false ? null : new HmrServer2(port);
303
+ const builder = new ExtensionBuilder2(cwd);
304
+ await builder.build(config.entries);
305
+ if (config.manifest) {
306
+ await builder.writeManifest(config.manifest);
307
+ }
308
+ if (config.assets) {
309
+ await builder.copy(config.assets);
310
+ }
311
+ if (config.copy) {
312
+ await builder.copy(config.copy);
313
+ }
314
+ if (options?.watch === false) {
315
+ return;
316
+ }
317
+ const watcher = new FileWatcher2(watchDir, async () => {
318
+ config.onBuildStart?.();
319
+ try {
320
+ await builder.build(config.entries);
321
+ if (config.manifest) {
322
+ await builder.writeManifest(config.manifest);
323
+ }
324
+ if (config.assets) {
325
+ await builder.copy(config.assets);
326
+ }
327
+ if (config.copy) {
328
+ await builder.copy(config.copy);
329
+ }
330
+ server?.reload();
331
+ } catch (error) {
332
+ logger.error(`Build failed: ${error}`);
333
+ }
334
+ config.onBuildEnd?.();
335
+ });
336
+ watcher.start();
337
+ process.on("SIGINT", () => {
338
+ logger.info("Shutting down");
339
+ watcher.stop();
340
+ server?.close();
341
+ process.exit(0);
342
+ });
343
+ }
344
+ async function startDevServer(cwd = process.cwd()) {
345
+ await runBuild(cwd, { watch: true, injectHmr: true });
346
+ }
347
+ async function buildOnce(cwd = process.cwd()) {
348
+ await runBuild(cwd, { watch: false, injectHmr: false });
349
+ }
350
+
351
+ // src/scaffold.ts
352
+ init_logger();
353
+ import * as fs3 from "fs/promises";
354
+ import * as path3 from "path";
355
+ async function scaffold(options) {
356
+ const { projectName: projectName2, targetDir } = options;
357
+ try {
358
+ logger.info(`Creating project: ${projectName2}`);
359
+ await fs3.mkdir(path3.join(targetDir, "src", "background"), {
360
+ recursive: true
361
+ });
362
+ await fs3.mkdir(path3.join(targetDir, "src", "content"), {
363
+ recursive: true
364
+ });
365
+ await fs3.mkdir(path3.join(targetDir, "src", "ui"), { recursive: true });
366
+ const manifest = {
367
+ manifest_version: 3,
368
+ name: projectName2,
369
+ version: "1.0.0",
370
+ description: "A Chrome extension built with Hotwire",
371
+ permissions: ["tabs"],
372
+ background: {
373
+ service_worker: "background/index.js"
374
+ },
375
+ action: {
376
+ default_popup: "ui/popup.html",
377
+ default_title: projectName2
378
+ }
379
+ };
380
+ await fs3.writeFile(
381
+ path3.join(targetDir, "src", "manifest.json"),
382
+ JSON.stringify(manifest, null, 2)
383
+ );
384
+ const backgroundScript = `console.log("Background script loaded");
385
+
386
+ chrome.runtime.onInstalled.addListener(() => {
387
+ console.log("Extension installed");
388
+ });
389
+ `;
390
+ await fs3.writeFile(
391
+ path3.join(targetDir, "src", "background", "index.ts"),
392
+ backgroundScript
393
+ );
394
+ const popupHtml = `<!DOCTYPE html>
395
+ <html lang="en">
396
+ <head>
397
+ <meta charset="UTF-8">
398
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
399
+ <title>${projectName2}</title>
400
+ <link rel="stylesheet" href="popup.css">
401
+ </head>
402
+ <body>
403
+ <h1>${projectName2}</h1>
404
+ <p>Extension ready</p>
405
+ <script src="popup.js"></script>
406
+ </body>
407
+ </html>
408
+ `;
409
+ await fs3.writeFile(
410
+ path3.join(targetDir, "src", "ui", "popup.html"),
411
+ popupHtml
412
+ );
413
+ const popupCss = `body {
414
+ width: 400px;
415
+ padding: 16px;
416
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
417
+ background: #f5f5f5;
418
+ }
419
+
420
+ h1 {
421
+ margin: 0 0 12px 0;
422
+ font-size: 18px;
423
+ color: #333;
424
+ }
425
+
426
+ p {
427
+ margin: 0;
428
+ color: #666;
429
+ font-size: 14px;
430
+ }
431
+ `;
432
+ await fs3.writeFile(
433
+ path3.join(targetDir, "src", "ui", "popup.css"),
434
+ popupCss
435
+ );
436
+ const popupScript = `document.addEventListener("DOMContentLoaded", () => {
437
+ console.log("Popup loaded");
438
+ });
439
+ `;
440
+ await fs3.writeFile(
441
+ path3.join(targetDir, "src", "ui", "popup.ts"),
442
+ popupScript
443
+ );
444
+ const configFile = `export default {
445
+ port: 3000,
446
+ watchDir: "src",
447
+ outDir: "dist",
448
+ buildOutDir: "build",
449
+ entries: [
450
+ { input: "src/background/index.ts", output: "dist/background/index.js" },
451
+ { input: "src/ui/popup.ts", output: "dist/ui/popup.js" },
452
+ ],
453
+ manifest: {
454
+ src: "src/manifest.json",
455
+ dest: "dist/manifest.json",
456
+ injectHmr: true,
457
+ hmrScript: "content/hmr.js",
458
+ hmrMatches: ["<all_urls>"],
459
+ hmrRunAt: "document_start",
460
+ },
461
+ assets: [
462
+ { src: "src/ui/popup.html", dest: "dist/ui/popup.html" },
463
+ { src: "src/ui/popup.css", dest: "dist/ui/popup.css" },
464
+ ],
465
+ };
466
+ `;
467
+ await fs3.writeFile(path3.join(targetDir, "hotwire.config.js"), configFile);
468
+ const packageJson = {
469
+ name: projectName2,
470
+ version: "1.0.0",
471
+ description: `A Chrome extension: ${projectName2}`,
472
+ type: "module",
473
+ scripts: {
474
+ dev: "hotwire dev",
475
+ build: "hotwire build"
476
+ },
477
+ devDependencies: {
478
+ "@types/chrome": "^0.1.37",
479
+ typescript: "^5.9.3"
480
+ }
481
+ };
482
+ await fs3.writeFile(
483
+ path3.join(targetDir, "package.json"),
484
+ JSON.stringify(packageJson, null, 2)
485
+ );
486
+ const tsconfig = {
487
+ compilerOptions: {
488
+ target: "ES2020",
489
+ module: "ESNext",
490
+ lib: ["ES2020"],
491
+ skipLibCheck: true,
492
+ esModuleInterop: true,
493
+ resolveJsonModule: true,
494
+ strict: true
495
+ },
496
+ include: ["src"]
497
+ };
498
+ await fs3.writeFile(
499
+ path3.join(targetDir, "tsconfig.json"),
500
+ JSON.stringify(tsconfig, null, 2)
501
+ );
502
+ const gitignore = `node_modules/
503
+ dist/
504
+ build/
505
+ *.log
506
+ .DS_Store
507
+ `;
508
+ await fs3.writeFile(path3.join(targetDir, ".gitignore"), gitignore);
509
+ logger.success(`Project created at ${targetDir}`);
510
+ logger.info("Next steps:");
511
+ logger.info(` cd ${projectName2}`);
512
+ logger.info(` npm install`);
513
+ logger.info(` npm run dev`);
514
+ } catch (error) {
515
+ logger.error(`Scaffolding failed: ${error}`);
516
+ throw error;
517
+ }
518
+ }
519
+
520
+ // src/cli.ts
521
+ init_logger();
522
+ import * as path4 from "path";
523
+ import * as fs4 from "fs/promises";
524
+ var command = process.argv[2];
525
+ var projectName = process.argv[3];
526
+ if (command === "dev") {
527
+ logger.info("Starting Dev Server");
528
+ startDevServer(process.cwd());
529
+ } else if (command === "build") {
530
+ logger.info("Building extension (no HMR)");
531
+ buildOnce(process.cwd());
532
+ } else if (command === "init") {
533
+ if (!projectName) {
534
+ logger.error("Project name required: hotwire init <project-name>");
535
+ process.exit(1);
536
+ }
537
+ const targetDir = path4.join(process.cwd(), projectName);
538
+ fs4.mkdir(targetDir, { recursive: true }).then(() => scaffold({ projectName, targetDir })).catch((error) => {
539
+ logger.error(`Init failed: ${error}`);
540
+ process.exit(1);
541
+ });
542
+ } else {
543
+ console.log(`
544
+ Hotloop - Hot Module Reloading for Chrome Extensions
545
+
546
+ Usage:
547
+ hotloop init <name> Create a new project
548
+ hotloop dev Start the dev server
549
+ hotloop build Build once (no HMR)
550
+
551
+ Examples:
552
+ hotloop init my-extension
553
+ hotloop dev
554
+ hotloop build
555
+ `);
556
+ }
package/dist/index.js ADDED
@@ -0,0 +1,357 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/logger.ts
12
+ var timestamp, logger;
13
+ var init_logger = __esm({
14
+ "src/logger.ts"() {
15
+ timestamp = () => (/* @__PURE__ */ new Date()).toLocaleTimeString();
16
+ logger = {
17
+ info: (msg) => {
18
+ console.log(
19
+ `\x1B[36m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
20
+ );
21
+ },
22
+ success: (msg) => {
23
+ console.log(
24
+ `\x1B[32m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
25
+ );
26
+ },
27
+ warn: (msg) => {
28
+ console.warn(
29
+ `\x1B[33m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
30
+ );
31
+ },
32
+ error: (msg) => {
33
+ console.error(
34
+ `\x1B[31m[hotloop]\x1B[0m \x1B[90m${timestamp()}\x1B[0m ${msg}`
35
+ );
36
+ }
37
+ };
38
+ }
39
+ });
40
+
41
+ // src/server.ts
42
+ var server_exports = {};
43
+ __export(server_exports, {
44
+ HmrServer: () => HmrServer
45
+ });
46
+ import { WebSocketServer, WebSocket } from "ws";
47
+ var HmrServer;
48
+ var init_server = __esm({
49
+ "src/server.ts"() {
50
+ init_logger();
51
+ HmrServer = class {
52
+ wss;
53
+ clients = /* @__PURE__ */ new Set();
54
+ constructor(port) {
55
+ this.wss = new WebSocketServer({ port }, () => {
56
+ logger.info(`HMR Server running on ws://localhost:${port}`);
57
+ });
58
+ this.wss.on("connection", (ws) => {
59
+ this.clients.add(ws);
60
+ ws.on("close", () => {
61
+ this.clients.delete(ws);
62
+ });
63
+ });
64
+ }
65
+ broadcast(message) {
66
+ const data = JSON.stringify(message);
67
+ this.clients.forEach((client) => {
68
+ if (client.readyState === WebSocket.OPEN) {
69
+ client.send(data);
70
+ }
71
+ });
72
+ }
73
+ reload() {
74
+ this.broadcast({ type: "reload" });
75
+ }
76
+ close() {
77
+ this.wss.close();
78
+ }
79
+ };
80
+ }
81
+ });
82
+
83
+ // src/builder.ts
84
+ var builder_exports = {};
85
+ __export(builder_exports, {
86
+ ExtensionBuilder: () => ExtensionBuilder
87
+ });
88
+ import * as esbuild from "esbuild";
89
+ import * as fs from "fs/promises";
90
+ import * as path from "path";
91
+ var ExtensionBuilder;
92
+ var init_builder = __esm({
93
+ "src/builder.ts"() {
94
+ init_logger();
95
+ ExtensionBuilder = class {
96
+ constructor(cwd = process.cwd()) {
97
+ this.cwd = cwd;
98
+ }
99
+ async build(entries) {
100
+ const start = Date.now();
101
+ try {
102
+ logger.info(`Building ${entries.length} entry(ies)...`);
103
+ for (const entry of entries) {
104
+ const inputPath = path.join(this.cwd, entry.input);
105
+ const outputPath = path.join(this.cwd, entry.output);
106
+ const outputDir = path.dirname(outputPath);
107
+ await fs.mkdir(outputDir, { recursive: true });
108
+ await esbuild.build({
109
+ entryPoints: [inputPath],
110
+ bundle: true,
111
+ format: "iife",
112
+ target: "es2020",
113
+ outfile: outputPath,
114
+ sourcemap: false,
115
+ logLevel: "silent"
116
+ });
117
+ logger.success(`${entry.input} \u2192 ${entry.output}`);
118
+ }
119
+ const elapsed = Date.now() - start;
120
+ logger.success(
121
+ `Compilation complete (${entries.length} files) in ${elapsed}ms`
122
+ );
123
+ } catch (error) {
124
+ logger.error(`Build failed: ${error}`);
125
+ throw error;
126
+ }
127
+ }
128
+ async copy(files) {
129
+ for (const { src, dest } of files) {
130
+ const srcPath = path.join(this.cwd, src);
131
+ const destPath = path.join(this.cwd, dest);
132
+ const destDir = path.dirname(destPath);
133
+ await fs.mkdir(destDir, { recursive: true });
134
+ await fs.copyFile(srcPath, destPath);
135
+ }
136
+ }
137
+ async writeManifest(options) {
138
+ const srcPath = path.join(this.cwd, options.src);
139
+ const destPath = path.join(this.cwd, options.dest);
140
+ const destDir = path.dirname(destPath);
141
+ const raw = await fs.readFile(srcPath, "utf-8");
142
+ const manifest = JSON.parse(raw);
143
+ if (options.injectHmr) {
144
+ const hmrScript = options.hmrScript || "content/hmr.js";
145
+ const hmrMatches = options.hmrMatches || ["<all_urls>"];
146
+ const hmrRunAt = options.hmrRunAt || "document_start";
147
+ const existing = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
148
+ const alreadyInjected = existing.some((entry) => {
149
+ const scripts = entry.js || [];
150
+ return Array.isArray(scripts) && scripts.includes(hmrScript);
151
+ });
152
+ if (!alreadyInjected) {
153
+ existing.push({
154
+ matches: hmrMatches,
155
+ js: [hmrScript],
156
+ run_at: hmrRunAt
157
+ });
158
+ }
159
+ manifest.content_scripts = existing;
160
+ }
161
+ await fs.mkdir(destDir, { recursive: true });
162
+ await fs.writeFile(destPath, JSON.stringify(manifest, null, 2), "utf-8");
163
+ }
164
+ };
165
+ }
166
+ });
167
+
168
+ // src/watcher.ts
169
+ var watcher_exports = {};
170
+ __export(watcher_exports, {
171
+ FileWatcher: () => FileWatcher
172
+ });
173
+ import * as fs2 from "fs";
174
+ var FileWatcher;
175
+ var init_watcher = __esm({
176
+ "src/watcher.ts"() {
177
+ init_logger();
178
+ FileWatcher = class {
179
+ constructor(watchDir, onChange) {
180
+ this.watchDir = watchDir;
181
+ this.onChange = onChange;
182
+ }
183
+ watchers = [];
184
+ debounceTimer = null;
185
+ debounceDelay = 300;
186
+ start() {
187
+ const watch2 = (dir) => {
188
+ const watcher = fs2.watch(dir, { recursive: true }, async (_, file) => {
189
+ if (!file) return;
190
+ if (String(file).includes("node_modules")) return;
191
+ if (String(file).includes("dist")) return;
192
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
193
+ this.debounceTimer = setTimeout(async () => {
194
+ try {
195
+ await this.onChange();
196
+ } catch (error) {
197
+ logger.error(`Build error: ${error}`);
198
+ }
199
+ }, this.debounceDelay);
200
+ });
201
+ this.watchers.push(watcher);
202
+ };
203
+ watch2(this.watchDir);
204
+ logger.info(`Watching ${this.watchDir} for changes`);
205
+ }
206
+ stop() {
207
+ this.watchers.forEach((w) => w.close());
208
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
209
+ }
210
+ };
211
+ }
212
+ });
213
+
214
+ // src/config.ts
215
+ var config_exports = {};
216
+ __export(config_exports, {
217
+ loadConfig: () => loadConfig,
218
+ validateConfig: () => validateConfig
219
+ });
220
+ import * as path2 from "path";
221
+ import { pathToFileURL } from "url";
222
+ async function loadConfig(cwd) {
223
+ const configPath = path2.join(cwd, "hotloop.config.js");
224
+ const configUrl = pathToFileURL(configPath).href;
225
+ try {
226
+ const module = await import(configUrl);
227
+ return module.default;
228
+ } catch (error) {
229
+ throw new Error(
230
+ `Failed to load hotloop.config.js: ${error instanceof Error ? error.message : String(error)}`
231
+ );
232
+ }
233
+ }
234
+ function validateConfig(config) {
235
+ if (!config.entries || config.entries.length === 0) {
236
+ throw new Error("No entries configured. Provide at least one entry point.");
237
+ }
238
+ config.entries.forEach((entry) => {
239
+ if (!entry.input || !entry.output) {
240
+ throw new Error("Each entry must have input and output properties.");
241
+ }
242
+ });
243
+ }
244
+ var init_config = __esm({
245
+ "src/config.ts"() {
246
+ }
247
+ });
248
+
249
+ // src/index.ts
250
+ init_logger();
251
+ init_server();
252
+ init_builder();
253
+ init_watcher();
254
+ init_config();
255
+ async function runBuild(cwd, options) {
256
+ const { loadConfig: loadConfig2, validateConfig: validateConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
257
+ const { HmrServer: HmrServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
258
+ const { ExtensionBuilder: ExtensionBuilder2 } = await Promise.resolve().then(() => (init_builder(), builder_exports));
259
+ const { FileWatcher: FileWatcher2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
260
+ const config = await loadConfig2(cwd);
261
+ validateConfig2(config);
262
+ if (config.manifest && options?.injectHmr === false) {
263
+ config.manifest = { ...config.manifest, injectHmr: false };
264
+ }
265
+ const outDir = config.outDir || "dist";
266
+ const buildOutDir = config.buildOutDir || outDir;
267
+ const replaceOutDir = (target) => {
268
+ const normalized = target.replace(/\\/g, "/");
269
+ const prefix = `${outDir.replace(/\\/g, "/")}/`;
270
+ if (normalized.startsWith(prefix)) {
271
+ return normalized.replace(prefix, `${buildOutDir}/`);
272
+ }
273
+ return target;
274
+ };
275
+ if (options?.watch === false && buildOutDir !== outDir) {
276
+ config.entries = config.entries.map((entry) => ({
277
+ ...entry,
278
+ output: replaceOutDir(entry.output)
279
+ }));
280
+ if (config.manifest) {
281
+ config.manifest = {
282
+ ...config.manifest,
283
+ dest: replaceOutDir(config.manifest.dest)
284
+ };
285
+ }
286
+ if (config.assets) {
287
+ config.assets = config.assets.map((asset) => ({
288
+ ...asset,
289
+ dest: replaceOutDir(asset.dest)
290
+ }));
291
+ }
292
+ if (config.copy) {
293
+ config.copy = config.copy.map((asset) => ({
294
+ ...asset,
295
+ dest: replaceOutDir(asset.dest)
296
+ }));
297
+ }
298
+ }
299
+ const port = config.port || 3e3;
300
+ const watchDir = config.watchDir || "src";
301
+ const server = options?.watch === false ? null : new HmrServer2(port);
302
+ const builder = new ExtensionBuilder2(cwd);
303
+ await builder.build(config.entries);
304
+ if (config.manifest) {
305
+ await builder.writeManifest(config.manifest);
306
+ }
307
+ if (config.assets) {
308
+ await builder.copy(config.assets);
309
+ }
310
+ if (config.copy) {
311
+ await builder.copy(config.copy);
312
+ }
313
+ if (options?.watch === false) {
314
+ return;
315
+ }
316
+ const watcher = new FileWatcher2(watchDir, async () => {
317
+ config.onBuildStart?.();
318
+ try {
319
+ await builder.build(config.entries);
320
+ if (config.manifest) {
321
+ await builder.writeManifest(config.manifest);
322
+ }
323
+ if (config.assets) {
324
+ await builder.copy(config.assets);
325
+ }
326
+ if (config.copy) {
327
+ await builder.copy(config.copy);
328
+ }
329
+ server?.reload();
330
+ } catch (error) {
331
+ logger.error(`Build failed: ${error}`);
332
+ }
333
+ config.onBuildEnd?.();
334
+ });
335
+ watcher.start();
336
+ process.on("SIGINT", () => {
337
+ logger.info("Shutting down");
338
+ watcher.stop();
339
+ server?.close();
340
+ process.exit(0);
341
+ });
342
+ }
343
+ async function startDevServer(cwd = process.cwd()) {
344
+ await runBuild(cwd, { watch: true, injectHmr: true });
345
+ }
346
+ async function buildOnce(cwd = process.cwd()) {
347
+ await runBuild(cwd, { watch: false, injectHmr: false });
348
+ }
349
+ export {
350
+ ExtensionBuilder,
351
+ FileWatcher,
352
+ HmrServer,
353
+ buildOnce,
354
+ loadConfig,
355
+ startDevServer,
356
+ validateConfig
357
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@kohi9noor/hotloop",
3
+ "version": "1.3.0",
4
+ "description": "Minimal, framework-free development tool for small-scale browser extensions. TypeScript, HMR, and Node module support—nothing else.",
5
+ "author": "kohi9noor",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "bin": {
10
+ "hotloop": "dist/cli.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/kohi9noor/hotloop"
15
+ },
16
+ "keywords": [
17
+ "browser-extension",
18
+ "chrome-extension",
19
+ "hmr",
20
+ "hot-module-reloading",
21
+ "typescript",
22
+ "esbuild",
23
+ "development-tool"
24
+ ],
25
+ "scripts": {
26
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --external:ws --external:esbuild --external:fs --external:fs/promises --external:path --external:child_process --external:url --outdir=dist && esbuild src/cli.ts --bundle --platform=node --format=esm --external:ws --external:esbuild --external:fs --external:fs/promises --external:path --external:child_process --external:url --outfile=dist/cli.js",
27
+ "prepare": "npm run build"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "dependencies": {
33
+ "ws": "^8.19.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^25.3.2",
37
+ "esbuild": "^0.27.3",
38
+ "typescript": "^5.9.3"
39
+ }
40
+ }
package/readme.md ADDED
@@ -0,0 +1,187 @@
1
+ # Hotloop
2
+
3
+ Minimal development tooling for browser extensions.
4
+
5
+ TypeScript. HMR. Node modules.
6
+ No frameworks. No plugin circus.
7
+
8
+ ---
9
+
10
+ ## Why Hotloop Exists
11
+
12
+ Most extension tooling assumes you're building a mini web app.
13
+
14
+ They integrate Vite pipelines, plugin systems, framework layers, and opinionated structures. That’s great for complex extensions with routing, heavy UI, or framework-driven architecture.
15
+
16
+ But what if you're not?
17
+
18
+ What if:
19
+
20
+ - You're building a small extension with 2–5 focused features
21
+ - You want clean architecture
22
+ - You want TypeScript
23
+ - You want fast rebuilds
24
+ - You want node module support
25
+ - But you **don’t** want React, Vue, or framework assumptions
26
+
27
+ Hotloop is built for that.
28
+
29
+ It gives you modern development essentials without pushing you into a frontend framework ecosystem.
30
+
31
+ It doesn’t try to define your architecture.
32
+ It just improves your development loop.
33
+
34
+ ---
35
+
36
+ ## Philosophy
37
+
38
+ Hotloop is built around one idea:
39
+
40
+ You should be able to write vanilla extension code
41
+ with proper engineering discipline
42
+ without importing half the frontend world.
43
+
44
+ You get:
45
+
46
+ - TypeScript for safety
47
+ - esbuild for speed
48
+ - HMR for fast iteration
49
+ - Node module support
50
+ - A single config file
51
+ - No framework lock-in
52
+
53
+ That’s it.
54
+
55
+ If you want routing systems, plugin ecosystems, or integrated framework support, tools like WXT or CRXJS are excellent choices.
56
+
57
+ Hotloop intentionally stays smaller.
58
+
59
+ ---
60
+
61
+ ## Comparison
62
+
63
+ | Feature | Hotloop | WXT | CRXJS |
64
+ | --------------------- | ---------------- | -------------------------- | ----------------------- |
65
+ | TypeScript Support | ✅ | ✅ | ✅ |
66
+ | Hot Module Reloading | ✅ | ✅ | ✅ |
67
+ | Node Module Support | ✅ | ✅ | ✅ |
68
+ | Build System | esbuild | Vite | Vite |
69
+ | Framework Assumptions | ❌ | Optional | Optional |
70
+ | Plugin Ecosystem | ❌ | ✅ | ✅ |
71
+ | Config Complexity | Minimal | Moderate | Moderate |
72
+ | Best For | Small extensions | Structured / scalable apps | Chrome-focused projects |
73
+
74
+ Hotloop does less on purpose.
75
+
76
+ ---
77
+
78
+ ## Getting Started
79
+
80
+ ### Create a New Project
81
+
82
+ Scaffold a new extension project using `npx`:
83
+
84
+ ```bash
85
+ npx hotloop init my-extension
86
+ cd my-extension
87
+ npm install
88
+ ```
89
+
90
+ Then start development:
91
+
92
+ ```bash
93
+ npm run dev
94
+ ```
95
+
96
+ ### Install in an Existing Project
97
+
98
+ Install as a dev dependency:
99
+
100
+ ```bash
101
+ npm install --save-dev hotloop
102
+ ```
103
+
104
+ Add scripts to your `package.json`:
105
+
106
+ ```json
107
+ {
108
+ "scripts": {
109
+ "dev": "hotloop dev",
110
+ "build": "hotloop build"
111
+ }
112
+ }
113
+ ```
114
+
115
+ Run:
116
+
117
+ ```bash
118
+ npm run dev
119
+ ```
120
+
121
+ Build for production:
122
+
123
+ ```bash
124
+ npm run build
125
+ ```
126
+
127
+ Load the `dist` directory as an unpacked extension.
128
+
129
+ Configuration
130
+
131
+ Create a hotloop.config.js in your project root:
132
+
133
+ export default {
134
+ entries: [
135
+ { input: "src/background/index.ts", output: "dist/background/index.js" },
136
+ { input: "src/content/index.ts", output: "dist/content/index.js" },
137
+ { input: "src/ui/popup.ts", output: "dist/ui/popup.js" },
138
+ ],
139
+
140
+ manifest: {
141
+ src: "src/manifest.json",
142
+ dest: "dist/manifest.json",
143
+
144
+ // Injects HMR client only in development mode
145
+ hmrMatches: ["<all_urls>"],
146
+
147
+ },
148
+
149
+ copy: [
150
+ { src: "src/ui/popup.html", dest: "dist/ui/popup.html" },
151
+ { src: "src/ui/popup.css", dest: "dist/ui/popup.css" },
152
+ ],
153
+
154
+ port: 8000,
155
+ };
156
+
157
+ That’s the entire configuration surface.
158
+
159
+ No plugin arrays.
160
+ No layered overrides.
161
+ No framework presets.
162
+
163
+ ## Features
164
+
165
+ **Entry Points** – Bundle background scripts, content scripts, and UI scripts independently.
166
+
167
+ **Manifest Handling** – Copies and optionally injects development scripts during dev mode.
168
+
169
+ **Hot Module Reloading** – WebSocket-based HMR improves iteration speed without full extension reload cycles.
170
+
171
+ **Node Modules** – Import npm packages compatible with browser environments.
172
+
173
+ **Fast Builds** – Powered by esbuild for near-instant rebuilds.
174
+
175
+ ## What Hotloop Is Not
176
+
177
+ - Not a UI framework
178
+ - Not a plugin platform
179
+ - Not an opinionated architecture
180
+ - Not a meta-framework
181
+
182
+ It’s a dev loop optimizer for vanilla extensions.
183
+
184
+ ## License
185
+
186
+ MIT
187
+