Monorepo with Turborepo (#651)

* Moved everything

* Figuring out how to make imports work

* Updated exports

* Added missing exports

* Added @vercel/commerce-local to `site`

* Updated commerce config

* Updated exports and commerce config

* Updated commerce hoc

* Fixed exports in local

* Added publish config

* Updated imports in site

* It's actually working

* Don't use debugger in dev for better speeds

* Improved DX when editing packages

* Set up eslint with husky

* Updated prettier config

* Added prettier setup to every package

* Moved bigcommerce

* Moved Bigcommerce to src and package updates

* Updated setup of bigcommerce

* Moved definitions script

* Moved commercejs

* Move to src

* Fixed types in commercejs

* Moved kibocommerce

* Moved kibocommerce to src

* Added package/tsconfig to kibocommerce

* Fixed imports and other things

* Moved ordercloud

* Moved ordercloud to src

* Fixed imports

* Added missing prettier files

* Moved Saleor

* Moved Saleor to src

* Fixed imports

* Replaced all imports to @commerce

* Added prettierignore/rc to all providers

* Moved shopify to src

* Build shopify in packages

* Moved Spree

* Moved spree to src

* Updated spree

* Moved swell

* Moved swell to src

* Fixed type imports in swell

* Moved Vendure to packages

* Moved vendure to src

* Fixed imports in vendure

* Added codegen to saleor

* Updated codegen setup for shopify

* Added codegen to vendure

* Added codegen to kibocommerce

* Added all packages to site's deps

* Updated codegen setup in bigcommerce

* Minor fixes

* Updated providers' names in site

* Updated packages based on Bel's changes

* Updated turbo to latest

* Fixed ts complains

* Set npm engine in root

* New lockfile install

* remove engines

* Regen lockfile

* Switched from npm to yarn

* Updated typesVersions in all packages

* Moved dep

* Updated SWR to the just released 1.2.0

* Removed "isolatedModules" from packages

* Updated list of providers and default

* Updated swell declaration

* Removed next import from kibocommerce

* Added COMMERCE_PROVIDER log

* Added another log

* Updated turbo config

* Updated docs

* Removed test logs

Co-authored-by: Jared Palmer <jared@jaredpalmer.com>
This commit is contained in:
Luis Alvarez D
2022-02-01 14:14:05 -05:00
committed by GitHub
parent d0ef346189
commit 0afe686fe9
1326 changed files with 9109 additions and 19494 deletions

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1,58 @@
import { NextApiHandler } from 'next'
import { CommerceAPI, createEndpoint, GetAPISchema } from '@vercel/commerce/api'
import { CheckoutSchema } from '@vercel/commerce/types/checkout'
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req,
res,
config,
}) => {
try {
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout</title>
</head>
<body>
<div style='margin: 10rem auto; text-align: center; font-family: SansSerif, "Segoe UI", Helvetica; color: #888;'>
<svg xmlns="http://www.w3.org/2000/svg" style='height: 60px; width: 60px;' fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<h1>Checkout not yet implemented :(</h1>
<p>
See <a href='https://github.com/vercel/commerce/issues/64' target='_blank'>#64</a>
</p>
</div>
</body>
</html>
`
res.status(200)
res.setHeader('Content-Type', 'text/html')
res.write(html)
res.end()
} catch (error) {
console.error(error)
const message = 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export type CheckoutAPI = GetAPISchema<CommerceAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = { getCheckout }
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -0,0 +1,56 @@
import type { CommerceAPIConfig } from '@vercel/commerce/api'
import {
CommerceAPI,
getCommerceApi as commerceApi,
} from '@vercel/commerce/api'
import fetchGraphqlApi from './utils/fetch-graphql-api'
import login from './operations/login'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getCustomerWishlist from './operations/get-customer-wishlist'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
export interface VendureConfig extends CommerceAPIConfig {}
const API_URL = process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
if (!API_URL) {
throw new Error(
`The environment variable NEXT_PUBLIC_VENDURE_SHOP_API_URL is missing and it's required to access your store`
)
}
const ONE_DAY = 60 * 60 * 24
const config: VendureConfig = {
commerceUrl: API_URL,
apiToken: '',
cartCookie: '',
customerCookie: '',
cartCookieMaxAge: ONE_DAY * 30,
fetch: fetchGraphqlApi,
}
const operations = {
login,
getAllPages,
getPage,
getSiteInfo,
getCustomerWishlist,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): CommerceAPI<P> {
return commerceApi(customProvider)
}

View File

@@ -0,0 +1,40 @@
import { VendureConfig } from '../'
import { OperationContext } from '@vercel/commerce/api/operations'
import { Provider } from '../'
export type Page = any
export type GetAllPagesResult<T extends { pages: any[] } = { pages: Page[] }> =
T
export default function getAllPagesOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllPages(opts?: {
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetAllPagesResult>
async function getAllPages<T extends { pages: any[] }>(opts: {
url: string
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetAllPagesResult<T>>
async function getAllPages({
config: cfg,
preview,
}: {
url?: string
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<GetAllPagesResult> {
const config = commerce.getConfig(cfg)
return {
pages: [],
}
}
return getAllPages
}

View File

@@ -0,0 +1,55 @@
import {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import type { GetAllProductPathsQuery } from '../../../schema'
import { Provider } from '../index'
import { getAllProductPathsQuery } from '../../utils/queries/get-all-product-paths-query'
import { GetAllProductPathsOperation } from '@vercel/commerce/types/product'
import { VendureConfig } from '../'
export type GetAllProductPathsResult = {
products: Array<{ node: { path: string } }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<
T extends GetAllProductPathsOperation
>(opts?: {
variables?: T['variables']
config?: VendureConfig
}): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
opts: {
variables?: T['variables']
config?: VendureConfig
} & OperationOptions
): Promise<T['data']>
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
query = getAllProductPathsQuery,
variables,
config: cfg,
}: {
query?: string
variables?: T['variables']
config?: VendureConfig
} = {}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<GetAllProductPathsQuery>(query, {
variables,
})
const products = data.products.items
return {
products: products.map((p) => ({ path: `/${p.slug}` })),
}
}
return getAllProductPaths
}

View File

@@ -0,0 +1,46 @@
import { Product } from '@vercel/commerce/types/product'
import { Provider, VendureConfig } from '../'
import { GetAllProductsQuery } from '../../../schema'
import { normalizeSearchResult } from '../../utils/normalize'
import { getAllProductsQuery } from '../../utils/queries/get-all-products-query'
import { OperationContext } from '@vercel/commerce/api/operations'
export type ProductVariables = { first?: number }
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts(opts?: {
variables?: ProductVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<{ products: Product[] }>
async function getAllProducts({
query = getAllProductsQuery,
variables: { ...vars } = {},
config: cfg,
}: {
query?: string
variables?: ProductVariables
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<{ products: Product[] | any[] }> {
const config = commerce.getConfig(cfg)
const variables = {
input: {
take: vars.first,
groupByProduct: true,
},
}
const { data } = await config.fetch<GetAllProductsQuery>(query, {
variables,
})
return {
products: data.search.items.map((item) => normalizeSearchResult(item)),
}
}
return getAllProducts
}

View File

@@ -0,0 +1,23 @@
import { OperationContext } from '@vercel/commerce/api/operations'
import { Provider, VendureConfig } from '../'
export default function getCustomerWishlistOperation({
commerce,
}: OperationContext<Provider>) {
async function getCustomerWishlist({
config: cfg,
variables,
includeProducts,
}: {
url?: string
variables: any
config?: Partial<VendureConfig>
includeProducts?: boolean
}): Promise<any> {
// Not implemented as Vendure does not ship with wishlist functionality at present
const config = commerce.getConfig(cfg)
return { wishlist: {} }
}
return getCustomerWishlist
}

View File

@@ -0,0 +1,45 @@
import { VendureConfig, Provider } from '../'
import { OperationContext } from '@vercel/commerce/api/operations'
export type Page = any
export type GetPageResult<T extends { page?: any } = { page?: Page }> = T
export type PageVariables = {
id: number
}
export default function getPageOperation({
commerce,
}: OperationContext<Provider>) {
async function getPage(opts: {
url?: string
variables: PageVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetPageResult>
async function getPage<T extends { page?: any }, V = any>(opts: {
url: string
variables: V
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetPageResult<T>>
async function getPage({
url,
variables,
config: cfg,
preview,
}: {
url?: string
variables: PageVariables
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<GetPageResult> {
const config = commerce.getConfig(cfg)
return {}
}
return getPage
}

View File

@@ -0,0 +1,69 @@
import { Product } from '@vercel/commerce/types/product'
import { OperationContext } from '@vercel/commerce/api/operations'
import { Provider, VendureConfig } from '../'
import { GetProductQuery } from '../../../schema'
import { getProductQuery } from '../../utils/queries/get-product-query'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct({
query = getProductQuery,
variables,
config: cfg,
}: {
query?: string
variables: { slug: string }
config?: Partial<VendureConfig>
preview?: boolean
}): Promise<Product | {} | any> {
const config = commerce.getConfig(cfg)
const locale = config.locale
const { data } = await config.fetch<GetProductQuery>(query, { variables })
const product = data.product
if (product) {
const getOptionGroupName = (id: string): string => {
return product.optionGroups.find((og) => og.id === id)!.name
}
return {
product: {
id: product.id,
name: product.name,
description: product.description,
slug: product.slug,
images: product.assets.map((a) => ({
url: a.preview,
alt: a.name,
})),
variants: product.variants.map((v) => ({
id: v.id,
options: v.options.map((o) => ({
// This __typename property is required in order for the correct
// variant selection to work, see `components/product/helpers.ts`
// `getVariant()` function.
__typename: 'MultipleChoiceOption',
id: o.id,
displayName: getOptionGroupName(o.groupId),
values: [{ label: o.name }],
})),
})),
price: {
value: product.variants[0].priceWithTax / 100,
currencyCode: product.variants[0].currencyCode,
},
options: product.optionGroups.map((og) => ({
id: og.id,
displayName: og.name,
values: og.options.map((o) => ({ label: o.name })),
})),
} as Product,
}
}
return {}
}
return getProduct
}

View File

@@ -0,0 +1,50 @@
import { Provider, VendureConfig } from '../'
import { GetCollectionsQuery } from '../../../schema'
import { arrayToTree } from '../../utils/array-to-tree'
import { getCollectionsQuery } from '../../utils/queries/get-collections-query'
import { OperationContext } from '@vercel/commerce/api/operations'
import { Category } from '@vercel/commerce/types/site'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo({
query = getCollectionsQuery,
variables,
config: cfg,
}: {
query?: string
variables?: any
config?: Partial<VendureConfig>
preview?: boolean
} = {}): Promise<GetSiteInfoResult> {
const config = commerce.getConfig(cfg)
// RecursivePartial forces the method to check for every prop in the data, which is
// required in case there's a custom `query`
const { data } = await config.fetch<GetCollectionsQuery>(query, {
variables,
})
const collections = data.collections?.items.map((i) => ({
...i,
entityId: i.id,
path: i.slug,
productCount: i.productVariants.totalItems,
}))
const categories = arrayToTree(collections).children
const brands = [] as any[]
return {
categories: categories ?? [],
brands,
}
}
return getSiteInfo
}

View File

@@ -0,0 +1,60 @@
import type { ServerResponse } from 'http'
import type {
OperationContext,
OperationOptions,
} from '@vercel/commerce/api/operations'
import { ValidationError } from '@vercel/commerce/utils/errors'
import type { LoginOperation } from '../../types/login'
import type { LoginMutation } from '../../../schema'
import { Provider, VendureConfig } from '..'
import { loginMutation } from '../../utils/mutations/log-in-mutation'
export default function loginOperation({
commerce,
}: OperationContext<Provider>) {
async function login<T extends LoginOperation>(opts: {
variables: T['variables']
config?: Partial<VendureConfig>
res: ServerResponse
}): Promise<T['data']>
async function login<T extends LoginOperation>(
opts: {
variables: T['variables']
config?: Partial<VendureConfig>
res: ServerResponse
} & OperationOptions
): Promise<T['data']>
async function login<T extends LoginOperation>({
query = loginMutation,
variables,
res: response,
config: cfg,
}: {
query?: string
variables: T['variables']
res: ServerResponse
config?: Partial<VendureConfig>
}): Promise<T['data']> {
const config = commerce.getConfig(cfg)
const { data, res } = await config.fetch<LoginMutation>(query, {
variables,
})
switch (data.login.__typename) {
case 'NativeAuthStrategyError':
case 'InvalidCredentialsError':
case 'NotVerifiedError':
throw new ValidationError({
code: data.login.errorCode,
message: data.login.message,
})
}
return {
result: data.login.id,
}
}
return login
}

View File

@@ -0,0 +1,36 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
import type { GraphQLFetcher } from '@vercel/commerce/api'
import { getCommerceApi } from '../'
import fetch from './fetch'
const fetchGraphqlApi: GraphQLFetcher = async (
query: string,
{ variables, preview } = {},
fetchOptions
) => {
const config = getCommerceApi().getConfig()
const res = await fetch(config.commerceUrl, {
...fetchOptions,
method: 'POST',
headers: {
...fetchOptions?.headers,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
const json = await res.json()
if (json.errors) {
throw new FetcherError({
errors: json.errors ?? [{ message: 'Failed to fetch Vendure API' }],
status: res.status,
})
}
return { data: json.data, res }
}
export default fetchGraphqlApi

View File

@@ -0,0 +1,3 @@
import zeitFetch from '@vercel/fetch'
export default zeitFetch()

View File

@@ -0,0 +1,3 @@
export { default as useLogin } from './use-login'
export { default as useLogout } from './use-logout'
export { default as useSignup } from './use-signup'

View File

@@ -0,0 +1,53 @@
import { useCallback } from 'react'
import { MutationHook } from '@vercel/commerce/utils/types'
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
import { LoginHook } from '../types/login'
import { CommerceError, ValidationError } from '@vercel/commerce/utils/errors'
import useCustomer from '../customer/use-customer'
import { LoginMutation, LoginMutationVariables } from '../../schema'
import { loginMutation } from '../utils/mutations/log-in-mutation'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
query: loginMutation,
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message: 'A email and password are required to login',
})
}
const variables: LoginMutationVariables = {
username: email,
password,
}
const { login } = await fetch<LoginMutation>({
...options,
variables,
})
if (login.__typename !== 'CurrentUser') {
throw new ValidationError(login)
}
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function login(input) {
const data = await fetch({ input })
await mutate()
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,35 @@
import { useCallback } from 'react'
import { MutationHook } from '@vercel/commerce/utils/types'
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
import useCustomer from '../customer/use-customer'
import { LogoutMutation } from '../../schema'
import { logoutMutation } from '../utils/mutations/log-out-mutation'
import { LogoutHook } from '../types/logout'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
query: logoutMutation,
},
async fetcher({ options, fetch }) {
await fetch<LogoutMutation>({
...options,
})
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function logout() {
const data = await fetch()
await mutate(null, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,71 @@
import { useCallback } from 'react'
import { MutationHook } from '@vercel/commerce/utils/types'
import { CommerceError, ValidationError } from '@vercel/commerce/utils/errors'
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
import useCustomer from '../customer/use-customer'
import {
RegisterCustomerInput,
SignupMutation,
SignupMutationVariables,
} from '../../schema'
import { signupMutation } from '../utils/mutations/sign-up-mutation'
import { SignupHook } from '../types/signup'
export default useSignup as UseSignup<typeof handler>
export type SignupInput = {
email: string
firstName: string
lastName: string
password: string
}
export const handler: MutationHook<SignupHook> = {
fetchOptions: {
query: signupMutation,
},
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
const variables: SignupMutationVariables = {
input: {
firstName,
lastName,
emailAddress: email,
password,
},
}
const { registerCustomerAccount } = await fetch<SignupMutation>({
...options,
variables,
})
if (registerCustomerAccount.__typename !== 'Success') {
throw new ValidationError(registerCustomerAccount)
}
return null
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await mutate()
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,4 @@
export { default as useCart } from './use-cart'
export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item'
export { default as useUpdateItem } from './use-update-item'

View File

@@ -0,0 +1,54 @@
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
import { CommerceError } from '@vercel/commerce/utils/errors'
import { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useCart from './use-cart'
import { AddItemToOrderMutation } from '../../schema'
import { normalizeCart } from '../utils/normalize'
import { addItemToOrderMutation } from '../utils/mutations/add-item-to-order-mutation'
import { AddItemHook } from '../types/cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: addItemToOrderMutation,
},
async fetcher({ input, options, fetch }) {
if (
input.quantity &&
(!Number.isInteger(input.quantity) || input.quantity! < 1)
) {
throw new CommerceError({
message: 'The item quantity has to be a valid integer greater than 0',
})
}
const { addItemToOrder } = await fetch<AddItemToOrderMutation>({
...options,
variables: {
quantity: input.quantity || 1,
variantId: input.variantId,
},
})
if (addItemToOrder.__typename === 'Order') {
return normalizeCart(addItemToOrder)
}
throw new CommerceError(addItemToOrder)
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,46 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
import { ActiveOrderQuery, CartFragment } from '../../schema'
import { normalizeCart } from '../utils/normalize'
import { useMemo } from 'react'
import { getCartQuery } from '../utils/queries/get-cart-query'
import { GetCartHook } from '../types/cart'
export type CartResult = {
activeOrder?: CartFragment
addItemToOrder?: CartFragment
adjustOrderLine?: CartFragment
removeOrderLine?: CartFragment
}
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
query: getCartQuery,
},
async fetcher({ input: { cartId }, options, fetch }) {
const { activeOrder } = await fetch<ActiveOrderQuery>(options)
return activeOrder ? normalizeCart(activeOrder) : null
},
useHook:
({ useData }) =>
(input) => {
const response = useData({
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
})
return useMemo(
() =>
Object.create(response, {
isEmpty: {
get() {
return (response.data?.lineItems.length ?? 0) <= 0
},
enumerable: true,
},
}),
[response]
)
},
}

View File

@@ -0,0 +1,56 @@
import { useCallback } from 'react'
import {
HookFetcherContext,
MutationHook,
MutationHookContext,
SWRHook,
} from '@vercel/commerce/utils/types'
import useRemoveItem, {
UseRemoveItem,
} from '@vercel/commerce/cart/use-remove-item'
import { CommerceError } from '@vercel/commerce/utils/errors'
import { Cart } from '@vercel/commerce/types/cart'
import useCart from './use-cart'
import {
RemoveOrderLineMutation,
RemoveOrderLineMutationVariables,
} from '../../schema'
import { normalizeCart } from '../utils/normalize'
import { RemoveItemHook } from '../types/cart'
import { removeOrderLineMutation } from '../utils/mutations/remove-order-line-mutation'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: {
query: removeOrderLineMutation,
},
async fetcher({ input, options, fetch }) {
const variables: RemoveOrderLineMutationVariables = {
orderLineId: input.itemId,
}
const { removeOrderLine } = await fetch<RemoveOrderLineMutation>({
...options,
variables,
})
if (removeOrderLine.__typename === 'Order') {
return normalizeCart(removeOrderLine)
}
throw new CommerceError(removeOrderLine)
},
useHook:
({ fetch }) =>
() => {
const { mutate } = useCart()
return useCallback(
async function removeItem(input) {
const data = await fetch({ input: { itemId: input.id } })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,84 @@
import { useCallback } from 'react'
import {
HookFetcherContext,
MutationHook,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import { CommerceError, ValidationError } from '@vercel/commerce/utils/errors'
import useUpdateItem, {
UseUpdateItem,
} from '@vercel/commerce/cart/use-update-item'
import { CartItemBody, LineItem } from '@vercel/commerce/types/cart'
import useCart from './use-cart'
import {
AdjustOrderLineMutation,
AdjustOrderLineMutationVariables,
} from '../../schema'
import { normalizeCart } from '../utils/normalize'
import { adjustOrderLineMutation } from '../utils/mutations/adjust-order-line-mutation'
import { UpdateItemHook } from '../types/cart'
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<typeof handler>
export const handler = {
fetchOptions: {
query: adjustOrderLineMutation,
},
async fetcher(context: HookFetcherContext<UpdateItemHook>) {
const { input, options, fetch } = context
const variables: AdjustOrderLineMutationVariables = {
quantity: input.item.quantity || 1,
orderLineId: input.itemId,
}
const { adjustOrderLine } = await fetch<AdjustOrderLineMutation>({
...options,
variables,
})
if (adjustOrderLine.__typename === 'Order') {
return normalizeCart(adjustOrderLine)
}
throw new CommerceError(adjustOrderLine)
},
useHook:
({ fetch }: MutationHookContext<UpdateItemHook>) =>
(
ctx: {
item?: LineItem
wait?: number
} = {}
) => {
const { item } = ctx
const { mutate } = useCart()
return useCallback(
async function addItem(input: UpdateItemActionInput) {
const itemId = item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
item: {
productId,
variantId,
quantity: input.quantity,
},
itemId,
},
})
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@@ -0,0 +1,16 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useCheckout, {
UseCheckout,
} from '@vercel/commerce/checkout/use-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({}),
}

View File

@@ -0,0 +1,6 @@
{
"provider": "vendure",
"features": {
"wishlist": false
}
}

View File

@@ -0,0 +1,17 @@
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/address/use-add-item'
import { MutationHook } from '@vercel/commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@@ -0,0 +1,17 @@
import useAddItem, {
UseAddItem,
} from '@vercel/commerce/customer/card/use-add-item'
import { MutationHook } from '@vercel/commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@@ -0,0 +1 @@
export { default as useCustomer } from './use-customer'

View File

@@ -0,0 +1,37 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useCustomer, {
UseCustomer,
} from '@vercel/commerce/customer/use-customer'
import { ActiveCustomerQuery } from '../../schema'
import { activeCustomerQuery } from '../utils/queries/active-customer-query'
import { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
query: activeCustomerQuery,
},
async fetcher({ options, fetch }) {
const { activeCustomer } = await fetch<ActiveCustomerQuery>({
...options,
})
return activeCustomer
? ({
firstName: activeCustomer.firstName ?? '',
lastName: activeCustomer.lastName ?? '',
email: activeCustomer.emailAddress ?? '',
} as any)
: null
},
useHook:
({ useData }) =>
(input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@@ -0,0 +1,53 @@
import { Fetcher } from '@vercel/commerce/utils/types'
import { FetcherError } from '@vercel/commerce/utils/errors'
async function getText(res: Response) {
try {
return (await res.text()) || res.statusText
} catch (error) {
return res.statusText
}
}
async function getError(res: Response) {
if (res.headers.get('Content-Type')?.includes('application/json')) {
const data = await res.json()
return new FetcherError({ errors: data.errors, status: res.status })
}
return new FetcherError({ message: await getText(res), status: res.status })
}
export const fetcher: Fetcher = async ({
url,
method = 'POST',
variables,
query,
body: bodyObj,
}) => {
const shopApiUrl =
process.env.NEXT_PUBLIC_VENDURE_LOCAL_URL ||
process.env.NEXT_PUBLIC_VENDURE_SHOP_API_URL
if (!shopApiUrl) {
throw new Error(
'The Vendure Shop API url has not been provided. Please define NEXT_PUBLIC_VENDURE_SHOP_API_URL in .env.local'
)
}
const hasBody = Boolean(variables || query)
const body = hasBody ? JSON.stringify({ query, variables }) : undefined
const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
const res = await fetch(shopApiUrl, {
method,
body,
headers,
credentials: 'include',
})
if (res.ok) {
const { data, errors } = await res.json()
if (errors) {
throw await new FetcherError({ status: res.status, errors })
}
return data
}
throw await getError(res)
}

View File

@@ -0,0 +1,12 @@
import {
getCommerceProvider,
useCommerce as useCoreCommerce,
} from '@vercel/commerce'
import { vendureProvider, VendureProvider } from './provider'
export { vendureProvider }
export type { VendureProvider }
export const CommerceProvider = getCommerceProvider(vendureProvider)
export const useCommerce = () => useCoreCommerce()

View File

@@ -0,0 +1,8 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['localhost', 'demo.vendure.io'],
},
}

View File

@@ -0,0 +1,2 @@
export { default as usePrice } from './use-price'
export { default as useSearch } from './use-search'

View File

@@ -0,0 +1,2 @@
export * from '@vercel/commerce/product/use-price'
export { default } from '@vercel/commerce/product/use-price'

View File

@@ -0,0 +1,64 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
import { Product } from '@vercel/commerce/types/product'
import { SearchQuery, SearchQueryVariables } from '../../schema'
import { normalizeSearchResult } from '../utils/normalize'
import { searchQuery } from '../utils/queries/search-query'
import { SearchProductsHook } from '../types/product'
export default useSearch as UseSearch<typeof handler>
export type SearchProductsInput = {
search?: string
categoryId?: string
brandId?: string
sort?: string
}
export type SearchProductsData = {
products: Product[]
found: boolean
}
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
query: searchQuery,
},
async fetcher({ input, options, fetch }) {
const { categoryId, brandId } = input
const variables: SearchQueryVariables = {
input: {
term: input.search,
collectionId: input.categoryId?.toString(),
groupByProduct: true,
// TODO: what is the "sort" value?
},
}
const { search } = await fetch<SearchQuery>({
query: searchQuery,
variables,
})
return {
found: search.totalItems > 0,
products: search.items.map((item) => normalizeSearchResult(item)) ?? [],
}
},
useHook:
({ useData }) =>
(input = {}) => {
return useData({
input: [
['search', input.search],
['categoryId', input.categoryId],
['brandId', input.brandId],
['sort', input.sort],
],
swrOptions: {
revalidateOnFocus: false,
...input.swrOptions,
},
})
},
}

View File

@@ -0,0 +1,22 @@
import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item'
import { handler as useUpdateItem } from './cart/use-update-item'
import { handler as useRemoveItem } from './cart/use-remove-item'
import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup'
import { fetcher } from './fetcher'
export const vendureProvider = {
locale: 'en-us',
cartCookie: 'session',
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type VendureProvider = typeof vendureProvider

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/cart'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/checkout'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/common'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/customer'

View File

@@ -0,0 +1,25 @@
import * as Cart from './cart'
import * as Checkout from './checkout'
import * as Common from './common'
import * as Customer from './customer'
import * as Login from './login'
import * as Logout from './logout'
import * as Page from './page'
import * as Product from './product'
import * as Signup from './signup'
import * as Site from './site'
import * as Wishlist from './wishlist'
export type {
Cart,
Checkout,
Common,
Customer,
Login,
Logout,
Page,
Product,
Signup,
Site,
Wishlist,
}

View File

@@ -0,0 +1,12 @@
import * as Core from '@vercel/commerce/types/login'
import type { LoginMutationVariables } from '../../schema'
import { LoginBody, LoginTypes } from '@vercel/commerce/types/login'
export * from '@vercel/commerce/types/login'
export type LoginHook<T extends LoginTypes = LoginTypes> = {
data: null
actionInput: LoginBody
fetcherInput: LoginBody
body: T['body']
}

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/logout'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/page'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/product'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/signup'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/site'

View File

@@ -0,0 +1 @@
export * from '@vercel/commerce/types/wishlist'

View File

@@ -0,0 +1,67 @@
export type HasParent = { id: string; parent?: { id: string } | null }
export type TreeNode<T extends HasParent> = T & {
children: Array<TreeNode<T>>
expanded: boolean
}
export type RootNode<T extends HasParent> = {
id?: string
children: Array<TreeNode<T>>
}
export function arrayToTree<T extends HasParent>(
nodes: T[],
currentState?: RootNode<T>
): RootNode<T> {
const topLevelNodes: Array<TreeNode<T>> = []
const mappedArr: { [id: string]: TreeNode<T> } = {}
const currentStateMap = treeToMap(currentState)
// First map the nodes of the array to an object -> create a hash table.
for (const node of nodes) {
mappedArr[node.id] = { ...(node as any), children: [] }
}
for (const id of nodes.map((n) => n.id)) {
if (mappedArr.hasOwnProperty(id)) {
const mappedElem = mappedArr[id]
mappedElem.expanded = currentStateMap.get(id)?.expanded ?? false
const parent = mappedElem.parent
if (!parent) {
continue
}
// If the element is not at the root level, add it to its parent array of children.
const parentIsRoot = !mappedArr[parent.id]
if (!parentIsRoot) {
if (mappedArr[parent.id]) {
mappedArr[parent.id].children.push(mappedElem)
} else {
mappedArr[parent.id] = { children: [mappedElem] } as any
}
} else {
topLevelNodes.push(mappedElem)
}
}
}
// tslint:disable-next-line:no-non-null-assertion
const rootId = topLevelNodes.length ? topLevelNodes[0].parent!.id : undefined
return { id: rootId, children: topLevelNodes }
}
/**
* Converts an existing tree (as generated by the arrayToTree function) into a flat
* Map. This is used to persist certain states (e.g. `expanded`) when re-building the
* tree.
*/
function treeToMap<T extends HasParent>(
tree?: RootNode<T>
): Map<string, TreeNode<T>> {
const nodeMap = new Map<string, TreeNode<T>>()
function visit(node: TreeNode<T>) {
nodeMap.set(node.id, node)
node.children.forEach(visit)
}
if (tree) {
visit(tree as TreeNode<T>)
}
return nodeMap
}

View File

@@ -0,0 +1,44 @@
export const cartFragment = /* GraphQL */ `
fragment Cart on Order {
id
code
createdAt
totalQuantity
subTotal
subTotalWithTax
total
totalWithTax
currencyCode
customer {
id
}
lines {
id
quantity
linePriceWithTax
discountedLinePriceWithTax
unitPriceWithTax
discountedUnitPriceWithTax
featuredAsset {
id
preview
}
discounts {
description
amount
}
productVariant {
id
name
sku
price
priceWithTax
stockLevel
product {
slug
}
productId
}
}
}
`

View File

@@ -0,0 +1,23 @@
export const searchResultFragment = /* GraphQL */ `
fragment SearchResult on SearchResult {
productId
productName
description
slug
sku
currencyCode
productAsset {
id
preview
}
priceWithTax {
... on SinglePrice {
value
}
... on PriceRange {
min
max
}
}
}
`

View File

@@ -0,0 +1,15 @@
import { cartFragment } from '../fragments/cart-fragment'
export const addItemToOrderMutation = /* GraphQL */ `
mutation addItemToOrder($variantId: ID!, $quantity: Int!) {
addItemToOrder(productVariantId: $variantId, quantity: $quantity) {
__typename
...Cart
... on ErrorResult {
errorCode
message
}
}
}
${cartFragment}
`

View File

@@ -0,0 +1,15 @@
import { cartFragment } from '../fragments/cart-fragment'
export const adjustOrderLineMutation = /* GraphQL */ `
mutation adjustOrderLine($orderLineId: ID!, $quantity: Int!) {
adjustOrderLine(orderLineId: $orderLineId, quantity: $quantity) {
__typename
...Cart
... on ErrorResult {
errorCode
message
}
}
}
${cartFragment}
`

View File

@@ -0,0 +1,14 @@
export const loginMutation = /* GraphQL */ `
mutation login($username: String!, $password: String!) {
login(username: $username, password: $password) {
__typename
... on CurrentUser {
id
}
... on ErrorResult {
errorCode
message
}
}
}
`

View File

@@ -0,0 +1,7 @@
export const logoutMutation = /* GraphQL */ `
mutation logout {
logout {
success
}
}
`

View File

@@ -0,0 +1,15 @@
import { cartFragment } from '../fragments/cart-fragment'
export const removeOrderLineMutation = /* GraphQL */ `
mutation removeOrderLine($orderLineId: ID!) {
removeOrderLine(orderLineId: $orderLineId) {
__typename
...Cart
... on ErrorResult {
errorCode
message
}
}
}
${cartFragment}
`

View File

@@ -0,0 +1,14 @@
export const signupMutation = /* GraphQL */ `
mutation signup($input: RegisterCustomerInput!) {
registerCustomerAccount(input: $input) {
__typename
... on Success {
success
}
... on ErrorResult {
errorCode
message
}
}
}
`

View File

@@ -0,0 +1,62 @@
import { Product } from '@vercel/commerce/types/product'
import { Cart } from '@vercel/commerce/types/cart'
import { CartFragment, SearchResultFragment } from '../../schema'
export function normalizeSearchResult(item: SearchResultFragment): Product {
return {
id: item.productId,
name: item.productName,
description: item.description,
slug: item.slug,
path: item.slug,
images: [
{
url: item.productAsset?.preview
? item.productAsset?.preview + '?w=800&mode=crop'
: '',
},
],
variants: [],
price: {
value: (item.priceWithTax as any).min / 100,
currencyCode: item.currencyCode,
},
options: [],
sku: item.sku,
}
}
export function normalizeCart(order: CartFragment): Cart {
return {
id: order.id.toString(),
createdAt: order.createdAt,
taxesIncluded: true,
lineItemsSubtotalPrice: order.subTotalWithTax / 100,
currency: { code: order.currencyCode },
subtotalPrice: order.subTotalWithTax / 100,
totalPrice: order.totalWithTax / 100,
customerId: order.customer?.id,
lineItems: order.lines?.map((l) => ({
id: l.id,
name: l.productVariant.name,
quantity: l.quantity,
url: l.productVariant.product.slug,
variantId: l.productVariant.id,
productId: l.productVariant.productId,
images: [{ url: l.featuredAsset?.preview + '?preset=thumb' || '' }],
discounts: l.discounts.map((d) => ({ value: d.amount / 100 })),
path: '',
variant: {
id: l.productVariant.id,
name: l.productVariant.name,
sku: l.productVariant.sku,
price: l.discountedUnitPriceWithTax / 100,
listPrice: l.unitPriceWithTax / 100,
image: {
url: l.featuredAsset?.preview + '?preset=thumb' || '',
},
requiresShipping: true,
},
})),
}
}

View File

@@ -0,0 +1,10 @@
export const activeCustomerQuery = /* GraphQL */ `
query activeCustomer {
activeCustomer {
id
firstName
lastName
emailAddress
}
}
`

View File

@@ -0,0 +1,9 @@
export const getAllProductPathsQuery = /* GraphQL */ `
query getAllProductPaths($first: Int = 100) {
products(options: { take: $first }) {
items {
slug
}
}
}
`

View File

@@ -0,0 +1,12 @@
import { searchResultFragment } from '../fragments/search-result-fragment'
export const getAllProductsQuery = /* GraphQL */ `
query getAllProducts($input: SearchInput!) {
search(input: $input) {
items {
...SearchResult
}
}
}
${searchResultFragment}
`

View File

@@ -0,0 +1,10 @@
import { cartFragment } from '../fragments/cart-fragment'
export const getCartQuery = /* GraphQL */ `
query activeOrder {
activeOrder {
...Cart
}
}
${cartFragment}
`

View File

@@ -0,0 +1,21 @@
export const getCollectionsQuery = /* GraphQL */ `
query getCollections {
collections {
items {
id
name
description
slug
productVariants {
totalItems
}
parent {
id
}
children {
id
}
}
}
}
`

View File

@@ -0,0 +1,41 @@
export const getProductQuery = /* GraphQL */ `
query getProduct($slug: String!) {
product(slug: $slug) {
id
name
slug
description
assets {
id
preview
name
}
variants {
id
priceWithTax
currencyCode
options {
id
name
code
groupId
group {
id
options {
name
}
}
}
}
optionGroups {
id
code
name
options {
id
name
}
}
}
}
`

View File

@@ -0,0 +1,13 @@
import { searchResultFragment } from '../fragments/search-result-fragment'
export const searchQuery = /* GraphQL */ `
query search($input: SearchInput!) {
search(input: $input) {
items {
...SearchResult
}
totalItems
}
}
${searchResultFragment}
`

View File

@@ -0,0 +1,13 @@
import { useCallback } from 'react'
export function emptyHook() {
const useEmptyHook = async (options = {}) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@@ -0,0 +1,17 @@
import { useCallback } from 'react'
type Options = {
includeProducts?: boolean
}
export function emptyHook(options?: Options) {
const useEmptyHook = async ({ id }: { id: string | number }) => {
return useCallback(async function () {
return Promise.resolve()
}, [])
}
return useEmptyHook
}
export default emptyHook

View File

@@ -0,0 +1,46 @@
// TODO: replace this hook and other wishlist hooks with a handler, or remove them if
// Vendure doesn't have a built-in wishlist
import { HookFetcher } from '@vercel/commerce/utils/types'
import { Product } from '../../schema'
const defaultOpts = {}
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: Product
}
]
}
export interface UseWishlistOptions {
includeProducts?: boolean
}
export interface UseWishlistInput extends UseWishlistOptions {
customerId?: number
}
export const fetcher: HookFetcher<Wishlist | null, UseWishlistInput> = () => {
return null
}
export function extendHook(
customFetcher: typeof fetcher,
// swrOptions?: SwrOptions<Wishlist | null, UseWishlistInput>
swrOptions?: any
) {
const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
return { data: null }
}
useWishlist.extend = extendHook
return useWishlist
}
export default extendHook(fetcher)