@addfox/rsbuild-plugin-extension-hmr 0.1.1-beta.2

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) 2025 addfox
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/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # @addfox/rsbuild-plugin-extension-hmr
2
+
3
+ [中文](README-zh_CN.md) | English
4
+
5
+ ---
6
+
7
+ Rsbuild plugin: in dev, waits for dist then launches the browser (Chrome/Edge/Brave/Vivaldi/Opera/Santa/Firefox) and enables HMR reload via WebSocket. Used by CLI dev command.
8
+
9
+ - Options: distPath, browser, chromePath/edgePath/bravePath/vivaldiPath/operaPath/santaPath/firefoxPath, wsPort, enableReload, persist
10
+ - **Chromium launch**: uses `chrome-launcher` with CDP `Extensions.loadUnpacked` for native extension loading; falls back to `--load-extension` on older Chrome versions
11
+ - **Firefox launch**: uses [web-ext run](https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#web-ext-run) (`--target=firefox-desktop`)
12
+ - **Reload manager**: a helper extension that listens to WebSocket and toggles (disable+enable) dev extensions on rebuild
13
+ - **Browser path registry**: all browser default paths are managed via a single `BROWSER_DEFAULT_PATHS` map; user can override via `config.launch.*`
14
+ - Injected by pipeline from config.launch and CLI `-b`/`--browser`
@@ -0,0 +1,4 @@
1
+ export { getLaunchPathFromOptions, buildDefaultPaths, getBrowserPath, isChromiumBrowser, type LaunchPathOptions, } from "./paths";
2
+ export { runChromiumRunner, type ChromiumRunnerOptions, } from "./runner";
3
+ export { launchBrowser, launchBrowserOnly, cleanup, registerCleanupHandlers, statsHasErrors, setBrowserLaunched, getBrowserLaunched, type LaunchContext, type HmrPluginOptionsForLaunch, type LaunchOnlyOptions, type ChromiumRunnerOverride, } from "./launcher";
4
+ export { getCacheTempRoot, getChromiumUserDataDir, getReloadManagerPath, findExistingReloadManager, ensureDistReady, } from "../manager/extension";
@@ -0,0 +1,55 @@
1
+ import type { LaunchTarget } from "@addfox/core";
2
+ import type { ChromiumRunnerOptions } from "./runner";
3
+ import { type LaunchPathOptions } from "./paths";
4
+ export type ChromiumRunnerOverride = (opts: ChromiumRunnerOptions) => Promise<{
5
+ exit: () => Promise<void>;
6
+ }>;
7
+ export interface LaunchContext {
8
+ distPath: string;
9
+ browser: LaunchTarget;
10
+ pathOpts: LaunchPathOptions;
11
+ cache: boolean;
12
+ enableReload: boolean;
13
+ wsPort: number;
14
+ chromiumRunnerOverride?: ChromiumRunnerOverride;
15
+ ensureDistReadyOverride?: (distPath: string) => Promise<boolean>;
16
+ getBrowserPathOverride?: (b: LaunchTarget, o: LaunchPathOptions) => string | null;
17
+ onBrowserExit: () => void;
18
+ debug?: boolean;
19
+ root?: string;
20
+ outputRoot?: string;
21
+ }
22
+ export declare function cleanup(): Promise<void>;
23
+ export declare function registerCleanupHandlers(): void;
24
+ export declare function launchBrowserCore(ctx: LaunchContext): Promise<void>;
25
+ export interface HmrPluginOptionsForLaunch {
26
+ distPath: string;
27
+ browser?: LaunchTarget;
28
+ cache?: boolean;
29
+ wsPort?: number;
30
+ enableReload?: boolean;
31
+ debug?: boolean;
32
+ root?: string;
33
+ outputRoot?: string;
34
+ chromePath?: string;
35
+ chromiumPath?: string;
36
+ edgePath?: string;
37
+ bravePath?: string;
38
+ vivaldiPath?: string;
39
+ operaPath?: string;
40
+ santaPath?: string;
41
+ arcPath?: string;
42
+ yandexPath?: string;
43
+ browserosPath?: string;
44
+ customPath?: string;
45
+ firefoxPath?: string;
46
+ }
47
+ export declare function launchBrowser(options: HmrPluginOptionsForLaunch, chromiumRunnerOverride?: ChromiumRunnerOverride, ensureDistReadyOverride?: (distPath: string) => Promise<boolean>, getBrowserPathOverride?: (b: LaunchTarget, o: LaunchPathOptions) => string | null): Promise<void>;
48
+ export declare function setBrowserLaunched(value: boolean): void;
49
+ export declare function getBrowserLaunched(): boolean;
50
+ export declare function statsHasErrors(stats: unknown): boolean;
51
+ export { getReloadManagerPath, getChromiumUserDataDir, getCacheRoot, getBrowserProfileDir,
52
+ /** @deprecated Use getCacheRoot/getBrowserProfileDir instead */
53
+ getCacheTempRoot, findExistingReloadManager, ensureDistReady, } from "../manager/extension";
54
+ export declare function launchBrowserOnly(options: LaunchOnlyOptions, chromiumRunnerOverride?: ChromiumRunnerOverride): Promise<void>;
55
+ export type LaunchOnlyOptions = Pick<HmrPluginOptionsForLaunch, "distPath" | "browser" | "chromePath" | "chromiumPath" | "edgePath" | "bravePath" | "vivaldiPath" | "operaPath" | "santaPath" | "arcPath" | "yandexPath" | "browserosPath" | "customPath" | "firefoxPath" | "cache" | "outputRoot" | "debug" | "enableReload">;
@@ -0,0 +1,6 @@
1
+ import type { LaunchTarget, ChromiumLaunchTarget } from "@addfox/core";
2
+ export type LaunchPathOptions = Pick<Record<string, string | undefined>, "chromePath" | "chromiumPath" | "edgePath" | "bravePath" | "vivaldiPath" | "operaPath" | "santaPath" | "arcPath" | "yandexPath" | "browserosPath" | "customPath" | "firefoxPath">;
3
+ export declare function getLaunchPathFromOptions(browser: LaunchTarget, options: LaunchPathOptions): string | undefined;
4
+ export declare function buildDefaultPaths(browser: LaunchTarget, platform: string): string[] | undefined;
5
+ export declare function getBrowserPath(browser: LaunchTarget, options: LaunchPathOptions): string | null;
6
+ export declare function isChromiumBrowser(browser: LaunchTarget): browser is ChromiumLaunchTarget;
@@ -0,0 +1,12 @@
1
+ export type ChromiumRunnerOptions = {
2
+ chromePath: string;
3
+ extensions: string[];
4
+ startUrl?: string;
5
+ userDataDir?: string;
6
+ args?: string[];
7
+ verbose?: boolean;
8
+ onExit?: () => void;
9
+ };
10
+ export declare function runChromiumRunner(options: ChromiumRunnerOptions): Promise<{
11
+ exit: () => Promise<void>;
12
+ }>;
@@ -0,0 +1,7 @@
1
+ export type WebExtConsoleStreamHookUninstall = () => void;
2
+ /**
3
+ * Wraps web-ext's pino `ConsoleStream.write` so CLI line-prefixing can tag [Web-ext] at the real
4
+ * output source (not string matching). Idempotent while active.
5
+ */
6
+ export declare function installWebExtConsoleStreamHook(): Promise<void>;
7
+ export declare function removeWebExtConsoleStreamHook(): void;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Entry names and HMR snippets used across the HMR plugin.
3
+ * RELOAD_MANAGER_ENTRY_NAMES comes from @addfox/core (single source of truth).
4
+ */
5
+ /** Entry names that require chrome.runtime.reload() when changed (background). */
6
+ export declare const RELOAD_ENTRY_NAMES: Set<string>;
7
+ /** Entry names injected into pages; when changed, reload manager refreshes active tab. */
8
+ export declare const CONTENT_ENTRY_NAMES: Set<string>;
9
+ /** Injected at top of entry module for precise identification; only content/background. */
10
+ export declare const ADDFOX_ENTRY_TAG_PREFIX = "/* addfox-entry:";
11
+ /** Build entry tag comment for a given entry name (content/background). Used for precise entry identification in built output. */
12
+ export declare function getEntryTag(entryName: string): string;
13
+ /** Prepended to content/background entry modules: force full invalidation so reload is via reloadManager only. */
14
+ export declare const HMR_INVALIDATE_PREPEND = "if(typeof module!=='undefined'&&module.hot){module.hot.invalidate();}\n";
@@ -0,0 +1,3 @@
1
+ export declare function collectHotUpdateAssetNames(statsList: unknown[]): Set<string>;
2
+ export declare function removeStaleHotUpdateFiles(distPath: string, keepNames: Set<string>): Promise<void>;
3
+ export declare function clearOutdatedHotUpdateFiles(distPath: string, stats: unknown): Promise<void>;
@@ -0,0 +1,10 @@
1
+ import type { ReloadManagerEntry } from "@addfox/core";
2
+ export declare function normalizePathForCompare(p: string): string;
3
+ /** Clear the path cache (useful for testing or memory management) */
4
+ export declare function clearPathCache(): void;
5
+ /**
6
+ * Transforms content/background entry modules:
7
+ * - Injects entry tag comment (addfox-entry:content / addfox-entry:background) at top when entries have names.
8
+ * - Prepends module.hot.invalidate() so reload is via reloadManager only.
9
+ */
10
+ export declare function transformCodeToDisableHmr(resourcePath: string, entriesOrPaths: ReloadManagerEntry[] | string[], code: string): string;
@@ -0,0 +1,4 @@
1
+ export { transformCodeToDisableHmr, normalizePathForCompare, clearPathCache, } from "./disable";
2
+ export { clearOutdatedHotUpdateFiles, collectHotUpdateAssetNames, removeStaleHotUpdateFiles, } from "./cleanup";
3
+ export { createHmrRspackPlugin, getLastCompiler, getModifiedFilesFromCompiler, } from "./rspack-plugin";
4
+ export { getReloadManagerDecision, getReloadKindFromDecision, isContentChanged, getCompilationFromStats, getEntrypointSignature, getEntriesSignature, getReloadEntriesSignature, getContentEntriesSignature, getEntryToModulePaths, getEntriesForFile, type ReloadManagerDecision, type ReloadKind, } from "./scope";
@@ -0,0 +1,16 @@
1
+ import type { Compiler } from "@rspack/core";
2
+ import type { HmrPluginOptions, HmrPluginTestDeps } from "../types";
3
+ export declare function getLastCompiler(): Compiler | null;
4
+ /**
5
+ * Collects modified file paths from compiler:
6
+ * - Prefers compiler.modifiedFiles when available (Rspack >= 0.5)
7
+ * - Falls back to watchFileSystem.watcher.mtimes for older versions
8
+ */
9
+ export declare function getModifiedFilesFromCompiler(compiler: Compiler | null): Set<string>;
10
+ /**
11
+ * Creates the HMR Rspack plugin that handles browser launch on first successful compilation.
12
+ */
13
+ export declare function createHmrRspackPlugin(options: HmrPluginOptions, testDeps?: HmrPluginTestDeps): {
14
+ name: string;
15
+ apply(compiler: Compiler): void;
16
+ };
@@ -0,0 +1,63 @@
1
+ type AssetLike = {
2
+ filename?: string;
3
+ info?: {
4
+ contenthash?: string[];
5
+ chunkhash?: string[];
6
+ };
7
+ };
8
+ type ChunkLike = {
9
+ id?: string;
10
+ name?: string;
11
+ hash?: string;
12
+ };
13
+ type EntrypointLike = {
14
+ name?: string;
15
+ chunks?: ReadonlyArray<ChunkLike>;
16
+ getFiles?: () => ReadonlyArray<string>;
17
+ };
18
+ type CompilationLike = {
19
+ entrypoints?: ReadonlyMap<string, EntrypointLike>;
20
+ getAsset?: (name: string) => AssetLike | void;
21
+ getAssets?: () => ReadonlyArray<AssetLike>;
22
+ getStats?: () => {
23
+ toJson: (opts?: unknown) => unknown;
24
+ };
25
+ };
26
+ export interface ReloadManagerDecision {
27
+ /** True only when content or background entry output actually changed (precise). */
28
+ shouldNotify: boolean;
29
+ /** True when content entry changed (for toggle-extension-refresh-page). */
30
+ contentChanged: boolean;
31
+ /** True when background entry changed (for reload-extension). */
32
+ backgroundChanged: boolean;
33
+ }
34
+ export type ReloadKind = "reload-extension" | "toggle-extension" | "toggle-extension-refresh-page";
35
+ export declare function getCompilationFromStats(stats: unknown): CompilationLike | null;
36
+ export declare function getEntrypointSignature(entrypoint: EntrypointLike | undefined): string | null;
37
+ export declare function getEntriesSignature(compilation: CompilationLike, entryNames: Set<string>): string | null;
38
+ type EntryToPaths = Map<string, Set<string>>;
39
+ export declare function getEntryToModulePaths(stats: unknown): EntryToPaths;
40
+ /** Which entry names contain this file path (using path normalization / endsWith). */
41
+ export declare function getEntriesForFile(entryToPaths: EntryToPaths, filePath: string): string[];
42
+ /**
43
+ * Single place to decide reloadManager WS notification and content-refresh.
44
+ * Extension.js-style: when compiler.modifiedFiles is available, only notify if a modified file
45
+ * belongs to content/background chunk. Otherwise fall back to signature comparison.
46
+ */
47
+ export declare function getReloadManagerDecision(stats: unknown, context?: {
48
+ compiler?: {
49
+ modifiedFiles?: ReadonlySet<string>;
50
+ };
51
+ }): ReloadManagerDecision;
52
+ /**
53
+ * Choose reload kind from decision and config.
54
+ * - backgroundChanged → reload-extension (browser reload API).
55
+ * - contentChanged + autoRefreshContentPage → toggle-extension-refresh-page (toggle + refresh current page).
56
+ * - else → toggle-extension (toggle only).
57
+ */
58
+ export declare function getReloadKindFromDecision(contentChanged: boolean, backgroundChanged: boolean, autoRefreshContentPage: boolean): ReloadKind;
59
+ /** True when content entry signature changed (uses same state as getReloadManagerDecision). */
60
+ export declare function isContentChanged(stats: unknown): boolean;
61
+ export declare function getReloadEntriesSignature(stats: unknown): string | null;
62
+ export declare function getContentEntriesSignature(stats: unknown): string | null;
63
+ export {};
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @addfox/rsbuild-plugin-extension-hmr
3
+ *
4
+ * Dev mode: launch browser, WebSocket reload manager, and precise HMR/liveReload
5
+ * control for content/background entries (reload via WS only).
6
+ */
7
+ import type { RsbuildPlugin } from "@rsbuild/core";
8
+ import type { HmrPluginOptions, HmrPluginTestDeps } from "./types";
9
+ export type { HmrPluginOptions, HmrPluginTestDeps } from "./types";
10
+ export { RELOAD_ENTRY_NAMES, CONTENT_ENTRY_NAMES, getEntryTag } from "./constants";
11
+ export { clearOutdatedHotUpdateFiles } from "./hmr/cleanup";
12
+ export { getLaunchPathFromOptions, buildDefaultPaths, getBrowserPath, isChromiumBrowser } from "./browser/paths";
13
+ export type { LaunchPathOptions } from "./browser/paths";
14
+ export { startWebSocketServer, notifyReload } from "./server/ws-server";
15
+ export type { ExtensionErrorPayload, DebugServerOpts, WsServerMode } from "./server/ws-server";
16
+ export { getCompilationFromStats, getEntrypointSignature, getEntriesSignature, getReloadEntriesSignature, getContentEntriesSignature, getReloadManagerDecision, getReloadKindFromDecision, isContentChanged, getEntryToModulePaths, getEntriesForFile, } from "./hmr/scope";
17
+ export type { ReloadManagerDecision, ReloadKind } from "./hmr/scope";
18
+ export { normalizePathForCompare } from "./hmr/disable";
19
+ export { getCacheRoot, getBrowserProfileDir,
20
+ /** @deprecated Use getCacheRoot/getBrowserProfileDir instead */
21
+ getCacheTempRoot, getChromiumUserDataDir, getReloadManagerPath, findExistingReloadManager, ensureDistReady, } from "./manager/extension";
22
+ export { launchBrowser, launchBrowserOnly, cleanup, registerCleanupHandlers, statsHasErrors, } from "./browser/launcher";
23
+ export type { LaunchOnlyOptions, ChromiumRunnerOverride } from "./browser/launcher";
24
+ export { createTestWsServer } from "./server/test-server";
25
+ export { createHmrRspackPlugin } from "./hmr/rspack-plugin";
26
+ /**
27
+ * Rsbuild plugin: in dev mode launches browser after first compile;
28
+ * notifies reload only when content or background entry (or their dependencies) changed;
29
+ * disables HMR/liveReload for content/background via transform and optional entry tag injection.
30
+ */
31
+ export declare function hmrPlugin(options: HmrPluginOptions, testDeps?: HmrPluginTestDeps): RsbuildPlugin;