diff --git a/README.md b/README.md index 8be54816a..ea6248c51 100644 --- a/README.md +++ b/README.md @@ -42,57 +42,6 @@ Additionally, we need to ensure feature parity (not all providers have e.g. wish People actively working on this project: @okbel & @lfades. -## Troubleshoot - -
-I already own a BigCommerce store. What should I do? -
-First thing you do is: set your environment variables -
-
-.env.local - -```sh -BIGCOMMERCE_STOREFRONT_API_URL=<> -BIGCOMMERCE_STOREFRONT_API_TOKEN=<> -BIGCOMMERCE_STORE_API_URL=<> -BIGCOMMERCE_STORE_API_TOKEN=<> -BIGCOMMERCE_STORE_API_CLIENT_ID=<> -``` - -If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials. - -1. Install Vercel CLI: `npm i -g vercel` -2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link` -3. Download your environment variables: `vercel env pull .env.local` - -Next, you're free to customize the starter. More updates coming soon. Stay tuned. - -
- -
-BigCommerce shows a Coming Soon page and requests a Preview Code -
-After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. -
-
-BigCommerce team has been notified and they plan to add more detailed about this subject. -
- -## Contribute - -Our commitment to Open Source can be found [here](https://vercel.com/oss). - -1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. -2. Create a new branch `git checkout -b MY_BRANCH_NAME` -3. Install yarn: `npm install -g yarn` -4. Install the dependencies: `yarn` -5. Duplicate `.env.template` and rename it to `.env.local`. -6. Add proper store values to `.env.local`. -7. Run `yarn dev` to build and watch for code changes -8. The development branch is `canary` (this is the branch pull requests should be made against). - On a release, `canary` branch is rebased into `master`. - ## Framework Framework is where the data comes from. It contains mostly hooks and functions. @@ -132,3 +81,70 @@ import { useUI } from '@components/ui' import { useCustomer } from '@framework/customer' import { useAddItem, useWishlist, useRemoveItem } from '@framework/wishlist' ``` + +## Config + +### Features + +In order to make the UI entirely functional, we need to specify which features certain providers do not **provide**. + +**Disabling wishlist:** + +``` +{ + "features": { + "wishlist": false + } +} +``` + +## Contribute + +Our commitment to Open Source can be found [here](https://vercel.com/oss). + +1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. +2. Create a new branch `git checkout -b MY_BRANCH_NAME` +3. Install yarn: `npm install -g yarn` +4. Install the dependencies: `yarn` +5. Duplicate `.env.template` and rename it to `.env.local`. +6. Add proper store values to `.env.local`. +7. Run `yarn dev` to build and watch for code changes +8. The development branch is `canary` (this is the branch pull requests should be made against). + On a release, `canary` branch is rebased into `master`. + +## Troubleshoot + +
+I already own a BigCommerce store. What should I do? +
+First thing you do is: set your environment variables +
+
+.env.local + +```sh +BIGCOMMERCE_STOREFRONT_API_URL=<> +BIGCOMMERCE_STOREFRONT_API_TOKEN=<> +BIGCOMMERCE_STORE_API_URL=<> +BIGCOMMERCE_STORE_API_TOKEN=<> +BIGCOMMERCE_STORE_API_CLIENT_ID=<> +``` + +If your project was started with a "Deploy with Vercel" button, you can use Vercel's CLI to retrieve these credentials. + +1. Install Vercel CLI: `npm i -g vercel` +2. Link local instance with Vercel and Github accounts (creates .vercel file): `vercel link` +3. Download your environment variables: `vercel env pull .env.local` + +Next, you're free to customize the starter. More updates coming soon. Stay tuned. + +
+ +
+BigCommerce shows a Coming Soon page and requests a Preview Code +
+After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. +
+
+BigCommerce team has been notified and they plan to add more detailed about this subject. +
diff --git a/components/cart/CartSidebarView/CartSidebarView.tsx b/components/cart/CartSidebarView/CartSidebarView.tsx index c25bd7c95..5b28fde27 100644 --- a/components/cart/CartSidebarView/CartSidebarView.tsx +++ b/components/cart/CartSidebarView/CartSidebarView.tsx @@ -9,7 +9,7 @@ import usePrice from '@framework/product/use-price' import CartItem from '../CartItem' import s from './CartSidebarView.module.css' -const CartSidebarView: FC = () => { +const CartSidebarView: FC<{ wishlist?: boolean }> = ({ wishlist }) => { const { closeSidebar } = useUI() const { data, isLoading, isEmpty } = useCart() @@ -48,7 +48,7 @@ const CartSidebarView: FC = () => {
- +
diff --git a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx index 49e115df6..4b838e1a4 100644 --- a/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx +++ b/components/common/HomeAllProductsGrid/HomeAllProductsGrid.tsx @@ -5,14 +5,21 @@ import { Grid } from '@components/ui' import { ProductCard } from '@components/product' import s from './HomeAllProductsGrid.module.css' import { getCategoryPath, getDesignerPath } from '@lib/search' +import wishlist from '@framework/api/wishlist' interface Props { categories?: any brands?: any products?: Product[] + wishlist?: boolean } -const Head: FC = ({ categories, brands, products = [] }) => { +const HomeAllProductsGrid: FC = ({ + categories, + brands, + products = [], + wishlist = false, +}) => { return (
@@ -58,6 +65,7 @@ const Head: FC = ({ categories, brands, products = [] }) => { width: 480, height: 480, }} + wishlist={wishlist} /> ))} @@ -66,4 +74,4 @@ const Head: FC = ({ categories, brands, products = [] }) => { ) } -export default Head +export default HomeAllProductsGrid diff --git a/components/common/Layout/Layout.tsx b/components/common/Layout/Layout.tsx index 204c3a871..f4376bbf3 100644 --- a/components/common/Layout/Layout.tsx +++ b/components/common/Layout/Layout.tsx @@ -41,10 +41,14 @@ const FeatureBar = dynamic( interface Props { pageProps: { pages?: Page[] + commerceFeatures: Record } } -const Layout: FC = ({ children, pageProps }) => { +const Layout: FC = ({ + children, + pageProps: { commerceFeatures, ...pageProps }, +}) => { const { displaySidebar, displayModal, @@ -54,11 +58,11 @@ const Layout: FC = ({ children, pageProps }) => { } = useUI() const { acceptedCookies, onAcceptCookies } = useAcceptCookies() const { locale = 'en-US' } = useRouter() - + const isWishlistEnabled = commerceFeatures.wishlist return (
- +
{children}
@@ -69,7 +73,7 @@ const Layout: FC = ({ children, pageProps }) => { - + ( +const Navbar: FC<{ wishlist?: boolean }> = ({ wishlist }) => (
@@ -33,7 +33,7 @@ const Navbar: FC = () => (
- +
diff --git a/components/common/UserNav/UserNav.tsx b/components/common/UserNav/UserNav.tsx index c615c18b1..5d9d58fff 100644 --- a/components/common/UserNav/UserNav.tsx +++ b/components/common/UserNav/UserNav.tsx @@ -12,11 +12,12 @@ import { Avatar } from '@components/common' interface Props { className?: string + wishlist?: boolean } const countItem = (count: number, item: LineItem) => count + item.quantity -const UserNav: FC = ({ className }) => { +const UserNav: FC = ({ className, wishlist = false }) => { const { data } = useCart() const { data: customer } = useCustomer() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() @@ -30,13 +31,15 @@ const UserNav: FC = ({ className }) => { {itemsCount > 0 && {itemsCount}} -
  • - - - - - -
  • + {wishlist && ( +
  • + + + + + +
  • + )}
  • {customer ? ( diff --git a/components/product/ProductCard/ProductCard.tsx b/components/product/ProductCard/ProductCard.tsx index a3bd73576..a9eaf8568 100644 --- a/components/product/ProductCard/ProductCard.tsx +++ b/components/product/ProductCard/ProductCard.tsx @@ -4,13 +4,14 @@ import Link from 'next/link' import type { Product } from '@commerce/types' import s from './ProductCard.module.css' import Image, { ImageProps } from 'next/image' -// import WishlistButton from '@components/wishlist/WishlistButton' +import WishlistButton from '@components/wishlist/WishlistButton' interface Props { className?: string product: Product variant?: 'slim' | 'simple' imgProps?: Omit + wishlist?: boolean } const placeholderImg = '/product-img-placeholder.svg' @@ -20,6 +21,7 @@ const ProductCard: FC = ({ product, variant, imgProps, + wishlist = false, ...props }) => ( @@ -57,11 +59,13 @@ const ProductCard: FC = ({ {product.price.currencyCode}
  • - {/* */} + /> + )}
    {product?.images && ( diff --git a/components/product/ProductSlider/ProductSlider.tsx b/components/product/ProductSlider/ProductSlider.tsx index 4ea7d2ec4..02244f5ba 100644 --- a/components/product/ProductSlider/ProductSlider.tsx +++ b/components/product/ProductSlider/ProductSlider.tsx @@ -50,10 +50,12 @@ const ProductSlider: FC = ({ children }) => { ) return () => { - sliderContainerRef.current!.removeEventListener( - 'touchstart', - preventNavigation - ) + if (sliderContainerRef.current) { + sliderContainerRef.current!.removeEventListener( + 'touchstart', + preventNavigation + ) + } } }, []) diff --git a/components/product/ProductView/ProductView.tsx b/components/product/ProductView/ProductView.tsx index 61beda7fe..c502303c4 100644 --- a/components/product/ProductView/ProductView.tsx +++ b/components/product/ProductView/ProductView.tsx @@ -13,15 +13,16 @@ import usePrice from '@framework/product/use-price' import { useAddItem } from '@framework/cart' import { getVariant, SelectedOptions } from '../helpers' -// import WishlistButton from '@components/wishlist/WishlistButton' +import WishlistButton from '@components/wishlist/WishlistButton' interface Props { className?: string children?: any product: Product + wishlist?: boolean } -const ProductView: FC = ({ product }) => { +const ProductView: FC = ({ product, wishlist = false }) => { const addItem = useAddItem() const { price } = usePrice({ amount: product.price.value, @@ -151,11 +152,13 @@ const ProductView: FC = ({ product }) => {
    - {/* */} + {wishlist && ( + + )} ) diff --git a/components/wishlist/WishlistButton/index.ts b/components/wishlist/WishlistButton/index.ts new file mode 100644 index 000000000..66e88074b --- /dev/null +++ b/components/wishlist/WishlistButton/index.ts @@ -0,0 +1 @@ +export { default } from './WishlistButton' diff --git a/components/wishlist/index.ts b/components/wishlist/index.ts index 470e6682c..8aee9f816 100644 --- a/components/wishlist/index.ts +++ b/components/wishlist/index.ts @@ -1 +1,2 @@ export { default as WishlistCard } from './WishlistCard' +export { default as WishlistButton } from './WishlistButton' diff --git a/framework/bigcommerce/config.json b/framework/bigcommerce/config.json new file mode 100644 index 000000000..17ef37e25 --- /dev/null +++ b/framework/bigcommerce/config.json @@ -0,0 +1,5 @@ +{ + "features": { + "wishlist": false + } +} diff --git a/framework/bigcommerce/product/use-search.tsx b/framework/bigcommerce/product/use-search.tsx index 393a8c0b9..0ff767d8a 100644 --- a/framework/bigcommerce/product/use-search.tsx +++ b/framework/bigcommerce/product/use-search.tsx @@ -1,5 +1,5 @@ import { HookHandler } from '@commerce/utils/types' -import useSearch, { UseSearch } from '@commerce/products/use-search' +import useSearch, { UseSearch } from '@commerce/product/use-search' import type { SearchProductsData } from '../api/catalog/products' import type { BigcommerceProvider } from '..' diff --git a/framework/commerce/config.json b/framework/commerce/config.json new file mode 100644 index 000000000..a0e7afc5d --- /dev/null +++ b/framework/commerce/config.json @@ -0,0 +1,5 @@ +{ + "features": { + "wishlist": true + } +} diff --git a/framework/commerce/products/use-search.tsx b/framework/commerce/product/use-search.tsx similarity index 100% rename from framework/commerce/products/use-search.tsx rename to framework/commerce/product/use-search.tsx diff --git a/framework/commerce/types.ts b/framework/commerce/types.ts index bf635c9dc..0ae766095 100644 --- a/framework/commerce/types.ts +++ b/framework/commerce/types.ts @@ -2,6 +2,10 @@ import type { Wishlist as BCWishlist } from '@framework/api/wishlist' import type { Customer as BCCustomer } from '@framework/api/customers' import type { SearchProductsData as BCSearchProductsData } from '@framework/api/catalog/products' +export type CommerceProviderConfig = { + features: Record +} + export type Discount = { // The value of the discount, can be an amount or percentage value: number diff --git a/framework/commerce/utils/features.ts b/framework/commerce/utils/features.ts new file mode 100644 index 000000000..d84321967 --- /dev/null +++ b/framework/commerce/utils/features.ts @@ -0,0 +1,37 @@ +import commerceProviderConfig from '@framework/config.json' +import type { CommerceProviderConfig } from '../types' +import memo from 'lodash.memoize' + +type FeaturesAPI = { + isEnabled: (desideredFeature: string) => boolean +} + +function isFeatureEnabled(config: CommerceProviderConfig) { + const features = config.features + return (desideredFeature: string) => + Object.keys(features) + .filter((k) => features[k]) + .includes(desideredFeature) +} + +function boostrap(): FeaturesAPI { + const basis = { + isEnabled: () => false, + } + + if (!commerceProviderConfig) { + console.log('No config.json found - Please add a config.json') + return basis + } + + if (commerceProviderConfig.features) { + return { + ...basis, + isEnabled: memo(isFeatureEnabled(commerceProviderConfig)), + } + } + + return basis +} + +export default boostrap() diff --git a/package.json b/package.json index 268b8d4a5..2d8e32772 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@reach/portal": "^0.11.2", + "@types/lodash.memoize": "^4.1.6", "@vercel/fetch": "^6.1.0", "body-scroll-lock": "^3.1.5", "bowser": "^2.11.0", @@ -32,6 +33,7 @@ "js-cookie": "^2.2.1", "keen-slider": "^5.2.4", "lodash.debounce": "^4.0.8", + "lodash.memoize": "^4.1.2", "lodash.random": "^3.2.0", "lodash.throttle": "^4.1.1", "next": "^10.0.7-canary.3", diff --git a/pages/_app.tsx b/pages/_app.tsx index 132ce5f18..dae0311b4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,12 +1,11 @@ import '@assets/main.css' -import 'keen-slider/keen-slider.min.css' import '@assets/chrome-bug.css' +import 'keen-slider/keen-slider.min.css' import { FC, useEffect } from 'react' import type { AppProps } from 'next/app' - -import { ManagedUIContext } from '@components/ui/context' import { Head } from '@components/common' +import { ManagedUIContext } from '@components/ui/context' const Noop: FC = ({ children }) => <>{children} diff --git a/pages/index.tsx b/pages/index.tsx index 811472674..acb1474be 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,13 +1,14 @@ import { Layout } from '@components/common' import { Grid, Marquee, Hero } from '@components/ui' import { ProductCard } from '@components/product' -import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' +// import HomeAllProductsGrid from '@components/common/HomeAllProductsGrid' import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' import { getConfig } from '@framework/api' import getAllProducts from '@framework/product/get-all-products' import getSiteInfo from '@framework/common/get-site-info' import getAllPages from '@framework/common/get-all-pages' +import Features from '@commerce/utils/features' export async function getStaticProps({ preview, @@ -23,6 +24,7 @@ export async function getStaticProps({ const { categories, brands } = await getSiteInfo({ config, preview }) const { pages } = await getAllPages({ config, preview }) + const isWishlistEnabled = Features.isEnabled('wishlist') return { props: { @@ -30,6 +32,9 @@ export async function getStaticProps({ categories, brands, pages, + commerceFeatures: { + wishlist: isWishlistEnabled, + }, }, revalidate: 14400, } @@ -39,6 +44,7 @@ export default function Home({ products, brands, categories, + commerceFeatures, }: InferGetStaticPropsType) { return ( <> @@ -51,6 +57,7 @@ export default function Home({ width: i === 0 ? 1080 : 540, height: i === 0 ? 1080 : 540, }} + wishlist={commerceFeatures.wishlist} /> ))} @@ -64,6 +71,7 @@ export default function Home({ width: 320, height: 320, }} + wishlist={commerceFeatures.wishlist} /> ))} @@ -86,6 +94,7 @@ export default function Home({ width: i === 0 ? 1080 : 540, height: i === 0 ? 1080 : 540, }} + wishlist={commerceFeatures.wishlist} /> ))} @@ -99,6 +108,7 @@ export default function Home({ width: 320, height: 320, }} + wishlist={commerceFeatures.wishlist} /> ))} diff --git a/pages/orders.tsx b/pages/orders.tsx index 08e32c2b2..db4ab55b2 100644 --- a/pages/orders.tsx +++ b/pages/orders.tsx @@ -1,9 +1,9 @@ import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' -import getAllPages from '@framework/common/get-all-pages' +import { Bag } from '@components/icons' import { Layout } from '@components/common' import { Container, Text } from '@components/ui' -import { Bag } from '@components/icons' +import { getConfig } from '@framework/api' +import getAllPages from '@framework/common/get-all-pages' export async function getStaticProps({ preview, diff --git a/pages/product/[slug].tsx b/pages/product/[slug].tsx index 83aeaa54c..a705c001b 100644 --- a/pages/product/[slug].tsx +++ b/pages/product/[slug].tsx @@ -11,14 +11,15 @@ import { getConfig } from '@framework/api' import getProduct from '@framework/product/get-product' import getAllPages from '@framework/common/get-all-pages' import getAllProductPaths from '@framework/product/get-all-product-paths' +import Features from '@commerce/utils/features' export async function getStaticProps({ params, locale, preview, }: GetStaticPropsContext<{ slug: string }>) { + const isWishlistEnabled = Features.isEnabled('wishlist') const config = getConfig({ locale }) - const { pages } = await getAllPages({ config, preview }) const { product } = await getProduct({ variables: { slug: params!.slug }, @@ -31,7 +32,13 @@ export async function getStaticProps({ } return { - props: { pages, product }, + props: { + pages, + product, + commerceFeatures: { + wishlist: isWishlistEnabled, + }, + }, revalidate: 200, } } @@ -55,13 +62,17 @@ export async function getStaticPaths({ locales }: GetStaticPathsContext) { export default function Slug({ product, + commerceFeatures, }: InferGetStaticPropsType) { const router = useRouter() return router.isFallback ? (

    Loading...

    // TODO (BC) Add Skeleton Views ) : ( - + ) } diff --git a/pages/search.tsx b/pages/search.tsx index 821b5a9f5..c9958a9f8 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -26,6 +26,8 @@ const SORT = Object.entries({ 'price-desc': 'Price: High to low', }) +import Features from '@commerce/utils/features' + import { filterQuery, getCategoryPath, @@ -40,14 +42,23 @@ export async function getStaticProps({ const config = getConfig({ locale }) const { pages } = await getAllPages({ config, preview }) const { categories, brands } = await getSiteInfo({ config, preview }) + const isWishlistEnabled = Features.isEnabled('wishlist') return { - props: { pages, categories, brands }, + props: { + pages, + categories, + brands, + commerceFeatures: { + wishlist: isWishlistEnabled, + }, + }, } } export default function Search({ categories, brands, + commerceFeatures: { wishlist }, }: InferGetStaticPropsType) { const [activeFilter, setActiveFilter] = useState('') const [toggleFilter, setToggleFilter] = useState(false) @@ -337,7 +348,7 @@ export default function Search({ {data ? ( - {data.products.map((product) => ( + {data.products.map((product: Product) => ( ))} diff --git a/pages/wishlist.tsx b/pages/wishlist.tsx index 6de798411..ca11152f4 100644 --- a/pages/wishlist.tsx +++ b/pages/wishlist.tsx @@ -1,28 +1,43 @@ +import { useEffect } from 'react' +import { useRouter } from 'next/router' import type { GetStaticPropsContext } from 'next' -import { getConfig } from '@framework/api' -import getAllPages from '@framework/common/get-all-pages' -import useWishlist from '@framework/wishlist/use-wishlist' -import { Layout } from '@components/common' + import { Heart } from '@components/icons' +import { Layout } from '@components/common' import { Text, Container } from '@components/ui' -import { WishlistCard } from '@components/wishlist' import { defaultPageProps } from '@lib/defaults' +import { getConfig } from '@framework/api' import { useCustomer } from '@framework/customer' +import { WishlistCard } from '@components/wishlist' +import useWishlist from '@framework/wishlist/use-wishlist' +import getAllPages from '@framework/common/get-all-pages' +import Features from '@commerce/utils/features' export async function getStaticProps({ preview, locale, }: GetStaticPropsContext) { + // Disabling page if Feature is not available + if (Features.isEnabled('wishlist')) { + return { + notFound: true, + } + } + const config = getConfig({ locale }) const { pages } = await getAllPages({ config, preview }) return { - props: { ...defaultPageProps, pages }, + props: { + pages, + ...defaultPageProps, + }, } } export default function Wishlist() { const { data: customer } = useCustomer() const { data, isLoading, isEmpty } = useWishlist() + const router = useRouter() return ( diff --git a/tsconfig.json b/tsconfig.json index 67de1ee36..43dfd2a27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,6 @@ "@framework": ["framework/bigcommerce"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], - "exclude": ["node_modules", "components/wishlist"] + "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], + "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index a3e80eb6c..e7cd08438 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1060,6 +1060,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.memoize@^4.1.6": + version "4.1.6" + resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23" + integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ== + dependencies: + "@types/lodash" "*" + "@types/lodash.random@^3.2.6": version "3.2.6" resolved "https://registry.yarnpkg.com/@types/lodash.random/-/lodash.random-3.2.6.tgz#64b08abad168dca39c778ed40cce75b2f9e168eb" @@ -4232,6 +4239,11 @@ lodash.isstring@^4.0.1: resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"