diff --git a/packages/opencommerce/.env.template b/packages/opencommerce/.env.template new file mode 100644 index 000000000..424ac8ff2 --- /dev/null +++ b/packages/opencommerce/.env.template @@ -0,0 +1,3 @@ +COMMERCE_PROVIDER=shopify + +OPENCOMMERCE_STOREFRONT_API_URL= \ No newline at end of file diff --git a/packages/opencommerce/.prettierignore b/packages/opencommerce/.prettierignore new file mode 100644 index 000000000..f06235c46 --- /dev/null +++ b/packages/opencommerce/.prettierignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/opencommerce/.prettierrc b/packages/opencommerce/.prettierrc new file mode 100644 index 000000000..e1076edfa --- /dev/null +++ b/packages/opencommerce/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false +} diff --git a/packages/opencommerce/package.json b/packages/opencommerce/package.json new file mode 100644 index 000000000..6cfa24bd5 --- /dev/null +++ b/packages/opencommerce/package.json @@ -0,0 +1,79 @@ +{ + "name": "@vercel/commerce-opencommerce", + "version": "0.0.1", + "license": "MIT", + "scripts": { + "release": "taskr release", + "build": "taskr build", + "dev": "taskr", + "types": "tsc --emitDeclarationOnly", + "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": { + "@vercel/commerce": "^0.0.1", + "@vercel/fetch": "^6.1.1" + }, + "peerDependencies": { + "next": "^12", + "react": "^17", + "react-dom": "^17" + }, + "devDependencies": { + "@taskr/clear": "^1.1.0", + "@taskr/esnext": "^1.1.0", + "@taskr/watch": "^1.1.0", + "@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", + "taskr": "^1.1.0", + "taskr-swc": "^0.0.1", + "typescript": "^4.5.4" + }, + "lint-staged": { + "**/*.{js,jsx,ts,tsx,json}": [ + "prettier --write", + "git add" + ] + } +} diff --git a/packages/opencommerce/src/api/endpoints/cart.ts b/packages/opencommerce/src/api/endpoints/cart.ts new file mode 100644 index 000000000..d09c976c3 --- /dev/null +++ b/packages/opencommerce/src/api/endpoints/cart.ts @@ -0,0 +1 @@ +export default function (_commerce: any) {} diff --git a/packages/opencommerce/src/api/index.ts b/packages/opencommerce/src/api/index.ts new file mode 100644 index 000000000..09ecf45c2 --- /dev/null +++ b/packages/opencommerce/src/api/index.ts @@ -0,0 +1,40 @@ +import { + CommerceAPI, + CommerceAPIConfig, + getCommerceApi as commerceApi, +} from '@vercel/commerce/api' +import createFetchGraphqlApi from './utils/fetch-grapql-api' + +const API_URL = process.env.OPENCOMMERCE_STOREFRONT_API_URL + +if (!API_URL) { + throw new Error( + `The environment variable OPENCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store` + ) +} + +export interface OpenCommerceConfig extends CommerceAPIConfig {} + +const ONE_DAY = 60 * 60 * 24 + +const config: OpenCommerceConfig = { + commerceUrl: `${API_URL}/graphql`, + apiToken: '', + customerCookie: 'opencommerce_customerToken', + cartCookie: 'opencommerce_cartId', + cartCookieMaxAge: ONE_DAY * 30, + fetch: createFetchGraphqlApi(() => getCommerceApi().getConfig()), +} + +const operations = {} +export const provider = { config, operations } + +export type Provider = typeof provider + +export type OpenCommerceAPI

= CommerceAPI

+ +export function getCommerceApi

( + customProvider: P = provider as any +): OpenCommerceAPI

{ + return commerceApi(customProvider) +} diff --git a/packages/opencommerce/src/api/operations/get-all-products.ts b/packages/opencommerce/src/api/operations/get-all-products.ts new file mode 100644 index 000000000..e0491f3b9 --- /dev/null +++ b/packages/opencommerce/src/api/operations/get-all-products.ts @@ -0,0 +1,59 @@ +import type { + OperationContext, + OperationOptions, +} from '@vercel/commerce/api/operations' +import { GetAllProductsOperation } from '../../types/product' +import { + GetAllProductsQuery, + GetAllProductsQueryVariables, + Product as ShopifyProduct, +} from '../../../schema' +import type { ShopifyConfig, Provider } from '..' +import getAllProductsQuery from '../../utils/queries/get-all-products-query' +import { normalizeProduct } from '../../utils' + +export default function getAllProductsOperation({ + commerce, +}: OperationContext) { + async function getAllProducts(opts?: { + variables?: T['variables'] + config?: Partial + preview?: boolean + }): Promise + + async function getAllProducts({ + query = getAllProductsQuery, + variables, + config, + }: { + query?: string + variables?: T['variables'] + config?: Partial + preview?: boolean + } = {}): Promise { + const { fetch, locale } = commerce.getConfig(config) + + const { data } = await fetch< + GetAllProductsQuery, + GetAllProductsQueryVariables + >( + query, + { variables }, + { + ...(locale && { + headers: { + 'Accept-Language': locale, + }, + }), + } + ) + + return { + products: data.products.edges.map(({ node }) => + normalizeProduct(node as ShopifyProduct) + ), + } + } + + return getAllProducts +} diff --git a/packages/opencommerce/src/api/utils/fetch-grapql-api.ts b/packages/opencommerce/src/api/utils/fetch-grapql-api.ts new file mode 100644 index 000000000..d5f20ebaf --- /dev/null +++ b/packages/opencommerce/src/api/utils/fetch-grapql-api.ts @@ -0,0 +1,39 @@ +import { FetcherError } from '@vercel/commerce/utils/errors' +import type { GraphQLFetcher } from '@vercel/commerce/api' +import type { OpenCommerceConfig } from '../index' +import fetch from './fetch' + +const fetchGraphqlApi: ( + getConfig: () => OpenCommerceConfig +) => GraphQLFetcher = + (getConfig) => + async (query: string, { variables, preview } = {}, fetchOptions) => { + // log.warn(query) + const config = getConfig() + const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), { + ...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 OpenCommerce API' }, + ], + status: res.status, + }) + } + + return { data: json.data, res } + } + +export default fetchGraphqlApi diff --git a/packages/opencommerce/src/api/utils/fetch.ts b/packages/opencommerce/src/api/utils/fetch.ts new file mode 100644 index 000000000..26f9ab674 --- /dev/null +++ b/packages/opencommerce/src/api/utils/fetch.ts @@ -0,0 +1,3 @@ +import vercelFetch from '@vercel/fetch' + +export default vercelFetch() diff --git a/packages/opencommerce/src/commerce.config.json b/packages/opencommerce/src/commerce.config.json new file mode 100644 index 000000000..0b3fcd138 --- /dev/null +++ b/packages/opencommerce/src/commerce.config.json @@ -0,0 +1,10 @@ +{ + "provider": "opencommerce", + "features": { + "wishlist": false, + "cart": true, + "search": true, + "customerAuth": false, + "customCheckout": true + } +} diff --git a/packages/opencommerce/src/index.ts b/packages/opencommerce/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/opencommerce/src/next.config.cjs b/packages/opencommerce/src/next.config.cjs new file mode 100644 index 000000000..ce46b706f --- /dev/null +++ b/packages/opencommerce/src/next.config.cjs @@ -0,0 +1,8 @@ +const commerce = require('./commerce.config.json') + +module.exports = { + commerce, + images: { + domains: ['localhost'], + }, +} diff --git a/packages/opencommerce/src/provider.ts b/packages/opencommerce/src/provider.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/opencommerce/taskfile.js b/packages/opencommerce/taskfile.js new file mode 100644 index 000000000..39b1b2a86 --- /dev/null +++ b/packages/opencommerce/taskfile.js @@ -0,0 +1,20 @@ +export async function build(task, opts) { + await task + .source('src/**/*.+(ts|tsx|js)') + .swc({ dev: opts.dev, outDir: 'dist', baseUrl: 'src' }) + .target('dist') + .source('src/**/*.+(cjs|json)') + .target('dist') + task.$.log('Compiled src files') +} + +export async function release(task) { + await task.clear('dist').start('build') +} + +export default async function dev(task) { + const opts = { dev: true } + await task.clear('dist') + await task.start('build', opts) + await task.watch('src/**/*.+(ts|tsx|js|cjs|json)', 'build', opts) +} diff --git a/packages/opencommerce/tsconfig.json b/packages/opencommerce/tsconfig.json new file mode 100644 index 000000000..cd04ab2ff --- /dev/null +++ b/packages/opencommerce/tsconfig.json @@ -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"], + "exclude": ["node_modules", "dist"] +}