Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/jsHelper/spicetifyWrapper/webpack/component-resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ export function findDropdownComponent({ modules, chunks, require }) {
);
}

// From 1.3.3 the menu item and submenu item are exports that only switch
// between the v2 and legacy rows; the legacy row is no longer exported.
export function findMenuItem({ functionModules }) {
return (
functionModules.find((f) => /forceV2LeadingIcon:\s*\w+\.forceV2LeadingIcon/.test(fnStr(f))) ??
functionModules.find((f) => fnStr(f).includes("handleMouseEnter") && fnStr(f).includes("onClick"))
);
}

export function findSubMenuItem({ functionModules, chunks, require }) {
const legacy = functionModules.find((f) => fnStr(f).includes("subMenuIcon"));
if (legacy) return legacy;

const chunk = chunks.find(([, value]) => fnStr(value).includes("data-context-menu-submenu-header"));
if (!chunk) return undefined;
const exports = getObjectValues(require(chunk[0])).filter((m) => typeof m === "function");
return exports.length === 1 ? exports[0] : undefined;
}

export function wrapProvider(component) {
if (!component) return null;
return (props) =>
Expand Down
56 changes: 56 additions & 0 deletions src/jsHelper/spicetifyWrapper/webpack/component-resolvers.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import assert from "node:assert/strict";
import test from "node:test";

import { findMenuItem, findSubMenuItem } from "./component-resolvers.js";

type Props = Record<string, unknown>;

// Shapes taken from Spotify 1.3.3: a waveform scrubber that also mentions
// handleMouseEnter and onClick, the menu item dispatcher, and the v2 row it renders.
const waveform = ({ onTriggerPlay }: Props) => ({ handleMouseEnter: () => undefined, onClick: onTriggerPlay });
const menuItemDispatcher = (e: Props) => ({
textValue: e.textValue,
forceV2LeadingIcon: e.forceV2LeadingIcon,
showFullTextOnHover: e.showFullTextOnHover,
});
const menuRowV2 = function ({ forceV2LeadingIcon: a, showFullTextOnHover: b }: Props) {
return [a, b];
};
const legacyMenuItem = ({ onClick }: Props) => {
const handleMouseEnter = () => undefined;
return { handleMouseEnter, onClick };
};

test("MenuItem resolves to the dispatcher ahead of other handleMouseEnter components", () => {
assert.equal(findMenuItem({ functionModules: [waveform, menuRowV2, menuItemDispatcher] }), menuItemDispatcher);
});

test("MenuItem falls back to the exported legacy row on builds without the dispatcher", () => {
assert.equal(findMenuItem({ functionModules: [legacyMenuItem] }), legacyMenuItem);
});

test("MenuSubMenuItem resolves through the module that renders the submenu header", () => {
const subMenuDispatcher = (e: Props) => e;
const modules: Record<string, unknown> = {
"11": { A: { menuItem: "menu-item" } },
"93262": { g: subMenuDispatcher },
};
const chunks = [
["11", () => "unrelated factory"],
["93262", () => '{"data-context-menu-submenu-header":!0}'],
];
const require = (id: string) => modules[id];

assert.equal(findSubMenuItem({ functionModules: [waveform], chunks, require }), subMenuDispatcher);
});

test("MenuSubMenuItem keeps the exported subMenuIcon component on older builds", () => {
const legacySubMenu = () => ({ className: "main-contextMenu-subMenuIcon" });
assert.equal(findSubMenuItem({ functionModules: [waveform, legacySubMenu], chunks: [], require: () => undefined }), legacySubMenu);
});

test("MenuSubMenuItem is undefined when the header module exports more than one component", () => {
const chunks = [["1", () => "data-context-menu-submenu-header"]];
const require = () => ({ a: () => 1, b: () => 2 });
assert.equal(findSubMenuItem({ functionModules: [], chunks, require }), undefined);
});
6 changes: 3 additions & 3 deletions src/jsHelper/spicetifyWrapper/webpack/react-components.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fnStr } from "../shared/string.js";
import { findCards } from "./cards.js";
import { findDropdownComponent, wrapProvider } from "./component-resolvers.js";
import { findDropdownComponent, findMenuItem, findSubMenuItem, wrapProvider } from "./component-resolvers.js";
import { findMenuOverrides, findMenus } from "./menus.js";

function findNavigationComponent({ require, exportedMemoFRefs }) {
Expand Down Expand Up @@ -36,8 +36,8 @@ export function createReactComponents({
...Spicetify.ReactComponent,
TextComponent: modules.find((m) => m?.h1 && m?.render),
Menu: functionModules.find((m) => fnStr(m).includes("getInitialFocusElement") && fnStr(m).includes("children")),
MenuItem: functionModules.find((m) => fnStr(m).includes("handleMouseEnter") && fnStr(m).includes("onClick")),
MenuSubMenuItem: functionModules.find((f) => fnStr(f).includes("subMenuIcon")),
MenuItem: findMenuItem({ functionModules }),
MenuSubMenuItem: findSubMenuItem({ functionModules, chunks, require }),
Slider: wrapProvider(functionModules.find((m) => fnStr(m).includes("progressBarRef"))),
RemoteConfigProvider: functionModules.find((m) => fnStr(m).includes("resolveSuspense") && fnStr(m).includes("configuration")),
RightClickMenu: functionModules.find(
Expand Down
Loading