@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 +32 -47
- package/dist/hotloop-hmr.js +53 -0
- package/dist/index.js +25 -46
- package/package.json +3 -2
- package/dist/hmr-client.js +0 -31
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
|
|
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
|
-
*
|
|
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
|
-
|
|
326
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
325
|
-
|
|
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.
|
|
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
|
|
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
|
}
|
package/dist/hmr-client.js
DELETED
|
@@ -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
|
-
};
|