@kohi9noor/hotloop 1.3.7 → 1.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -89,24 +89,35 @@ __export(builder_exports, {
89
89
  import * as esbuild from "esbuild";
90
90
  import * as fs from "fs/promises";
91
91
  import * as path from "path";
92
- var HMR_CLIENT_NAME, ExtensionBuilder;
92
+ var ExtensionBuilder;
93
93
  var init_builder = __esm({
94
94
  "src/builder.ts"() {
95
95
  init_logger();
96
- HMR_CLIENT_NAME = "__hotloop_hmr__.js";
97
96
  ExtensionBuilder = class {
98
97
  constructor(cwd = process.cwd()) {
99
98
  this.cwd = cwd;
100
99
  }
101
- async build(entries) {
100
+ async build(entries, hmrPort) {
102
101
  const start = Date.now();
103
102
  try {
104
103
  logger.info(`Building ${entries.length} entry(ies)...`);
104
+ let hmrCode = null;
105
+ if (hmrPort) {
106
+ const hmrPath = path.join(
107
+ path.dirname(new URL(import.meta.url).pathname),
108
+ "hotloop-hmr.js"
109
+ );
110
+ hmrCode = await fs.readFile(hmrPath, "utf-8");
111
+ }
105
112
  for (const entry of entries) {
106
113
  const inputPath = path.join(this.cwd, entry.input);
107
114
  const outputPath = path.join(this.cwd, entry.output);
108
115
  const outputDir = path.dirname(outputPath);
109
116
  await fs.mkdir(outputDir, { recursive: true });
117
+ const define = {};
118
+ if (hmrPort && entry.input.toLowerCase().includes("background")) {
119
+ define.__HOTLOOP_PORT__ = hmrPort.toString();
120
+ }
110
121
  await esbuild.build({
111
122
  entryPoints: [inputPath],
112
123
  bundle: true,
@@ -114,8 +125,14 @@ var init_builder = __esm({
114
125
  target: "es2020",
115
126
  outfile: outputPath,
116
127
  sourcemap: false,
117
- logLevel: "silent"
128
+ logLevel: "silent",
129
+ define
118
130
  });
131
+ if (hmrPort && hmrCode && entry.input.toLowerCase().includes("background")) {
132
+ const builtCode = await fs.readFile(outputPath, "utf-8");
133
+ const finalCode = builtCode + "\n\n" + hmrCode;
134
+ await fs.writeFile(outputPath, finalCode, "utf-8");
135
+ }
119
136
  logger.success(`${entry.input} \u2192 ${entry.output}`);
120
137
  }
121
138
  const elapsed = Date.now() - start;
@@ -138,7 +155,7 @@ var init_builder = __esm({
138
155
  }
139
156
  /**
140
157
  * Writes the manifest file to the destination.
141
- * If injectHmr is true, it automatically injects the HMR client script into content_scripts.
158
+ * HMR is now handled directly in the background service worker.
142
159
  */
143
160
  async writeManifest(options, injectHmr = false) {
144
161
  const srcPath = path.join(this.cwd, options.src);
@@ -146,45 +163,9 @@ var init_builder = __esm({
146
163
  const destDir = path.dirname(destPath);
147
164
  const raw = await fs.readFile(srcPath, "utf-8");
148
165
  const manifest = JSON.parse(raw);
149
- if (injectHmr) {
150
- const hmrMatches = options.hmrMatches || ["<all_urls>"];
151
- const existing = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
152
- const alreadyInjected = existing.some((entry) => {
153
- const scripts = entry.js || [];
154
- return scripts.includes(HMR_CLIENT_NAME);
155
- });
156
- if (!alreadyInjected) {
157
- existing.push({
158
- matches: hmrMatches,
159
- js: [HMR_CLIENT_NAME],
160
- run_at: "document_start"
161
- });
162
- }
163
- manifest.content_scripts = existing;
164
- }
165
166
  await fs.mkdir(destDir, { recursive: true });
166
167
  await fs.writeFile(destPath, JSON.stringify(manifest, null, 2), "utf-8");
167
168
  }
168
- async buildHmrClient(port, manifestDest) {
169
- const hmrClientPath = path.join(import.meta.dirname, "hmr-client.js");
170
- const outputDir = path.dirname(path.join(this.cwd, manifestDest));
171
- const outputPath = path.join(outputDir, HMR_CLIENT_NAME);
172
- try {
173
- await fs.mkdir(outputDir, { recursive: true });
174
- await esbuild.build({
175
- entryPoints: [hmrClientPath],
176
- bundle: true,
177
- format: "iife",
178
- logLevel: "silent",
179
- outfile: outputPath,
180
- define: {
181
- __HOTLOOP_PORT__: port.toString()
182
- }
183
- });
184
- } catch (error) {
185
- logger.error(`Failed to build HMR client: ${error}`);
186
- }
187
- }
188
169
  };
189
170
  }
190
171
  });
@@ -322,10 +303,8 @@ async function runBuild(cwd, options) {
322
303
  const watchDir = config.watchDir || "src";
323
304
  const server = options?.watch === false ? null : new HmrServer2(port);
324
305
  const builder = new ExtensionBuilder2(cwd);
325
- if (injectHmr && config.manifest) {
326
- await builder.buildHmrClient(port, config.manifest.dest);
327
- }
328
- await builder.build(config.entries);
306
+ const hmrPort = options?.watch === true ? port : 8e3;
307
+ await builder.build(config.entries, hmrPort);
329
308
  if (config.manifest) {
330
309
  await builder.writeManifest(config.manifest, injectHmr);
331
310
  }
@@ -341,7 +320,7 @@ async function runBuild(cwd, options) {
341
320
  const watcher = new FileWatcher2(watchDir, async () => {
342
321
  config.onBuildStart?.();
343
322
  try {
344
- await builder.build(config.entries);
323
+ await builder.build(config.entries, hmrPort);
345
324
  if (config.manifest) {
346
325
  await builder.writeManifest(config.manifest, injectHmr);
347
326
  }
@@ -399,7 +378,13 @@ async function scaffold(options) {
399
378
  action: {
400
379
  default_popup: "ui/popup.html",
401
380
  default_title: projectName2
402
- }
381
+ },
382
+ content_scripts: [
383
+ {
384
+ matches: ["<all_urls>"],
385
+ js: ["content/index.js"]
386
+ }
387
+ ]
403
388
  };
404
389
  await fs3.writeFile(
405
390
  path3.join(targetDir, "src", "manifest.json"),
@@ -0,0 +1,53 @@
1
+ /**
2
+ * HMR Client for Background Service Worker
3
+ * This code runs only in the background script and manages the entire extension reload.
4
+ * When __HOTLOOP_PORT__ is defined (development mode), it connects to the HMR server.
5
+ * On reload signal, it calls chrome.runtime.reload() which:
6
+ * - Restarts the background script
7
+ * - Re-injects all content scripts
8
+ * - Reloads the popup
9
+ * - Synchronizes everything in one shot
10
+ */
11
+
12
+ (function setupHMR() {
13
+ // __HOTLOOP_PORT__ is injected by esbuild during the build
14
+ // Default to 8000 if not defined (for development fallback)
15
+ const port =
16
+ typeof __HOTLOOP_PORT__ !== "undefined" ? __HOTLOOP_PORT__ : 8000;
17
+
18
+ function connectHMR() {
19
+ const host = "localhost";
20
+ const ws = new WebSocket(`ws://${host}:${port}`);
21
+
22
+ ws.onopen = () => {
23
+ console.log("[hotloop] HMR connected in background");
24
+ };
25
+
26
+ ws.onmessage = (event) => {
27
+ try {
28
+ const data = JSON.parse(event.data);
29
+ if (data.type === "reload") {
30
+ console.log(
31
+ "[hotloop] Reload signal received, reloading entire extension...",
32
+ );
33
+ // This is the magic: one reload triggers everything
34
+ chrome.runtime.reload();
35
+ }
36
+ } catch (err) {
37
+ console.error("[hotloop] Failed to parse HMR message:", err);
38
+ }
39
+ };
40
+
41
+ ws.onclose = () => {
42
+ console.log("[hotloop] HMR disconnected. Retrying in 2s...");
43
+ setTimeout(connectHMR, 2000);
44
+ };
45
+
46
+ ws.onerror = (error) => {
47
+ console.error("[hotloop] HMR WebSocket error:", error);
48
+ };
49
+ }
50
+
51
+ // Only connect to HMR in development
52
+ connectHMR();
53
+ })();
package/dist/index.js CHANGED
@@ -88,24 +88,35 @@ __export(builder_exports, {
88
88
  import * as esbuild from "esbuild";
89
89
  import * as fs from "fs/promises";
90
90
  import * as path from "path";
91
- var HMR_CLIENT_NAME, ExtensionBuilder;
91
+ var ExtensionBuilder;
92
92
  var init_builder = __esm({
93
93
  "src/builder.ts"() {
94
94
  init_logger();
95
- HMR_CLIENT_NAME = "__hotloop_hmr__.js";
96
95
  ExtensionBuilder = class {
97
96
  constructor(cwd = process.cwd()) {
98
97
  this.cwd = cwd;
99
98
  }
100
- async build(entries) {
99
+ async build(entries, hmrPort) {
101
100
  const start = Date.now();
102
101
  try {
103
102
  logger.info(`Building ${entries.length} entry(ies)...`);
103
+ let hmrCode = null;
104
+ if (hmrPort) {
105
+ const hmrPath = path.join(
106
+ path.dirname(new URL(import.meta.url).pathname),
107
+ "hotloop-hmr.js"
108
+ );
109
+ hmrCode = await fs.readFile(hmrPath, "utf-8");
110
+ }
104
111
  for (const entry of entries) {
105
112
  const inputPath = path.join(this.cwd, entry.input);
106
113
  const outputPath = path.join(this.cwd, entry.output);
107
114
  const outputDir = path.dirname(outputPath);
108
115
  await fs.mkdir(outputDir, { recursive: true });
116
+ const define = {};
117
+ if (hmrPort && entry.input.toLowerCase().includes("background")) {
118
+ define.__HOTLOOP_PORT__ = hmrPort.toString();
119
+ }
109
120
  await esbuild.build({
110
121
  entryPoints: [inputPath],
111
122
  bundle: true,
@@ -113,8 +124,14 @@ var init_builder = __esm({
113
124
  target: "es2020",
114
125
  outfile: outputPath,
115
126
  sourcemap: false,
116
- logLevel: "silent"
127
+ logLevel: "silent",
128
+ define
117
129
  });
130
+ if (hmrPort && hmrCode && entry.input.toLowerCase().includes("background")) {
131
+ const builtCode = await fs.readFile(outputPath, "utf-8");
132
+ const finalCode = builtCode + "\n\n" + hmrCode;
133
+ await fs.writeFile(outputPath, finalCode, "utf-8");
134
+ }
118
135
  logger.success(`${entry.input} \u2192 ${entry.output}`);
119
136
  }
120
137
  const elapsed = Date.now() - start;
@@ -137,7 +154,7 @@ var init_builder = __esm({
137
154
  }
138
155
  /**
139
156
  * Writes the manifest file to the destination.
140
- * If injectHmr is true, it automatically injects the HMR client script into content_scripts.
157
+ * HMR is now handled directly in the background service worker.
141
158
  */
142
159
  async writeManifest(options, injectHmr = false) {
143
160
  const srcPath = path.join(this.cwd, options.src);
@@ -145,45 +162,9 @@ var init_builder = __esm({
145
162
  const destDir = path.dirname(destPath);
146
163
  const raw = await fs.readFile(srcPath, "utf-8");
147
164
  const manifest = JSON.parse(raw);
148
- if (injectHmr) {
149
- const hmrMatches = options.hmrMatches || ["<all_urls>"];
150
- const existing = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
151
- const alreadyInjected = existing.some((entry) => {
152
- const scripts = entry.js || [];
153
- return scripts.includes(HMR_CLIENT_NAME);
154
- });
155
- if (!alreadyInjected) {
156
- existing.push({
157
- matches: hmrMatches,
158
- js: [HMR_CLIENT_NAME],
159
- run_at: "document_start"
160
- });
161
- }
162
- manifest.content_scripts = existing;
163
- }
164
165
  await fs.mkdir(destDir, { recursive: true });
165
166
  await fs.writeFile(destPath, JSON.stringify(manifest, null, 2), "utf-8");
166
167
  }
167
- async buildHmrClient(port, manifestDest) {
168
- const hmrClientPath = path.join(import.meta.dirname, "hmr-client.js");
169
- const outputDir = path.dirname(path.join(this.cwd, manifestDest));
170
- const outputPath = path.join(outputDir, HMR_CLIENT_NAME);
171
- try {
172
- await fs.mkdir(outputDir, { recursive: true });
173
- await esbuild.build({
174
- entryPoints: [hmrClientPath],
175
- bundle: true,
176
- format: "iife",
177
- logLevel: "silent",
178
- outfile: outputPath,
179
- define: {
180
- __HOTLOOP_PORT__: port.toString()
181
- }
182
- });
183
- } catch (error) {
184
- logger.error(`Failed to build HMR client: ${error}`);
185
- }
186
- }
187
168
  };
188
169
  }
189
170
  });
@@ -321,10 +302,8 @@ async function runBuild(cwd, options) {
321
302
  const watchDir = config.watchDir || "src";
322
303
  const server = options?.watch === false ? null : new HmrServer2(port);
323
304
  const builder = new ExtensionBuilder2(cwd);
324
- if (injectHmr && config.manifest) {
325
- await builder.buildHmrClient(port, config.manifest.dest);
326
- }
327
- await builder.build(config.entries);
305
+ const hmrPort = options?.watch === true ? port : 8e3;
306
+ await builder.build(config.entries, hmrPort);
328
307
  if (config.manifest) {
329
308
  await builder.writeManifest(config.manifest, injectHmr);
330
309
  }
@@ -340,7 +319,7 @@ async function runBuild(cwd, options) {
340
319
  const watcher = new FileWatcher2(watchDir, async () => {
341
320
  config.onBuildStart?.();
342
321
  try {
343
- await builder.build(config.entries);
322
+ await builder.build(config.entries, hmrPort);
344
323
  if (config.manifest) {
345
324
  await builder.writeManifest(config.manifest, injectHmr);
346
325
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohi9noor/hotloop",
3
- "version": "1.3.7",
3
+ "version": "1.3.10",
4
4
  "description": "Minimal, framework-free development tool for small-scale browser extensions. TypeScript, HMR, and Node module support—nothing else.",
5
5
  "author": "kohi9noor",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  "development-tool"
24
24
  ],
25
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 && cp src/hmr-client.js dist/",
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 && cp src/hotloop-hmr.js dist/",
27
27
  "prepare": "npm run build"
28
28
  },
29
29
  "files": [
@@ -35,6 +35,7 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^25.3.2",
38
+ "@types/ws": "^8.18.1",
38
39
  "typescript": "^5.9.3"
39
40
  }
40
41
  }
@@ -1,31 +0,0 @@
1
- /* src/hmr-client.js */
2
-
3
- // These constants will be replaced by esbuild during the build process
4
- const port = __HOTLOOP_PORT__ || 8000;
5
- const host = "localhost";
6
-
7
- const socket = new WebSocket(`ws://${host}:${port}`);
8
-
9
- socket.onopen = () => {
10
- console.log("[hotloop] HMR connected");
11
- };
12
-
13
- socket.onmessage = (event) => {
14
- try {
15
- const data = JSON.parse(event.data);
16
- if (data.type === "reload") {
17
- console.log("[hotloop] Change detected, reloading extension...");
18
- chrome.runtime.reload();
19
- }
20
- } catch (err) {
21
- console.error("[hotloop] Failed to parse HMR message:", err);
22
- }
23
- };
24
-
25
- socket.onclose = () => {
26
- console.log("[hotloop] HMR disconnected. Refresh the page to reconnect.");
27
- };
28
-
29
- socket.onerror = (error) => {
30
- console.error("[hotloop] HMR WebSocket error:", error);
31
- };