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,7 @@
COMMERCE_PROVIDER=commercejs
# Public key for your Commerce.js account
NEXT_PUBLIC_COMMERCEJS_PUBLIC_KEY=
# The URL for the current deployment, optional but should be used for production deployments
NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL=

View File

@@ -0,0 +1,2 @@
node_modules
dist

View File

@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false
}

View File

@@ -0,0 +1,13 @@
# [Commerce.js](https://commercejs.com/) Provider
**Demo:** https://commercejs.vercel.store/
To use this provider you must have a [Commerce.js account](https://commercejs.com/) and you should add some products in the Commerce.js dashboard.
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash
cp packages/commercejs/.env.template .env.local
```
Then, set the environment variables in `.env.local` to match the ones from your store. You'll need your Commerce.js public API key, which can be found in your Commerce.js dashboard in the `Developer -> API keys` section.

1
packages/commercejs/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module '@components/checkout/context'

View File

@@ -0,0 +1,73 @@
{
"name": "@vercel/commerce-commercejs",
"version": "0.0.1",
"license": "MIT",
"scripts": {
"build": "rm -fr dist/* && tsc",
"dev": "npm run build -- --watch",
"prettier-fix": "prettier --write ."
},
"sideEffects": false,
"type": "module",
"exports": {
".": "./dist/index.js",
"./*": [
"./dist/*.js",
"./dist/*/index.js"
],
"./next.config": "./dist/next.config.cjs"
},
"typesVersions": {
"*": {
"*": [
"src/*",
"src/*/index"
],
"next.config": [
"dist/next.config.d.cts"
]
}
},
"files": [
"dist"
],
"publishConfig": {
"typesVersions": {
"*": {
"*": [
"dist/*.d.ts",
"dist/*/index.d.ts"
],
"next.config": [
"dist/next.config.d.cts"
]
}
}
},
"dependencies": {
"@chec/commerce.js": "^2.8.0",
"@vercel/commerce": "^0.0.1"
},
"peerDependencies": {
"next": "^12",
"react": "^17",
"react-dom": "^17"
},
"devDependencies": {
"@types/chec__commerce.js": "^2.8.4",
"@types/node": "^17.0.8",
"@types/react": "^17.0.38",
"lint-staged": "^12.1.7",
"next": "^12.0.8",
"prettier": "^2.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.5.4"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx,json}": [
"prettier --write",
"git add"
]
}
}

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,23 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
import type { CheckoutSchema } from '../../../types/checkout'
import type { CommercejsAPI } from '../..'
import submitCheckout from './submit-checkout'
import getCheckout from './get-checkout'
export type CheckoutAPI = GetAPISchema<CommercejsAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = {
submitCheckout,
getCheckout,
}
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@@ -0,0 +1,44 @@
import type { CardFields } from '@vercel/commerce/types/customer/card'
import type { AddressFields } from '@vercel/commerce/types/customer/address'
import type { CheckoutEndpoint } from '.'
import sdkFetcherFunction from '../../utils/sdk-fetch'
import { normalizeTestCheckout } from '../../../utils/normalize-checkout'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
res,
body: { item, cartId },
config: { sdkFetch },
}) => {
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
// Generate a checkout token
const { id: checkoutToken } = await sdkFetcher(
'checkout',
'generateTokenFrom',
'cart',
cartId
)
const shippingMethods = await sdkFetcher(
'checkout',
'getShippingOptions',
checkoutToken,
{
country: 'US',
}
)
const shippingMethodToUse = shippingMethods?.[0]?.id || ''
const checkoutData = normalizeTestCheckout({
paymentInfo: item?.card as CardFields,
shippingInfo: item?.address as AddressFields,
shippingOption: shippingMethodToUse,
})
// Capture the order
await sdkFetcher('checkout', 'capture', checkoutToken, checkoutData)
res.status(200).json({ data: null, errors: [] })
}
export default submitCheckout

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,18 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import loginEndpoint from '@vercel/commerce/api/endpoints/login'
import type { LoginSchema } from '../../../types/login'
import type { CommercejsAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<CommercejsAPI, LoginSchema>
export type LoginEndpoint = LoginAPI['endpoint']
export const handlers: LoginEndpoint['handlers'] = { login }
const loginApi = createEndpoint<LoginAPI>({
handler: loginEndpoint,
handlers,
})
export default loginApi

View File

@@ -0,0 +1,33 @@
import { serialize } from 'cookie'
import sdkFetcherFunction from '../../utils/sdk-fetch'
import { getDeploymentUrl } from '../../../utils/get-deployment-url'
import type { LoginEndpoint } from '.'
const login: LoginEndpoint['handlers']['login'] = async ({
req,
res,
config: { sdkFetch, customerCookie },
}) => {
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
const redirectUrl = getDeploymentUrl()
try {
const loginToken = req.query?.token as string
if (!loginToken) {
res.redirect(redirectUrl)
}
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
res.setHeader(
'Set-Cookie',
serialize(customerCookie, jwt, {
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24,
path: '/',
})
)
res.redirect(redirectUrl)
} catch {
res.redirect(redirectUrl)
}
}
export default login

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,46 @@
import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
import getAllPages from './operations/get-all-pages'
import getPage from './operations/get-page'
import getSiteInfo from './operations/get-site-info'
import getAllProductPaths from './operations/get-all-product-paths'
import getAllProducts from './operations/get-all-products'
import getProduct from './operations/get-product'
import sdkFetch from './utils/sdk-fetch'
import createGraphqlFetcher from './utils/graphql-fetch'
import { API_URL, CART_COOKIE, CUSTOMER_COOKIE } from '../constants'
export interface CommercejsConfig extends CommerceAPIConfig {
sdkFetch: typeof sdkFetch
}
const config: CommercejsConfig = {
commerceUrl: API_URL,
cartCookie: CART_COOKIE,
cartCookieMaxAge: 2592000,
customerCookie: CUSTOMER_COOKIE,
apiToken: '',
fetch: createGraphqlFetcher(() => getCommerceApi().getConfig()),
sdkFetch,
}
const operations = {
getAllPages,
getPage,
getSiteInfo,
getAllProductPaths,
getAllProducts,
getProduct,
}
export const provider = { config, operations }
export type Provider = typeof provider
export type CommercejsAPI<P extends Provider = Provider> = CommerceAPI<P | any>
export function getCommerceApi<P extends Provider>(
customProvider: P = provider as any
): CommercejsAPI<P> {
return commerceApi(customProvider as any)
}

View File

@@ -0,0 +1,21 @@
import type { CommercejsConfig } from '..'
import { GetAllPagesOperation } from '../../types/page'
export type Page = { url: string }
export type GetAllPagesResult = { pages: Page[] }
export default function getAllPagesOperation() {
async function getAllPages<T extends GetAllPagesOperation>({
config,
preview,
}: {
url?: string
config?: Partial<CommercejsConfig>
preview?: boolean
} = {}): Promise<T['data']> {
return Promise.resolve({
pages: [],
})
}
return getAllPages
}

View File

@@ -0,0 +1,35 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type {
GetAllProductPathsOperation,
CommercejsProduct,
} from '../../types/product'
import type { CommercejsConfig, Provider } from '..'
export type GetAllProductPathsResult = {
products: Array<{ path: string }>
}
export default function getAllProductPathsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
config,
}: {
config?: Partial<CommercejsConfig>
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
const { data } = await sdkFetch('products', 'list')
// Match a path for every product retrieved
const productPaths = data.map(({ permalink }: CommercejsProduct) => ({
path: `/${permalink}`,
}))
return {
products: productPaths,
}
}
return getAllProductPaths
}

View File

@@ -0,0 +1,29 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { GetAllProductsOperation } from '../../types/product'
import type { CommercejsConfig, Provider } from '../index'
import { normalizeProduct } from '../../utils/normalize-product'
export default function getAllProductsOperation({
commerce,
}: OperationContext<Provider>) {
async function getAllProducts<T extends GetAllProductsOperation>({
config,
}: {
config?: Partial<CommercejsConfig>
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
const { data } = await sdkFetch('products', 'list', {
sortBy: 'sort_order',
})
const productsFormatted =
data?.map((product: any) => normalizeProduct(product)) || []
return {
products: productsFormatted,
}
}
return getAllProducts
}

View File

@@ -0,0 +1,15 @@
import { GetPageOperation } from '../../types/page'
export type Page = any
export type GetPageResult = { page?: Page }
export type PageVariables = {
id: number
}
export default function getPageOperation() {
async function getPage<T extends GetPageOperation>(): Promise<T['data']> {
return Promise.resolve({})
}
return getPage
}

View File

@@ -0,0 +1,44 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { GetProductOperation } from '../../types/product'
import type { CommercejsConfig, Provider } from '../index'
import { normalizeProduct } from '../../utils/normalize-product'
export default function getProductOperation({
commerce,
}: OperationContext<Provider>) {
async function getProduct<T extends GetProductOperation>({
config,
variables,
}: {
query?: string
variables?: T['variables']
config?: Partial<CommercejsConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
// Fetch a product by its permalink.
const product = await sdkFetch(
'products',
'retrieve',
variables?.slug || '',
{
type: 'permalink',
}
)
const { data: variants } = await sdkFetch(
'products',
'getVariants',
product.id
)
const productFormatted = normalizeProduct(product, variants)
return {
product: productFormatted,
}
}
return getProduct
}

View File

@@ -0,0 +1,36 @@
import type { OperationContext } from '@vercel/commerce/api/operations'
import type { Category, GetSiteInfoOperation } from '../../types/site'
import { normalizeCategory } from '../../utils/normalize-category'
import type { CommercejsConfig, Provider } from '../index'
export type GetSiteInfoResult<
T extends { categories: any[]; brands: any[] } = {
categories: Category[]
brands: any[]
}
> = T
export default function getSiteInfoOperation({
commerce,
}: OperationContext<Provider>) {
async function getSiteInfo<T extends GetSiteInfoOperation>({
config,
}: {
query?: string
variables?: any
config?: Partial<CommercejsConfig>
preview?: boolean
} = {}): Promise<T['data']> {
const { sdkFetch } = commerce.getConfig(config)
const { data: categories } = await sdkFetch('categories', 'list')
const formattedCategories = categories.map(normalizeCategory)
return {
categories: formattedCategories,
brands: [],
}
}
return getSiteInfo
}

View File

@@ -0,0 +1,6 @@
export { default as getAllPages } from './get-all-pages'
export { default as getPage } from './get-page'
export { default as getSiteInfo } from './get-site-info'
export { default as getProduct } from './get-product'
export { default as getAllProducts } from './get-all-products'
export { default as getAllProductPaths } from './get-all-product-paths'

View File

@@ -0,0 +1,14 @@
import type { GraphQLFetcher } from '@vercel/commerce/api'
import type { CommercejsConfig } from '../'
import { FetcherError } from '@vercel/commerce/utils/errors'
const fetchGraphqlApi: (getConfig: () => CommercejsConfig) => GraphQLFetcher =
() => async () => {
throw new FetcherError({
errors: [{ message: 'GraphQL fetch is not implemented' }],
status: 500,
})
}
export default fetchGraphqlApi

View File

@@ -0,0 +1,21 @@
import { commerce } from '../../lib/commercejs'
import Commerce from '@chec/commerce.js'
type MethodKeys<T> = {
[K in keyof T]: T[K] extends (...args: any) => infer R ? K : never
}[keyof T]
// Calls the relevant Commerce.js SDK method based on resource and method arguments.
export default async function sdkFetch<
Resource extends keyof Commerce,
Method extends MethodKeys<Commerce[Resource]>
>(
resource: Resource,
method: Method,
...variables: Parameters<Commerce[Resource][Method] | any>
): Promise<ReturnType<Commerce[Resource][Method] | any>> {
//@ts-ignore
// Provider TODO: Fix types here.
const data = await commerce[resource][method](...variables)
return data
}

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,34 @@
import { useCallback } from 'react'
import { MutationHook } from '@vercel/commerce/utils/types'
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
import type { LoginHook } from '@vercel/commerce/types/login'
import { getDeploymentUrl } from '../utils/get-deployment-url'
export default useLogin as UseLogin<typeof handler>
const getLoginCallbackUrl = () => {
const baseUrl = getDeploymentUrl()
const API_ROUTE_PATH = 'api/login'
return `${baseUrl}/${API_ROUTE_PATH}`
}
export const handler: MutationHook<LoginHook> = {
fetchOptions: {
query: 'customer',
method: 'login',
},
async fetcher({ input, options: { query, method }, fetch }) {
await fetch({
query,
method,
variables: [input.email, getLoginCallbackUrl()],
})
return null
},
useHook: ({ fetch }) =>
function useHook() {
return useCallback(async function login(input) {
return fetch({ input })
}, [])
},
}

View File

@@ -0,0 +1,27 @@
import { useCallback } from 'react'
import Cookies from 'js-cookie'
import { MutationHook } from '@vercel/commerce/utils/types'
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
import type { LogoutHook } from '@vercel/commerce/types/logout'
import useCustomer from '../customer/use-customer'
import { CUSTOMER_COOKIE } from '../constants'
export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<LogoutHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () => () => {
const { mutate } = useCustomer()
return useCallback(
async function logout() {
Cookies.remove(CUSTOMER_COOKIE)
await mutate(null, false)
return null
},
[mutate]
)
},
}

View File

@@ -0,0 +1,17 @@
import { MutationHook } from '@vercel/commerce/utils/types'
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher() {
return null
},
useHook:
({ fetch }) =>
() =>
() => {},
}

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,45 @@
import type { AddItemHook } from '@vercel/commerce/types/cart'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@vercel/commerce/cart/use-add-item'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
import useCart from './use-cart'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: 'cart',
method: 'add',
},
async fetcher({ input: item, options, fetch }) {
// Frontend stringifies variantId even if undefined.
const hasVariant = !item.variantId || item.variantId !== 'undefined'
const variables = [item.productId, item?.quantity || 1]
if (hasVariant) {
variables.push(item.variantId)
}
const { cart } = await fetch<{ cart: CommercejsCart }>({
query: options.query,
method: options.method,
variables,
})
return normalizeCart(cart)
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCart()
return useCallback(
async function addItem(input) {
const cart = await fetch({ input })
await mutate(cart, false)
return cart
},
[mutate]
)
},
}

View File

@@ -0,0 +1,41 @@
import { useMemo } from 'react'
import type { GetCartHook } from '@vercel/commerce/types/cart'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCart, { UseCart } from '@vercel/commerce/cart/use-cart'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
export default useCart as UseCart<typeof handler>
export const handler: SWRHook<GetCartHook> = {
fetchOptions: {
query: 'cart',
method: 'retrieve',
},
async fetcher({ options, fetch }) {
const cart = await fetch<CommercejsCart>({
query: options.query,
method: options.method,
})
return normalizeCart(cart)
},
useHook: ({ useData }) =>
function useHook(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,36 @@
import { useCallback } from 'react'
import type { MutationHook } from '@vercel/commerce/utils/types'
import type { RemoveItemHook } from '@vercel/commerce/types/cart'
import useRemoveItem, { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
import useCart from './use-cart'
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler: MutationHook<RemoveItemHook> = {
fetchOptions: {
query: 'cart',
method: 'remove',
},
async fetcher({ input, options, fetch }) {
const { cart } = await fetch<{ cart: CommercejsCart }>({
query: options.query,
method: options.method,
variables: input.itemId,
})
return normalizeCart(cart)
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCart()
return useCallback(
async function removeItem(input) {
const cart = await fetch({ input: { itemId: input.id } })
await mutate(cart, false)
return cart
},
[mutate]
)
},
}

View File

@@ -0,0 +1,76 @@
import type { UpdateItemHook, LineItem } from '@vercel/commerce/types/cart'
import type {
HookFetcherContext,
MutationHookContext,
} from '@vercel/commerce/utils/types'
import { ValidationError } from '@vercel/commerce/utils/errors'
import debounce from 'lodash.debounce'
import { useCallback } from 'react'
import useUpdateItem, { UseUpdateItem } from '@vercel/commerce/cart/use-update-item'
import type { CommercejsCart } from '../types/cart'
import { normalizeCart } from '../utils/normalize-cart'
import useCart from './use-cart'
export default useUpdateItem as UseUpdateItem<typeof handler>
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export const handler = {
fetchOptions: {
query: 'cart',
method: 'update',
},
async fetcher({ input, options, fetch }: HookFetcherContext<UpdateItemHook>) {
const variables = [input.itemId, { quantity: input.item.quantity }]
const { cart } = await fetch<{ cart: CommercejsCart }>({
query: options.query,
method: options.method,
variables,
})
return normalizeCart(cart)
},
useHook:
({ fetch }: MutationHookContext<UpdateItemHook>) =>
<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { mutate } = useCart() as any
const { item } = ctx
// eslint-disable-next-line react-hooks/rules-of-hooks
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
const quantity = input?.quantity ?? item?.quantity
if (!itemId || !productId || !variantId) {
throw new ValidationError({
message: 'Invalid input for updating cart item',
})
}
const cart = await fetch({
input: {
itemId,
item: {
quantity,
productId,
variantId,
},
},
})
await mutate(cart, false)
return cart
}, ctx.wait ?? 500),
[mutate, item]
)
},
}

View File

@@ -0,0 +1,2 @@
export { default as useSubmitCheckout } from './use-submit-checkout'
export { default as useCheckout } from './use-checkout'

View File

@@ -0,0 +1,52 @@
import type { GetCheckoutHook } from '@vercel/commerce/types/checkout'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCheckout, { UseCheckout } from '@vercel/commerce/checkout/use-checkout'
import useSubmitCheckout from './use-submit-checkout'
import { useCheckoutContext } from '@components/checkout/context'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () =>
function useHook() {
const { cardFields, addressFields } = useCheckoutContext()
const submit = useSubmitCheckout()
// Basic validation - check that at least one field has a value.
const hasEnteredCard = Object.values(cardFields).some(
(fieldValue) => !!fieldValue
)
const hasEnteredAddress = Object.values(addressFields).some(
(fieldValue) => !!fieldValue
)
const response = useMemo(
() => ({
data: {
hasPayment: hasEnteredCard,
hasShipping: hasEnteredAddress,
},
}),
[hasEnteredCard, hasEnteredAddress]
)
return useMemo(
() =>
Object.create(response, {
submit: {
get() {
return submit
},
enumerable: true,
},
}),
[submit, response]
)
},
}

View File

@@ -0,0 +1,38 @@
import type { SubmitCheckoutHook } from '@vercel/commerce/types/checkout'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useSubmitCheckout, {
UseSubmitCheckout,
} from '@vercel/commerce/checkout/use-submit-checkout'
import { useCheckoutContext } from '@components/checkout/context'
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { cardFields, addressFields } = useCheckoutContext()
return useCallback(
async function onSubmitCheckout(input) {
const data = await fetch({
input: { card: cardFields, address: addressFields },
})
return data
},
[cardFields, addressFields]
)
},
}

View File

@@ -0,0 +1,10 @@
{
"provider": "commercejs",
"features": {
"cart": true,
"search": true,
"customCheckout": true,
"customerAuth": true,
"wishlist": false
}
}

View File

@@ -0,0 +1,4 @@
export const CART_COOKIE = 'commercejs_cart_id'
export const CUSTOMER_COOKIE = 'commercejs_customer_token'
export const API_URL = 'https://api.chec.io/v1'
export const LOCALE = 'en-us'

View File

@@ -0,0 +1,2 @@
export { default as useAddresses } from './use-addresses'
export { default as useAddItem } from './use-add-item'

View File

@@ -0,0 +1,25 @@
import type { AddItemHook } from '@vercel/commerce/types/customer/address'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@vercel/commerce/customer/address/use-add-item'
import { useCheckoutContext } from '@components/checkout/context'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () =>
function useHook() {
const { setAddressFields } = useCheckoutContext()
return useCallback(
async function addItem(input) {
setAddressFields(input)
return undefined
},
[setAddressFields]
)
},
}

View File

@@ -0,0 +1,34 @@
import type { GetAddressesHook } from '@vercel/commerce/types/customer/address'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useAddresses, {
UseAddresses,
} from '@vercel/commerce/customer/address/use-addresses'
export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: {
url: '_',
method: '_',
},
useHook: () =>
function useHook() {
return useMemo(
() =>
Object.create(
{},
{
isEmpty: {
get() {
return true
},
enumerable: true,
},
}
),
[]
)
},
}

View File

@@ -0,0 +1,2 @@
export { default as useCards } from './use-cards'
export { default as useAddItem } from './use-add-item'

View File

@@ -0,0 +1,25 @@
import type { AddItemHook } from '@vercel/commerce/types/customer/card'
import type { MutationHook } from '@vercel/commerce/utils/types'
import { useCallback } from 'react'
import useAddItem, { UseAddItem } from '@vercel/commerce/customer/card/use-add-item'
import { useCheckoutContext } from '@components/checkout/context'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '_',
method: '_',
},
useHook: () =>
function useHook() {
const { setCardFields } = useCheckoutContext()
return useCallback(
async function addItem(input) {
setCardFields(input)
return undefined
},
[setCardFields]
)
},
}

View File

@@ -0,0 +1,31 @@
import type { GetCardsHook } from '@vercel/commerce/types/customer/card'
import { useMemo } from 'react'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCard, { UseCards } from '@vercel/commerce/customer/card/use-cards'
export default useCard as UseCards<typeof handler>
export const handler: SWRHook<GetCardsHook> = {
fetchOptions: {
query: '_',
method: '_',
},
useHook: () =>
function useHook() {
return useMemo(
() =>
Object.create(
{},
{
isEmpty: {
get() {
return true
},
enumerable: true,
},
}
),
[]
)
},
}

View File

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

View File

@@ -0,0 +1,44 @@
import Cookies from 'js-cookie'
import { decode } from 'jsonwebtoken'
import { SWRHook } from '@vercel/commerce/utils/types'
import useCustomer, { UseCustomer } from '@vercel/commerce/customer/use-customer'
import { CUSTOMER_COOKIE, API_URL } from '../constants'
import type { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
query: 'customer',
method: '_request',
},
async fetcher({ options, fetch }) {
const token = Cookies.get(CUSTOMER_COOKIE)
if (!token) {
return null
}
const decodedToken = decode(token) as { cid: string }
const customer = await fetch({
query: options.query,
method: options.method,
variables: [
`${API_URL}/customers/${decodedToken.cid}`,
'get',
null,
{},
token,
],
})
return customer
},
useHook:
({ useData }) =>
(input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@@ -0,0 +1,61 @@
import { commerce } from './lib/commercejs'
import type { Fetcher } from '@vercel/commerce/utils/types'
import { FetcherError } from '@vercel/commerce/utils/errors'
function isValidSDKQuery(query?: string): query is keyof typeof commerce {
if (!query) return false
return query in commerce
}
// Fetches from an API route within /api/endpoints directory
const customFetcher: Fetcher = async ({ method, url, body }) => {
const response = await fetch(url!, {
method,
body: body ? JSON.stringify(body) : undefined,
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => response.json())
.then((response) => response.data)
return response
}
const fetcher: Fetcher = async ({ url, query, method, variables, body }) => {
// If a URL is passed, it means that the fetch needs to be passed on to a custom API route.
const isCustomFetch = !!url
if (isCustomFetch) {
const data = await customFetcher({ url, method, body })
return data
}
// Fetch using the Commerce.js SDK, but make sure that it's a valid method.
if (!isValidSDKQuery(query)) {
throw new FetcherError({
errors: [
{ message: `Query ${query} does not exist on Commerce.js SDK.` },
],
status: 400,
})
}
const resource: any = commerce[query]
if (!method || !resource[method]) {
throw new FetcherError({
errors: [
{
message: `Method ${method} does not exist on Commerce.js SDK ${query} resource.`,
},
],
status: 400,
})
}
const variablesArgument = Array.isArray(variables) ? variables : [variables]
const data = await resource[method](...variablesArgument)
return data
}
export default fetcher

View File

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

View File

@@ -0,0 +1,11 @@
import Commerce from '@chec/commerce.js'
const commercejsPublicKey = process.env
.NEXT_PUBLIC_COMMERCEJS_PUBLIC_KEY as string
const devEnvironment = process.env.NODE_ENV === 'development'
if (devEnvironment && !commercejsPublicKey) {
throw Error('A Commerce.js public API key must be provided')
}
export const commerce = new Commerce(commercejsPublicKey, devEnvironment)

View File

@@ -0,0 +1,16 @@
const commerce = require('./commerce.config.json')
module.exports = {
commerce,
images: {
domains: ['cdn.chec.io'],
},
rewrites() {
return [
{
source: '/api/login/:token',
destination: '/api/login?token=:token',
},
]
},
}

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,53 @@
import { SWRHook } from '@vercel/commerce/utils/types'
import useSearch, { UseSearch } from '@vercel/commerce/product/use-search'
import { SearchProductsHook } from '@vercel/commerce/types/product'
import type { CommercejsProduct } from '../types/product'
import { getProductSearchVariables } from '../utils/product-search'
import { normalizeProduct } from '../utils/normalize-product'
export default useSearch as UseSearch<typeof handler>
export const handler: SWRHook<SearchProductsHook> = {
fetchOptions: {
query: 'products',
method: 'list',
},
async fetcher({ input, options, fetch }) {
const { data, meta } = await fetch<{
data: CommercejsProduct[]
meta: {
pagination: {
total: number
}
}
}>({
query: options.query,
method: options.method,
variables: getProductSearchVariables(input),
})
const formattedProducts =
data?.map((product) => normalizeProduct(product)) || []
return {
products: formattedProducts,
found: meta.pagination.total > 0,
}
},
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,55 @@
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 { handler as useCheckout } from './checkout/use-checkout'
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
import { handler as useCards } from './customer/card/use-cards'
import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useAddresses } from './customer/address/use-addresses'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { CART_COOKIE, CUSTOMER_COOKIE, LOCALE } from './constants'
import { default as sdkFetcher } from './fetcher'
export const commercejsProvider = {
locale: LOCALE,
cartCookie: CART_COOKIE,
customerCookie: CUSTOMER_COOKIE,
fetcher: sdkFetcher,
cart: {
useCart,
useAddItem,
useUpdateItem,
useRemoveItem,
},
checkout: {
useCheckout,
useSubmitCheckout,
},
customer: {
useCustomer,
card: {
useCards,
useAddItem: useAddCardItem,
},
address: {
useAddresses,
useAddItem: useAddAddressItem,
},
},
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}
export type CommercejsProvider = typeof commercejsProvider

View File

@@ -0,0 +1,4 @@
export * from '@vercel/commerce/types/cart'
export type { Cart as CommercejsCart } from '@chec/commerce.js/types/cart'
export type { LineItem as CommercejsLineItem } from '@chec/commerce.js/types/line-item'

View File

@@ -0,0 +1,3 @@
export * from '@vercel/commerce/types/checkout'
export type { CheckoutCapture as CommercejsCheckoutCapture } from '@chec/commerce.js/types/checkout-capture'

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,9 @@
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,4 @@
export * from '@vercel/commerce/types/product'
export type { Product as CommercejsProduct } from '@chec/commerce.js/types/product'
export type { Variant as CommercejsVariant } from '@chec/commerce.js/types/variant'

View File

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

View File

@@ -0,0 +1,3 @@
export * from '@vercel/commerce/types/site'
export type { Category as CommercejsCategory } from '@chec/commerce.js/types/category'

View File

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

View File

@@ -0,0 +1,12 @@
export const getDeploymentUrl = () => {
// Custom environment variable.
if (process.env.NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL) {
return process.env.NEXT_PUBLIC_COMMERCEJS_DEPLOYMENT_URL
}
// Automatic Vercel deployment URL.
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
}
// Assume local development.
return 'http://localhost:3000'
}

View File

@@ -0,0 +1,74 @@
import type {
Cart,
LineItem,
CommercejsCart,
CommercejsLineItem,
} from '../types/cart'
type CommercejsLineItemType = CommercejsLineItem & { image: { url: string } }
const normalizeLineItem = (
commercejsLineItem: CommercejsLineItemType
): LineItem => {
const {
id,
sku,
quantity,
price,
product_id,
product_name,
permalink,
variant,
image,
selected_options,
} = commercejsLineItem
return {
id,
variantId: variant?.id ?? '',
productId: product_id,
name: product_name,
quantity,
discounts: [],
path: permalink,
options: selected_options?.map(({ group_name, option_name }) => ({
name: group_name,
value: option_name,
})),
variant: {
id: variant?.id ?? id,
sku: variant?.sku ?? sku,
name: product_name,
requiresShipping: false,
price: variant?.price?.raw ?? price.raw,
listPrice: variant?.price?.raw ?? price.raw,
image: {
url: image?.url,
},
},
}
}
export const normalizeCart = (commercejsCart: CommercejsCart): Cart => {
const {
id,
created,
subtotal: { raw: rawPrice },
currency,
line_items,
} = commercejsCart
return {
id,
createdAt: new Date(created * 1000).toISOString(),
currency: {
code: currency.code,
},
taxesIncluded: false,
lineItems: line_items.map((item) => {
return normalizeLineItem(item as CommercejsLineItemType)
}),
lineItemsSubtotalPrice: rawPrice,
subtotalPrice: rawPrice,
totalPrice: rawPrice,
}
}

View File

@@ -0,0 +1,14 @@
import type { Category } from '@vercel/commerce/types/site'
import type { Category as CommercejsCategory } from '@chec/commerce.js/types/category'
export function normalizeCategory(
commercejsCatgeory: CommercejsCategory
): Category {
const { id, name, slug } = commercejsCatgeory
return {
id,
name,
slug,
path: slug,
}
}

View File

@@ -0,0 +1,63 @@
import type { CardFields } from '@vercel/commerce/types/customer/card'
import type { AddressFields } from '@vercel/commerce/types/customer/address'
import type { CommercejsCheckoutCapture } from '../types/checkout'
/**
* Creates a checkout payload suitable for test checkouts.
* 1. Hard-codes the payment values for the Commerce.js test gateway.
* 2. Hard-codes the email until an email field exists on the checkout form.
* 3. Gets as much as much checkout info as possible from the checkout form, and uses fallback values.
*/
export function normalizeTestCheckout({
paymentInfo,
shippingInfo,
shippingOption,
}: {
paymentInfo?: CardFields
shippingInfo?: AddressFields
shippingOption: string
}): CommercejsCheckoutCapture {
const firstName =
shippingInfo?.firstName || paymentInfo?.firstName || 'Nextjs'
const lastName = shippingInfo?.lastName || paymentInfo?.lastName || 'Commerce'
const fullName = `${firstName} ${lastName}`
const postalCode = shippingInfo?.zipCode || paymentInfo?.zipCode || '94103'
const street =
shippingInfo?.streetNumber || paymentInfo?.streetNumber || 'Test Street'
const townCity = shippingInfo?.city || paymentInfo?.city || 'Test Town'
return {
payment: {
gateway: 'test_gateway',
card: {
number: '4242 4242 4242 4242',
expiry_month: '01',
expiry_year: '2024',
cvc: '123',
postal_zip_code: postalCode,
},
},
customer: {
email: 'nextcommerce@test.com',
firstname: firstName,
lastname: lastName,
},
shipping: {
name: fullName,
street,
town_city: townCity,
country: 'US',
},
billing: {
name: fullName,
street,
town_city: townCity,
postal_zip_code: postalCode,
county_state: 'California',
country: 'US',
},
fulfillment: {
shipping_method: shippingOption,
},
}
}

View File

@@ -0,0 +1,77 @@
import type {
Product,
CommercejsProduct,
CommercejsVariant,
} from '../types/product'
function getOptionsFromVariantGroups(
variantGroups: CommercejsProduct['variant_groups']
): Product['options'] {
const optionsFromVariantGroups = variantGroups.map(
({ id, name: variantName, options }) => ({
id,
displayName: variantName,
values: options.map(({ name: optionName }) => ({
label: optionName,
})),
})
)
return optionsFromVariantGroups
}
function normalizeVariants(
variants: Array<CommercejsVariant> = [],
variantGroups: CommercejsProduct['variant_groups']
) {
if (!Array.isArray(variants)) return []
return variants?.map((variant) => ({
id: variant.id,
options: Object.entries(variant.options).map(
([variantGroupId, variantOptionId]) => {
const variantGroupFromId = variantGroups.find(
(group) => group.id === variantGroupId
)
const valueLabel = variantGroupFromId?.options.find(
(option) => option.id === variantOptionId
)?.name
return {
id: variantOptionId,
displayName: variantGroupFromId?.name || '',
__typename: 'MultipleChoiceOption' as 'MultipleChoiceOption',
values: [
{
label: valueLabel || '',
},
],
}
}
),
}))
}
export function normalizeProduct(
commercejsProduct: CommercejsProduct,
commercejsProductVariants: Array<CommercejsVariant> = []
): Product {
const { id, name, description, permalink, assets, price, variant_groups } =
commercejsProduct
return {
id,
name,
description,
descriptionHtml: description,
slug: permalink,
path: permalink,
images: assets.map(({ url, description, filename }) => ({
url,
alt: description || filename,
})),
price: {
value: price.raw,
currencyCode: 'USD',
},
variants: normalizeVariants(commercejsProductVariants, variant_groups),
options: getOptionsFromVariantGroups(variant_groups),
}
}

View File

@@ -0,0 +1,54 @@
import { SearchProductsBody } from '@vercel/commerce/types/product'
const getFilterVariables = ({
search,
categoryId,
}: {
search?: string
categoryId?: string | number
}) => {
let filterVariables: { [key: string]: any } = {}
if (search) {
filterVariables.query = search
}
if (categoryId) {
filterVariables['category_id'] = categoryId
}
return filterVariables
}
const getSortVariables = ({ sort }: { sort?: string }) => {
let sortVariables: { [key: string]: any } = {}
switch (sort) {
case 'trending-desc':
case 'latest-desc':
sortVariables = {
sortBy: 'updated',
sortDirection: 'desc',
}
break
case 'price-asc':
sortVariables = {
sortBy: 'price',
sortDirection: 'asc',
}
break
case 'price-desc':
sortVariables = {
sortBy: 'price',
sortDirection: 'desc',
}
break
}
return sortVariables
}
export const getProductSearchVariables = (input: SearchProductsBody) => {
const { search, categoryId, sort } = input
const filterVariables = getFilterVariables({ search, categoryId })
const sortVariables = getSortVariables({ sort })
return {
...filterVariables,
...sortVariables,
}
}

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,40 @@
import { HookFetcher } from '@vercel/commerce/utils/types'
export type Wishlist = {
items: [
{
product_id: number
variant_id: number
id: number
product: any
}
]
}
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)

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"outDir": "dist",
"baseUrl": "src",
"lib": ["dom", "dom.iterable", "esnext"],
"declaration": true,
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"incremental": true,
"jsx": "react-jsx"
},
"include": ["src", "global.d.ts"],
"exclude": ["node_modules", "dist"]
}