diff --git a/crates/rspack_core/src/options/module.rs b/crates/rspack_core/src/options/module.rs index 6f1927ede849..c716e64c6f9b 100644 --- a/crates/rspack_core/src/options/module.rs +++ b/crates/rspack_core/src/options/module.rs @@ -394,6 +394,7 @@ define_import_meta_known_properties! { const DIRNAME = "dirname"; const FILENAME = "filename"; const GLOB = "glob"; + const HOT = "hot"; const MAIN = "main"; const RESOLVE = "resolve"; const RSPACK_BASE_URI = "rspackBaseUri"; @@ -1932,4 +1933,22 @@ mod tests { assert_eq!(options.rules[0].effect.id, 0); } + + #[test] + fn import_meta_hot_options_are_independent() { + let hot_disabled = ImportMetaOptions::new(HashMap::from_iter([("hot".to_string(), false)])); + assert!(!hot_disabled.is_known_property_enabled(ImportMetaKnownProperties::HOT)); + assert!( + hot_disabled.is_known_property_enabled(ImportMetaKnownProperties::WEBPACK_HOT), + "disabling import.meta.hot should not disable import.meta.webpackHot" + ); + + let webpack_hot_disabled = + ImportMetaOptions::new(HashMap::from_iter([("webpackHot".to_string(), false)])); + assert!(webpack_hot_disabled.is_known_property_enabled(ImportMetaKnownProperties::HOT)); + assert!( + !webpack_hot_disabled.is_known_property_enabled(ImportMetaKnownProperties::WEBPACK_HOT), + "disabling import.meta.webpackHot should not disable import.meta.hot" + ); + } } diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/hot_module_replacement_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/hot_module_replacement_plugin.rs index ca851aeee916..2dcb25bede4c 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/hot_module_replacement_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/hot_module_replacement_plugin.rs @@ -16,6 +16,48 @@ use crate::{ type CreateDependency = fn(Atom, DependencyRange) -> BoxDependency; +fn is_import_meta_hot(for_name: &str) -> bool { + matches!( + for_name, + expr_name::IMPORT_META_HOT | expr_name::IMPORT_META_HOT_ALIAS + ) +} + +fn is_import_meta_hot_accept(for_name: &str) -> bool { + matches!( + for_name, + expr_name::IMPORT_META_HOT_ACCEPT | expr_name::IMPORT_META_HOT_ALIAS_ACCEPT + ) +} + +fn is_import_meta_hot_decline(for_name: &str) -> bool { + matches!( + for_name, + expr_name::IMPORT_META_HOT_DECLINE | expr_name::IMPORT_META_HOT_ALIAS_DECLINE + ) +} + +fn import_meta_hot_property(for_name: &str) -> Option { + match for_name { + expr_name::IMPORT_META_HOT + | expr_name::IMPORT_META_HOT_ACCEPT + | expr_name::IMPORT_META_HOT_DECLINE => Some(ImportMetaKnownProperties::WEBPACK_HOT), + expr_name::IMPORT_META_HOT_ALIAS + | expr_name::IMPORT_META_HOT_ALIAS_ACCEPT + | expr_name::IMPORT_META_HOT_ALIAS_DECLINE => Some(ImportMetaKnownProperties::HOT), + _ => None, + } +} + +fn is_import_meta_hot_enabled(parser: &JavascriptParser, for_name: &str) -> bool { + import_meta_hot_property(for_name).is_some_and(|property| { + parser + .javascript_options + .import_meta() + .is_known_property_enabled(property) + }) +} + fn extract_deps( parser: &mut JavascriptParser, call_expr: &CallExpr, @@ -211,14 +253,9 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ImportMetaHotReplacementParserPl start: u32, end: u32, ) -> Option> { - if for_name == expr_name::IMPORT_META_HOT - && parser - .javascript_options - .import_meta() - .is_known_property_enabled(ImportMetaKnownProperties::WEBPACK_HOT) - { + if is_import_meta_hot(for_name) && is_import_meta_hot_enabled(parser, for_name) { Some(eval::evaluate_to_identifier( - expr_name::IMPORT_META_HOT.into(), + for_name.into(), expr_name::IMPORT_META.into(), Some(true), start, @@ -235,12 +272,7 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ImportMetaHotReplacementParserPl expr: &MemberExpr, for_name: &str, ) -> Option { - if for_name == expr_name::IMPORT_META_HOT - && parser - .javascript_options - .import_meta() - .is_known_property_enabled(ImportMetaKnownProperties::WEBPACK_HOT) - { + if is_import_meta_hot(for_name) && is_import_meta_hot_enabled(parser, for_name) { parser.create_hmr_expression_handler(expr.span()); Some(true) } else { @@ -254,19 +286,15 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ImportMetaHotReplacementParserPl call_expr: &CallExpr, for_name: &str, ) -> Option { - if !parser - .javascript_options - .import_meta() - .is_known_property_enabled(ImportMetaKnownProperties::WEBPACK_HOT) - { + if !is_import_meta_hot_enabled(parser, for_name) { return None; } - if for_name == expr_name::IMPORT_META_HOT_ACCEPT { + if is_import_meta_hot_accept(for_name) { parser.create_accept_handler(call_expr, |request, range| { Box::new(ImportMetaHotAcceptDependency::new(request, range)) }) - } else if for_name == expr_name::IMPORT_META_HOT_DECLINE { + } else if is_import_meta_hot_decline(for_name) { parser.create_decline_handler(call_expr, |request, range| { Box::new(ImportMetaHotDeclineDependency::new(request, range)) }) diff --git a/crates/rspack_plugin_javascript/src/parser_plugin/import_meta_plugin.rs b/crates/rspack_plugin_javascript/src/parser_plugin/import_meta_plugin.rs index 991f2bac1260..d9b47187096a 100644 --- a/crates/rspack_plugin_javascript/src/parser_plugin/import_meta_plugin.rs +++ b/crates/rspack_plugin_javascript/src/parser_plugin/import_meta_plugin.rs @@ -297,7 +297,11 @@ impl ImportMetaPlugin { if let Some(api) = import_meta_runtime_api_from_name(name) { return Some(api.property); } - None + match name { + expr_name::IMPORT_META_HOT => Some(ImportMetaKnownProperties::WEBPACK_HOT), + expr_name::IMPORT_META_HOT_ALIAS => Some(ImportMetaKnownProperties::HOT), + _ => None, + } } fn preserve_property(&self, property: Option<&str>) -> bool { @@ -488,6 +492,12 @@ impl<'p, 'a> JavascriptParserPlugin<'p, 'a> for ImportMetaPlugin { && self.runtime_api_enabled(api) { evaluated = Some(api.type_of.to_string()) + } else if Self::known_property_from_name(for_name) + .is_some_and(|property| self.known_property_enabled(property)) + { + // HMR fields are evaluated by HotModuleReplacementPlugin. Keep `typeof` + // unknown here so the member expression can be rewritten to `module.hot`. + return None; } else if let Some(member_expr) = expr.arg.as_member() && let Some(meta_expr) = member_expr.obj.as_meta_prop() && meta_expr diff --git a/crates/rspack_plugin_javascript/src/visitors/dependency/util.rs b/crates/rspack_plugin_javascript/src/visitors/dependency/util.rs index 1076d04a27b6..d59de9e40e4a 100644 --- a/crates/rspack_plugin_javascript/src/visitors/dependency/util.rs +++ b/crates/rspack_plugin_javascript/src/visitors/dependency/util.rs @@ -32,6 +32,9 @@ pub mod expr_name { pub const IMPORT_META_HOT: &str = "import.meta.webpackHot"; pub const IMPORT_META_HOT_ACCEPT: &str = "import.meta.webpackHot.accept"; pub const IMPORT_META_HOT_DECLINE: &str = "import.meta.webpackHot.decline"; + pub const IMPORT_META_HOT_ALIAS: &str = "import.meta.hot"; + pub const IMPORT_META_HOT_ALIAS_ACCEPT: &str = "import.meta.hot.accept"; + pub const IMPORT_META_HOT_ALIAS_DECLINE: &str = "import.meta.hot.decline"; pub const IMPORT_META_CONTEXT: &str = "import.meta.webpackContext"; pub const IMPORT_META_GLOB: &str = "import.meta.glob"; } diff --git a/packages/rspack/module.d.ts b/packages/rspack/module.d.ts index f00dbf6c28ef..0ab6315d5925 100644 --- a/packages/rspack/module.d.ts +++ b/packages/rspack/module.d.ts @@ -229,6 +229,7 @@ declare namespace Rspack { interface ImportMeta { url: string; + hot?: Rspack.Hot; webpackHot?: Rspack.Hot; webpackContext: ( request: string, diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 131a071c66b7..66c4f380b57f 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -1199,6 +1199,11 @@ export type ImportMetaParserOptions = { */ glob?: boolean; + /** + * Enable/disable evaluating import.meta.hot. + */ + hot?: boolean; + /** * Enable/disable evaluating import.meta.main. */ diff --git a/tests/rspack-test/configCases/parsing/import-meta-hot/dep.js b/tests/rspack-test/configCases/parsing/import-meta-hot/dep.js new file mode 100644 index 000000000000..9daf104bca2a --- /dev/null +++ b/tests/rspack-test/configCases/parsing/import-meta-hot/dep.js @@ -0,0 +1 @@ +export default "import.meta.hot"; diff --git a/tests/rspack-test/configCases/parsing/import-meta-hot/index.js b/tests/rspack-test/configCases/parsing/import-meta-hot/index.js new file mode 100644 index 000000000000..b0056f60100a --- /dev/null +++ b/tests/rspack-test/configCases/parsing/import-meta-hot/index.js @@ -0,0 +1,32 @@ +import value from "./dep"; + +const fs = require("fs"); + +let typeofGuardMatched = false; +let webpackHotTypeofGuardMatched = false; + +if (typeof import.meta.hot !== "undefined") { + typeofGuardMatched = true; +} + +if (typeof import.meta.webpackHot !== "undefined") { + webpackHotTypeofGuardMatched = true; +} + +if (import.meta.hot) { + import.meta.hot.accept(["./dep"], () => {}); +} + +it("should support import.meta.hot as an HMR alias", () => { + expect(value).toBe("import.meta.hot"); + expect(typeofGuardMatched).toBe(true); + expect(webpackHotTypeofGuardMatched).toBe(true); + expect(typeof import.meta.hot).toBe("object"); + expect(typeof import.meta.hot.accept).toBe("function"); + expect(typeof import.meta.webpackHot).toBe("object"); + + const source = fs.readFileSync(__filename, "utf-8"); + const typeofKeyword = ["type", "of"].join(""); + const runtimeTypeof = `${typeofKeyword} module.hot !== "undefined"`; + expect(source.split(runtimeTypeof)).toHaveLength(3); +}); diff --git a/tests/rspack-test/configCases/parsing/import-meta-hot/parser-options.js b/tests/rspack-test/configCases/parsing/import-meta-hot/parser-options.js new file mode 100644 index 000000000000..6fa248375c33 --- /dev/null +++ b/tests/rspack-test/configCases/parsing/import-meta-hot/parser-options.js @@ -0,0 +1,15 @@ +const fs = require("fs"); + +it("should disable the hot alias independently from the canonical HMR field", () => { + expect(typeof import.meta.hot).toBe("undefined"); + expect(typeof import.meta.webpackHot).toBe("object"); + if (import.meta.hot) { + import.meta.hot.accept(); + import.meta.hot.decline(); + } + + const source = fs.readFileSync(__filename, "utf-8"); + const importMeta = ["import", "meta"].join("."); + expect(source.split(`${importMeta}.hot`)).toHaveLength(5); + expect(source).not.toContain(`${importMeta}.webpackHot`); +}); diff --git a/tests/rspack-test/configCases/parsing/import-meta-hot/production.js b/tests/rspack-test/configCases/parsing/import-meta-hot/production.js new file mode 100644 index 000000000000..a3b43e047585 --- /dev/null +++ b/tests/rspack-test/configCases/parsing/import-meta-hot/production.js @@ -0,0 +1,9 @@ +it("should transform import.meta.hot to false without HMR", () => { + let hot = false; + if (import.meta.hot) { + hot = true; + import.meta.hot.accept(); + } + + expect(hot).toBe(false); +}); diff --git a/tests/rspack-test/configCases/parsing/import-meta-hot/rspack.config.js b/tests/rspack-test/configCases/parsing/import-meta-hot/rspack.config.js new file mode 100644 index 000000000000..3a63ff14cbc8 --- /dev/null +++ b/tests/rspack-test/configCases/parsing/import-meta-hot/rspack.config.js @@ -0,0 +1,47 @@ +const { HotModuleReplacementPlugin } = require('@rspack/core'); + +/** @type {import("@rspack/core").Configuration[]} */ +module.exports = [ + { + entry: './index.js', + devtool: false, + externals: { + fs: 'node-commonjs fs', + }, + node: { + __filename: false, + }, + plugins: [new HotModuleReplacementPlugin()], + }, + { + entry: './production.js', + mode: 'production', + target: 'web', + devServer: { + hot: true, + }, + }, + { + entry: './parser-options.js', + mode: 'development', + devtool: false, + target: 'node', + experiments: { + outputModule: true, + }, + output: { + module: true, + chunkFormat: 'module', + }, + module: { + parser: { + javascript: { + importMeta: { + hot: false, + }, + }, + }, + }, + plugins: [new HotModuleReplacementPlugin()], + }, +]; diff --git a/website/docs/en/api/runtime-api/hmr.mdx b/website/docs/en/api/runtime-api/hmr.mdx index ef887242dc98..a731312a005f 100644 --- a/website/docs/en/api/runtime-api/hmr.mdx +++ b/website/docs/en/api/runtime-api/hmr.mdx @@ -10,9 +10,11 @@ import WebpackLicense from '@components/WebpackLicense'; Rspack provides the same interface as webpack for implementing HMR. -If you've enabled Hot Module Replacement via the [HotModuleReplacementPlugin](/plugins/webpack/hot-module-replacement-plugin), its interface is exposed on the `module.hot` and `import.meta.webpackHot` objects. +If you've enabled Hot Module Replacement via the [HotModuleReplacementPlugin](/plugins/webpack/hot-module-replacement-plugin), its interface is exposed on the `module.hot`, `import.meta.webpackHot`, and `import.meta.hot` objects. -In ESM, use `import.meta.webpackHot` instead of `module.hot`. +In ESM, use `import.meta.webpackHot` or `import.meta.hot` instead of `module.hot`. + +`import.meta.hot` is available since Rspack 2.1.5. ## Example @@ -33,7 +35,7 @@ if (import.meta.webpackHot) { } ``` -The following methods are supported by `module.hot` and `import.meta.webpackHot`. +The following methods are supported by `module.hot`, `import.meta.webpackHot`, and `import.meta.hot`. ## Module API diff --git a/website/docs/zh/api/runtime-api/hmr.mdx b/website/docs/zh/api/runtime-api/hmr.mdx index 1c27548491f1..be087e029fdf 100644 --- a/website/docs/zh/api/runtime-api/hmr.mdx +++ b/website/docs/zh/api/runtime-api/hmr.mdx @@ -10,9 +10,11 @@ import WebpackLicense from '@components/WebpackLicense'; Rspack 提供了与 webpack 相同的接口来实现模块热替换。 -如果你通过 [HotModuleReplacementPlugin](/plugins/webpack/hot-module-replacement-plugin) 启用了模块热替换,它的接口将暴露在 `module.hot` 和 `import.meta.webpackHot` 对象下。 +如果你通过 [HotModuleReplacementPlugin](/plugins/webpack/hot-module-replacement-plugin) 启用了模块热替换,它的接口将暴露在 `module.hot`、`import.meta.webpackHot` 和 `import.meta.hot` 对象下。 -注意,在 ESM 中请使用 `import.meta.webpackHot` 来代替 `module.hot`。 +注意,在 ESM 中请使用 `import.meta.webpackHot` 或 `import.meta.hot` 来代替 `module.hot`。 + +`import.meta.hot` 从 Rspack 2.1.5 开始可用。 ## 示例 @@ -33,7 +35,7 @@ if (import.meta.webpackHot) { } ``` -`module.hot` 和 `import.meta.webpackHot` 支持以下方法。 +`module.hot`、`import.meta.webpackHot` 和 `import.meta.hot` 支持以下方法。 ## 模块 API