@kohi9noor/hotloop 1.3.5 → 1.3.7

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,10 +89,11 @@ __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 ExtensionBuilder;
92
+ var HMR_CLIENT_NAME, ExtensionBuilder;
93
93
  var init_builder = __esm({
94
94
  "src/builder.ts"() {
95
95
  init_logger();
96
+ HMR_CLIENT_NAME = "__hotloop_hmr__.js";
96
97
  ExtensionBuilder = class {
97
98
  constructor(cwd = process.cwd()) {
98
99
  this.cwd = cwd;
@@ -135,26 +136,28 @@ var init_builder = __esm({
135
136
  await fs.copyFile(srcPath, destPath);
136
137
  }
137
138
  }
138
- async writeManifest(options) {
139
+ /**
140
+ * Writes the manifest file to the destination.
141
+ * If injectHmr is true, it automatically injects the HMR client script into content_scripts.
142
+ */
143
+ async writeManifest(options, injectHmr = false) {
139
144
  const srcPath = path.join(this.cwd, options.src);
140
145
  const destPath = path.join(this.cwd, options.dest);
141
146
  const destDir = path.dirname(destPath);
142
147
  const raw = await fs.readFile(srcPath, "utf-8");
143
148
  const manifest = JSON.parse(raw);
144
- if (options.injectHmr) {
145
- const hmrScript = options.hmrScript || "content/hmr.js";
149
+ if (injectHmr) {
146
150
  const hmrMatches = options.hmrMatches || ["<all_urls>"];
147
- const hmrRunAt = options.hmrRunAt || "document_start";
148
151
  const existing = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
149
152
  const alreadyInjected = existing.some((entry) => {
150
153
  const scripts = entry.js || [];
151
- return Array.isArray(scripts) && scripts.includes(hmrScript);
154
+ return scripts.includes(HMR_CLIENT_NAME);
152
155
  });
153
156
  if (!alreadyInjected) {
154
157
  existing.push({
155
158
  matches: hmrMatches,
156
- js: [hmrScript],
157
- run_at: hmrRunAt
159
+ js: [HMR_CLIENT_NAME],
160
+ run_at: "document_start"
158
161
  });
159
162
  }
160
163
  manifest.content_scripts = existing;
@@ -162,6 +165,26 @@ var init_builder = __esm({
162
165
  await fs.mkdir(destDir, { recursive: true });
163
166
  await fs.writeFile(destPath, JSON.stringify(manifest, null, 2), "utf-8");
164
167
  }
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
+ }
165
188
  };
166
189
  }
167
190
  });
@@ -260,11 +283,9 @@ async function runBuild(cwd, options) {
260
283
  const { FileWatcher: FileWatcher2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
261
284
  const config = await loadConfig2(cwd);
262
285
  validateConfig2(config);
263
- if (config.manifest && options?.injectHmr === false) {
264
- config.manifest = { ...config.manifest, injectHmr: false };
265
- }
266
286
  const outDir = config.outDir || "dist";
267
287
  const buildOutDir = config.buildOutDir || outDir;
288
+ const injectHmr = options?.injectHmr ?? false;
268
289
  const replaceOutDir = (target) => {
269
290
  const normalized = target.replace(/\\/g, "/");
270
291
  const prefix = `${outDir.replace(/\\/g, "/")}/`;
@@ -297,13 +318,16 @@ async function runBuild(cwd, options) {
297
318
  }));
298
319
  }
299
320
  }
300
- const port = config.port || 3e3;
321
+ const port = config.port || 8e3;
301
322
  const watchDir = config.watchDir || "src";
302
323
  const server = options?.watch === false ? null : new HmrServer2(port);
303
324
  const builder = new ExtensionBuilder2(cwd);
325
+ if (injectHmr && config.manifest) {
326
+ await builder.buildHmrClient(port, config.manifest.dest);
327
+ }
304
328
  await builder.build(config.entries);
305
329
  if (config.manifest) {
306
- await builder.writeManifest(config.manifest);
330
+ await builder.writeManifest(config.manifest, injectHmr);
307
331
  }
308
332
  if (config.assets) {
309
333
  await builder.copy(config.assets);
@@ -319,7 +343,7 @@ async function runBuild(cwd, options) {
319
343
  try {
320
344
  await builder.build(config.entries);
321
345
  if (config.manifest) {
322
- await builder.writeManifest(config.manifest);
346
+ await builder.writeManifest(config.manifest, injectHmr);
323
347
  }
324
348
  if (config.assets) {
325
349
  await builder.copy(config.assets);
@@ -441,24 +465,28 @@ p {
441
465
  path3.join(targetDir, "src", "ui", "popup.ts"),
442
466
  popupScript
443
467
  );
468
+ const contentScript = `console.log("Content script loaded");
469
+ `;
470
+ await fs3.writeFile(
471
+ path3.join(targetDir, "src", "content", "index.ts"),
472
+ contentScript
473
+ );
444
474
  const configFile = `export default {
445
- port: 3000,
446
- watchDir: "src",
447
- outDir: "dist",
448
- buildOutDir: "build",
475
+ port: 8000,
449
476
  entries: [
450
477
  { input: "src/background/index.ts", output: "dist/background/index.js" },
478
+ { input: "src/content/index.ts", output: "dist/content/index.js" },
451
479
  { input: "src/ui/popup.ts", output: "dist/ui/popup.js" },
452
480
  ],
481
+
453
482
  manifest: {
454
483
  src: "src/manifest.json",
455
484
  dest: "dist/manifest.json",
456
- injectHmr: true,
457
- hmrScript: "content/hmr.js",
485
+ // Injects HMR client only in development mode
458
486
  hmrMatches: ["<all_urls>"],
459
- hmrRunAt: "document_start",
460
487
  },
461
- assets: [
488
+
489
+ copy: [
462
490
  { src: "src/ui/popup.html", dest: "dist/ui/popup.html" },
463
491
  { src: "src/ui/popup.css", dest: "dist/ui/popup.css" },
464
492
  ],
@@ -475,6 +503,7 @@ p {
475
503
  build: "hotloop build"
476
504
  },
477
505
  devDependencies: {
506
+ "@kohi9noor/hotloop": "latest",
478
507
  "@types/chrome": "^0.1.37",
479
508
  typescript: "^5.9.3"
480
509
  }
@@ -487,10 +516,11 @@ p {
487
516
  compilerOptions: {
488
517
  target: "ES2020",
489
518
  module: "ESNext",
490
- lib: ["ES2020"],
519
+ lib: ["ES2020", "DOM", "WebWorker"],
491
520
  skipLibCheck: true,
492
521
  esModuleInterop: true,
493
522
  resolveJsonModule: true,
523
+ moduleResolution: "node",
494
524
  strict: true
495
525
  },
496
526
  include: ["src"]
@@ -0,0 +1,31 @@
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
+ };
package/dist/index.js CHANGED
@@ -88,10 +88,11 @@ __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 ExtensionBuilder;
91
+ var HMR_CLIENT_NAME, ExtensionBuilder;
92
92
  var init_builder = __esm({
93
93
  "src/builder.ts"() {
94
94
  init_logger();
95
+ HMR_CLIENT_NAME = "__hotloop_hmr__.js";
95
96
  ExtensionBuilder = class {
96
97
  constructor(cwd = process.cwd()) {
97
98
  this.cwd = cwd;
@@ -134,26 +135,28 @@ var init_builder = __esm({
134
135
  await fs.copyFile(srcPath, destPath);
135
136
  }
136
137
  }
137
- async writeManifest(options) {
138
+ /**
139
+ * Writes the manifest file to the destination.
140
+ * If injectHmr is true, it automatically injects the HMR client script into content_scripts.
141
+ */
142
+ async writeManifest(options, injectHmr = false) {
138
143
  const srcPath = path.join(this.cwd, options.src);
139
144
  const destPath = path.join(this.cwd, options.dest);
140
145
  const destDir = path.dirname(destPath);
141
146
  const raw = await fs.readFile(srcPath, "utf-8");
142
147
  const manifest = JSON.parse(raw);
143
- if (options.injectHmr) {
144
- const hmrScript = options.hmrScript || "content/hmr.js";
148
+ if (injectHmr) {
145
149
  const hmrMatches = options.hmrMatches || ["<all_urls>"];
146
- const hmrRunAt = options.hmrRunAt || "document_start";
147
150
  const existing = Array.isArray(manifest.content_scripts) ? manifest.content_scripts : [];
148
151
  const alreadyInjected = existing.some((entry) => {
149
152
  const scripts = entry.js || [];
150
- return Array.isArray(scripts) && scripts.includes(hmrScript);
153
+ return scripts.includes(HMR_CLIENT_NAME);
151
154
  });
152
155
  if (!alreadyInjected) {
153
156
  existing.push({
154
157
  matches: hmrMatches,
155
- js: [hmrScript],
156
- run_at: hmrRunAt
158
+ js: [HMR_CLIENT_NAME],
159
+ run_at: "document_start"
157
160
  });
158
161
  }
159
162
  manifest.content_scripts = existing;
@@ -161,6 +164,26 @@ var init_builder = __esm({
161
164
  await fs.mkdir(destDir, { recursive: true });
162
165
  await fs.writeFile(destPath, JSON.stringify(manifest, null, 2), "utf-8");
163
166
  }
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
+ }
164
187
  };
165
188
  }
166
189
  });
@@ -259,11 +282,9 @@ async function runBuild(cwd, options) {
259
282
  const { FileWatcher: FileWatcher2 } = await Promise.resolve().then(() => (init_watcher(), watcher_exports));
260
283
  const config = await loadConfig2(cwd);
261
284
  validateConfig2(config);
262
- if (config.manifest && options?.injectHmr === false) {
263
- config.manifest = { ...config.manifest, injectHmr: false };
264
- }
265
285
  const outDir = config.outDir || "dist";
266
286
  const buildOutDir = config.buildOutDir || outDir;
287
+ const injectHmr = options?.injectHmr ?? false;
267
288
  const replaceOutDir = (target) => {
268
289
  const normalized = target.replace(/\\/g, "/");
269
290
  const prefix = `${outDir.replace(/\\/g, "/")}/`;
@@ -296,13 +317,16 @@ async function runBuild(cwd, options) {
296
317
  }));
297
318
  }
298
319
  }
299
- const port = config.port || 3e3;
320
+ const port = config.port || 8e3;
300
321
  const watchDir = config.watchDir || "src";
301
322
  const server = options?.watch === false ? null : new HmrServer2(port);
302
323
  const builder = new ExtensionBuilder2(cwd);
324
+ if (injectHmr && config.manifest) {
325
+ await builder.buildHmrClient(port, config.manifest.dest);
326
+ }
303
327
  await builder.build(config.entries);
304
328
  if (config.manifest) {
305
- await builder.writeManifest(config.manifest);
329
+ await builder.writeManifest(config.manifest, injectHmr);
306
330
  }
307
331
  if (config.assets) {
308
332
  await builder.copy(config.assets);
@@ -318,7 +342,7 @@ async function runBuild(cwd, options) {
318
342
  try {
319
343
  await builder.build(config.entries);
320
344
  if (config.manifest) {
321
- await builder.writeManifest(config.manifest);
345
+ await builder.writeManifest(config.manifest, injectHmr);
322
346
  }
323
347
  if (config.assets) {
324
348
  await builder.copy(config.assets);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohi9noor/hotloop",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
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",
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/",
27
27
  "prepare": "npm run build"
28
28
  },
29
29
  "files": [