@salesforce/retail-react-app 8.3.0 → 8.4.0-nightly-20260116000329
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 +5 -0
- package/app/components/multiship/multiship-order-summary.jsx +6 -2
- package/app/components/order-summary/index.jsx +33 -25
- package/app/constants.js +0 -6
- package/app/hooks/use-password-reset.js +2 -1
- package/app/hooks/use-password-reset.test.js +11 -2
- package/app/pages/account/order-detail.jsx +46 -32
- package/app/pages/account/orders.test.js +116 -0
- package/app/pages/checkout/confirmation.jsx +52 -42
- package/app/pages/login/index.jsx +2 -2
- package/app/pages/login/passwordless-landing.test.js +114 -7
- package/app/pages/reset-password/index.jsx +2 -3
- package/app/pages/reset-password/index.test.jsx +31 -1
- package/app/routes.jsx +34 -28
- package/app/routes.test.js +272 -0
- package/app/utils/bonus-product/cart.js +32 -0
- package/app/utils/bonus-product/cart.test.js +353 -0
- package/app/utils/routes-utils.js +131 -13
- package/app/utils/routes-utils.test.js +177 -0
- package/config/mocks/default.js +5 -1
- package/package.json +6 -6
|
@@ -1639,4 +1639,357 @@ describe('Bonus Product Cart Utilities', () => {
|
|
|
1639
1639
|
expect(result[0].productId).toBe('bonus-product-1')
|
|
1640
1640
|
})
|
|
1641
1641
|
})
|
|
1642
|
+
|
|
1643
|
+
describe('consolidateDuplicateBonusProducts', () => {
|
|
1644
|
+
test('returns empty array for null input', () => {
|
|
1645
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(null)
|
|
1646
|
+
expect(result).toEqual([])
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
test('returns empty array for undefined input', () => {
|
|
1650
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(undefined)
|
|
1651
|
+
expect(result).toEqual([])
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
test('returns empty array for empty array input', () => {
|
|
1655
|
+
const result = cartUtils.consolidateDuplicateBonusProducts([])
|
|
1656
|
+
expect(result).toEqual([])
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
test('returns regular products as-is when no bonus products', () => {
|
|
1660
|
+
const productItems = [
|
|
1661
|
+
{
|
|
1662
|
+
productId: 'regular-product-1',
|
|
1663
|
+
itemId: 'item-1',
|
|
1664
|
+
quantity: 2
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
productId: 'regular-product-2',
|
|
1668
|
+
itemId: 'item-2',
|
|
1669
|
+
quantity: 1
|
|
1670
|
+
}
|
|
1671
|
+
]
|
|
1672
|
+
|
|
1673
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1674
|
+
|
|
1675
|
+
expect(result).toHaveLength(2)
|
|
1676
|
+
expect(result[0].productId).toBe('regular-product-1')
|
|
1677
|
+
expect(result[0].quantity).toBe(2)
|
|
1678
|
+
expect(result[1].productId).toBe('regular-product-2')
|
|
1679
|
+
expect(result[1].quantity).toBe(1)
|
|
1680
|
+
})
|
|
1681
|
+
|
|
1682
|
+
test('consolidates duplicate bonus products by productId', () => {
|
|
1683
|
+
const productItems = [
|
|
1684
|
+
{
|
|
1685
|
+
productId: 'bonus-product-1',
|
|
1686
|
+
itemId: 'bonus-item-1',
|
|
1687
|
+
quantity: 1,
|
|
1688
|
+
bonusProductLineItem: true,
|
|
1689
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
productId: 'bonus-product-1',
|
|
1693
|
+
itemId: 'bonus-item-2',
|
|
1694
|
+
quantity: 2,
|
|
1695
|
+
bonusProductLineItem: true,
|
|
1696
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
productId: 'bonus-product-1',
|
|
1700
|
+
itemId: 'bonus-item-3',
|
|
1701
|
+
quantity: 1,
|
|
1702
|
+
bonusProductLineItem: true,
|
|
1703
|
+
bonusDiscountLineItemId: 'bonus-456'
|
|
1704
|
+
}
|
|
1705
|
+
]
|
|
1706
|
+
|
|
1707
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1708
|
+
|
|
1709
|
+
expect(result).toHaveLength(1)
|
|
1710
|
+
expect(result[0].productId).toBe('bonus-product-1')
|
|
1711
|
+
expect(result[0].quantity).toBe(4) // 1 + 2 + 1
|
|
1712
|
+
expect(result[0].bonusProductLineItem).toBe(true)
|
|
1713
|
+
})
|
|
1714
|
+
|
|
1715
|
+
test('keeps different bonus products separate', () => {
|
|
1716
|
+
const productItems = [
|
|
1717
|
+
{
|
|
1718
|
+
productId: 'bonus-product-1',
|
|
1719
|
+
itemId: 'bonus-item-1',
|
|
1720
|
+
quantity: 2,
|
|
1721
|
+
bonusProductLineItem: true,
|
|
1722
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1723
|
+
},
|
|
1724
|
+
{
|
|
1725
|
+
productId: 'bonus-product-2',
|
|
1726
|
+
itemId: 'bonus-item-2',
|
|
1727
|
+
quantity: 1,
|
|
1728
|
+
bonusProductLineItem: true,
|
|
1729
|
+
bonusDiscountLineItemId: 'bonus-456'
|
|
1730
|
+
}
|
|
1731
|
+
]
|
|
1732
|
+
|
|
1733
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1734
|
+
|
|
1735
|
+
expect(result).toHaveLength(2)
|
|
1736
|
+
expect(result[0].productId).toBe('bonus-product-1')
|
|
1737
|
+
expect(result[0].quantity).toBe(2)
|
|
1738
|
+
expect(result[1].productId).toBe('bonus-product-2')
|
|
1739
|
+
expect(result[1].quantity).toBe(1)
|
|
1740
|
+
})
|
|
1741
|
+
|
|
1742
|
+
test('preserves order: regular products first, then bonus products', () => {
|
|
1743
|
+
const productItems = [
|
|
1744
|
+
{
|
|
1745
|
+
productId: 'bonus-product-1',
|
|
1746
|
+
itemId: 'bonus-item-1',
|
|
1747
|
+
quantity: 1,
|
|
1748
|
+
bonusProductLineItem: true,
|
|
1749
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
productId: 'regular-product-1',
|
|
1753
|
+
itemId: 'regular-item-1',
|
|
1754
|
+
quantity: 2
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
productId: 'bonus-product-2',
|
|
1758
|
+
itemId: 'bonus-item-2',
|
|
1759
|
+
quantity: 1,
|
|
1760
|
+
bonusProductLineItem: true,
|
|
1761
|
+
bonusDiscountLineItemId: 'bonus-456'
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
productId: 'regular-product-2',
|
|
1765
|
+
itemId: 'regular-item-2',
|
|
1766
|
+
quantity: 1
|
|
1767
|
+
}
|
|
1768
|
+
]
|
|
1769
|
+
|
|
1770
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1771
|
+
|
|
1772
|
+
expect(result).toHaveLength(4)
|
|
1773
|
+
// Regular products should come first
|
|
1774
|
+
expect(result[0].productId).toBe('regular-product-1')
|
|
1775
|
+
expect(result[0].bonusProductLineItem).toBeUndefined()
|
|
1776
|
+
expect(result[1].productId).toBe('regular-product-2')
|
|
1777
|
+
expect(result[1].bonusProductLineItem).toBeUndefined()
|
|
1778
|
+
// Bonus products should come after
|
|
1779
|
+
expect(result[2].productId).toBe('bonus-product-1')
|
|
1780
|
+
expect(result[2].bonusProductLineItem).toBe(true)
|
|
1781
|
+
expect(result[3].productId).toBe('bonus-product-2')
|
|
1782
|
+
expect(result[3].bonusProductLineItem).toBe(true)
|
|
1783
|
+
})
|
|
1784
|
+
|
|
1785
|
+
test('consolidates multiple duplicate bonus products and preserves other properties', () => {
|
|
1786
|
+
const productItems = [
|
|
1787
|
+
{
|
|
1788
|
+
productId: 'bonus-product-1',
|
|
1789
|
+
itemId: 'bonus-item-1',
|
|
1790
|
+
quantity: 1,
|
|
1791
|
+
bonusProductLineItem: true,
|
|
1792
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
productId: 'bonus-product-1',
|
|
1796
|
+
itemId: 'bonus-item-2',
|
|
1797
|
+
quantity: 3,
|
|
1798
|
+
bonusProductLineItem: true,
|
|
1799
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
productId: 'bonus-product-2',
|
|
1803
|
+
itemId: 'bonus-item-3',
|
|
1804
|
+
quantity: 2,
|
|
1805
|
+
bonusProductLineItem: true,
|
|
1806
|
+
bonusDiscountLineItemId: 'bonus-456'
|
|
1807
|
+
}
|
|
1808
|
+
]
|
|
1809
|
+
|
|
1810
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1811
|
+
|
|
1812
|
+
expect(result).toHaveLength(2)
|
|
1813
|
+
// First bonus product should be consolidated
|
|
1814
|
+
expect(result[0].productId).toBe('bonus-product-1')
|
|
1815
|
+
expect(result[0].quantity).toBe(4) // 1 + 3
|
|
1816
|
+
expect(result[0].bonusProductLineItem).toBe(true)
|
|
1817
|
+
expect(result[0].bonusDiscountLineItemId).toBe('bonus-123')
|
|
1818
|
+
// Second bonus product should remain separate
|
|
1819
|
+
expect(result[1].productId).toBe('bonus-product-2')
|
|
1820
|
+
expect(result[1].quantity).toBe(2)
|
|
1821
|
+
})
|
|
1822
|
+
|
|
1823
|
+
test('handles mixed regular and bonus products with duplicates', () => {
|
|
1824
|
+
const productItems = [
|
|
1825
|
+
{
|
|
1826
|
+
productId: 'regular-product-1',
|
|
1827
|
+
itemId: 'regular-item-1',
|
|
1828
|
+
quantity: 1
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
productId: 'bonus-product-1',
|
|
1832
|
+
itemId: 'bonus-item-1',
|
|
1833
|
+
quantity: 1,
|
|
1834
|
+
bonusProductLineItem: true,
|
|
1835
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1836
|
+
},
|
|
1837
|
+
{
|
|
1838
|
+
productId: 'regular-product-2',
|
|
1839
|
+
itemId: 'regular-item-2',
|
|
1840
|
+
quantity: 2
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
productId: 'bonus-product-1',
|
|
1844
|
+
itemId: 'bonus-item-2',
|
|
1845
|
+
quantity: 2,
|
|
1846
|
+
bonusProductLineItem: true,
|
|
1847
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
productId: 'bonus-product-2',
|
|
1851
|
+
itemId: 'bonus-item-3',
|
|
1852
|
+
quantity: 1,
|
|
1853
|
+
bonusProductLineItem: true,
|
|
1854
|
+
bonusDiscountLineItemId: 'bonus-456'
|
|
1855
|
+
}
|
|
1856
|
+
]
|
|
1857
|
+
|
|
1858
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1859
|
+
|
|
1860
|
+
expect(result).toHaveLength(4)
|
|
1861
|
+
// Regular products first
|
|
1862
|
+
expect(result[0].productId).toBe('regular-product-1')
|
|
1863
|
+
expect(result[0].quantity).toBe(1)
|
|
1864
|
+
expect(result[1].productId).toBe('regular-product-2')
|
|
1865
|
+
expect(result[1].quantity).toBe(2)
|
|
1866
|
+
// Consolidated bonus products
|
|
1867
|
+
expect(result[2].productId).toBe('bonus-product-1')
|
|
1868
|
+
expect(result[2].quantity).toBe(3) // 1 + 2
|
|
1869
|
+
expect(result[3].productId).toBe('bonus-product-2')
|
|
1870
|
+
expect(result[3].quantity).toBe(1)
|
|
1871
|
+
})
|
|
1872
|
+
|
|
1873
|
+
test('handles bonus products with zero quantity', () => {
|
|
1874
|
+
const productItems = [
|
|
1875
|
+
{
|
|
1876
|
+
productId: 'bonus-product-1',
|
|
1877
|
+
itemId: 'bonus-item-1',
|
|
1878
|
+
quantity: 0,
|
|
1879
|
+
bonusProductLineItem: true,
|
|
1880
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1881
|
+
},
|
|
1882
|
+
{
|
|
1883
|
+
productId: 'bonus-product-1',
|
|
1884
|
+
itemId: 'bonus-item-2',
|
|
1885
|
+
quantity: 2,
|
|
1886
|
+
bonusProductLineItem: true,
|
|
1887
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1888
|
+
}
|
|
1889
|
+
]
|
|
1890
|
+
|
|
1891
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1892
|
+
|
|
1893
|
+
expect(result).toHaveLength(1)
|
|
1894
|
+
expect(result[0].productId).toBe('bonus-product-1')
|
|
1895
|
+
expect(result[0].quantity).toBe(2) // 0 + 2
|
|
1896
|
+
})
|
|
1897
|
+
|
|
1898
|
+
test('handles bonus products with missing quantity property', () => {
|
|
1899
|
+
const productItems = [
|
|
1900
|
+
{
|
|
1901
|
+
productId: 'bonus-product-1',
|
|
1902
|
+
itemId: 'bonus-item-1',
|
|
1903
|
+
bonusProductLineItem: true,
|
|
1904
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1905
|
+
},
|
|
1906
|
+
{
|
|
1907
|
+
productId: 'bonus-product-1',
|
|
1908
|
+
itemId: 'bonus-item-2',
|
|
1909
|
+
quantity: 2,
|
|
1910
|
+
bonusProductLineItem: true,
|
|
1911
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1912
|
+
}
|
|
1913
|
+
]
|
|
1914
|
+
|
|
1915
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1916
|
+
|
|
1917
|
+
expect(result).toHaveLength(1)
|
|
1918
|
+
expect(result[0].productId).toBe('bonus-product-1')
|
|
1919
|
+
expect(result[0].quantity).toBe(2) // undefined treated as 0, then + 2
|
|
1920
|
+
})
|
|
1921
|
+
|
|
1922
|
+
test('consolidates same bonus product from different qualifying products', () => {
|
|
1923
|
+
// Scenario: Same product (bonus-product-1) is a bonus for two different qualifying products
|
|
1924
|
+
// This tests that consolidation happens by productId only, regardless of which
|
|
1925
|
+
// qualifying product triggered the bonus or which bonusDiscountLineItemId it has
|
|
1926
|
+
const productItems = [
|
|
1927
|
+
{
|
|
1928
|
+
productId: 'regular-product-A',
|
|
1929
|
+
itemId: 'regular-item-A',
|
|
1930
|
+
quantity: 1
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
productId: 'bonus-product-1',
|
|
1934
|
+
itemId: 'bonus-item-1',
|
|
1935
|
+
quantity: 2,
|
|
1936
|
+
bonusProductLineItem: true,
|
|
1937
|
+
bonusDiscountLineItemId: 'bonus-123' // From regular-product-A
|
|
1938
|
+
},
|
|
1939
|
+
{
|
|
1940
|
+
productId: 'regular-product-B',
|
|
1941
|
+
itemId: 'regular-item-B',
|
|
1942
|
+
quantity: 1
|
|
1943
|
+
},
|
|
1944
|
+
{
|
|
1945
|
+
productId: 'bonus-product-1',
|
|
1946
|
+
itemId: 'bonus-item-2',
|
|
1947
|
+
quantity: 1,
|
|
1948
|
+
bonusProductLineItem: true,
|
|
1949
|
+
bonusDiscountLineItemId: 'bonus-456' // From regular-product-B (different line item)
|
|
1950
|
+
}
|
|
1951
|
+
]
|
|
1952
|
+
|
|
1953
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1954
|
+
|
|
1955
|
+
expect(result).toHaveLength(3)
|
|
1956
|
+
// Regular products first
|
|
1957
|
+
expect(result[0].productId).toBe('regular-product-A')
|
|
1958
|
+
expect(result[1].productId).toBe('regular-product-B')
|
|
1959
|
+
// Bonus products consolidated - same productId from different sources merged
|
|
1960
|
+
expect(result[2].productId).toBe('bonus-product-1')
|
|
1961
|
+
expect(result[2].quantity).toBe(3) // 2 + 1, consolidated from both qualifying products
|
|
1962
|
+
expect(result[2].bonusProductLineItem).toBe(true)
|
|
1963
|
+
// Note: The bonusDiscountLineItemId will be from the first item found (bonus-123)
|
|
1964
|
+
// This is a side effect of using sampleItem - information about the second source is lost
|
|
1965
|
+
})
|
|
1966
|
+
|
|
1967
|
+
test('itemId is not used in consolidation logic (only for display keys)', () => {
|
|
1968
|
+
// itemId is unique per line item but consolidation only uses productId
|
|
1969
|
+
const productItems = [
|
|
1970
|
+
{
|
|
1971
|
+
productId: 'bonus-product-1',
|
|
1972
|
+
itemId: 'unique-item-id-1',
|
|
1973
|
+
quantity: 1,
|
|
1974
|
+
bonusProductLineItem: true,
|
|
1975
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
productId: 'bonus-product-1',
|
|
1979
|
+
itemId: 'unique-item-id-2', // Different itemId
|
|
1980
|
+
quantity: 2,
|
|
1981
|
+
bonusProductLineItem: true,
|
|
1982
|
+
bonusDiscountLineItemId: 'bonus-123'
|
|
1983
|
+
}
|
|
1984
|
+
]
|
|
1985
|
+
|
|
1986
|
+
const result = cartUtils.consolidateDuplicateBonusProducts(productItems)
|
|
1987
|
+
|
|
1988
|
+
expect(result).toHaveLength(1)
|
|
1989
|
+
expect(result[0].productId).toBe('bonus-product-1')
|
|
1990
|
+
expect(result[0].quantity).toBe(3) // Consolidated despite different itemIds
|
|
1991
|
+
// The itemId in result will be from the first item found (unique-item-id-1)
|
|
1992
|
+
// This is preserved for React key generation but not used in consolidation logic
|
|
1993
|
+
})
|
|
1994
|
+
})
|
|
1642
1995
|
})
|
|
@@ -9,25 +9,99 @@ import {getSites} from '@salesforce/retail-react-app/app/utils/site-utils'
|
|
|
9
9
|
import {urlPartPositions} from '@salesforce/retail-react-app/app/constants'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Build regex patterns for site and locale route parameters.
|
|
13
|
+
* Creates patterns like "siteA|siteB|siteC" from all valid site/locale refs.
|
|
14
|
+
*
|
|
15
|
+
* @param {array} allSites - array of site configurations
|
|
16
|
+
* @returns {object} - { sitePattern, localePattern }
|
|
17
|
+
*/
|
|
18
|
+
const buildRoutePatterns = (allSites) => {
|
|
19
|
+
const siteRefs = allSites.flatMap((site) => [site.alias, site.id]).filter(Boolean)
|
|
20
|
+
|
|
21
|
+
const localeRefs = allSites
|
|
22
|
+
.flatMap((site) => site.l10n.supportedLocales)
|
|
23
|
+
.flatMap((locale) => [locale.alias, locale.id])
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
|
|
26
|
+
// Remove duplicates and join into regex pattern
|
|
27
|
+
const sitePattern = [...new Set(siteRefs)].join('|')
|
|
28
|
+
const localePattern = [...new Set(localeRefs)].join('|')
|
|
29
|
+
|
|
30
|
+
return {sitePattern, localePattern}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Configure routes using parameterized paths with regex constraints.
|
|
35
|
+
* This approach generates fewer routes by using patterns like:
|
|
36
|
+
* /:site(siteA|siteB)/:locale(en|fr)/path
|
|
37
|
+
*
|
|
38
|
+
* Note: This may match site/locale combinations that aren't valid together
|
|
39
|
+
* (e.g., a locale not supported by a specific site). Runtime validation
|
|
40
|
+
* should be performed after route matching.
|
|
14
41
|
*
|
|
15
42
|
* @param {array} routes - array of routes to be reconstructed
|
|
16
|
-
* @param {object} urlConfig
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {array}
|
|
19
|
-
* @
|
|
43
|
+
* @param {object} urlConfig - url configuration with site/locale positions
|
|
44
|
+
* @param {array} allSites - array of site configurations
|
|
45
|
+
* @param {array} ignoredRoutes - routes that should not be reconstructed
|
|
46
|
+
* @returns {array} - list of parameterized route objects
|
|
20
47
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
48
|
+
const configureRoutesWithFuzzyMatching = (routes, urlConfig, allSites, ignoredRoutes) => {
|
|
49
|
+
const {sitePattern, localePattern} = buildRoutePatterns(allSites)
|
|
50
|
+
const {locale: localePosition, site: sitePosition} = urlConfig
|
|
24
51
|
|
|
25
|
-
const
|
|
52
|
+
const outputRoutes = []
|
|
26
53
|
|
|
27
|
-
const
|
|
28
|
-
|
|
54
|
+
for (const route of routes) {
|
|
55
|
+
const {path, ...rest} = route
|
|
56
|
+
|
|
57
|
+
if (ignoredRoutes.includes(path)) {
|
|
58
|
+
outputRoutes.push(route)
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (localePosition === urlPartPositions.PATH && sitePosition === urlPartPositions.PATH) {
|
|
63
|
+
// Both site and locale in path
|
|
64
|
+
outputRoutes.push({
|
|
65
|
+
path: `/:site(${sitePattern})/:locale(${localePattern})${path}`,
|
|
66
|
+
...rest
|
|
67
|
+
})
|
|
68
|
+
} else if (sitePosition === urlPartPositions.PATH) {
|
|
69
|
+
// Site only in path
|
|
70
|
+
outputRoutes.push({
|
|
71
|
+
path: `/:site(${sitePattern})${path}`,
|
|
72
|
+
...rest
|
|
73
|
+
})
|
|
74
|
+
} else if (localePosition === urlPartPositions.PATH) {
|
|
75
|
+
// Locale only in path
|
|
76
|
+
outputRoutes.push({
|
|
77
|
+
path: `/:locale(${localePattern})${path}`,
|
|
78
|
+
...rest
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Original route as fallback
|
|
83
|
+
outputRoutes.push(route)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return outputRoutes
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Configure routes using explicit paths for each site/locale combination.
|
|
91
|
+
* This is the original approach that generates literal routes like:
|
|
92
|
+
* /siteA/en/path, /siteA/fr/path, /siteB/en/path, etc.
|
|
93
|
+
*
|
|
94
|
+
* @param {array} routes - array of routes to be reconstructed
|
|
95
|
+
* @param {object} urlConfig - url configuration with site/locale positions
|
|
96
|
+
* @param {array} allSites - array of site configurations
|
|
97
|
+
* @param {array} ignoredRoutes - routes that should not be reconstructed
|
|
98
|
+
* @returns {array} - list of explicit route objects
|
|
99
|
+
*/
|
|
100
|
+
const configureRoutesWithExplicitMatching = (routes, urlConfig, allSites, ignoredRoutes) => {
|
|
101
|
+
const {locale: localePosition, site: sitePosition} = urlConfig
|
|
29
102
|
|
|
30
103
|
let outputRoutes = []
|
|
104
|
+
|
|
31
105
|
for (let i = 0; i < routes.length; i++) {
|
|
32
106
|
const {path, ...rest} = routes[i]
|
|
33
107
|
|
|
@@ -44,7 +118,6 @@ export const configureRoutes = (routes = [], config, {ignoredRoutes = []}) => {
|
|
|
44
118
|
localeRefs.push(locale.id)
|
|
45
119
|
})
|
|
46
120
|
localeRefs = localeRefs.filter(Boolean)
|
|
47
|
-
const {locale: localePosition, site: sitePosition} = urlConfig
|
|
48
121
|
|
|
49
122
|
if (
|
|
50
123
|
localePosition === urlPartPositions.PATH &&
|
|
@@ -100,6 +173,7 @@ export const configureRoutes = (routes = [], config, {ignoredRoutes = []}) => {
|
|
|
100
173
|
outputRoutes.push(routes[i])
|
|
101
174
|
}
|
|
102
175
|
}
|
|
176
|
+
|
|
103
177
|
// Remove any duplicate routes
|
|
104
178
|
outputRoutes = outputRoutes.reduce((res, route) => {
|
|
105
179
|
if (!res.some(({path}) => path === route.path)) {
|
|
@@ -107,5 +181,49 @@ export const configureRoutes = (routes = [], config, {ignoredRoutes = []}) => {
|
|
|
107
181
|
}
|
|
108
182
|
return res
|
|
109
183
|
}, [])
|
|
184
|
+
|
|
185
|
+
return outputRoutes
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Construct routes based on url config with site and locale references
|
|
190
|
+
* (ids and aliases) from each site in your application config.
|
|
191
|
+
*
|
|
192
|
+
* @param {array} routes - array of routes to be reconstructed
|
|
193
|
+
* @param {object} config - application configuration
|
|
194
|
+
* @param {object} options - options if there are any
|
|
195
|
+
* @param {array} options.ignoredRoutes - routes that should not be reconstructed
|
|
196
|
+
* @param {boolean} options.fuzzyPathMatching - when true, uses parameterized routes with
|
|
197
|
+
* regex constraints (e.g., /:site(a|b)/:locale(en|fr)/path) for fewer, more efficient
|
|
198
|
+
* route configurations. When false (default), generates explicit routes for each
|
|
199
|
+
* site/locale combination. Fuzzy matching may match invalid site/locale combinations
|
|
200
|
+
* that require runtime validation.
|
|
201
|
+
* @returns {array} - list of route objects with site and locale refs
|
|
202
|
+
*/
|
|
203
|
+
export const configureRoutes = (
|
|
204
|
+
routes = [],
|
|
205
|
+
config,
|
|
206
|
+
{ignoredRoutes = [], fuzzyPathMatching = false}
|
|
207
|
+
) => {
|
|
208
|
+
if (!routes.length) return []
|
|
209
|
+
if (!config) return routes
|
|
210
|
+
|
|
211
|
+
let outputRoutes = []
|
|
212
|
+
const {url: urlConfig} = config.app
|
|
213
|
+
|
|
214
|
+
const allSites = getSites()
|
|
215
|
+
if (!allSites) return routes
|
|
216
|
+
|
|
217
|
+
if (fuzzyPathMatching) {
|
|
218
|
+
outputRoutes = configureRoutesWithFuzzyMatching(routes, urlConfig, allSites, ignoredRoutes)
|
|
219
|
+
} else {
|
|
220
|
+
outputRoutes = configureRoutesWithExplicitMatching(
|
|
221
|
+
routes,
|
|
222
|
+
urlConfig,
|
|
223
|
+
allSites,
|
|
224
|
+
ignoredRoutes
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
110
228
|
return outputRoutes
|
|
111
229
|
}
|