diff --git a/lib/Hook.js b/lib/Hook.js index 560bb96..5bf7417 100644 --- a/lib/Hook.js +++ b/lib/Hook.js @@ -90,13 +90,17 @@ class Hook { // Fast path: only `name` is set. Build the descriptor as a literal // so `_insert` and downstream consumers see the same hidden class // as the string-options path, avoiding a polymorphic call site. - if ( - options.before === undefined && - options.stage === undefined && - options.context === undefined && - options.type === undefined && - options.fn === undefined - ) { + // Scan with `for...in` (cheaper than allocating `Object.keys`) + // to verify no other user-provided properties exist - e.g. + // webpack's `additionalAssets` - otherwise they'd be dropped. + let onlyName = true; + for (const key in options) { + if (key !== "name") { + onlyName = false; + break; + } + } + if (onlyName) { options = { type, fn, name }; } else { options.name = name; diff --git a/test/Hook.test.js b/test/Hook.test.js index bb6d2a8..6b551d4 100644 --- a/test/Hook.test.js +++ b/test/Hook.test.js @@ -183,6 +183,23 @@ describe("Hook", () => { ); }); + it("should preserve custom tap options (e.g. webpack's `additionalAssets`) on the tap descriptor", () => { + const hook = new SyncHook(); + + // Options with only `name` plus a custom property - used by webpack's + // processAssets (`additionalAssets: true`). The fast-path in `_tap` + // must not drop the custom property. + hook.tap({ name: "A", additionalAssets: true }, () => {}); + // Options with `name`, `stage` and a custom property go through the + // slow path - also checked for completeness. + hook.tap({ name: "B", stage: 10, extra: "value" }, () => {}); + + expect(hook.taps[0].name).toBe("A"); + expect(hook.taps[0].additionalAssets).toBe(true); + expect(hook.taps[1].name).toBe("B"); + expect(hook.taps[1].extra).toBe("value"); + }); + it("should not ignore invalid before values", () => { // A plugin may use a hook that will never be executed const hook = new SyncHook();