@chemmangat/msal-next 4.2.2 → 5.0.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,91 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [5.0.0] - 2026-03-16
6
+
7
+ ### ⚠️ Breaking Changes
8
+
9
+ - **Node.js 18+ required** — The CLI and codemod tools require Node.js 18 or higher.
10
+ - **CLI package renamed** — `@chemmangat/msal-next-cli` is now the canonical CLI package, invokable via `npx @chemmangat/msal-next init`.
11
+ - **Codemod is a breaking addition** — Running `npx @chemmangat/msal-next migrate` will rewrite popup API calls in your project. Review changes with `git diff` before committing.
12
+
13
+ ### ✨ New Features
14
+
15
+ #### 1. Interactive CLI — `npx @chemmangat/msal-next init`
16
+ The `init` command now interactively collects all required configuration:
17
+ - Azure AD **Client ID** and **Tenant ID**
18
+ - **Authority type** (`common`, `organizations`, `consumers`, `tenant`)
19
+ - **Cache location** (`sessionStorage`, `localStorage`, `memoryStorage`)
20
+
21
+ After collecting answers it automatically:
22
+ - Creates `.env.local` with all environment variables
23
+ - Updates (or creates) `app/layout.tsx` with `MSALProvider` wired up
24
+ - Creates a starter `app/auth/page.tsx` using `useMsalAuth` and `MicrosoftSignInButton`
25
+
26
+ ```bash
27
+ npx @chemmangat/msal-next init
28
+ ```
29
+
30
+ #### 2. UI Component Slots — `renderAccount` prop
31
+ Both `AccountSwitcher` and `AccountList` now accept a `renderAccount` render prop that lets consumers fully customize how each account row is displayed.
32
+
33
+ ```tsx
34
+ <AccountSwitcher
35
+ renderAccount={(account, isActive) => (
36
+ <div style={{ fontWeight: isActive ? 'bold' : 'normal' }}>
37
+ {account.name} — {account.username}
38
+ </div>
39
+ )}
40
+ />
41
+
42
+ <AccountList
43
+ renderAccount={(account, isActive) => (
44
+ <span>{account.name} {isActive ? '✓' : ''}</span>
45
+ )}
46
+ />
47
+ ```
48
+
49
+ #### 3. Comprehensive Test Coverage (80%+)
50
+ Full Vitest + `@testing-library/react` test suite covering:
51
+ - All hooks: `useMsalAuth`, `useUserProfile`, `useRoles`, `useTokenRefresh`, `useMultiAccount`, `useGraphApi`
52
+ - All components: `MicrosoftSignInButton`, `SignOutButton`, `UserAvatar`, `AuthStatus`, `AuthGuard`, `AccountSwitcher`, `AccountList`
53
+
54
+ Run tests:
55
+ ```bash
56
+ npm test # single run
57
+ npm run test:coverage # with coverage report
58
+ ```
59
+
60
+ #### 4. Codemod — `npx @chemmangat/msal-next migrate`
61
+ Scans your project and replaces deprecated popup API calls with their redirect equivalents:
62
+
63
+ | Before | After |
64
+ |--------|-------|
65
+ | `loginPopup()` | `loginRedirect()` |
66
+ | `logoutPopup()` | `logoutRedirect()` |
67
+ | `acquireTokenPopup()` | `acquireTokenRedirect()` |
68
+ | `useRedirect={false}` | *(removed)* |
69
+
70
+ Prints a summary of all files modified and occurrences replaced.
71
+
72
+ ```bash
73
+ npx @chemmangat/msal-next migrate
74
+ ```
75
+
76
+ ### 🔄 Migration from v4.x
77
+
78
+ ```bash
79
+ npm install @chemmangat/msal-next@5.0.0
80
+ ```
81
+
82
+ If you have any popup API usage, run the codemod:
83
+ ```bash
84
+ npx @chemmangat/msal-next migrate
85
+ git diff # review changes
86
+ ```
87
+
88
+ ---
89
+
5
90
  ## [4.2.0] - 2026-03-08
6
91
 
7
92
  ### 🎉 Major Feature Release - Multi-Account Management
package/dist/index.d.mts CHANGED
@@ -741,6 +741,26 @@ interface AccountSwitcherProps {
741
741
  * @defaultValue true
742
742
  */
743
743
  showRemoveButton?: boolean;
744
+ /**
745
+ * Custom render function for each account item in the dropdown.
746
+ * When provided, replaces the default account row rendering.
747
+ *
748
+ * @param account - The AccountInfo for this row
749
+ * @param isActive - Whether this account is the currently active account
750
+ * @returns ReactNode to render in place of the default row
751
+ *
752
+ * @example
753
+ * ```tsx
754
+ * <AccountSwitcher
755
+ * renderAccount={(account, isActive) => (
756
+ * <div style={{ fontWeight: isActive ? 'bold' : 'normal' }}>
757
+ * {account.name} ({account.username})
758
+ * </div>
759
+ * )}
760
+ * />
761
+ * ```
762
+ */
763
+ renderAccount?: (account: AccountInfo, isActive: boolean) => ReactNode;
744
764
  }
745
765
  /**
746
766
  * Account Switcher Component
@@ -769,7 +789,7 @@ interface AccountSwitcherProps {
769
789
  * }
770
790
  * ```
771
791
  */
772
- declare function AccountSwitcher({ showAvatars, maxAccounts, onSwitch, onAdd, onRemove, className, style, variant, showAddButton, showRemoveButton, }: AccountSwitcherProps): react_jsx_runtime.JSX.Element | null;
792
+ declare function AccountSwitcher({ showAvatars, maxAccounts, onSwitch, onAdd, onRemove, className, style, variant, showAddButton, showRemoveButton, renderAccount, }: AccountSwitcherProps): react_jsx_runtime.JSX.Element | null;
773
793
 
774
794
  interface AccountListProps {
775
795
  /**
@@ -809,6 +829,26 @@ interface AccountListProps {
809
829
  * @defaultValue 'vertical'
810
830
  */
811
831
  orientation?: 'vertical' | 'horizontal';
832
+ /**
833
+ * Custom render function for each account item.
834
+ * When provided, replaces the default account row rendering.
835
+ *
836
+ * @param account - The AccountInfo for this row
837
+ * @param isActive - Whether this account is the currently active account
838
+ * @returns ReactNode to render in place of the default row content
839
+ *
840
+ * @example
841
+ * ```tsx
842
+ * <AccountList
843
+ * renderAccount={(account, isActive) => (
844
+ * <div style={{ color: isActive ? 'blue' : 'black' }}>
845
+ * {account.name} — {account.username}
846
+ * </div>
847
+ * )}
848
+ * />
849
+ * ```
850
+ */
851
+ renderAccount?: (account: AccountInfo, isActive: boolean) => ReactNode;
812
852
  }
813
853
  /**
814
854
  * Account List Component
@@ -838,7 +878,7 @@ interface AccountListProps {
838
878
  * }
839
879
  * ```
840
880
  */
841
- declare function AccountList({ showAvatars, showDetails, showActiveIndicator, clickToSwitch, onAccountClick, className, style, orientation, }: AccountListProps): react_jsx_runtime.JSX.Element;
881
+ declare function AccountList({ showAvatars, showDetails, showActiveIndicator, clickToSwitch, onAccountClick, className, style, orientation, renderAccount, }: AccountListProps): react_jsx_runtime.JSX.Element;
842
882
 
843
883
  interface UseMsalAuthReturn {
844
884
  /**
package/dist/index.d.ts CHANGED
@@ -741,6 +741,26 @@ interface AccountSwitcherProps {
741
741
  * @defaultValue true
742
742
  */
743
743
  showRemoveButton?: boolean;
744
+ /**
745
+ * Custom render function for each account item in the dropdown.
746
+ * When provided, replaces the default account row rendering.
747
+ *
748
+ * @param account - The AccountInfo for this row
749
+ * @param isActive - Whether this account is the currently active account
750
+ * @returns ReactNode to render in place of the default row
751
+ *
752
+ * @example
753
+ * ```tsx
754
+ * <AccountSwitcher
755
+ * renderAccount={(account, isActive) => (
756
+ * <div style={{ fontWeight: isActive ? 'bold' : 'normal' }}>
757
+ * {account.name} ({account.username})
758
+ * </div>
759
+ * )}
760
+ * />
761
+ * ```
762
+ */
763
+ renderAccount?: (account: AccountInfo, isActive: boolean) => ReactNode;
744
764
  }
745
765
  /**
746
766
  * Account Switcher Component
@@ -769,7 +789,7 @@ interface AccountSwitcherProps {
769
789
  * }
770
790
  * ```
771
791
  */
772
- declare function AccountSwitcher({ showAvatars, maxAccounts, onSwitch, onAdd, onRemove, className, style, variant, showAddButton, showRemoveButton, }: AccountSwitcherProps): react_jsx_runtime.JSX.Element | null;
792
+ declare function AccountSwitcher({ showAvatars, maxAccounts, onSwitch, onAdd, onRemove, className, style, variant, showAddButton, showRemoveButton, renderAccount, }: AccountSwitcherProps): react_jsx_runtime.JSX.Element | null;
773
793
 
774
794
  interface AccountListProps {
775
795
  /**
@@ -809,6 +829,26 @@ interface AccountListProps {
809
829
  * @defaultValue 'vertical'
810
830
  */
811
831
  orientation?: 'vertical' | 'horizontal';
832
+ /**
833
+ * Custom render function for each account item.
834
+ * When provided, replaces the default account row rendering.
835
+ *
836
+ * @param account - The AccountInfo for this row
837
+ * @param isActive - Whether this account is the currently active account
838
+ * @returns ReactNode to render in place of the default row content
839
+ *
840
+ * @example
841
+ * ```tsx
842
+ * <AccountList
843
+ * renderAccount={(account, isActive) => (
844
+ * <div style={{ color: isActive ? 'blue' : 'black' }}>
845
+ * {account.name} — {account.username}
846
+ * </div>
847
+ * )}
848
+ * />
849
+ * ```
850
+ */
851
+ renderAccount?: (account: AccountInfo, isActive: boolean) => ReactNode;
812
852
  }
813
853
  /**
814
854
  * Account List Component
@@ -838,7 +878,7 @@ interface AccountListProps {
838
878
  * }
839
879
  * ```
840
880
  */
841
- declare function AccountList({ showAvatars, showDetails, showActiveIndicator, clickToSwitch, onAccountClick, className, style, orientation, }: AccountListProps): react_jsx_runtime.JSX.Element;
881
+ declare function AccountList({ showAvatars, showDetails, showActiveIndicator, clickToSwitch, onAccountClick, className, style, orientation, renderAccount, }: AccountListProps): react_jsx_runtime.JSX.Element;
842
882
 
843
883
  interface UseMsalAuthReturn {
844
884
  /**
package/dist/index.js CHANGED
@@ -1918,7 +1918,8 @@ function AccountSwitcher({
1918
1918
  style,
1919
1919
  variant = "default",
1920
1920
  showAddButton = true,
1921
- showRemoveButton = true
1921
+ showRemoveButton = true,
1922
+ renderAccount
1922
1923
  }) {
1923
1924
  const {
1924
1925
  accounts,
@@ -2101,40 +2102,42 @@ function AccountSwitcher({
2101
2102
  }
2102
2103
  },
2103
2104
  children: [
2104
- showAvatars && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: avatarStyle, children: getInitials(account.name) }),
2105
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2106
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontWeight: "500", fontSize: "14px" }, children: account.name || account.username }),
2107
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2108
- "div",
2105
+ renderAccount ? renderAccount(account, isActiveAccount(account)) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
2106
+ showAvatars && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: avatarStyle, children: getInitials(account.name) }),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2108
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontWeight: "500", fontSize: "14px" }, children: account.name || account.username }),
2109
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2110
+ "div",
2111
+ {
2112
+ style: {
2113
+ fontSize: "12px",
2114
+ color: "#6b7280",
2115
+ overflow: "hidden",
2116
+ textOverflow: "ellipsis",
2117
+ whiteSpace: "nowrap"
2118
+ },
2119
+ children: account.username
2120
+ }
2121
+ )
2122
+ ] }),
2123
+ isActiveAccount(account) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2124
+ "svg",
2109
2125
  {
2110
- style: {
2111
- fontSize: "12px",
2112
- color: "#6b7280",
2113
- overflow: "hidden",
2114
- textOverflow: "ellipsis",
2115
- whiteSpace: "nowrap"
2116
- },
2117
- children: account.username
2126
+ width: "20",
2127
+ height: "20",
2128
+ viewBox: "0 0 20 20",
2129
+ fill: "none",
2130
+ style: { flexShrink: 0 },
2131
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2132
+ "path",
2133
+ {
2134
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
2135
+ fill: "#3b82f6"
2136
+ }
2137
+ )
2118
2138
  }
2119
2139
  )
2120
2140
  ] }),
2121
- isActiveAccount(account) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2122
- "svg",
2123
- {
2124
- width: "20",
2125
- height: "20",
2126
- viewBox: "0 0 20 20",
2127
- fill: "none",
2128
- style: { flexShrink: 0 },
2129
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2130
- "path",
2131
- {
2132
- d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
2133
- fill: "#3b82f6"
2134
- }
2135
- )
2136
- }
2137
- ),
2138
2141
  showRemoveButton && accounts.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2139
2142
  "button",
2140
2143
  {
@@ -2243,7 +2246,8 @@ function AccountList({
2243
2246
  onAccountClick,
2244
2247
  className = "",
2245
2248
  style,
2246
- orientation = "vertical"
2249
+ orientation = "vertical",
2250
+ renderAccount
2247
2251
  }) {
2248
2252
  const { accounts, switchAccount, isActiveAccount } = useMultiAccount();
2249
2253
  const handleAccountClick = (account) => {
@@ -2310,7 +2314,7 @@ function AccountList({
2310
2314
  }
2311
2315
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className, style: containerStyle, children: accounts.map((account) => {
2312
2316
  const isActive = isActiveAccount(account);
2313
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2317
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2314
2318
  "div",
2315
2319
  {
2316
2320
  onClick: () => handleAccountClick(account),
@@ -2332,7 +2336,7 @@ function AccountList({
2332
2336
  e.currentTarget.style.borderColor = "#e5e7eb";
2333
2337
  }
2334
2338
  },
2335
- children: [
2339
+ children: renderAccount ? renderAccount(account, isActive) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
2336
2340
  showAvatars && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: avatarStyle, children: getInitials(account.name) }),
2337
2341
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { flex: 1, minWidth: 0 }, children: [
2338
2342
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
@@ -2408,7 +2412,7 @@ function AccountList({
2408
2412
  ]
2409
2413
  }
2410
2414
  )
2411
- ]
2415
+ ] })
2412
2416
  },
2413
2417
  account.homeAccountId
2414
2418
  );
package/dist/index.mjs CHANGED
@@ -1854,7 +1854,8 @@ function AccountSwitcher({
1854
1854
  style,
1855
1855
  variant = "default",
1856
1856
  showAddButton = true,
1857
- showRemoveButton = true
1857
+ showRemoveButton = true,
1858
+ renderAccount
1858
1859
  }) {
1859
1860
  const {
1860
1861
  accounts,
@@ -2037,40 +2038,42 @@ function AccountSwitcher({
2037
2038
  }
2038
2039
  },
2039
2040
  children: [
2040
- showAvatars && /* @__PURE__ */ jsx9("div", { style: avatarStyle, children: getInitials(account.name) }),
2041
- /* @__PURE__ */ jsxs6("div", { style: { flex: 1, minWidth: 0 }, children: [
2042
- /* @__PURE__ */ jsx9("div", { style: { fontWeight: "500", fontSize: "14px" }, children: account.name || account.username }),
2043
- /* @__PURE__ */ jsx9(
2044
- "div",
2041
+ renderAccount ? renderAccount(account, isActiveAccount(account)) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
2042
+ showAvatars && /* @__PURE__ */ jsx9("div", { style: avatarStyle, children: getInitials(account.name) }),
2043
+ /* @__PURE__ */ jsxs6("div", { style: { flex: 1, minWidth: 0 }, children: [
2044
+ /* @__PURE__ */ jsx9("div", { style: { fontWeight: "500", fontSize: "14px" }, children: account.name || account.username }),
2045
+ /* @__PURE__ */ jsx9(
2046
+ "div",
2047
+ {
2048
+ style: {
2049
+ fontSize: "12px",
2050
+ color: "#6b7280",
2051
+ overflow: "hidden",
2052
+ textOverflow: "ellipsis",
2053
+ whiteSpace: "nowrap"
2054
+ },
2055
+ children: account.username
2056
+ }
2057
+ )
2058
+ ] }),
2059
+ isActiveAccount(account) && /* @__PURE__ */ jsx9(
2060
+ "svg",
2045
2061
  {
2046
- style: {
2047
- fontSize: "12px",
2048
- color: "#6b7280",
2049
- overflow: "hidden",
2050
- textOverflow: "ellipsis",
2051
- whiteSpace: "nowrap"
2052
- },
2053
- children: account.username
2062
+ width: "20",
2063
+ height: "20",
2064
+ viewBox: "0 0 20 20",
2065
+ fill: "none",
2066
+ style: { flexShrink: 0 },
2067
+ children: /* @__PURE__ */ jsx9(
2068
+ "path",
2069
+ {
2070
+ d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
2071
+ fill: "#3b82f6"
2072
+ }
2073
+ )
2054
2074
  }
2055
2075
  )
2056
2076
  ] }),
2057
- isActiveAccount(account) && /* @__PURE__ */ jsx9(
2058
- "svg",
2059
- {
2060
- width: "20",
2061
- height: "20",
2062
- viewBox: "0 0 20 20",
2063
- fill: "none",
2064
- style: { flexShrink: 0 },
2065
- children: /* @__PURE__ */ jsx9(
2066
- "path",
2067
- {
2068
- d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z",
2069
- fill: "#3b82f6"
2070
- }
2071
- )
2072
- }
2073
- ),
2074
2077
  showRemoveButton && accounts.length > 1 && /* @__PURE__ */ jsx9(
2075
2078
  "button",
2076
2079
  {
@@ -2179,7 +2182,8 @@ function AccountList({
2179
2182
  onAccountClick,
2180
2183
  className = "",
2181
2184
  style,
2182
- orientation = "vertical"
2185
+ orientation = "vertical",
2186
+ renderAccount
2183
2187
  }) {
2184
2188
  const { accounts, switchAccount, isActiveAccount } = useMultiAccount();
2185
2189
  const handleAccountClick = (account) => {
@@ -2246,7 +2250,7 @@ function AccountList({
2246
2250
  }
2247
2251
  return /* @__PURE__ */ jsx10("div", { className, style: containerStyle, children: accounts.map((account) => {
2248
2252
  const isActive = isActiveAccount(account);
2249
- return /* @__PURE__ */ jsxs7(
2253
+ return /* @__PURE__ */ jsx10(
2250
2254
  "div",
2251
2255
  {
2252
2256
  onClick: () => handleAccountClick(account),
@@ -2268,7 +2272,7 @@ function AccountList({
2268
2272
  e.currentTarget.style.borderColor = "#e5e7eb";
2269
2273
  }
2270
2274
  },
2271
- children: [
2275
+ children: renderAccount ? renderAccount(account, isActive) : /* @__PURE__ */ jsxs7(Fragment5, { children: [
2272
2276
  showAvatars && /* @__PURE__ */ jsx10("div", { style: avatarStyle, children: getInitials(account.name) }),
2273
2277
  /* @__PURE__ */ jsxs7("div", { style: { flex: 1, minWidth: 0 }, children: [
2274
2278
  /* @__PURE__ */ jsx10(
@@ -2344,7 +2348,7 @@ function AccountList({
2344
2348
  ]
2345
2349
  }
2346
2350
  )
2347
- ]
2351
+ ] })
2348
2352
  },
2349
2353
  account.homeAccountId
2350
2354
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chemmangat/msal-next",
3
- "version": "4.2.2",
3
+ "version": "5.0.0",
4
4
  "description": "Production-ready Microsoft/Azure AD authentication for Next.js App Router. Zero-config setup, TypeScript-first, multi-account support, auto token refresh. The easiest way to add Microsoft login to your Next.js app.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -90,6 +90,7 @@
90
90
  "devDependencies": {
91
91
  "@azure/msal-browser": "^3.11.1",
92
92
  "@azure/msal-react": "^2.0.15",
93
+ "@testing-library/jest-dom": "^6.9.1",
93
94
  "@testing-library/react": "^14.0.0",
94
95
  "@testing-library/react-hooks": "^8.0.1",
95
96
  "@types/react": "^18.2.0",