From 187fc0bc8fa241b976296091140d0fd0b0960295 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 16:17:14 -0400 Subject: [PATCH 01/48] recs server-side working --- .../commerce-engine/commerce-engine.ssr.ts | 51 +++++++++++++---- .../src/app/commerce-ssr-engine/common.ts | 35 ++++++++++++ .../app/search-engine/search-engine.ssr.ts | 57 ++++++++++++++++--- .../headless/src/app/ssr-engine/common.ts | 6 +- .../src/app/ssr-engine/types/common.ts | 2 +- .../ssr-engine/types/hydrate-static-state.ts | 2 +- .../headless-recommendations.ssr.ts | 3 +- .../app/_components/pages/listing-page.tsx | 5 +- 8 files changed, 132 insertions(+), 29 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index dc32ca921ee..b3ff825c307 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -7,7 +7,10 @@ import {buildProductListing} from '../../controllers/commerce/product-listing/he import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; import {createWaitForActionMiddleware} from '../../utils/utils.js'; -import {buildControllerDefinitions} from '../commerce-ssr-engine/common.js'; +import { + buildControllerDefinitions, + buildRecommendationFilter, +} from '../commerce-ssr-engine/common.js'; import { ControllerDefinitionsMap, InferControllerStaticStateMapFromDefinitionsWithSolutionType, @@ -37,7 +40,7 @@ export interface SSRCommerceEngine extends CommerceEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForRequestCompletedAction(): Promise; + waitForRequestCompletedAction(): Promise[]; } export type CommerceEngineDefinitionOptions< @@ -56,13 +59,20 @@ function isSearchCompletedAction(action: unknown): action is Action { ); } +function isRecommendationCompletedAction(action: unknown): action is Action { + return /^commerce\/recommendations\/fetch\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + function noSearchActionRequired(_action: unknown): _action is Action { return true; } function buildSSRCommerceEngine( solutionType: SolutionType, - options: CommerceEngineOptions + options: CommerceEngineOptions, + recommendationCount: number ): SSRCommerceEngine { let actionCompletionMiddleware: ReturnType< typeof createWaitForActionMiddleware @@ -85,11 +95,17 @@ function buildSSRCommerceEngine( ); } + const recommendationActionMiddlewares = Array.from( + {length: recommendationCount}, + () => createWaitForActionMiddleware(isRecommendationCompletedAction) + ); + const commerceEngine = buildCommerceEngine({ ...options, middlewares: [ ...(options.middlewares ?? []), actionCompletionMiddleware.middleware, + ...recommendationActionMiddlewares.map(({middleware}) => middleware), ], }); @@ -101,7 +117,10 @@ function buildSSRCommerceEngine( }, waitForRequestCompletedAction() { - return actionCompletionMiddleware.promise; + return [ + actionCompletionMiddleware.promise, + ...recommendationActionMiddlewares.map(({promise}) => promise), + ]; }, }; } @@ -163,6 +182,10 @@ export function defineCommerceEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; + const recommendationHelper = buildRecommendationFilter( + controllerDefinitions ?? {} + ); + const getOptions = () => { return engineOptions; }; @@ -180,7 +203,8 @@ export function defineCommerceEngine< solutionType, buildOptions?.extend ? await buildOptions.extend(getOptions()) - : getOptions() + : getOptions(), + recommendationHelper.count ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -232,10 +256,14 @@ export function defineCommerceEngine< buildSearch(engine).executeFirstSearch(); } - const searchAction = await engine.waitForRequestCompletedAction(); + recommendationHelper.refresh(controllers); + + const searchActions = await Promise.all( + engine.waitForRequestCompletedAction() + ); return createStaticState({ - searchAction, + searchActions, controllers, }) as EngineStaticState< UnknownAction, @@ -266,7 +294,7 @@ export function defineCommerceEngine< solutionType ).fromBuildResult({ buildResult, - searchAction: params[0]!.searchAction, + searchActions: params[0]!.searchActions, }); return staticState; }, @@ -277,10 +305,13 @@ export function defineCommerceEngine< const [ { buildResult: {engine, controllers}, - searchAction, + searchActions, }, ] = params; - engine.dispatch(searchAction); + + searchActions.forEach((action) => { + engine.dispatch(action); + }); await engine.waitForRequestCompletedAction(); return {engine, controllers}; }, diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 2b8907bf678..e6a72dede23 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,3 +1,4 @@ +import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; import {InvalidControllerDefinition} from '../../utils/errors.js'; import {filterObject, mapObject} from '../../utils/utils.js'; @@ -103,3 +104,37 @@ export function ensureAtLeastOneSolutionType( throw new InvalidControllerDefinition(); } } +export function buildRecommendationFilter< + TEngine extends CoreEngine | CoreEngineNext, + TControllerDefinitions extends ControllerDefinitionsMap, +>(controllerDefinitions: TControllerDefinitions) { + const keys = Object.entries(controllerDefinitions) + .filter(([_, value]) => 'isRecs' in value && value.isRecs) + .map(([key, _]) => key); + + return { + /** + * Gets the number of recommendation controllers from the controller definitions map. + * + * @returns {number} The number of recommendation controllers in the controller definition map + */ + get count() { + return keys.length; + }, + + /** + * Go through all the controllers passed in argument and only refresh recommendation controllers. + * + * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. + */ + refresh(controllers: Record) { + const isRecommendationController = (key: string) => keys.includes(key); + + Object.entries(controllers) + .filter(([key, _]) => isRecommendationController(key)) + .forEach(([_, controller]) => + (controller as Recommendations).refresh?.() + ); + }, + }; +} diff --git a/packages/headless/src/app/search-engine/search-engine.ssr.ts b/packages/headless/src/app/search-engine/search-engine.ssr.ts index b1361574c7e..f14b5191c6f 100644 --- a/packages/headless/src/app/search-engine/search-engine.ssr.ts +++ b/packages/headless/src/app/search-engine/search-engine.ssr.ts @@ -5,6 +5,7 @@ import {UnknownAction} from '@reduxjs/toolkit'; import type {Controller} from '../../controllers/controller/headless-controller.js'; import {LegacySearchAction} from '../../features/analytics/analytics-utils.js'; import {createWaitForActionMiddleware} from '../../utils/utils.js'; +import {buildRecommendationFilter} from '../commerce-ssr-engine/common.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; import { buildControllerDefinitions, @@ -36,7 +37,7 @@ export interface SSRSearchEngine extends SearchEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForSearchCompletedAction(): Promise; + waitForSearchCompletedAction(): Promise[]; } /** @@ -60,13 +61,34 @@ function isSearchCompletedAction( ); } -function buildSSRSearchEngine(options: SearchEngineOptions): SSRSearchEngine { +function isRecommendationCompletedAction( + action: unknown +): action is SearchCompletedAction { + return /^recommendation\/get\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + +function buildSSRSearchEngine( + options: SearchEngineOptions, + recommendationCount: number +): SSRSearchEngine { const {middleware, promise} = createWaitForActionMiddleware( isSearchCompletedAction ); + + const recommendationActionMiddlewares = Array.from( + {length: recommendationCount}, + () => createWaitForActionMiddleware(isRecommendationCompletedAction) + ); + const searchEngine = buildSearchEngine({ ...options, - middlewares: [...(options.middlewares ?? []), middleware], + middlewares: [ + ...(options.middlewares ?? []), + middleware, + ...recommendationActionMiddlewares.map(({middleware}) => middleware), + ], }); return { ...searchEngine, @@ -74,7 +96,10 @@ function buildSSRSearchEngine(options: SearchEngineOptions): SSRSearchEngine { return searchEngine.state; }, waitForSearchCompletedAction() { - return promise; + return [ + promise, + ...recommendationActionMiddlewares.map(({promise}) => promise), + ]; }, }; } @@ -121,6 +146,10 @@ export function defineSearchEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; + const recommendationHelper = buildRecommendationFilter( + controllerDefinitions ?? {} + ); + const getOptions = () => { return engineOptions; }; @@ -135,7 +164,8 @@ export function defineSearchEngine< const engine = buildSSRSearchEngine( buildOptions?.extend ? await buildOptions.extend(getOptions()) - : getOptions() + : getOptions(), + recommendationHelper.count ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -175,8 +205,14 @@ export function defineSearchEngine< ] = params; engine.executeFirstSearch(); + recommendationHelper.refresh(controllers); + + const searchActions = await Promise.all( + engine.waitForSearchCompletedAction() + ); + return createStaticState({ - searchAction: await engine.waitForSearchCompletedAction(), + searchActions, controllers, }) as EngineStaticState< UnknownAction, @@ -197,7 +233,7 @@ export function defineSearchEngine< const buildResult = await build(...(params as BuildParameters)); const staticState = await hydrateStaticState.fromBuildResult({ buildResult, - searchAction: params[0]!.searchAction, + searchActions: params[0]!.searchActions, }); return staticState; }, @@ -208,10 +244,13 @@ export function defineSearchEngine< const [ { buildResult: {engine, controllers}, - searchAction, + searchActions, }, ] = params; - engine.dispatch(searchAction); + + searchActions.forEach((action) => { + engine.dispatch(action); + }); await engine.waitForSearchCompletedAction(); return {engine, controllers}; }, diff --git a/packages/headless/src/app/ssr-engine/common.ts b/packages/headless/src/app/ssr-engine/common.ts index 26b0503f1dd..79eef38ef33 100644 --- a/packages/headless/src/app/ssr-engine/common.ts +++ b/packages/headless/src/app/ssr-engine/common.ts @@ -58,10 +58,10 @@ export function buildControllerDefinitions< } export function createStaticState({ - searchAction, + searchActions, controllers, }: { - searchAction: TSearchAction; + searchActions: TSearchAction[]; controllers: ControllersMap; }): EngineStaticState< TSearchAction, @@ -71,7 +71,7 @@ export function createStaticState({ controllers: mapObject(controllers, (controller) => ({ state: clone(controller.state), })) as InferControllerStaticStateMapFromControllers, - searchAction: clone(searchAction), + searchActions: searchActions.map((action) => clone(action)), }; } diff --git a/packages/headless/src/app/ssr-engine/types/common.ts b/packages/headless/src/app/ssr-engine/types/common.ts index 831b23ec423..853beb17cc5 100644 --- a/packages/headless/src/app/ssr-engine/types/common.ts +++ b/packages/headless/src/app/ssr-engine/types/common.ts @@ -105,7 +105,7 @@ export interface EngineStaticState< TSearchAction extends UnknownAction, TControllers extends ControllerStaticStateMap, > { - searchAction: TSearchAction; + searchActions: TSearchAction[]; controllers: TControllers; } diff --git a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts index f7164fa0932..9f4a265badb 100644 --- a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts @@ -10,7 +10,7 @@ import { import {FromBuildResult} from './from-build-result.js'; export interface HydrateStaticStateOptions { - searchAction: TSearchAction; + searchActions: TSearchAction[]; } export type HydrateStaticState< diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index 2996ef6e9de..b0335ba6e90 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -20,11 +20,12 @@ export interface RecommendationsDefinition * */ export function defineRecommendations( props: RecommendationsProps -): RecommendationsDefinition { +): RecommendationsDefinition & {isRecs: true} { return { search: true, listing: true, standalone: true, + isRecs: true, build: (engine) => buildRecommendations(engine, props), }; } diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx index 05612968ed2..73e7718fa58 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx @@ -34,13 +34,10 @@ export default function ListingPage({ useEffect(() => { listingEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - controllers.popularBoughtRecs.refresh(); }); }, [staticState]); From d571439c51d5b64e0b7591cea1f6ed64f82fe5da Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 31 Oct 2024 21:25:12 -0400 Subject: [PATCH 02/48] recognize search action promises --- .../commerce-engine/commerce-engine.ssr.ts | 18 +++++++--- packages/headless/src/utils/utils.ts | 36 +++++++++++++++++++ .../app/_components/pages/listing-page.tsx | 4 +++ .../app/_components/pages/product-page.tsx | 2 +- .../app/_components/pages/recommendation.tsx | 2 +- .../app/_components/pages/search-page.tsx | 2 +- 6 files changed, 56 insertions(+), 8 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index b3ff825c307..cb12cf95f40 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -6,7 +6,10 @@ import {stateKey} from '../../app/state-key.js'; import {buildProductListing} from '../../controllers/commerce/product-listing/headless-product-listing.js'; import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; -import {createWaitForActionMiddleware} from '../../utils/utils.js'; +import { + createWaitForActionMiddleware, + createWaitForActionMiddlewareForRecommendation, +} from '../../utils/utils.js'; import { buildControllerDefinitions, buildRecommendationFilter, @@ -95,9 +98,14 @@ function buildSSRCommerceEngine( ); } + const memo: Set = new Set(); const recommendationActionMiddlewares = Array.from( {length: recommendationCount}, - () => createWaitForActionMiddleware(isRecommendationCompletedAction) + () => + createWaitForActionMiddlewareForRecommendation( + isRecommendationCompletedAction, + memo + ) ); const commerceEngine = buildCommerceEngine({ @@ -182,7 +190,7 @@ export function defineCommerceEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; - const recommendationHelper = buildRecommendationFilter( + const recommendationFilter = buildRecommendationFilter( controllerDefinitions ?? {} ); @@ -204,7 +212,7 @@ export function defineCommerceEngine< buildOptions?.extend ? await buildOptions.extend(getOptions()) : getOptions(), - recommendationHelper.count + recommendationFilter.count ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -256,7 +264,7 @@ export function defineCommerceEngine< buildSearch(engine).executeFirstSearch(); } - recommendationHelper.refresh(controllers); + recommendationFilter.refresh(controllers); const searchActions = await Promise.all( engine.waitForRequestCompletedAction() diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index 92af128c4d1..4e6a52ce460 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -157,3 +157,39 @@ export function createWaitForActionMiddleware( return {promise, middleware}; } + +export function createWaitForActionMiddlewareForRecommendation< + TAction extends Action, +>( + isDesiredAction: (action: unknown) => action is TAction, + memo: Set +): {promise: Promise; middleware: Middleware} { + const {promise, resolve} = createDeferredPromise(); + let resolved = false; + + const middleware: Middleware = () => (next) => (action) => { + next(action); + // [x] Should not resolve the same action more than once + // [x] Do not resolve a recommendation action if it is in the slot id + if ( + isDesiredAction(action) && + // TODO: merge these two conditions + !resolved && + !memo.has( + (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg + ?.slotId + ) //TODO:: this will not work for non recommendation action + ) { + resolved = true; + // TODO: fix this type casting + memo.add( + (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg + ?.slotId + ); + console.log(' --- isDesiredAction ---', action); + resolve(action); + } + }; + + return {promise, middleware}; +} diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx index 73e7718fa58..48f800bfa95 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/listing-page.tsx @@ -107,6 +107,10 @@ export default function ListingPage({ staticState={staticState.controllers.popularBoughtRecs.state} controller={hydratedState?.controllers.popularBoughtRecs} /> + diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx index d964f2b9b08..733217d24f1 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/product-page.tsx @@ -36,7 +36,7 @@ export default function ProductPage(props: IProductPageProps) { useEffect(() => { standaloneEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx index 55d7ce66d52..90ffd1d4600 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx @@ -28,7 +28,7 @@ export default function Recommendation({ useEffect(() => { standaloneEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx index 3e5d7644bcb..61cff84da1c 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/search-page.tsx @@ -33,7 +33,7 @@ export default function SearchPage({ useEffect(() => { searchEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); From 4dc2717481e6cf03edf130618ed87335f6ff05a3 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 1 Nov 2024 10:29:18 -0400 Subject: [PATCH 03/48] add typeguard --- packages/headless/src/utils/utils.ts | 41 +++++++++++-------- .../app/_lib/commerce-engine-config.ts | 8 ++++ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index 4e6a52ce460..badd60b0401 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -1,4 +1,5 @@ -import {Middleware, Action} from '@reduxjs/toolkit'; +import {Middleware, Action, PayloadAction} from '@reduxjs/toolkit'; +import {FetchRecommendationsPayload} from '../features/commerce/recommendations/recommendations-actions.js'; export const randomID = (prepend?: string, length = 5) => prepend + @@ -158,35 +159,41 @@ export function createWaitForActionMiddleware( return {promise, middleware}; } +function isRecommendationActionPayload

( + action: unknown +): action is PayloadAction { + // TODO: clean that thing!! + if (typeof action === 'object' && action !== null && 'meta' in action) { + return ( + (action as PayloadAction).meta + ?.arg?.slotId !== undefined + ); + } + return false; +} + export function createWaitForActionMiddlewareForRecommendation< TAction extends Action, >( isDesiredAction: (action: unknown) => action is TAction, memo: Set + //TODO:: this will not work for non recommendation action ): {promise: Promise; middleware: Middleware} { const {promise, resolve} = createDeferredPromise(); - let resolved = false; + let hasBeenResolved = false; + const hasSlotBeenProcessed = (slotId: string) => memo.has(slotId); const middleware: Middleware = () => (next) => (action) => { next(action); - // [x] Should not resolve the same action more than once - // [x] Do not resolve a recommendation action if it is in the slot id + if ( isDesiredAction(action) && - // TODO: merge these two conditions - !resolved && - !memo.has( - (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg - ?.slotId - ) //TODO:: this will not work for non recommendation action + !hasBeenResolved && + isRecommendationActionPayload(action) && + !hasSlotBeenProcessed(action.meta.arg.slotId) ) { - resolved = true; - // TODO: fix this type casting - memo.add( - (action as unknown as {meta: {arg: {slotId: string}}})?.meta?.arg - ?.slotId - ); - console.log(' --- isDesiredAction ---', action); + hasBeenResolved = true; + memo.add(action.meta.arg.slotId); resolve(action); } }; diff --git a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts index ce281375e97..e236754b8c3 100644 --- a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts @@ -53,6 +53,14 @@ export default { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, }), + // TODO: check for invalid slotId + // TODO: check for duplicate slotId + // TODO: encounter for multiple recommendations with same slot id + // popwularBoughtRecs: defineRecommendations({ + // options: { + // slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30172', + // }, + // }), cart: defineCart(), searchBox: defineSearchBox(), context: defineContext(), From 2e5f79451e5948e11efe6f907fcf2be6360a33c6 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 1 Nov 2024 16:29:04 -0400 Subject: [PATCH 04/48] prevent multiple recommendations with the same slot --- .../src/app/commerce-ssr-engine/common.ts | 24 ++++++++++++++++--- .../headless-recommendations.ssr.ts | 9 +++++-- packages/headless/src/utils/utils.ts | 10 ++++++++ .../app/_lib/commerce-engine-config.ts | 10 ++++++-- 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index e6a72dede23..a103fdf0546 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -108,9 +108,27 @@ export function buildRecommendationFilter< TEngine extends CoreEngine | CoreEngineNext, TControllerDefinitions extends ControllerDefinitionsMap, >(controllerDefinitions: TControllerDefinitions) { - const keys = Object.entries(controllerDefinitions) - .filter(([_, value]) => 'isRecs' in value && value.isRecs) - .map(([key, _]) => key); + const seenSlotIds = new Set(); + const filtered = Object.entries(controllerDefinitions).filter( + ([_, value]) => { + if ('isRecs' in value && value.isRecs) { + const slotId = (value as unknown as {slotId: string}).slotId; // TODO: fix type and CLEAN THAT! + // TODO: use a combination of slotId and productId name to identify the controller + if (seenSlotIds.has(slotId)) { + console.log( + 'WARNING: Multiple recommendation controllers found for the same slotId', + slotId + ); + return false; + } else { + seenSlotIds.add(slotId); + return true; + } + } + } + ); + + const keys = filtered.map(([key, _]) => key); return { /** diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index b0335ba6e90..3dcafd6118c 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -20,12 +20,17 @@ export interface RecommendationsDefinition * */ export function defineRecommendations( props: RecommendationsProps -): RecommendationsDefinition & {isRecs: true} { +): RecommendationsDefinition & { + isRecs: true; +} & RecommendationsProps['options'] { + // TODO: have an extended recommendationDefinition that is not exposed return { search: true, listing: true, standalone: true, - isRecs: true, + // TODO: encapsulate into a single object called meta (e.g. meta: {isRecs: true, ...props.options}) + isRecs: true, // TODO: mark internal + ...props.options, // TODO: mark internal build: (engine) => buildRecommendations(engine, props), }; } diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index badd60b0401..ee2ec13dd87 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -186,12 +186,22 @@ export function createWaitForActionMiddlewareForRecommendation< const middleware: Middleware = () => (next) => (action) => { next(action); + // if (isDesiredAction(action) && isRecommendationActionPayload(action)) { + // console.log(''); + // console.log('slotId:', action.meta.arg.slotId); + // console.log( + // 'condition: ', + // hasBeenResolved, + // memo.has(action.meta.arg.slotId) + // ); + // } if ( isDesiredAction(action) && !hasBeenResolved && isRecommendationActionPayload(action) && !hasSlotBeenProcessed(action.meta.arg.slotId) ) { + // console.log(' --- RESOLVE ---'); hasBeenResolved = true; memo.add(action.meta.arg.slotId); resolve(action); diff --git a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts index e236754b8c3..2598a2b4d54 100644 --- a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine-config.ts @@ -53,8 +53,14 @@ export default { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, }), - // TODO: check for invalid slotId - // TODO: check for duplicate slotId + popularBoughtRecs_DUPLICATE: defineRecommendations({ + // TODO: support option to run only on specific + options: { + slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', + }, + }), + // TODO: [x] check for invalid slotId => it will reject as expected + // TODO: [ ] check for duplicate slotId // TODO: encounter for multiple recommendations with same slot id // popwularBoughtRecs: defineRecommendations({ // options: { From de430495d723f33f333f9422cae35292e02b4e3b Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sun, 3 Nov 2024 07:05:24 -0500 Subject: [PATCH 05/48] clean --- .../src/app/commerce-ssr-engine/common.ts | 49 ++++++++++++------- .../headless-recommendations.ssr.ts | 18 ++++--- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index a103fdf0546..6c3d8cb039d 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,4 +1,5 @@ import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; +import {RecommendationsDefinitionMeta} from '../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; import {InvalidControllerDefinition} from '../../utils/errors.js'; import {filterObject, mapObject} from '../../utils/utils.js'; @@ -104,31 +105,45 @@ export function ensureAtLeastOneSolutionType( throw new InvalidControllerDefinition(); } } + export function buildRecommendationFilter< TEngine extends CoreEngine | CoreEngineNext, TControllerDefinitions extends ControllerDefinitionsMap, >(controllerDefinitions: TControllerDefinitions) { - const seenSlotIds = new Set(); + const slotIdSet = new Set(); + + const isRecommendationDefinition = < + C extends ControllerDefinition, + >( + controller: C + ): controller is C & RecommendationsDefinitionMeta => { + return '_recommendationProps' in controller; + }; + + const warnDuplicateRecommendation = (slotId: string, productId?: string) => { + console.warn( + 'Multiple recommendation controllers found for the same slotId and productId', + {slotId, productId} + ); + }; + const filtered = Object.entries(controllerDefinitions).filter( ([_, value]) => { - if ('isRecs' in value && value.isRecs) { - const slotId = (value as unknown as {slotId: string}).slotId; // TODO: fix type and CLEAN THAT! - // TODO: use a combination of slotId and productId name to identify the controller - if (seenSlotIds.has(slotId)) { - console.log( - 'WARNING: Multiple recommendation controllers found for the same slotId', - slotId - ); - return false; - } else { - seenSlotIds.add(slotId); - return true; - } + if (!isRecommendationDefinition(value)) { + return false; + } + const {slotId, productId} = value._recommendationProps; + const key = `${slotId}${productId || ''}`; + if (slotIdSet.has(key)) { + warnDuplicateRecommendation(slotId, productId); + return false; } + slotIdSet.add(key); + return true; } ); - const keys = filtered.map(([key, _]) => key); + const name = filtered.map(([name, _]) => name); return { /** @@ -137,7 +152,7 @@ export function buildRecommendationFilter< * @returns {number} The number of recommendation controllers in the controller definition map */ get count() { - return keys.length; + return name.length; }, /** @@ -146,7 +161,7 @@ export function buildRecommendationFilter< * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. */ refresh(controllers: Record) { - const isRecommendationController = (key: string) => keys.includes(key); + const isRecommendationController = (key: string) => name.includes(key); Object.entries(controllers) .filter(([key, _]) => isRecommendationController(key)) diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index 3dcafd6118c..542426a2377 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -8,6 +8,13 @@ import { export type {Recommendations, RecommendationsState}; +/** + * @internal + * */ +export type RecommendationsDefinitionMeta = { + _recommendationProps: {} & RecommendationsProps['options']; +}; + export interface RecommendationsDefinition extends UniversalControllerDefinitionWithoutProps {} /** @@ -20,17 +27,14 @@ export interface RecommendationsDefinition * */ export function defineRecommendations( props: RecommendationsProps -): RecommendationsDefinition & { - isRecs: true; -} & RecommendationsProps['options'] { - // TODO: have an extended recommendationDefinition that is not exposed +): RecommendationsDefinition & RecommendationsDefinitionMeta { return { search: true, listing: true, standalone: true, - // TODO: encapsulate into a single object called meta (e.g. meta: {isRecs: true, ...props.options}) - isRecs: true, // TODO: mark internal - ...props.options, // TODO: mark internal + _recommendationProps: { + ...props.options, + }, build: (engine) => buildRecommendations(engine, props), }; } From 9254b93383efb6b5fe79fe53d4ac1e0f4453e548 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Thu, 14 Nov 2024 10:38:58 -0500 Subject: [PATCH 06/48] draft --- .../commerce-engine/commerce-engine.ssr.ts | 113 +++++++++++++----- .../src/app/commerce-ssr-engine/common.ts | 4 +- .../app/commerce-ssr-engine/types/common.ts | 21 ++++ .../commerce-ssr-engine/types/core-engine.ts | 59 ++++++--- .../src/app/ssr-engine/types/build.ts | 18 +++ .../ssr-engine/types/fetch-static-state.ts | 29 +++++ .../headless-recommendations.ssr.ts | 12 +- 7 files changed, 201 insertions(+), 55 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index cb12cf95f40..ed2714de111 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -81,16 +81,31 @@ function buildSSRCommerceEngine( typeof createWaitForActionMiddleware >; + const middlewares: ReturnType[] = []; + const memo: Set = new Set(); + switch (solutionType) { case SolutionType.listing: actionCompletionMiddleware = createWaitForActionMiddleware( isListingFetchCompletedAction ); + middlewares.push(actionCompletionMiddleware); break; case SolutionType.search: actionCompletionMiddleware = createWaitForActionMiddleware( isSearchCompletedAction ); + middlewares.push(actionCompletionMiddleware); + break; + case SolutionType.recommendation: + middlewares.push( + ...Array.from({length: recommendationCount}, () => + createWaitForActionMiddlewareForRecommendation( + isRecommendationCompletedAction, + memo + ) + ) + ); break; default: actionCompletionMiddleware = createWaitForActionMiddleware( @@ -98,22 +113,11 @@ function buildSSRCommerceEngine( ); } - const memo: Set = new Set(); - const recommendationActionMiddlewares = Array.from( - {length: recommendationCount}, - () => - createWaitForActionMiddlewareForRecommendation( - isRecommendationCompletedAction, - memo - ) - ); - const commerceEngine = buildCommerceEngine({ ...options, middlewares: [ ...(options.middlewares ?? []), - actionCompletionMiddleware.middleware, - ...recommendationActionMiddlewares.map(({middleware}) => middleware), + ...middlewares.map(({middleware}) => middleware), ], }); @@ -125,10 +129,7 @@ function buildSSRCommerceEngine( }, waitForRequestCompletedAction() { - return [ - actionCompletionMiddleware.promise, - ...recommendationActionMiddlewares.map(({promise}) => promise), - ]; + return [...middlewares.map(({promise}) => promise)]; }, }; } @@ -169,6 +170,10 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.standalone >; + recommendationEngineDefinition: CommerceEngineDefinition< + TControllerDefinitions, + SolutionType.recommendation + >; } { const {controllers: controllerDefinitions, ...engineOptions} = options; type Definition = CommerceEngineDefinition< @@ -176,7 +181,7 @@ export function defineCommerceEngine< SolutionType >; type BuildFunction = Definition['build']; - type FetchStaticStateFunction = Definition['fetchStaticState']; + type FetchStaticStateFunction = Definition['fetchStaticState']; // TODO: avoir un fetch pour les recommendations type HydrateStaticStateFunction = Definition['hydrateStaticState']; type FetchStaticStateFromBuildResultFunction = FetchStaticStateFunction['fromBuildResult']; @@ -190,9 +195,9 @@ export function defineCommerceEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; - const recommendationFilter = buildRecommendationFilter( - controllerDefinitions ?? {} - ); + // TODO: ideally , we only want to execute that for recommendation stuff + const recommendationFilter = () => + buildRecommendationFilter(controllerDefinitions ?? {}); const getOptions = () => { return engineOptions; @@ -203,7 +208,6 @@ export function defineCommerceEngine< ) => { engineOptions.navigatorContextProvider = navigatorContextProvider; }; - const buildFactory = (solutionType: T) => async (...[buildOptions]: BuildParameters) => { @@ -212,7 +216,10 @@ export function defineCommerceEngine< buildOptions?.extend ? await buildOptions.extend(getOptions()) : getOptions(), - recommendationFilter.count + // TODO: clean that + solutionType === SolutionType.recommendation + ? recommendationFilter().count + : 0 ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -258,14 +265,19 @@ export function defineCommerceEngine< }, ] = params; - if (solutionType === SolutionType.listing) { - buildProductListing(engine).executeFirstRequest(); - } else if (solutionType === SolutionType.search) { - buildSearch(engine).executeFirstSearch(); + switch (solutionType) { + case SolutionType.listing: + buildProductListing(engine).executeFirstRequest(); + break; + case SolutionType.search: + buildSearch(engine).executeFirstSearch(); + break; + case SolutionType.recommendation: + recommendationFilter().refresh(controllers); + break; } - recommendationFilter.refresh(controllers); - + // TODO: should be only one searchAction for search and listing const searchActions = await Promise.all( engine.waitForRequestCompletedAction() ); @@ -317,6 +329,7 @@ export function defineCommerceEngine< }, ] = params; + // TODO: should be only one searchAction for search and listing searchActions.forEach((action) => { engine.dispatch(action); }); @@ -325,6 +338,7 @@ export function defineCommerceEngine< }, } ); + return { listingEngineDefinition: { build: buildFactory(SolutionType.listing), @@ -338,6 +352,18 @@ export function defineCommerceEngine< hydrateStaticState: hydrateStaticStateFactory(SolutionType.search), setNavigatorContextProvider, } as CommerceEngineDefinition, + recommendationEngineDefinition: { + build: buildFactory(SolutionType.recommendation), + fetchStaticState: fetchStaticStateFactory(SolutionType.recommendation), + hydrateStaticState: hydrateStaticStateFactory( + SolutionType.recommendation + ), + setNavigatorContextProvider, + } as CommerceEngineDefinition< + TControllerDefinitions, + SolutionType.recommendation + >, + // TODO: make the standaloneEngineDefinition not async since there are no search executed standaloneEngineDefinition: { build: buildFactory(SolutionType.standalone), fetchStaticState: fetchStaticStateFactory(SolutionType.standalone), @@ -349,3 +375,34 @@ export function defineCommerceEngine< >, }; } + +/// Sandbox +// const { +// recommendationEngineDefinition, +// searchEngineDefinition, +// standaloneEngineDefinition, +// } = defineCommerceEngine({ +// configuration: getSampleCommerceEngineConfiguration(), +// controllers: { +// standaloneSearchBox: defineStandaloneSearchBox({ +// options: {redirectionUrl: 'rest'}, +// }), +// facets: defineFacetGenerator(), +// trending: defineRecommendations({ +// options: {slotId: 'ttt'}, +// }), +// popular: defineRecommendations({ +// options: {slotId: 'ppp'}, +// }), +// }, +// }); + +// // TODO: should have a way to select which recommendation to fetch +// const r = await standaloneEngineDefinition.fetchStaticState(); +// r.controllers.standaloneSearchBox; + +// const b = await recommendationEngineDefinition.fetchStaticState(['trending']); +// b.controllers.trending; + +// const a = await searchEngineDefinition.fetchStaticState(); +// a.controllers; // TODO: should throw an error since it's not defined in search diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 6c3d8cb039d..248cb51ad5e 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -117,7 +117,7 @@ export function buildRecommendationFilter< >( controller: C ): controller is C & RecommendationsDefinitionMeta => { - return '_recommendationProps' in controller; + return 'recommendation' in controller; }; const warnDuplicateRecommendation = (slotId: string, productId?: string) => { @@ -132,7 +132,7 @@ export function buildRecommendationFilter< if (!isRecommendationDefinition(value)) { return false; } - const {slotId, productId} = value._recommendationProps; + const {slotId, productId} = value.options; const key = `${slotId}${productId || ''}`; if (slotIdSet.has(key)) { warnDuplicateRecommendation(slotId, productId); diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index fdf21309c0e..6bbbb3101ba 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -19,6 +19,7 @@ export enum SolutionType { search = 'search', listing = 'listing', standalone = 'standalone', + recommendation = 'recommendation', } export interface ControllerDefinitionWithoutProps< @@ -172,6 +173,13 @@ interface ListingOnlyController { [SolutionType.listing]: true; } +interface RecommendationOnlyController { + /** + * @internal + */ + [SolutionType.recommendation]: true; +} + interface SearchAndListingController { /** * @internal @@ -205,6 +213,17 @@ export type ListingOnlyControllerDefinitionWithProps< > = ControllerDefinitionWithProps & ListingOnlyController; +export type RecommendationOnlyControllerDefinitionWithoutProps< + TController extends Controller, +> = ControllerDefinitionWithoutProps & + RecommendationOnlyController; + +export type RecommendationOnlyControllerDefinitionWithProps< + TController extends Controller, + TProps, +> = ControllerDefinitionWithProps & + RecommendationOnlyController; + export type UniversalControllerDefinitionWithoutProps< TController extends Controller, > = ControllerDefinitionWithoutProps & @@ -233,6 +252,7 @@ export type SubControllerDefinitionWithoutProps< : TDefinition extends {listing: false; search: false} ? InvalidControllerDefinition : never; +// TODO: add recommendation type here export type SubControllerDefinitionWithProps< TController extends Controller, @@ -247,3 +267,4 @@ export type SubControllerDefinitionWithProps< : TDefinition extends {listing: false; search: false} ? InvalidControllerDefinition : never; +// TODO: add recommendation type here diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index a9d39706164..3ff206497fe 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -3,9 +3,12 @@ import type {Controller} from '../../../controllers/controller/headless-controll import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; -import {Build} from '../../ssr-engine/types/build.js'; +import {Build, BuildWithList} from '../../ssr-engine/types/build.js'; import {InferControllerPropsMapFromDefinitions} from '../../ssr-engine/types/common.js'; -import {FetchStaticState} from '../../ssr-engine/types/fetch-static-state.js'; +import { + FetchStaticState, + FetchStaticStateWithList, +} from '../../ssr-engine/types/fetch-static-state.js'; import {HydrateStaticState} from '../../ssr-engine/types/hydrate-static-state.js'; import { ControllerDefinitionsMap, @@ -37,19 +40,32 @@ export interface EngineDefinition< /** * Fetches the static state on the server side using your engine definition. */ - fetchStaticState: FetchStaticState< - TEngine, - InferControllersMapFromDefinition, - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >, - InferControllerPropsMapFromDefinitions - >; + // TODO: simplify be removing code duplication + fetchStaticState: TSolutionType extends SolutionType.recommendation + ? FetchStaticStateWithList< + TEngine, + InferControllersMapFromDefinition, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >, + InferControllerPropsMapFromDefinitions + > + : FetchStaticState< + TEngine, + InferControllersMapFromDefinition, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >, + InferControllerPropsMapFromDefinitions + >; /** * Fetches the hydrated state on the client side using your engine definition and the static state. */ + // TODO: Apply the same logic to recommendation hydration hydrateStaticState: HydrateStaticState< TEngine, InferControllersMapFromDefinition, @@ -59,12 +75,19 @@ export interface EngineDefinition< /** * Builds an engine and its controllers from an engine definition. */ - build: Build< - TEngine, - TEngineOptions, - InferControllersMapFromDefinition, - InferControllerPropsMapFromDefinitions - >; + build: TSolutionType extends SolutionType.recommendation + ? BuildWithList< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition, + InferControllerPropsMapFromDefinitions + > + : Build< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition, + InferControllerPropsMapFromDefinitions + >; /** * Sets the navigator context provider. diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index e7dcabba4dd..bb6c9eca00b 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -12,6 +12,24 @@ export interface BuildOptions { extend?: OptionsExtender; } +export interface BuildWithList< + TEngine extends CoreEngine | CoreEngineNext, + TEngineOptions, + TControllersMap extends ControllersMap, + TControllersProps extends ControllersPropsMap, +> { + /** + * Initializes an engine and controllers from the definition. + */ + ( + c: (keyof TControllersMap)[], + ...params: OptionsTuple< + BuildOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; +} + export interface Build< TEngine extends CoreEngine | CoreEngineNext, TEngineOptions, diff --git a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts index a1f87a7781a..ad02be7dba8 100644 --- a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts @@ -38,3 +38,32 @@ export type FetchStaticState< EngineStaticState >; }; + +// TODO: find a better name +export type FetchStaticStateWithList< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TSearchAction extends UnknownAction, + TControllersStaticState extends ControllerStaticStateMap, + TControllersProps extends ControllersPropsMap, +> = { + /** + * Executes only the initial search for a given configuration, then returns a resumable snapshot of engine state along with the state of the controllers. + * + * Useful for static generation and server-side rendering. + */ + ( + c: (keyof TControllers)[], + ...params: OptionsTuple< + FetchStaticStateOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; + + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + FetchStaticStateOptions, + EngineStaticState + >; +}; diff --git a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts index 542426a2377..23c6b0162ba 100644 --- a/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts +++ b/packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {RecommendationOnlyControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import {RecommendationsState} from '../recommendations/headless-recommendations.js'; import { RecommendationsProps, @@ -12,11 +12,11 @@ export type {Recommendations, RecommendationsState}; * @internal * */ export type RecommendationsDefinitionMeta = { - _recommendationProps: {} & RecommendationsProps['options']; + options: {} & RecommendationsProps['options']; }; export interface RecommendationsDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends RecommendationOnlyControllerDefinitionWithoutProps {} /** * @internal * Defines a `Recommendations` controller instance. @@ -29,10 +29,8 @@ export function defineRecommendations( props: RecommendationsProps ): RecommendationsDefinition & RecommendationsDefinitionMeta { return { - search: true, - listing: true, - standalone: true, - _recommendationProps: { + recommendation: true, + options: { ...props.options, }, build: (engine) => buildRecommendations(engine, props), From f2d1fa1413f443aee80b50d2a0ce11ffb9bc76f6 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 15 Nov 2024 14:20:23 -0500 Subject: [PATCH 07/48] working draft --- .../commerce-engine/commerce-engine.ssr.ts | 122 ++++++++++++++---- .../src/app/commerce-ssr-engine/common.ts | 35 ++++- .../app/commerce-ssr-engine/types/build.ts | 42 ++++++ .../app/commerce-ssr-engine/types/common.ts | 14 +- .../commerce-ssr-engine/types/core-engine.ts | 56 ++++---- .../types/fetch-static-state.ts | 60 +++++++++ .../types/from-build-result.ts | 23 ++++ .../types/hydrate-static-state.ts | 43 ++++++ .../app/search-engine/search-engine.ssr.ts | 57 ++------ .../headless/src/app/ssr-engine/common.ts | 6 +- .../src/app/ssr-engine/types/build.ts | 10 +- .../src/app/ssr-engine/types/common.ts | 2 +- .../ssr-engine/types/hydrate-static-state.ts | 2 +- .../headless-core-parameter-manager.ssr.ts | 1 + .../app/_components/pages/recommendation.tsx | 19 +-- .../app/_lib/commerce-engine.ts | 8 ++ .../app/recommendation/page.tsx | 10 +- 17 files changed, 372 insertions(+), 138 deletions(-) create mode 100644 packages/headless/src/app/commerce-ssr-engine/types/build.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index ed2714de111..09610a3e59c 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -6,6 +6,13 @@ import {stateKey} from '../../app/state-key.js'; import {buildProductListing} from '../../controllers/commerce/product-listing/headless-product-listing.js'; import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; +// import { +// defineContext, +// defineParameterManager, +// defineRecommendations, +// defineStandaloneSearchBox, +// getSampleCommerceEngineConfiguration, +// } from '../../ssr-commerce.index.js'; import { createWaitForActionMiddleware, createWaitForActionMiddlewareForRecommendation, @@ -13,9 +20,13 @@ import { import { buildControllerDefinitions, buildRecommendationFilter, + createStaticState, } from '../commerce-ssr-engine/common.js'; import { ControllerDefinitionsMap, + EngineStaticState, + InferControllerPropsMapFromDefinitions, + InferControllersMapFromDefinition, InferControllerStaticStateMapFromDefinitionsWithSolutionType, SolutionType, } from '../commerce-ssr-engine/types/common.js'; @@ -25,11 +36,6 @@ import { } from '../commerce-ssr-engine/types/core-engine.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; import {composeFunction} from '../ssr-engine/common.js'; -import {createStaticState} from '../ssr-engine/common.js'; -import { - EngineStaticState, - InferControllerPropsMapFromDefinitions, -} from '../ssr-engine/types/common.js'; import { CommerceEngine, CommerceEngineOptions, @@ -196,6 +202,7 @@ export function defineCommerceEngine< Parameters; // TODO: ideally , we only want to execute that for recommendation stuff + // TODO: get rid of that here. need to be computed in the fetch static state now const recommendationFilter = () => buildRecommendationFilter(controllerDefinitions ?? {}); @@ -209,17 +216,15 @@ export function defineCommerceEngine< engineOptions.navigatorContextProvider = navigatorContextProvider; }; const buildFactory = - (solutionType: T) => + (solutionType: T, count: number = 0) => async (...[buildOptions]: BuildParameters) => { const engine = buildSSRCommerceEngine( solutionType, - buildOptions?.extend + buildOptions && 'extend' in buildOptions && buildOptions?.extend ? await buildOptions.extend(getOptions()) : getOptions(), // TODO: clean that - solutionType === SolutionType.recommendation - ? recommendationFilter().count - : 0 + solutionType === SolutionType.recommendation ? count : 0 // TODO: avoid this by creating a build factory for recs ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -272,9 +277,6 @@ export function defineCommerceEngine< case SolutionType.search: buildSearch(engine).executeFirstSearch(); break; - case SolutionType.recommendation: - recommendationFilter().refresh(controllers); - break; } // TODO: should be only one searchAction for search and listing @@ -296,6 +298,77 @@ export function defineCommerceEngine< } ); + // TODO: enlever ce hack! + const controllerList: Set = new Set(); + + // TODO: remove factory + const fetchStaticStateFactoryForRecommendation: () => FetchStaticStateFunction = + () => + composeFunction( + async (...params: FetchStaticStateParameters) => { + // TODO: no need for ...params since it is a list of controllers + console.log(params, ((params || [])[0] as string[])?.length); // Something is wrong with the type here + // FIXME: just WOW + ((params || [])[0] as Array).forEach((controller) => { + controllerList.add(controller); + }); + if (!getOptions().navigatorContextProvider) { + // TODO: KIT-3409 - implement a logger to log SSR warnings/errors + console.warn( + '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' + ); + } + + const buildResult = (await buildFactory( + SolutionType.recommendation, + ((params || [])[0] as string[])?.length // TODO: fix that mess + )(...params)) as { + engine: SSRCommerceEngine; + controllers: InferControllersMapFromDefinition< + TControllerDefinitions, + SolutionType + >; + }; // TODO: check if can remove the cast + const staticState = + await fetchStaticStateFactoryForRecommendation().fromBuildResult({ + buildResult, + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: FetchStaticStateFromBuildResultParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + }, + ] = params; + + recommendationFilter().refresh( + controllers, + Array.from(controllerList) // TODO: find the right type + ); // TODO: filter out the controllers to only include the one in the static state params + + // TODO: should be only one searchAction for search and listing + const searchActions = await Promise.all( + engine.waitForRequestCompletedAction() + ); + + return createStaticState({ + searchActions, + controllers, + }) as EngineStaticState< + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllerDefinitions, + SolutionType + > + >; + }, + } + ); + const hydrateStaticStateFactory: ( solutionType: SolutionType ) => HydrateStaticStateFunction = (solutionType: SolutionType) => @@ -354,7 +427,7 @@ export function defineCommerceEngine< } as CommerceEngineDefinition, recommendationEngineDefinition: { build: buildFactory(SolutionType.recommendation), - fetchStaticState: fetchStaticStateFactory(SolutionType.recommendation), + fetchStaticState: fetchStaticStateFactoryForRecommendation(), hydrateStaticState: hydrateStaticStateFactory( SolutionType.recommendation ), @@ -376,7 +449,7 @@ export function defineCommerceEngine< }; } -/// Sandbox +// // Sandbox // const { // recommendationEngineDefinition, // searchEngineDefinition, @@ -387,7 +460,8 @@ export function defineCommerceEngine< // standaloneSearchBox: defineStandaloneSearchBox({ // options: {redirectionUrl: 'rest'}, // }), -// facets: defineFacetGenerator(), +// facets: defineContext(), +// searchParam: defineParameterManager({search: false}), // trending: defineRecommendations({ // options: {slotId: 'ttt'}, // }), @@ -398,11 +472,15 @@ export function defineCommerceEngine< // }); // // TODO: should have a way to select which recommendation to fetch -// const r = await standaloneEngineDefinition.fetchStaticState(); -// r.controllers.standaloneSearchBox; +// const r = await standaloneEngineDefinition.fetchStaticState({ +// controllers: {searchParam: {initialState: {parameters: {q: 'test'}}}}, +// }); -// const b = await recommendationEngineDefinition.fetchStaticState(['trending']); -// b.controllers.trending; +// const b = await recommendationEngineDefinition.fetchStaticState([ +// 'trending', +// 'popular', +// ]); +// // b.controllers.; -// const a = await searchEngineDefinition.fetchStaticState(); -// a.controllers; // TODO: should throw an error since it's not defined in search +// const a = await searchEngineDefinition.fetchStaticState(); // TODO: fix typing if controller is set to {search: false} +// // a.controllers.; // TODO: should throw an error since it's not defined in search diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 248cb51ad5e..2462bc462b3 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,20 +1,44 @@ +import {UnknownAction} from '@reduxjs/toolkit'; import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; import {RecommendationsDefinitionMeta} from '../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; import {InvalidControllerDefinition} from '../../utils/errors.js'; -import {filterObject, mapObject} from '../../utils/utils.js'; +import {clone, filterObject, mapObject} from '../../utils/utils.js'; import {CoreEngine, CoreEngineNext} from '../engine.js'; -import {InferControllerPropsMapFromDefinitions} from '../ssr-engine/types/common.js'; +import { + ControllersMap, + InferControllerStaticStateMapFromControllers, +} from '../ssr-engine/types/common.js'; import { ControllerDefinition, ControllerDefinitionOption, ControllerDefinitionsMap, + EngineStaticState, InferControllerFromDefinition, InferControllerPropsFromDefinition, + InferControllerPropsMapFromDefinitions, InferControllersMapFromDefinition, SolutionType, } from './types/common.js'; +export function createStaticState({ + searchActions, + controllers, +}: { + searchActions: TSearchAction[]; + controllers: ControllersMap; +}): EngineStaticState< + TSearchAction, + InferControllerStaticStateMapFromControllers +> { + return { + controllers: mapObject(controllers, (controller) => ({ + state: clone(controller.state), + })) as InferControllerStaticStateMapFromControllers, + searchActions: searchActions.map((action) => clone(action)), + }; +} + function buildControllerFromDefinition< TControllerDefinition extends ControllerDefinition, TEngine extends CoreEngine | CoreEngineNext, @@ -159,9 +183,12 @@ export function buildRecommendationFilter< * Go through all the controllers passed in argument and only refresh recommendation controllers. * * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. + * @param controllerNames - A list of all recommendation controllers to refresh */ - refresh(controllers: Record) { - const isRecommendationController = (key: string) => name.includes(key); + refresh(controllers: Record, whitelist: string[]) { + // TODO: FIND a better way + const isRecommendationController = (key: string) => + name.includes(key) && whitelist.includes(key); Object.entries(controllers) .filter(([key, _]) => isRecommendationController(key)) diff --git a/packages/headless/src/app/commerce-ssr-engine/types/build.ts b/packages/headless/src/app/commerce-ssr-engine/types/build.ts new file mode 100644 index 00000000000..eae01f77a15 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/types/build.ts @@ -0,0 +1,42 @@ +import {CoreEngine, CoreEngineNext} from '../../engine.js'; +import { + ControllersMap, + ControllersPropsMap, + EngineDefinitionBuildResult, + EngineDefinitionControllersPropsOption, + OptionsExtender, + OptionsTuple, +} from '../../ssr-engine/types/common.js'; + +export interface BuildOptions { + extend?: OptionsExtender; +} + +export interface BuildWithForRecommendations< + TEngine extends CoreEngine | CoreEngineNext, + TControllersMap extends ControllersMap, +> { + /** + * Initializes an engine and controllers from the definition. + */ + ( + c: (keyof TControllersMap)[] + ): Promise>; +} + +export interface Build< + TEngine extends CoreEngine | CoreEngineNext, + TEngineOptions, + TControllersMap extends ControllersMap, + TControllersProps extends ControllersPropsMap, +> { + /** + * Initializes an engine and controllers from the definition. + */ + ( + ...params: OptionsTuple< + BuildOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; +} diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index 6bbbb3101ba..845f0e0fd4f 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -1,3 +1,4 @@ +import {UnknownAction} from '@reduxjs/toolkit'; import type {Controller} from '../../../controllers/controller/headless-controller.js'; import type {InvalidControllerDefinition} from '../../../utils/errors.js'; import type {CommerceEngine} from '../../commerce-engine/commerce-engine.js'; @@ -7,6 +8,7 @@ import type { InferControllerStaticStateMapFromControllers, InferControllerStaticStateFromController, InferControllerPropsMapFromDefinitions, + ControllerStaticStateMap, } from '../../ssr-engine/types/common.js'; export type { @@ -56,6 +58,14 @@ export interface ControllerDefinitionWithProps< ): TController; } +export interface EngineStaticState< + TSearchAction extends UnknownAction, + TControllers extends ControllerStaticStateMap, +> { + searchActions: TSearchAction[]; + controllers: TControllers; +} + export type ControllerDefinition< TEngine extends CoreEngine | CoreEngineNext, TController extends Controller, @@ -252,7 +262,7 @@ export type SubControllerDefinitionWithoutProps< : TDefinition extends {listing: false; search: false} ? InvalidControllerDefinition : never; -// TODO: add recommendation type here +// TODO: add recommendation type here! export type SubControllerDefinitionWithProps< TController extends Controller, @@ -267,4 +277,4 @@ export type SubControllerDefinitionWithProps< : TDefinition extends {listing: false; search: false} ? InvalidControllerDefinition : never; -// TODO: add recommendation type here +// TODO: add recommendation type here! diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 3ff206497fe..cf1cb3a944f 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -3,19 +3,17 @@ import type {Controller} from '../../../controllers/controller/headless-controll import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; -import {Build, BuildWithList} from '../../ssr-engine/types/build.js'; -import {InferControllerPropsMapFromDefinitions} from '../../ssr-engine/types/common.js'; -import { - FetchStaticState, - FetchStaticStateWithList, -} from '../../ssr-engine/types/fetch-static-state.js'; -import {HydrateStaticState} from '../../ssr-engine/types/hydrate-static-state.js'; +import {Build} from '../../ssr-engine/types/build'; +import {BuildWithForRecommendations} from './build.js'; import { ControllerDefinitionsMap, InferControllersMapFromDefinition, SolutionType, InferControllerStaticStateMapFromDefinitionsWithSolutionType, + InferControllerPropsMapFromDefinitions, } from './common.js'; +import {FetchStaticState} from './fetch-static-state.js'; +import {HydrateStaticState} from './hydrate-static-state.js'; export type {HydrateStaticState, FetchStaticState}; export type EngineDefinitionOptions< @@ -40,32 +38,20 @@ export interface EngineDefinition< /** * Fetches the static state on the server side using your engine definition. */ - // TODO: simplify be removing code duplication - fetchStaticState: TSolutionType extends SolutionType.recommendation - ? FetchStaticStateWithList< - TEngine, - InferControllersMapFromDefinition, - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >, - InferControllerPropsMapFromDefinitions - > - : FetchStaticState< - TEngine, - InferControllersMapFromDefinition, - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >, - InferControllerPropsMapFromDefinitions - >; + fetchStaticState: FetchStaticState< + TEngine, + InferControllersMapFromDefinition, + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >, + InferControllerPropsMapFromDefinitions, + TSolutionType + >; /** * Fetches the hydrated state on the client side using your engine definition and the static state. */ - // TODO: Apply the same logic to recommendation hydration hydrateStaticState: HydrateStaticState< TEngine, InferControllersMapFromDefinition, @@ -76,11 +62,13 @@ export interface EngineDefinition< * Builds an engine and its controllers from an engine definition. */ build: TSolutionType extends SolutionType.recommendation - ? BuildWithList< + ? // TODO: THIS should not be a separate function, inside the build function instead + BuildWithForRecommendations< TEngine, - TEngineOptions, - InferControllersMapFromDefinition, - InferControllerPropsMapFromDefinitions + InferControllersMapFromDefinition< + TControllers, + SolutionType.recommendation + > > : Build< TEngine, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts new file mode 100644 index 00000000000..a1e128046e4 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -0,0 +1,60 @@ +import {UnknownAction} from '@reduxjs/toolkit'; +import {buildBaseCommerceAPIRequest} from '../../../features/commerce/common/actions.js'; +import { + EngineStaticState, + SolutionType, +} from '../../commerce-ssr-engine/types/common.js'; +import {CoreEngine, CoreEngineNext} from '../../engine.js'; +import { + ControllersMap, + ControllersPropsMap, + ControllerStaticStateMap, + EngineDefinitionControllersPropsOption, + OptionsTuple, +} from '../../ssr-engine/types/common.js'; +import {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; + +export type FetchStaticStateOptions = {}; + +export type FetchStaticState< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TSearchAction extends UnknownAction, + TControllersStaticState extends ControllerStaticStateMap, + TControllersProps extends ControllersPropsMap, + TSolutionType extends SolutionType, + FromBuildResultFunction = FromBuildResult< + TEngine, + TControllers, + FetchStaticStateOptions, + EngineStaticState + >, +> = TSolutionType extends SolutionType.recommendation + ? { + /** + * Executes only the initial search for a given configuration, then returns a resumable snapshot of engine state along with the state of the controllers. + * + * Useful for static generation and server-side rendering. + */ + ( + controllers: Array // TODO: make the array unique + ): Promise>; + + fromBuildResult: FromBuildResultFunction; + } + : { + /** + * Executes only the initial search for a given configuration, then returns a resumable snapshot of engine state along with the state of the controllers. + * + * Useful for static generation and server-side rendering. + */ + ( + ...params: OptionsTuple< + FetchStaticStateOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; + + fromBuildResult: FromBuildResultFunction; + }; +buildBaseCommerceAPIRequest; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts new file mode 100644 index 00000000000..184a9bec544 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts @@ -0,0 +1,23 @@ +import {CoreEngine, CoreEngineNext} from '../../engine.js'; +import { + ControllersMap, + EngineDefinitionBuildResult, +} from '../../ssr-engine/types/common.js'; + +export interface FromBuildResultOptions< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, +> { + buildResult: EngineDefinitionBuildResult; +} + +export interface FromBuildResult< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TOptions, + TReturn, +> { + ( + options: FromBuildResultOptions & TOptions + ): Promise; +} diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts new file mode 100644 index 00000000000..e84d47e3a12 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -0,0 +1,43 @@ +import {UnknownAction} from '@reduxjs/toolkit'; +import {CoreEngine, CoreEngineNext} from '../../engine.js'; +import { + ControllersMap, + ControllersPropsMap, + EngineDefinitionControllersPropsOption, + HydratedState, + OptionsTuple, +} from '../../ssr-engine/types/common.js'; +import {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; + +export interface HydrateStaticStateOptions { + searchActions: TSearchAction[]; +} + +// TODO: check if need to create one hydrate function specific for recommendations. +// - If yes, then adjust this. +// - If not, then remove and reuse the interface from ssr-engine +export type HydrateStaticState< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TSearchAction extends UnknownAction, + TControllersProps extends ControllersPropsMap, +> = { + /** + * Creates a new engine from the snapshot of the engine created in SSR with fetchStaticState. + * + * Useful when hydrating a server-side-rendered engine. + */ + ( + ...params: OptionsTuple< + HydrateStaticStateOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; + + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + HydrateStaticStateOptions, + HydratedState + >; +}; diff --git a/packages/headless/src/app/search-engine/search-engine.ssr.ts b/packages/headless/src/app/search-engine/search-engine.ssr.ts index f14b5191c6f..b1361574c7e 100644 --- a/packages/headless/src/app/search-engine/search-engine.ssr.ts +++ b/packages/headless/src/app/search-engine/search-engine.ssr.ts @@ -5,7 +5,6 @@ import {UnknownAction} from '@reduxjs/toolkit'; import type {Controller} from '../../controllers/controller/headless-controller.js'; import {LegacySearchAction} from '../../features/analytics/analytics-utils.js'; import {createWaitForActionMiddleware} from '../../utils/utils.js'; -import {buildRecommendationFilter} from '../commerce-ssr-engine/common.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; import { buildControllerDefinitions, @@ -37,7 +36,7 @@ export interface SSRSearchEngine extends SearchEngine { /** * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. */ - waitForSearchCompletedAction(): Promise[]; + waitForSearchCompletedAction(): Promise; } /** @@ -61,34 +60,13 @@ function isSearchCompletedAction( ); } -function isRecommendationCompletedAction( - action: unknown -): action is SearchCompletedAction { - return /^recommendation\/get\/(fulfilled|rejected)$/.test( - (action as UnknownAction).type - ); -} - -function buildSSRSearchEngine( - options: SearchEngineOptions, - recommendationCount: number -): SSRSearchEngine { +function buildSSRSearchEngine(options: SearchEngineOptions): SSRSearchEngine { const {middleware, promise} = createWaitForActionMiddleware( isSearchCompletedAction ); - - const recommendationActionMiddlewares = Array.from( - {length: recommendationCount}, - () => createWaitForActionMiddleware(isRecommendationCompletedAction) - ); - const searchEngine = buildSearchEngine({ ...options, - middlewares: [ - ...(options.middlewares ?? []), - middleware, - ...recommendationActionMiddlewares.map(({middleware}) => middleware), - ], + middlewares: [...(options.middlewares ?? []), middleware], }); return { ...searchEngine, @@ -96,10 +74,7 @@ function buildSSRSearchEngine( return searchEngine.state; }, waitForSearchCompletedAction() { - return [ - promise, - ...recommendationActionMiddlewares.map(({promise}) => promise), - ]; + return promise; }, }; } @@ -146,10 +121,6 @@ export function defineSearchEngine< type HydrateStaticStateFromBuildResultParameters = Parameters; - const recommendationHelper = buildRecommendationFilter( - controllerDefinitions ?? {} - ); - const getOptions = () => { return engineOptions; }; @@ -164,8 +135,7 @@ export function defineSearchEngine< const engine = buildSSRSearchEngine( buildOptions?.extend ? await buildOptions.extend(getOptions()) - : getOptions(), - recommendationHelper.count + : getOptions() ); const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, @@ -205,14 +175,8 @@ export function defineSearchEngine< ] = params; engine.executeFirstSearch(); - recommendationHelper.refresh(controllers); - - const searchActions = await Promise.all( - engine.waitForSearchCompletedAction() - ); - return createStaticState({ - searchActions, + searchAction: await engine.waitForSearchCompletedAction(), controllers, }) as EngineStaticState< UnknownAction, @@ -233,7 +197,7 @@ export function defineSearchEngine< const buildResult = await build(...(params as BuildParameters)); const staticState = await hydrateStaticState.fromBuildResult({ buildResult, - searchActions: params[0]!.searchActions, + searchAction: params[0]!.searchAction, }); return staticState; }, @@ -244,13 +208,10 @@ export function defineSearchEngine< const [ { buildResult: {engine, controllers}, - searchActions, + searchAction, }, ] = params; - - searchActions.forEach((action) => { - engine.dispatch(action); - }); + engine.dispatch(searchAction); await engine.waitForSearchCompletedAction(); return {engine, controllers}; }, diff --git a/packages/headless/src/app/ssr-engine/common.ts b/packages/headless/src/app/ssr-engine/common.ts index 79eef38ef33..26b0503f1dd 100644 --- a/packages/headless/src/app/ssr-engine/common.ts +++ b/packages/headless/src/app/ssr-engine/common.ts @@ -58,10 +58,10 @@ export function buildControllerDefinitions< } export function createStaticState({ - searchActions, + searchAction, controllers, }: { - searchActions: TSearchAction[]; + searchAction: TSearchAction; controllers: ControllersMap; }): EngineStaticState< TSearchAction, @@ -71,7 +71,7 @@ export function createStaticState({ controllers: mapObject(controllers, (controller) => ({ state: clone(controller.state), })) as InferControllerStaticStateMapFromControllers, - searchActions: searchActions.map((action) => clone(action)), + searchAction: clone(searchAction), }; } diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index bb6c9eca00b..62b11178821 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -12,21 +12,15 @@ export interface BuildOptions { extend?: OptionsExtender; } -export interface BuildWithList< +export interface BuildWithForRecommendations< TEngine extends CoreEngine | CoreEngineNext, - TEngineOptions, TControllersMap extends ControllersMap, - TControllersProps extends ControllersPropsMap, > { /** * Initializes an engine and controllers from the definition. */ ( - c: (keyof TControllersMap)[], - ...params: OptionsTuple< - BuildOptions & - EngineDefinitionControllersPropsOption - > + controllers: (keyof TControllersMap)[] ): Promise>; } diff --git a/packages/headless/src/app/ssr-engine/types/common.ts b/packages/headless/src/app/ssr-engine/types/common.ts index 853beb17cc5..831b23ec423 100644 --- a/packages/headless/src/app/ssr-engine/types/common.ts +++ b/packages/headless/src/app/ssr-engine/types/common.ts @@ -105,7 +105,7 @@ export interface EngineStaticState< TSearchAction extends UnknownAction, TControllers extends ControllerStaticStateMap, > { - searchActions: TSearchAction[]; + searchAction: TSearchAction; controllers: TControllers; } diff --git a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts index 9f4a265badb..f7164fa0932 100644 --- a/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/hydrate-static-state.ts @@ -10,7 +10,7 @@ import { import {FromBuildResult} from './from-build-result.js'; export interface HydrateStaticStateOptions { - searchActions: TSearchAction[]; + searchAction: TSearchAction; } export type HydrateStaticState< diff --git a/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts b/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts index 2a4917db7d0..4b783b7453b 100644 --- a/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts @@ -46,6 +46,7 @@ export function defineParameterManager< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + // TODO: we do now want to have to set the props if we set standalone to false... same thing with other components like cart ...options, buildWithProps: (engine, props, solutionType) => { if (solutionType === SolutionType.listing) { diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx index 90ffd1d4600..e3c7d60fcc5 100644 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx +++ b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx @@ -3,9 +3,9 @@ import {NavigatorContext} from '@coveo/headless/ssr-commerce'; import {useEffect, useState} from 'react'; import { - StandaloneStaticState, - StandaloneHydratedState, - standaloneEngineDefinition, + RecommendationStaticState, + RecommendationHydratedState, + recommendationEngineDefinition, } from '../../_lib/commerce-engine'; import {Recommendations} from '../recommendation-list'; @@ -13,30 +13,25 @@ export default function Recommendation({ staticState, navigatorContext, }: { - staticState: StandaloneStaticState; + staticState: RecommendationStaticState; navigatorContext: NavigatorContext; }) { const [hydratedState, setHydratedState] = useState< - StandaloneHydratedState | undefined + RecommendationHydratedState | undefined >(undefined); // Setting the navigator context provider also in client-side before hydrating the application - standaloneEngineDefinition.setNavigatorContextProvider( + recommendationEngineDefinition.setNavigatorContextProvider( () => navigatorContext ); useEffect(() => { - standaloneEngineDefinition + recommendationEngineDefinition .hydrateStaticState({ searchActions: staticState.searchActions, }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); - - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - controllers.popularBoughtRecs.refresh(); - controllers.popularViewedRecs.refresh(); }); }, [staticState]); diff --git a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine.ts index 0d08774ac59..510d9aaaee8 100644 --- a/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/app/_lib/commerce-engine.ts @@ -11,6 +11,7 @@ export const { listingEngineDefinition, searchEngineDefinition, standaloneEngineDefinition, + recommendationEngineDefinition, } = engineDefinition; export type ListingStaticState = InferStaticState< @@ -31,3 +32,10 @@ export type StandaloneStaticState = InferStaticState< export type StandaloneHydratedState = InferHydratedState< typeof standaloneEngineDefinition >; + +export type RecommendationStaticState = InferStaticState< + typeof recommendationEngineDefinition +>; +export type RecommendationHydratedState = InferHydratedState< + typeof recommendationEngineDefinition +>; diff --git a/packages/samples/headless-ssr-commerce/app/recommendation/page.tsx b/packages/samples/headless-ssr-commerce/app/recommendation/page.tsx index 3a27fdbfe0f..1c60d4ab0f9 100644 --- a/packages/samples/headless-ssr-commerce/app/recommendation/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/recommendation/page.tsx @@ -1,6 +1,6 @@ import {headers} from 'next/headers'; import Recommendation from '../_components/pages/recommendation'; -import {standaloneEngineDefinition} from '../_lib/commerce-engine'; +import {recommendationEngineDefinition} from '../_lib/commerce-engine'; import {NextJsNavigatorContext} from '../_lib/navigatorContextProvider'; /** @@ -11,12 +11,16 @@ import {NextJsNavigatorContext} from '../_lib/navigatorContextProvider'; export default async function RecommendationPage() { // Sets the navigator context provider to use the newly created `navigatorContext` before fetching the app static state const navigatorContext = new NextJsNavigatorContext(headers()); - standaloneEngineDefinition.setNavigatorContextProvider( + recommendationEngineDefinition.setNavigatorContextProvider( () => navigatorContext ); // Fetches the static state of the app with initial state (when applicable) - const staticState = await standaloneEngineDefinition.fetchStaticState(); + const staticState = await recommendationEngineDefinition.fetchStaticState([ + 'popularBoughtRecs', + 'popularViewedRecs', + ]); + // TODO: cannot have the same controller twice return ( Date: Fri, 15 Nov 2024 14:21:14 -0500 Subject: [PATCH 08/48] test --- .../headless/src/app/commerce-engine/commerce-engine.ssr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 09610a3e59c..28e0399ae34 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -308,7 +308,7 @@ export function defineCommerceEngine< async (...params: FetchStaticStateParameters) => { // TODO: no need for ...params since it is a list of controllers console.log(params, ((params || [])[0] as string[])?.length); // Something is wrong with the type here - // FIXME: just WOW + // FIXME: just WOW! ((params || [])[0] as Array).forEach((controller) => { controllerList.add(controller); }); From e60bfe5c78876d4e05c020b8f66b23ddcbf6c1bc Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 15 Nov 2024 15:14:24 -0500 Subject: [PATCH 09/48] update sample --- .../src/app/commerce-ssr-engine/common.ts | 11 +++- .../app/(listing)/[category]/page.tsx | 18 +++++- .../components/providers/listing-provider.tsx | 2 +- .../providers/recommendation-provider.tsx | 63 +++++++++++++++++++ .../components/providers/search-provider.tsx | 2 +- 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 2462bc462b3..13911666d78 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -97,10 +97,19 @@ export function buildControllerDefinitions< ? definition['standalone'] === false : false; + const unavailabeInRecs = + // TODO: use this disjunction pattern for all other conditions + (solutionType === SolutionType['recommendation'] && + !('recommendation' in definition)) || + ('recommendation' in definition && + definition['recommendation'] === false && + solutionType === SolutionType['recommendation']); + if ( unavailableInSearchSolutionType || unavailableInListingSolutionType || - unavailableInStandaloneSolutionType + unavailableInStandaloneSolutionType || + unavailabeInRecs ) { return null; } diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index 740a6869fa6..fe2cd83548f 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -6,11 +6,15 @@ import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; import ListingProvider from '@/components/providers/listing-provider'; +import RecommendationProvider from '@/components/providers/recommendation-provider'; import Recommendations from '@/components/recommendation-list'; import Sort from '@/components/sort'; import StandaloneSearchBox from '@/components/standalone-search-box'; import Summary from '@/components/summary'; -import {listingEngineDefinition} from '@/lib/commerce-engine'; +import { + listingEngineDefinition, + recommendationEngineDefinition, +} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; @@ -54,6 +58,11 @@ export default async function Listing({params}: {params: {category: string}}) { }, }); + const recStaticState = await recommendationEngineDefinition.fetchStaticState([ + 'popularBoughtRecs', + 'popularViewedRecs', + ]); + return (

- + + +
diff --git a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx index d0112efb942..72641370628 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx @@ -28,7 +28,7 @@ export default function ListingProvider({ useEffect(() => { listingEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, controllers: { cart: { initialState: {items: staticState.controllers.cart.state.items}, diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx new file mode 100644 index 00000000000..deea79c460b --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { + recommendationEngineDefinition, + RecommendationHydratedState, + RecommendationStaticState, +} from '@/lib/commerce-engine'; +import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; +import {PropsWithChildren, useEffect, useState} from 'react'; + +interface RecommendationPageProps { + staticState: RecommendationStaticState; + navigatorContext: NavigatorContext; +} + +export default function RecommendationProvider({ + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + // Setting the navigator context provider also in client-side before hydrating the application + recommendationEngineDefinition.setNavigatorContextProvider( + () => navigatorContext + ); + + useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (recommendationEngineDefinition.hydrateStaticState as any)({ + searchActions: staticState.searchActions, + // controllers: { }, + }).then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + // Refreshing recommendations in the browser after hydrating the state in the client-side + // Recommendation refresh in the server is not supported yet. + // controllers.popularBoughtRecs.refresh(); // FIXME: does not work + }); + }, [staticState]); + + if (hydratedState) { + return ( + + <>{children} + + ); + } else { + return ( + + {/* // TODO: Add KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. + Type 'bigint' is not assignable to type 'ReactNode'.*/} + <>{children} + + ); + } +} diff --git a/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx index e9e099e2869..7467928bb6b 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx @@ -28,7 +28,7 @@ export default function SearchProvider({ useEffect(() => { searchEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, controllers: { cart: { initialState: {items: staticState.controllers.cart.state.items}, From be888c83c7e808a1f2fe3d383d61b9b85223b5ac Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 15 Nov 2024 15:48:48 -0500 Subject: [PATCH 10/48] recommendation hydration working --- .../commerce-engine/commerce-engine.ssr.ts | 53 +++++++++++++--- .../commerce-ssr-engine/types/core-engine.ts | 15 ++++- .../types/fetch-static-state.ts | 20 +++--- .../types/hydrate-static-state.ts | 61 ++++++++++++------- packages/headless/src/ssr-commerce.index.ts | 1 + .../app/(listing)/[category]/page.tsx | 1 - .../providers/recommendation-provider.tsx | 17 +++--- 7 files changed, 114 insertions(+), 54 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 0e0146e3d51..b1796f4e37b 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -201,6 +201,13 @@ export function defineCommerceEngine< Parameters; type HydrateStaticStateFromBuildResultParameters = Parameters; + type BuildResult = { + engine: SSRCommerceEngine; + controllers: InferControllersMapFromDefinition< + TControllerDefinitions, + SolutionType + >; + }; // TODO: check if can remove the cast // TODO: ideally , we only want to execute that for recommendation stuff // TODO: get rid of that here. need to be computed in the fetch static state now @@ -323,13 +330,7 @@ export function defineCommerceEngine< const buildResult = (await buildFactory( SolutionType.recommendation, ((params || [])[0] as string[])?.length // TODO: fix that mess - )(...params)) as { - engine: SSRCommerceEngine; - controllers: InferControllersMapFromDefinition< - TControllerDefinitions, - SolutionType - >; - }; // TODO: check if can remove the cast + )(...params)) as BuildResult; // TODO: check if can remove the cast const staticState = await fetchStaticStateFactoryForRecommendation().fromBuildResult({ buildResult, @@ -407,6 +408,40 @@ export function defineCommerceEngine< } ); + const hydrateStaticStateFactoryForRecommendation: () => HydrateStaticStateFunction = + () => + composeFunction( + async (...params: HydrateStaticStateParameters) => { + const buildResult = (await buildFactory(SolutionType.recommendation)( + ...(params as BuildParameters) + )) as BuildResult; // TODO: check if can remove the cast + const staticState = + await hydrateStaticStateFactoryForRecommendation().fromBuildResult({ + buildResult, + searchActions: params[0]!.searchActions, + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: HydrateStaticStateFromBuildResultParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + searchActions, + }, + ] = params; + + searchActions.forEach((action) => { + engine.dispatch(action); + }); + await engine.waitForRequestCompletedAction(); + return {engine, controllers}; + }, + } + ); + return { listingEngineDefinition: { build: buildFactory(SolutionType.listing), @@ -423,9 +458,7 @@ export function defineCommerceEngine< recommendationEngineDefinition: { build: buildFactory(SolutionType.recommendation), fetchStaticState: fetchStaticStateFactoryForRecommendation(), - hydrateStaticState: hydrateStaticStateFactory( - SolutionType.recommendation - ), + hydrateStaticState: hydrateStaticStateFactoryForRecommendation(), setNavigatorContextProvider, } as CommerceEngineDefinition< TControllerDefinitions, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 0b819d674c7..afee85158a9 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -15,9 +15,17 @@ import { FetchStaticState, FetchStaticStateOptions, } from './fetch-static-state.js'; -import {HydrateStaticState} from './hydrate-static-state.js'; +import { + HydrateStaticState, + HydrateStaticStateOptions, +} from './hydrate-static-state.js'; -export type {HydrateStaticState, FetchStaticState, FetchStaticStateOptions}; +export type { + HydrateStaticState, + HydrateStaticStateOptions, + FetchStaticState, + FetchStaticStateOptions, +}; export type EngineDefinitionOptions< TOptions extends {configuration: EngineConfiguration}, TControllers extends ControllerDefinitionsMap< @@ -58,7 +66,8 @@ export interface EngineDefinition< TEngine, InferControllersMapFromDefinition, UnknownAction, - InferControllerPropsMapFromDefinitions + InferControllerPropsMapFromDefinitions, + TSolutionType >; /** * Builds an engine and its controllers from an engine definition. diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index a1e128046e4..2a51e468ccd 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -23,12 +23,6 @@ export type FetchStaticState< TControllersStaticState extends ControllerStaticStateMap, TControllersProps extends ControllersPropsMap, TSolutionType extends SolutionType, - FromBuildResultFunction = FromBuildResult< - TEngine, - TControllers, - FetchStaticStateOptions, - EngineStaticState - >, > = TSolutionType extends SolutionType.recommendation ? { /** @@ -40,7 +34,12 @@ export type FetchStaticState< controllers: Array // TODO: make the array unique ): Promise>; - fromBuildResult: FromBuildResultFunction; + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + FetchStaticStateOptions, + EngineStaticState + >; } : { /** @@ -55,6 +54,11 @@ export type FetchStaticState< > ): Promise>; - fromBuildResult: FromBuildResultFunction; + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + FetchStaticStateOptions, + EngineStaticState + >; }; buildBaseCommerceAPIRequest; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts index e84d47e3a12..62e90c56f2b 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -8,36 +8,53 @@ import { OptionsTuple, } from '../../ssr-engine/types/common.js'; import {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; +import {SolutionType} from './common.js'; export interface HydrateStaticStateOptions { searchActions: TSearchAction[]; } -// TODO: check if need to create one hydrate function specific for recommendations. -// - If yes, then adjust this. -// - If not, then remove and reuse the interface from ssr-engine export type HydrateStaticState< TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllersMap, TSearchAction extends UnknownAction, TControllersProps extends ControllersPropsMap, -> = { - /** - * Creates a new engine from the snapshot of the engine created in SSR with fetchStaticState. - * - * Useful when hydrating a server-side-rendered engine. - */ - ( - ...params: OptionsTuple< - HydrateStaticStateOptions & - EngineDefinitionControllersPropsOption - > - ): Promise>; + TSolutionType extends SolutionType, +> = TSolutionType extends SolutionType.recommendation + ? { + /** + * Creates a new engine from the snapshot of the engine created in SSR with fetchStaticState. + * + * Useful when hydrating a server-side-rendered engine. + */ + ( + ...params: OptionsTuple> + ): Promise>; - fromBuildResult: FromBuildResult< - TEngine, - TControllers, - HydrateStaticStateOptions, - HydratedState - >; -}; + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + HydrateStaticStateOptions, + HydratedState + >; + } + : { + /** + * Creates a new engine from the snapshot of the engine created in SSR with fetchStaticState. + * + * Useful when hydrating a server-side-rendered engine. + */ + ( + ...params: OptionsTuple< + HydrateStaticStateOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; + + fromBuildResult: FromBuildResult< + TEngine, + TControllers, + HydrateStaticStateOptions, + HydratedState + >; + }; diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index 8489903b27b..a232944cd89 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -156,6 +156,7 @@ export type { HydrateStaticState, FetchStaticState, FetchStaticStateOptions, + HydrateStaticStateOptions, } from './app/commerce-ssr-engine/types/core-engine.js'; // export type { // FromBuildResult, diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index fe2cd83548f..269a0d5b346 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -60,7 +60,6 @@ export default async function Listing({params}: {params: {category: string}}) { const recStaticState = await recommendationEngineDefinition.fetchStaticState([ 'popularBoughtRecs', - 'popularViewedRecs', ]); return ( diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx index deea79c460b..fe80184f3a9 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx @@ -28,16 +28,13 @@ export default function RecommendationProvider({ ); useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (recommendationEngineDefinition.hydrateStaticState as any)({ - searchActions: staticState.searchActions, - // controllers: { }, - }).then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - // controllers.popularBoughtRecs.refresh(); // FIXME: does not work - }); + recommendationEngineDefinition + .hydrateStaticState({ + searchActions: staticState.searchActions, + }) // TODO: need to pass the search actions!!!! + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); }, [staticState]); if (hydratedState) { From c4ff2b9e596487a4128178fa8d86354ed7bb5ec4 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 15 Nov 2024 15:53:37 -0500 Subject: [PATCH 11/48] clean build function --- .../app/commerce-ssr-engine/types/build.ts | 47 +++++++++---------- .../commerce-ssr-engine/types/core-engine.ts | 24 ++++------ packages/headless/src/ssr-commerce.index.ts | 5 +- 3 files changed, 32 insertions(+), 44 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/types/build.ts b/packages/headless/src/app/commerce-ssr-engine/types/build.ts index 58ab8d5973b..b92c3eee557 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/build.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/build.ts @@ -7,36 +7,35 @@ import { OptionsExtender, OptionsTuple, } from '../../ssr-engine/types/common.js'; +import {SolutionType} from './common.js'; export interface BuildOptions { extend?: OptionsExtender; } -export interface BuildWithList< - TEngine extends CoreEngine | CoreEngineNext, - TControllersMap extends ControllersMap, -> { - /** - * Initializes an engine and controllers from the definition. - */ - ( - c: (keyof TControllersMap)[] - ): Promise>; -} - -export interface Build< +export type Build< TEngine extends CoreEngine | CoreEngineNext, TEngineOptions, TControllersMap extends ControllersMap, TControllersProps extends ControllersPropsMap, -> { - /** - * Initializes an engine and controllers from the definition. - */ - ( - ...params: OptionsTuple< - BuildOptions & - EngineDefinitionControllersPropsOption - > - ): Promise>; -} + TSolutionType extends SolutionType, +> = TSolutionType extends SolutionType.recommendation + ? { + /** + * Initializes an engine and controllers from the definition. + */ + ( + c: (keyof TControllersMap)[] + ): Promise>; + } + : { + /** + * Initializes an engine and controllers from the definition. + */ + ( + ...params: OptionsTuple< + BuildOptions & + EngineDefinitionControllersPropsOption + > + ): Promise>; + }; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index afee85158a9..75db55d7dcd 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -3,7 +3,7 @@ import type {Controller} from '../../../controllers/controller/headless-controll import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; -import {BuildWithList, Build} from './build.js'; +import {Build} from './build.js'; import { ControllerDefinitionsMap, InferControllersMapFromDefinition, @@ -72,21 +72,13 @@ export interface EngineDefinition< /** * Builds an engine and its controllers from an engine definition. */ - build: TSolutionType extends SolutionType.recommendation - ? // TODO: THIS should not be a separate function, inside the build function instead - BuildWithList< - TEngine, - InferControllersMapFromDefinition< - TControllers, - SolutionType.recommendation - > - > - : Build< - TEngine, - TEngineOptions, - InferControllersMapFromDefinition, - InferControllerPropsMapFromDefinitions - >; + build: Build< + TEngine, + TEngineOptions, + InferControllersMapFromDefinition, + InferControllerPropsMapFromDefinitions, + TSolutionType + >; /** * Sets the navigator context provider. diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index a232944cd89..7b8c9d67d98 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -144,10 +144,7 @@ export type { OptionsExtender, OptionsTuple, } from './app/ssr-engine/types/common.js'; -export type { - Build, - BuildWithList, // TODO: get rid of that once condition is inside Build type -} from './app/commerce-ssr-engine/types/build.js'; +export type {Build} from './app/commerce-ssr-engine/types/build.js'; export type { EngineDefinition, InferStaticState, From 2783740250cd47fe2a2a941a4c7997b6f9a25b82 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Fri, 15 Nov 2024 17:14:17 -0500 Subject: [PATCH 12/48] clean exports --- .../app/commerce-ssr-engine/types/build.ts | 4 +- .../app/commerce-ssr-engine/types/common.ts | 8 +++ .../commerce-ssr-engine/types/core-engine.ts | 12 +++- .../types/fetch-static-state.ts | 14 ++--- .../types/from-build-result.ts | 23 -------- .../types/hydrate-static-state.ts | 8 +-- packages/headless/src/ssr-commerce.index.ts | 56 ++----------------- 7 files changed, 35 insertions(+), 90 deletions(-) delete mode 100644 packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts diff --git a/packages/headless/src/app/commerce-ssr-engine/types/build.ts b/packages/headless/src/app/commerce-ssr-engine/types/build.ts index b92c3eee557..a3b5fe1a238 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/build.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/build.ts @@ -1,5 +1,5 @@ -import {CoreEngine, CoreEngineNext} from '../../engine.js'; -import { +import type {CoreEngine, CoreEngineNext} from '../../engine.js'; +import type { ControllersMap, ControllersPropsMap, EngineDefinitionBuildResult, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index af5fd9ab8a6..74cad63a186 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -9,9 +9,17 @@ import type { InferControllerStaticStateFromController, InferControllerPropsMapFromDefinitions, ControllerStaticStateMap, + EngineDefinitionBuildResult, + EngineDefinitionControllersPropsOption, + HydratedState, + OptionsTuple, } from '../../ssr-engine/types/common.js'; export type { + EngineDefinitionBuildResult, + EngineDefinitionControllersPropsOption, + HydratedState, + OptionsTuple, InferControllerStaticStateFromController, InferControllerStaticStateMapFromControllers, InferControllerPropsMapFromDefinitions, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 75db55d7dcd..e821601f3c6 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -3,7 +3,11 @@ import type {Controller} from '../../../controllers/controller/headless-controll import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; -import {Build} from './build.js'; +import type { + FromBuildResult, + FromBuildResultOptions, +} from '../../ssr-engine/types/from-build-result.js'; +import {Build, BuildOptions} from './build.js'; import { ControllerDefinitionsMap, InferControllersMapFromDefinition, @@ -20,11 +24,17 @@ import { HydrateStaticStateOptions, } from './hydrate-static-state.js'; +// TODO: why not use the one for commerce + export type { + FromBuildResult, + FromBuildResultOptions, HydrateStaticState, HydrateStaticStateOptions, FetchStaticState, FetchStaticStateOptions, + Build, + BuildOptions, }; export type EngineDefinitionOptions< TOptions extends {configuration: EngineConfiguration}, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index 2a51e468ccd..d05a9cb5c15 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -1,18 +1,16 @@ -import {UnknownAction} from '@reduxjs/toolkit'; +import type {UnknownAction} from '@reduxjs/toolkit'; import {buildBaseCommerceAPIRequest} from '../../../features/commerce/common/actions.js'; -import { - EngineStaticState, - SolutionType, -} from '../../commerce-ssr-engine/types/common.js'; -import {CoreEngine, CoreEngineNext} from '../../engine.js'; -import { +import {SolutionType} from '../../commerce-ssr-engine/types/common.js'; +import type {EngineStaticState} from '../../commerce-ssr-engine/types/common.js'; +import type {CoreEngine, CoreEngineNext} from '../../engine.js'; +import type { ControllersMap, ControllersPropsMap, ControllerStaticStateMap, EngineDefinitionControllersPropsOption, OptionsTuple, } from '../../ssr-engine/types/common.js'; -import {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; +import type {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; export type FetchStaticStateOptions = {}; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts deleted file mode 100644 index 184a9bec544..00000000000 --- a/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {CoreEngine, CoreEngineNext} from '../../engine.js'; -import { - ControllersMap, - EngineDefinitionBuildResult, -} from '../../ssr-engine/types/common.js'; - -export interface FromBuildResultOptions< - TEngine extends CoreEngine | CoreEngineNext, - TControllers extends ControllersMap, -> { - buildResult: EngineDefinitionBuildResult; -} - -export interface FromBuildResult< - TEngine extends CoreEngine | CoreEngineNext, - TControllers extends ControllersMap, - TOptions, - TReturn, -> { - ( - options: FromBuildResultOptions & TOptions - ): Promise; -} diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts index 62e90c56f2b..34669305f9d 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -1,13 +1,13 @@ -import {UnknownAction} from '@reduxjs/toolkit'; -import {CoreEngine, CoreEngineNext} from '../../engine.js'; -import { +import type {UnknownAction} from '@reduxjs/toolkit'; +import type {CoreEngine, CoreEngineNext} from '../../engine.js'; +import type { ControllersMap, ControllersPropsMap, EngineDefinitionControllersPropsOption, HydratedState, OptionsTuple, } from '../../ssr-engine/types/common.js'; -import {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; +import type {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; import {SolutionType} from './common.js'; export interface HydrateStaticStateOptions { diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index 7b8c9d67d98..708cbbefe4f 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -97,54 +97,12 @@ export type { InferControllerStaticStateMapFromControllers, InferControllerStaticStateMapFromDefinitionsWithSolutionType, InferControllerPropsMapFromDefinitions, - // TODO: check if need to export these types - RecommendationOnlyControllerDefinitionWithProps, - RecommendationOnlyControllerDefinitionWithoutProps, - UniversalControllerDefinitionWithProps, - ControllerDefinitionWithoutProps, - ControllerDefinitionWithProps, - ControllerDefinition, - ControllerDefinitionOption, EngineStaticState, - InferControllerPropsFromDefinition, - ListingOnlyControllerDefinitionWithProps, - ListingOnlyControllerDefinitionWithoutProps, - SearchAndListingControllerDefinitionWithProps, - SearchAndListingControllerDefinitionWithoutProps, - SearchOnlyControllerDefinitionWithProps, - SearchOnlyControllerDefinitionWithoutProps, - SubControllerDefinitionWithProps, - SubControllerDefinitionWithoutProps, - UniversalControllerDefinitionWithoutProps, -} from './app/commerce-ssr-engine/types/common.js'; -// TODO: check if need to export these types -export type { - // ControllerDefinition, - // ControllerDefinitionWithProps, - // ControllerDefinitionWithoutProps, - // ControllerDefinitionsMap, - ControllerStaticState, - ControllerStaticStateMap, - ControllersMap, - ControllersPropsMap, EngineDefinitionBuildResult, EngineDefinitionControllersPropsOption, - // EngineStaticState, - ExtractRequiredOptions, - HasKey, - HasKeys, HydratedState, - // InferControllerFromDefinition, - // InferControllerPropsFromDefinition, - // InferControllerPropsMapFromDefinitions, - // InferControllerStaticStateFromController, - // InferControllerStaticStateMapFromControllers, - InferControllerStaticStateMapFromDefinitions, - // InferControllersMapFromDefinition, - OptionsExtender, OptionsTuple, -} from './app/ssr-engine/types/common.js'; -export type {Build} from './app/commerce-ssr-engine/types/build.js'; +} from './app/commerce-ssr-engine/types/common.js'; export type { EngineDefinition, InferStaticState, @@ -154,17 +112,11 @@ export type { FetchStaticState, FetchStaticStateOptions, HydrateStaticStateOptions, -} from './app/commerce-ssr-engine/types/core-engine.js'; -// export type { -// FromBuildResult, -// // TODO: check if need to export these types -// FromBuildResultOptions, -// } from './app/commerce-ssr-engine/types/from-build-result.js'; -export type { - // // TODO: check if need to export these types + Build, + BuildOptions, FromBuildResult, FromBuildResultOptions, -} from './app/ssr-engine/types/from-build-result.js'; +} from './app/commerce-ssr-engine/types/core-engine.js'; export type {LoggerOptions} from './app/logger.js'; export type { NavigatorContext, From 8a6f9d8f6844a9c7e2978d84dd6d5dbca371796a Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sat, 16 Nov 2024 13:17:35 -0500 Subject: [PATCH 13/48] filter invalid and duplicate recommendations --- .../commerce-engine/commerce-engine.ssr.ts | 67 ++++++++++--------- .../src/app/commerce-ssr-engine/common.ts | 31 ++++----- .../app/ssr-engine/types/from-build-result.ts | 9 +++ 3 files changed, 60 insertions(+), 47 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index b1796f4e37b..516236dac7f 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -19,8 +19,8 @@ import { } from '../../utils/utils.js'; import { buildControllerDefinitions, - buildRecommendationFilter, createStaticState, + filterRecommendationControllers, } from '../commerce-ssr-engine/common.js'; import { ControllerDefinitionsMap, @@ -188,7 +188,7 @@ export function defineCommerceEngine< SolutionType >; type BuildFunction = Definition['build']; - type FetchStaticStateFunction = Definition['fetchStaticState']; // TODO: avoir un fetch pour les recommendations + type FetchStaticStateFunction = Definition['fetchStaticState']; type HydrateStaticStateFunction = Definition['hydrateStaticState']; type FetchStaticStateFromBuildResultFunction = FetchStaticStateFunction['fromBuildResult']; @@ -201,18 +201,16 @@ export function defineCommerceEngine< Parameters; type HydrateStaticStateFromBuildResultParameters = Parameters; + type Controllers = InferControllersMapFromDefinition< + TControllerDefinitions, + SolutionType + >; + type ControllerDefinitionKeys = keyof Controllers; type BuildResult = { engine: SSRCommerceEngine; - controllers: InferControllersMapFromDefinition< - TControllerDefinitions, - SolutionType - >; - }; // TODO: check if can remove the cast - - // TODO: ideally , we only want to execute that for recommendation stuff - // TODO: get rid of that here. need to be computed in the fetch static state now - const recommendationFilter = () => - buildRecommendationFilter(controllerDefinitions ?? {}); + controllers: Controllers; + }; + const logger = buildLogger(options.loggerOptions); const getOptions = () => { return engineOptions; @@ -226,7 +224,6 @@ export function defineCommerceEngine< const buildFactory = (solutionType: T, count: number = 0) => async (...[buildOptions]: BuildParameters) => { - const logger = buildLogger(options.loggerOptions); if (!getOptions().navigatorContextProvider) { logger.warn( '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' @@ -306,34 +303,40 @@ export function defineCommerceEngine< } ); - // TODO: enlever ce hack! - const controllerList: Set = new Set(); - // TODO: remove factory const fetchStaticStateFactoryForRecommendation: () => FetchStaticStateFunction = () => composeFunction( - async (...params: FetchStaticStateParameters) => { - // TODO: no need for ...params since it is a list of controllers - console.log(params, ((params || [])[0] as string[])?.length); // Something is wrong with the type here - // FIXME: just WOW! - ((params || [])[0] as Array).forEach((controller) => { - controllerList.add(controller); - }); + async ( + ...params: [controllerKeys: Array] + ) => { + const [controllerKeys] = params; + const uniqueControllerKeys = Array.from(new Set(controllerKeys)); + if (uniqueControllerKeys.length !== controllerKeys.length) { + logger.warn( + '[WARNING] Duplicate controller keys detected in recommendation fetchStaticState call. Make sure to provide only unique controller keys.' + ); + } + + const validControllerNames = Object.keys(controllerDefinitions ?? {}); + const allowedRecommendationKeys = uniqueControllerKeys.filter( + (key: string) => validControllerNames.includes(key) + ); + if (!getOptions().navigatorContextProvider) { - // TODO: KIT-3409 - implement a logger to log SSR warnings/errors - console.warn( + logger.warn( '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' ); } const buildResult = (await buildFactory( SolutionType.recommendation, - ((params || [])[0] as string[])?.length // TODO: fix that mess + allowedRecommendationKeys.length )(...params)) as BuildResult; // TODO: check if can remove the cast const staticState = await fetchStaticStateFactoryForRecommendation().fromBuildResult({ buildResult, + recommendationControllerKeys: allowedRecommendationKeys, }); return staticState; }, @@ -344,15 +347,15 @@ export function defineCommerceEngine< const [ { buildResult: {engine, controllers}, + recommendationControllerKeys, }, ] = params; - recommendationFilter().refresh( + filterRecommendationControllers( controllers, - Array.from(controllerList) // TODO: find the right type - ); // TODO: filter out the controllers to only include the one in the static state params + controllerDefinitions ?? {} + ).refresh(recommendationControllerKeys); - // TODO: should be only one searchAction for search and listing const searchActions = await Promise.all( engine.waitForRequestCompletedAction() ); @@ -477,7 +480,7 @@ export function defineCommerceEngine< }; } -// // Sandbox +// Sandbox // const { // recommendationEngineDefinition, // searchEngineDefinition, @@ -506,8 +509,8 @@ export function defineCommerceEngine< // const b = await recommendationEngineDefinition.fetchStaticState([ // 'trending', -// 'popular', // ]); +// const b = await recommendationEngineDefinition.fetchStaticState.fromBuildResult({buildResult:{controllers:{}}}) // // b.controllers.; // const a = await searchEngineDefinition.fetchStaticState(); // TODO: fix typing if controller is set to {search: false} diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 13911666d78..7575ee88b31 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -139,18 +139,24 @@ export function ensureAtLeastOneSolutionType( } } -export function buildRecommendationFilter< +export function filterRecommendationControllers< TEngine extends CoreEngine | CoreEngineNext, TControllerDefinitions extends ControllerDefinitionsMap, ->(controllerDefinitions: TControllerDefinitions) { +>( + controllers: Record, // TODO: or InferControllersMapFromDefinition + controllerDefinitions: TControllerDefinitions +) { const slotIdSet = new Set(); const isRecommendationDefinition = < C extends ControllerDefinition, >( - controller: C - ): controller is C & RecommendationsDefinitionMeta => { - return 'recommendation' in controller; + controllerDefinition: C + ): controllerDefinition is C & RecommendationsDefinitionMeta => { + return ( + 'recommendation' in controllerDefinition && + controllerDefinition.recommendation === true + ); }; const warnDuplicateRecommendation = (slotId: string, productId?: string) => { @@ -179,23 +185,18 @@ export function buildRecommendationFilter< const name = filtered.map(([name, _]) => name); return { - /** - * Gets the number of recommendation controllers from the controller definitions map. - * - * @returns {number} The number of recommendation controllers in the controller definition map - */ - get count() { - return name.length; - }, - /** * Go through all the controllers passed in argument and only refresh recommendation controllers. * * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. * @param controllerNames - A list of all recommendation controllers to refresh */ - refresh(controllers: Record, whitelist: string[]) { + refresh(whitelist?: string[]) { + if (whitelist === undefined) { + return; + } // TODO: FIND a better way + // TODO: do not refresh multiple recommendation controllers for the same slotId and productId const isRecommendationController = (key: string) => name.includes(key) && whitelist.includes(key); diff --git a/packages/headless/src/app/ssr-engine/types/from-build-result.ts b/packages/headless/src/app/ssr-engine/types/from-build-result.ts index 5851651b59b..43212747352 100644 --- a/packages/headless/src/app/ssr-engine/types/from-build-result.ts +++ b/packages/headless/src/app/ssr-engine/types/from-build-result.ts @@ -5,7 +5,16 @@ export interface FromBuildResultOptions< TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllersMap, > { + /** + * The build result of the engine + */ buildResult: EngineDefinitionBuildResult; + /** + * An optional array of keys representing the recommendation controllers to refresh. + * If a recommendation key defined in your engine definition is present in this list, a recommendation query will be triggered against the API. + * This is applicable only if the engine is a recommendation engine. + */ + recommendationControllerKeys?: (keyof TControllers)[]; } export interface FromBuildResult< From 025b34f72449dfef34d78c926d4190589e3acaf7 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sat, 16 Nov 2024 15:19:50 -0500 Subject: [PATCH 14/48] refacto --- .../commerce-engine/commerce-engine.ssr.ts | 425 ++---------------- .../factories/build-factory.ts | 175 ++++++++ .../factories/hydrate-state-factory.ts | 68 +++ .../recommendation-hydrate-state-factory.ts | 61 +++ .../recommendation-static-state-factory.ts | 104 +++++ .../factories/static-state-factory.ts | 88 ++++ .../commerce-ssr-engine/types/core-engine.ts | 60 +++ .../types/fetch-static-state.ts | 2 - packages/headless/src/ssr-commerce.index.ts | 11 +- 9 files changed, 604 insertions(+), 390 deletions(-) create mode 100644 packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts create mode 100644 packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 516236dac7f..6538f7e5756 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -1,10 +1,6 @@ /** * Utility functions to be used for Commerce Server Side Rendering. */ -import {Action, UnknownAction} from '@reduxjs/toolkit'; -import {stateKey} from '../../app/state-key.js'; -import {buildProductListing} from '../../controllers/commerce/product-listing/headless-product-listing.js'; -import {buildSearch} from '../../controllers/commerce/search/headless-search.js'; import type {Controller} from '../../controllers/controller/headless-controller.js'; // import { // defineContext, @@ -14,132 +10,21 @@ import type {Controller} from '../../controllers/controller/headless-controller. // getSampleCommerceEngineConfiguration, // } from '../../ssr-commerce.index.js'; import { - createWaitForActionMiddleware, - createWaitForActionMiddlewareForRecommendation, -} from '../../utils/utils.js'; -import { - buildControllerDefinitions, - createStaticState, - filterRecommendationControllers, -} from '../commerce-ssr-engine/common.js'; + buildFactory, + CommerceEngineDefinitionOptions, + SSRCommerceEngine, +} from '../commerce-ssr-engine/factories/build-factory.js'; +import {hydrateStaticStateFactory} from '../commerce-ssr-engine/factories/hydrate-state-factory.js'; +import {hydrateRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-hydrate-state-factory.js'; +import {fetchRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-static-state-factory.js'; +import {fetchStaticStateFactory} from '../commerce-ssr-engine/factories/static-state-factory.js'; import { ControllerDefinitionsMap, - EngineStaticState, - InferControllerPropsMapFromDefinitions, - InferControllersMapFromDefinition, - InferControllerStaticStateMapFromDefinitionsWithSolutionType, SolutionType, } from '../commerce-ssr-engine/types/common.js'; -import { - EngineDefinition, - EngineDefinitionOptions, -} from '../commerce-ssr-engine/types/core-engine.js'; -import {buildLogger} from '../logger.js'; +import {EngineDefinition} from '../commerce-ssr-engine/types/core-engine.js'; import {NavigatorContextProvider} from '../navigatorContextProvider.js'; -import {composeFunction} from '../ssr-engine/common.js'; -import { - CommerceEngine, - CommerceEngineOptions, - buildCommerceEngine, -} from './commerce-engine.js'; - -/** - * The SSR commerce engine. - */ -export interface SSRCommerceEngine extends CommerceEngine { - /** - * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. - */ - waitForRequestCompletedAction(): Promise[]; -} - -export type CommerceEngineDefinitionOptions< - TControllers extends ControllerDefinitionsMap, -> = EngineDefinitionOptions; - -function isListingFetchCompletedAction(action: unknown): action is Action { - return /^commerce\/productListing\/fetch\/(fulfilled|rejected)$/.test( - (action as UnknownAction).type - ); -} - -function isSearchCompletedAction(action: unknown): action is Action { - return /^commerce\/search\/executeSearch\/(fulfilled|rejected)$/.test( - (action as UnknownAction).type - ); -} - -function isRecommendationCompletedAction(action: unknown): action is Action { - return /^commerce\/recommendations\/fetch\/(fulfilled|rejected)$/.test( - (action as UnknownAction).type - ); -} - -function noSearchActionRequired(_action: unknown): _action is Action { - return true; -} - -function buildSSRCommerceEngine( - solutionType: SolutionType, - options: CommerceEngineOptions, - recommendationCount: number -): SSRCommerceEngine { - let actionCompletionMiddleware: ReturnType< - typeof createWaitForActionMiddleware - >; - - const middlewares: ReturnType[] = []; - const memo: Set = new Set(); - - switch (solutionType) { - case SolutionType.listing: - actionCompletionMiddleware = createWaitForActionMiddleware( - isListingFetchCompletedAction - ); - middlewares.push(actionCompletionMiddleware); - break; - case SolutionType.search: - actionCompletionMiddleware = createWaitForActionMiddleware( - isSearchCompletedAction - ); - middlewares.push(actionCompletionMiddleware); - break; - case SolutionType.recommendation: - middlewares.push( - ...Array.from({length: recommendationCount}, () => - createWaitForActionMiddlewareForRecommendation( - isRecommendationCompletedAction, - memo - ) - ) - ); - break; - default: - actionCompletionMiddleware = createWaitForActionMiddleware( - noSearchActionRequired - ); - } - - const commerceEngine = buildCommerceEngine({ - ...options, - middlewares: [ - ...(options.middlewares ?? []), - ...middlewares.map(({middleware}) => middleware), - ], - }); - - return { - ...commerceEngine, - - get [stateKey]() { - return commerceEngine[stateKey]; - }, - - waitForRequestCompletedAction() { - return [...middlewares.map(({promise}) => promise)]; - }, - }; -} +import {CommerceEngineOptions} from './commerce-engine.js'; export interface CommerceEngineDefinition< TControllers extends ControllerDefinitionsMap, @@ -183,34 +68,6 @@ export function defineCommerceEngine< >; } { const {controllers: controllerDefinitions, ...engineOptions} = options; - type Definition = CommerceEngineDefinition< - TControllerDefinitions, - SolutionType - >; - type BuildFunction = Definition['build']; - type FetchStaticStateFunction = Definition['fetchStaticState']; - type HydrateStaticStateFunction = Definition['hydrateStaticState']; - type FetchStaticStateFromBuildResultFunction = - FetchStaticStateFunction['fromBuildResult']; - type HydrateStaticStateFromBuildResultFunction = - HydrateStaticStateFunction['fromBuildResult']; - type BuildParameters = Parameters; - type FetchStaticStateParameters = Parameters; - type HydrateStaticStateParameters = Parameters; - type FetchStaticStateFromBuildResultParameters = - Parameters; - type HydrateStaticStateFromBuildResultParameters = - Parameters; - type Controllers = InferControllersMapFromDefinition< - TControllerDefinitions, - SolutionType - >; - type ControllerDefinitionKeys = keyof Controllers; - type BuildResult = { - engine: SSRCommerceEngine; - controllers: Controllers; - }; - const logger = buildLogger(options.loggerOptions); const getOptions = () => { return engineOptions; @@ -221,247 +78,49 @@ export function defineCommerceEngine< ) => { engineOptions.navigatorContextProvider = navigatorContextProvider; }; - const buildFactory = - (solutionType: T, count: number = 0) => - async (...[buildOptions]: BuildParameters) => { - if (!getOptions().navigatorContextProvider) { - logger.warn( - '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' - ); - } - const engine = buildSSRCommerceEngine( - solutionType, - buildOptions && 'extend' in buildOptions && buildOptions?.extend - ? await buildOptions.extend(getOptions()) - : getOptions(), - // TODO: clean that - solutionType === SolutionType.recommendation ? count : 0 // TODO: avoid this by creating a build factory for recs - ); - const controllers = buildControllerDefinitions({ - definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, - engine, - solutionType, - propsMap: (buildOptions && 'controllers' in buildOptions - ? buildOptions.controllers - : {}) as InferControllerPropsMapFromDefinitions, - }); - - return { - engine, - controllers, - }; - }; - - const fetchStaticStateFactory: ( - solutionType: SolutionType - ) => FetchStaticStateFunction = (solutionType: SolutionType) => - composeFunction( - async (...params: FetchStaticStateParameters) => { - const buildResult = await buildFactory(solutionType)(...params); - const staticState = await fetchStaticStateFactory( - solutionType - ).fromBuildResult({ - buildResult, - }); - return staticState; - }, - { - fromBuildResult: async ( - ...params: FetchStaticStateFromBuildResultParameters - ) => { - const [ - { - buildResult: {engine, controllers}, - }, - ] = params; - - switch (solutionType) { - case SolutionType.listing: - buildProductListing(engine).executeFirstRequest(); - break; - case SolutionType.search: - buildSearch(engine).executeFirstSearch(); - break; - } - // TODO: should be only one searchAction for search and listing - const searchActions = await Promise.all( - engine.waitForRequestCompletedAction() - ); + const build = buildFactory( + controllerDefinitions!, // TODO: find a way to get rid of the ! + getOptions() + ); + const fetchStaticState = fetchStaticStateFactory( + controllerDefinitions!, + getOptions() + ); + const hydrateStaticState = hydrateStaticStateFactory( + controllerDefinitions!, + getOptions() + ); - return createStaticState({ - searchActions, - controllers, - }) as EngineStaticState< - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllerDefinitions, - SolutionType - > - >; - }, - } + const fetchRecommendationStaticState = + fetchRecommendationStaticStateFactory( + controllerDefinitions!, + getOptions() // TODO: add count here ); - // TODO: remove factory - const fetchStaticStateFactoryForRecommendation: () => FetchStaticStateFunction = - () => - composeFunction( - async ( - ...params: [controllerKeys: Array] - ) => { - const [controllerKeys] = params; - const uniqueControllerKeys = Array.from(new Set(controllerKeys)); - if (uniqueControllerKeys.length !== controllerKeys.length) { - logger.warn( - '[WARNING] Duplicate controller keys detected in recommendation fetchStaticState call. Make sure to provide only unique controller keys.' - ); - } - - const validControllerNames = Object.keys(controllerDefinitions ?? {}); - const allowedRecommendationKeys = uniqueControllerKeys.filter( - (key: string) => validControllerNames.includes(key) - ); - - if (!getOptions().navigatorContextProvider) { - logger.warn( - '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' - ); - } - - const buildResult = (await buildFactory( - SolutionType.recommendation, - allowedRecommendationKeys.length - )(...params)) as BuildResult; // TODO: check if can remove the cast - const staticState = - await fetchStaticStateFactoryForRecommendation().fromBuildResult({ - buildResult, - recommendationControllerKeys: allowedRecommendationKeys, - }); - return staticState; - }, - { - fromBuildResult: async ( - ...params: FetchStaticStateFromBuildResultParameters - ) => { - const [ - { - buildResult: {engine, controllers}, - recommendationControllerKeys, - }, - ] = params; - - filterRecommendationControllers( - controllers, - controllerDefinitions ?? {} - ).refresh(recommendationControllerKeys); - - const searchActions = await Promise.all( - engine.waitForRequestCompletedAction() - ); - - return createStaticState({ - searchActions, - controllers, - }) as EngineStaticState< - UnknownAction, - InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllerDefinitions, - SolutionType - > - >; - }, - } - ); - - const hydrateStaticStateFactory: ( - solutionType: SolutionType - ) => HydrateStaticStateFunction = (solutionType: SolutionType) => - composeFunction( - async (...params: HydrateStaticStateParameters) => { - const buildResult = await buildFactory(solutionType)( - ...(params as BuildParameters) - ); - const staticState = await hydrateStaticStateFactory( - solutionType - ).fromBuildResult({ - buildResult, - searchActions: params[0]!.searchActions, - }); - return staticState; - }, - { - fromBuildResult: async ( - ...params: HydrateStaticStateFromBuildResultParameters - ) => { - const [ - { - buildResult: {engine, controllers}, - searchActions, - }, - ] = params; - - // TODO: should be only one searchAction for search and listing - searchActions.forEach((action) => { - engine.dispatch(action); - }); - await engine.waitForRequestCompletedAction(); - return {engine, controllers}; - }, - } + const hydrateRecommendationStaticState = + hydrateRecommendationStaticStateFactory( + controllerDefinitions!, + getOptions() ); - const hydrateStaticStateFactoryForRecommendation: () => HydrateStaticStateFunction = - () => - composeFunction( - async (...params: HydrateStaticStateParameters) => { - const buildResult = (await buildFactory(SolutionType.recommendation)( - ...(params as BuildParameters) - )) as BuildResult; // TODO: check if can remove the cast - const staticState = - await hydrateStaticStateFactoryForRecommendation().fromBuildResult({ - buildResult, - searchActions: params[0]!.searchActions, - }); - return staticState; - }, - { - fromBuildResult: async ( - ...params: HydrateStaticStateFromBuildResultParameters - ) => { - const [ - { - buildResult: {engine, controllers}, - searchActions, - }, - ] = params; - - searchActions.forEach((action) => { - engine.dispatch(action); - }); - await engine.waitForRequestCompletedAction(); - return {engine, controllers}; - }, - } - ); - return { listingEngineDefinition: { - build: buildFactory(SolutionType.listing), - fetchStaticState: fetchStaticStateFactory(SolutionType.listing), - hydrateStaticState: hydrateStaticStateFactory(SolutionType.listing), + build: build(SolutionType.listing), + fetchStaticState: fetchStaticState(SolutionType.listing), + hydrateStaticState: hydrateStaticState(SolutionType.listing), setNavigatorContextProvider, } as CommerceEngineDefinition, searchEngineDefinition: { - build: buildFactory(SolutionType.search), - fetchStaticState: fetchStaticStateFactory(SolutionType.search), - hydrateStaticState: hydrateStaticStateFactory(SolutionType.search), + build: build(SolutionType.search), + fetchStaticState: fetchStaticState(SolutionType.search), + hydrateStaticState: hydrateStaticState(SolutionType.search), setNavigatorContextProvider, } as CommerceEngineDefinition, recommendationEngineDefinition: { - build: buildFactory(SolutionType.recommendation), - fetchStaticState: fetchStaticStateFactoryForRecommendation(), - hydrateStaticState: hydrateStaticStateFactoryForRecommendation(), + build: build(SolutionType.recommendation), // TODO: add count here + fetchStaticState: fetchRecommendationStaticState, + hydrateStaticState: hydrateRecommendationStaticState, setNavigatorContextProvider, } as CommerceEngineDefinition< TControllerDefinitions, @@ -469,9 +128,9 @@ export function defineCommerceEngine< >, // TODO: make the standaloneEngineDefinition not async since there are no search executed standaloneEngineDefinition: { - build: buildFactory(SolutionType.standalone), - fetchStaticState: fetchStaticStateFactory(SolutionType.standalone), - hydrateStaticState: hydrateStaticStateFactory(SolutionType.standalone), + build: build(SolutionType.standalone), + fetchStaticState: fetchStaticState(SolutionType.standalone), + hydrateStaticState: hydrateStaticState(SolutionType.standalone), setNavigatorContextProvider, } as CommerceEngineDefinition< TControllerDefinitions, diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts new file mode 100644 index 00000000000..fcf386f7fa7 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts @@ -0,0 +1,175 @@ +import {Action, UnknownAction} from '@reduxjs/toolkit'; +import {stateKey} from '../../../app/state-key.js'; +import {Controller} from '../../../controllers/controller/headless-controller.js'; +import { + createWaitForActionMiddleware, + createWaitForActionMiddlewareForRecommendation, +} from '../../../utils/utils.js'; +import { + buildCommerceEngine, + CommerceEngine, + CommerceEngineOptions, +} from '../../commerce-engine/commerce-engine.js'; +import {buildLogger} from '../../logger.js'; +import {buildControllerDefinitions} from '../common.js'; +import { + ControllerDefinitionsMap, + InferControllerPropsMapFromDefinitions, + SolutionType, +} from '../types/common.js'; +import { + BuildParameters, + EngineDefinitionOptions, +} from '../types/core-engine.js'; + +// TODO: rename +export interface RecommendationExtraOptions { + // TODO: rename + count: number; +} + +/** + * The SSR commerce engine. + */ +export interface SSRCommerceEngine extends CommerceEngine { + /** + * Waits for the search to be completed and returns a promise that resolves to a `SearchCompletedAction`. + */ + waitForRequestCompletedAction(): Promise[]; +} + +export type CommerceEngineDefinitionOptions< + TControllers extends ControllerDefinitionsMap, +> = EngineDefinitionOptions; + +function isListingFetchCompletedAction(action: unknown): action is Action { + return /^commerce\/productListing\/fetch\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + +function isSearchCompletedAction(action: unknown): action is Action { + return /^commerce\/search\/executeSearch\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + +function isRecommendationCompletedAction(action: unknown): action is Action { + return /^commerce\/recommendations\/fetch\/(fulfilled|rejected)$/.test( + (action as UnknownAction).type + ); +} + +function noSearchActionRequired(_action: unknown): _action is Action { + return true; +} + +function buildSSRCommerceEngine( + solutionType: SolutionType, + options: CommerceEngineOptions, + recommendationCount: number +): SSRCommerceEngine { + let actionCompletionMiddleware: ReturnType< + typeof createWaitForActionMiddleware + >; + + const middlewares: ReturnType[] = []; + const memo: Set = new Set(); + + switch (solutionType) { + case SolutionType.listing: + actionCompletionMiddleware = createWaitForActionMiddleware( + isListingFetchCompletedAction + ); + middlewares.push(actionCompletionMiddleware); + break; + case SolutionType.search: + actionCompletionMiddleware = createWaitForActionMiddleware( + isSearchCompletedAction + ); + middlewares.push(actionCompletionMiddleware); + break; + case SolutionType.recommendation: + middlewares.push( + ...Array.from({length: recommendationCount}, () => + createWaitForActionMiddlewareForRecommendation( + isRecommendationCompletedAction, + memo + ) + ) + ); + break; + case SolutionType.standalone: + actionCompletionMiddleware = createWaitForActionMiddleware( + noSearchActionRequired + ); + break; + default: + throw new Error('Unsupported solution type', solutionType); + } + + const commerceEngine = buildCommerceEngine({ + ...options, + middlewares: [ + ...(options.middlewares ?? []), + ...middlewares.map(({middleware}) => middleware), + ], + }); + + return { + ...commerceEngine, + + get [stateKey]() { + return commerceEngine[stateKey]; + }, + + waitForRequestCompletedAction() { + return [...middlewares.map(({promise}) => promise)]; + }, + }; +} + +export const buildFactory = + < + TControllerDefinitions extends ControllerDefinitionsMap< + SSRCommerceEngine, + Controller + >, + >( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions + ) => + ( + solutionType: T, + solutionTypeOptions?: RecommendationExtraOptions + ) => + async (...[buildOptions]: BuildParameters) => { + const logger = buildLogger(options.loggerOptions); + if (!options.navigatorContextProvider) { + logger.warn( + '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' + ); + } + const engine = buildSSRCommerceEngine( + solutionType, + buildOptions && 'extend' in buildOptions && buildOptions?.extend + ? await buildOptions.extend(options) + : options, + solutionType === SolutionType.recommendation + ? solutionTypeOptions?.count || 0 + : 0 + ); + const controllers = buildControllerDefinitions({ + definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, + engine, + solutionType, + propsMap: (buildOptions && 'controllers' in buildOptions + ? buildOptions.controllers + : {}) as InferControllerPropsMapFromDefinitions, + }); + + return { + engine, + controllers, + }; + }; diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts new file mode 100644 index 00000000000..ba7bfeaf985 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts @@ -0,0 +1,68 @@ +import {composeFunction} from '../../ssr-engine/common.js'; +import {SolutionType} from '../types/common.js'; +import { + BuildParameters, + HydrateStaticStateFromBuildResultParameters, + HydrateStaticStateFunction, + HydrateStaticStateParameters, + CommerceControllerDefinitionsMap, +} from '../types/core-engine.js'; +import { + buildFactory, + CommerceEngineDefinitionOptions, +} from './build-factory.js'; + +export const hydrateStaticStateFactory: < + TControllerDefinitions extends CommerceControllerDefinitionsMap, +>( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions +) => ( + solutionType: SolutionType +) => HydrateStaticStateFunction = + ( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions + ) => + (solutionType: SolutionType) => + composeFunction( + async ( + ...params: HydrateStaticStateParameters + ) => { + const solutionTypeBuild = await buildFactory(controllerDefinitions, { + ...options, + })(solutionType); + const buildResult = await solutionTypeBuild( + ...(params as BuildParameters) + ); + const staticStateBuild = + await hydrateStaticStateFactory( + controllerDefinitions, + options + )(solutionType); + const staticState = await staticStateBuild.fromBuildResult({ + buildResult, + searchActions: params[0]!.searchActions, + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: HydrateStaticStateFromBuildResultParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + searchActions, + }, + ] = params; + + // TODO: should be only one searchAction for search and listing + searchActions.forEach((action) => { + engine.dispatch(action); + }); + await engine.waitForRequestCompletedAction(); + return {engine, controllers}; + }, + } + ); diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts new file mode 100644 index 00000000000..e2fbf28a605 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts @@ -0,0 +1,61 @@ +import {composeFunction} from '../../ssr-engine/common.js'; +import {SolutionType} from '../types/common.js'; +import { + BuildParameters, + BuildResult, + HydrateStaticStateFromBuildResultParameters, + HydrateStaticStateFunction, + HydrateStaticStateParameters, + CommerceControllerDefinitionsMap, +} from '../types/core-engine.js'; +import { + buildFactory, + CommerceEngineDefinitionOptions, +} from './build-factory.js'; + +export function hydrateRecommendationStaticStateFactory< + TControllerDefinitions extends CommerceControllerDefinitionsMap, +>( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions +): HydrateStaticStateFunction { + return composeFunction( + async (...params: HydrateStaticStateParameters) => { + const solutionTypeBuild = await buildFactory( + controllerDefinitions, + options + )(SolutionType.recommendation, {count: 1111}); // TODO: FIXME: + + const buildResult = (await solutionTypeBuild( + ...(params as BuildParameters) + )) as BuildResult; // TODO: check if can remove the cast + + const staticState = await hydrateRecommendationStaticStateFactory( + controllerDefinitions, + options + ).fromBuildResult({ + buildResult, + searchActions: params[0]!.searchActions, + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: HydrateStaticStateFromBuildResultParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + searchActions, + }, + ] = params; + + searchActions.forEach((action) => { + engine.dispatch(action); + }); + await engine.waitForRequestCompletedAction(); + return {engine, controllers}; + }, + } + ); +} diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts new file mode 100644 index 00000000000..39065944f4e --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -0,0 +1,104 @@ +import {UnknownAction} from '@reduxjs/toolkit'; +import {buildLogger} from '../../logger.js'; +import {composeFunction} from '../../ssr-engine/common.js'; +import {createStaticState, filterRecommendationControllers} from '../common.js'; +import { + EngineStaticState, + InferControllerStaticStateMapFromDefinitionsWithSolutionType, + SolutionType, +} from '../types/common.js'; +import { + BuildResult, + Controllers, + FetchStaticStateFromBuildResultParameters, + FetchStaticStateFunction, + CommerceControllerDefinitionsMap, +} from '../types/core-engine.js'; +import { + buildFactory, + CommerceEngineDefinitionOptions, +} from './build-factory.js'; + +export function fetchRecommendationStaticStateFactory< + TControllerDefinitions extends CommerceControllerDefinitionsMap, +>( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions +): FetchStaticStateFunction { + type ControllerDefinitionKeys = keyof Controllers; + + const logger = buildLogger(options.loggerOptions); + + return composeFunction( + async (...params: [controllerKeys: Array]) => { + const [controllerKeys] = params; + const uniqueControllerKeys = Array.from(new Set(controllerKeys)); + if (uniqueControllerKeys.length !== controllerKeys.length) { + logger.warn( + '[WARNING] Duplicate controller keys detected in recommendation fetchStaticState call. Make sure to provide only unique controller keys.' + ); + } + + const validControllerNames = Object.keys(controllerDefinitions ?? {}); + const allowedRecommendationKeys = uniqueControllerKeys.filter( + (key: string) => validControllerNames.includes(key) + ); + + if (!options.navigatorContextProvider) { + logger.warn( + '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' + ); + } + + const solutionTypeBuild = await buildFactory( + controllerDefinitions, + options + )(SolutionType.recommendation, {count: 1111}); // TODO: FIXME: + + const buildResult = (await solutionTypeBuild( + ...params + )) as BuildResult; // TODO: check if can remove the cast + + const staticState = await fetchRecommendationStaticStateFactory( + controllerDefinitions, + options + ).fromBuildResult({ + buildResult, + recommendationControllerKeys: allowedRecommendationKeys, + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: FetchStaticStateFromBuildResultParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + recommendationControllerKeys, + }, + ] = params; + + filterRecommendationControllers( + controllers, + controllerDefinitions ?? {} + ).refresh(recommendationControllerKeys); + + const searchActions = await Promise.all( + engine.waitForRequestCompletedAction() + ); + + return createStaticState({ + searchActions, + controllers, + }) as EngineStaticState< + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllerDefinitions, + SolutionType + > + >; + }, + } + ); +} diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts new file mode 100644 index 00000000000..10d7f8de969 --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts @@ -0,0 +1,88 @@ +import {UnknownAction} from '@reduxjs/toolkit'; +import {buildProductListing} from '../../../controllers/commerce/product-listing/headless-product-listing.js'; +import {buildSearch} from '../../../controllers/commerce/search/headless-search.js'; +import {composeFunction} from '../../ssr-engine/common.js'; +import {createStaticState} from '../common.js'; +import { + EngineStaticState, + InferControllerStaticStateMapFromDefinitionsWithSolutionType, + SolutionType, +} from '../types/common.js'; +import { + FetchStaticStateFromBuildResultParameters, + FetchStaticStateFunction, + FetchStaticStateParameters, + CommerceControllerDefinitionsMap, +} from '../types/core-engine.js'; +import { + buildFactory, + CommerceEngineDefinitionOptions, +} from './build-factory.js'; + +// TODO: this is not a factory. either make it a factory or rename it +export const fetchStaticStateFactory: < + TControllerDefinitions extends CommerceControllerDefinitionsMap, +>( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions +) => ( + solutionType: SolutionType +) => FetchStaticStateFunction = + ( + controllerDefinitions: TControllerDefinitions, + options: CommerceEngineDefinitionOptions + ) => + (solutionType: SolutionType) => + composeFunction( + async (...params: FetchStaticStateParameters) => { + const solutionTypeBuild = await buildFactory(controllerDefinitions, { + ...options, + })(solutionType); + const buildResult = await solutionTypeBuild(...params); + const staticStateBuild = await fetchStaticStateFactory( + controllerDefinitions, + options + )(solutionType); + const staticState = await staticStateBuild.fromBuildResult({ + buildResult, + // recommendationControllerKeys // TODO: is missing + }); + return staticState; + }, + { + fromBuildResult: async ( + ...params: FetchStaticStateFromBuildResultParameters + ) => { + const [ + { + buildResult: {engine, controllers}, + }, + ] = params; + + switch (solutionType) { + case SolutionType.listing: + buildProductListing(engine).executeFirstRequest(); + break; + case SolutionType.search: + buildSearch(engine).executeFirstSearch(); + break; + } + + // TODO: should be only one searchAction for search and listing + const searchActions = await Promise.all( + engine.waitForRequestCompletedAction() + ); + + return createStaticState({ + searchActions, + controllers, + }) as EngineStaticState< + UnknownAction, + InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllerDefinitions, + SolutionType + > + >; + }, + } + ); diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index e821601f3c6..37eba9acadd 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -1,5 +1,6 @@ import {UnknownAction} from '@reduxjs/toolkit'; import type {Controller} from '../../../controllers/controller/headless-controller.js'; +import {CommerceEngineDefinition} from '../../commerce-engine/commerce-engine.ssr.js'; import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; @@ -7,6 +8,7 @@ import type { FromBuildResult, FromBuildResultOptions, } from '../../ssr-engine/types/from-build-result.js'; +import {SSRCommerceEngine} from '../factories/build-factory.js'; import {Build, BuildOptions} from './build.js'; import { ControllerDefinitionsMap, @@ -118,3 +120,61 @@ export type InferBuildResult< build(...args: unknown[]): Promise; }, > = Awaited>; + +type CustomMap = ControllerDefinitionsMap; + +export type CommerceControllerDefinitionsMap = ControllerDefinitionsMap< + SSRCommerceEngine, + Controller +>; + +type Definition< + TControllerDefinitions extends CommerceControllerDefinitionsMap, +> = CommerceEngineDefinition; + +export type BuildFunction = + Definition['build']; + +export type FetchStaticStateFunction = + Definition['fetchStaticState']; + +export type HydrateStaticStateFunction< + TControllerDefinitions extends CustomMap, +> = Definition['hydrateStaticState']; + +export type FetchStaticStateFromBuildResultFunction< + TControllerDefinitions extends CustomMap, +> = FetchStaticStateFunction['fromBuildResult']; + +export type HydrateStaticStateFromBuildResultFunction< + TControllerDefinitions extends CustomMap, +> = HydrateStaticStateFunction['fromBuildResult']; + +export type BuildParameters = + Parameters>; + +export type FetchStaticStateParameters< + TControllerDefinitions extends CustomMap, +> = Parameters>; + +export type HydrateStaticStateParameters< + TControllerDefinitions extends CustomMap, +> = Parameters>; + +export type FetchStaticStateFromBuildResultParameters< + TControllerDefinitions extends CustomMap, +> = Parameters>; + +export type HydrateStaticStateFromBuildResultParameters< + TControllerDefinitions extends CustomMap, +> = Parameters< + HydrateStaticStateFromBuildResultFunction +>; + +export type Controllers = + InferControllersMapFromDefinition; + +export type BuildResult = { + engine: SSRCommerceEngine; + controllers: Controllers; +}; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index d05a9cb5c15..9bc2dbf0699 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -1,5 +1,4 @@ import type {UnknownAction} from '@reduxjs/toolkit'; -import {buildBaseCommerceAPIRequest} from '../../../features/commerce/common/actions.js'; import {SolutionType} from '../../commerce-ssr-engine/types/common.js'; import type {EngineStaticState} from '../../commerce-ssr-engine/types/common.js'; import type {CoreEngine, CoreEngineNext} from '../../engine.js'; @@ -59,4 +58,3 @@ export type FetchStaticState< EngineStaticState >; }; -buildBaseCommerceAPIRequest; diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index 708cbbefe4f..7c35d848638 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -67,17 +67,18 @@ * @module SSR Commerce */ +export type { + CommerceEngineDefinitionOptions, + SSRCommerceEngine, +} from './app/commerce-ssr-engine/factories/build-factory.js'; + export type {Unsubscribe, Middleware} from '@reduxjs/toolkit'; export type {Relay} from '@coveo/relay'; // Main App export type {CommerceEngineOptions} from './app/commerce-engine/commerce-engine.js'; export type {CommerceEngineConfiguration} from './app/commerce-engine/commerce-engine-configuration.js'; -export type { - SSRCommerceEngine as CommerceEngine, - CommerceEngineDefinition, - CommerceEngineDefinitionOptions, -} from './app/commerce-engine/commerce-engine.ssr.js'; +export type {CommerceEngineDefinition} from './app/commerce-engine/commerce-engine.ssr.js'; export {defineCommerceEngine} from './app/commerce-engine/commerce-engine.ssr.js'; export {getSampleCommerceEngineConfiguration} from './app/commerce-engine/commerce-engine-configuration.js'; From 07d1ec8e41b15dd810e6f6097d840f14d2c86965 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sat, 16 Nov 2024 23:35:14 -0500 Subject: [PATCH 15/48] simplify build factory --- .../commerce-engine/commerce-engine.ssr.ts | 33 ++++++++----------- .../factories/build-factory.ts | 26 +++++---------- ...e-factory.ts => hydrated-state-factory.ts} | 5 ++- ... recommendation-hydrated-state-factory.ts} | 8 ++--- .../recommendation-static-state-factory.ts | 12 +++---- .../factories/static-state-factory.ts | 3 -- .../app/ssr-engine/types/from-build-result.ts | 6 ++-- packages/headless/src/test/mock-engine-v2.ts | 2 +- 8 files changed, 39 insertions(+), 56 deletions(-) rename packages/headless/src/app/commerce-ssr-engine/factories/{hydrate-state-factory.ts => hydrated-state-factory.ts} (92%) rename packages/headless/src/app/commerce-ssr-engine/factories/{recommendation-hydrate-state-factory.ts => recommendation-hydrated-state-factory.ts} (85%) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 6538f7e5756..7ef14861c7c 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -14,8 +14,8 @@ import { CommerceEngineDefinitionOptions, SSRCommerceEngine, } from '../commerce-ssr-engine/factories/build-factory.js'; -import {hydrateStaticStateFactory} from '../commerce-ssr-engine/factories/hydrate-state-factory.js'; -import {hydrateRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-hydrate-state-factory.js'; +import {hydratedStaticStateFactory} from '../commerce-ssr-engine/factories/hydrated-state-factory.js'; +import {hydratedRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-hydrated-state-factory.js'; import {fetchRecommendationStaticStateFactory} from '../commerce-ssr-engine/factories/recommendation-static-state-factory.js'; import {fetchStaticStateFactory} from '../commerce-ssr-engine/factories/static-state-factory.js'; import { @@ -69,9 +69,7 @@ export function defineCommerceEngine< } { const {controllers: controllerDefinitions, ...engineOptions} = options; - const getOptions = () => { - return engineOptions; - }; + const getOptions = () => engineOptions; const setNavigatorContextProvider = ( navigatorContextProvider: NavigatorContextProvider @@ -87,7 +85,7 @@ export function defineCommerceEngine< controllerDefinitions!, getOptions() ); - const hydrateStaticState = hydrateStaticStateFactory( + const hydrateStaticState = hydratedStaticStateFactory( controllerDefinitions!, getOptions() ); @@ -95,11 +93,11 @@ export function defineCommerceEngine< const fetchRecommendationStaticState = fetchRecommendationStaticStateFactory( controllerDefinitions!, - getOptions() // TODO: add count here + getOptions() ); const hydrateRecommendationStaticState = - hydrateRecommendationStaticStateFactory( + hydratedRecommendationStaticStateFactory( controllerDefinitions!, getOptions() ); @@ -118,7 +116,7 @@ export function defineCommerceEngine< setNavigatorContextProvider, } as CommerceEngineDefinition, recommendationEngineDefinition: { - build: build(SolutionType.recommendation), // TODO: add count here + build: build(SolutionType.recommendation), fetchStaticState: fetchRecommendationStaticState, hydrateStaticState: hydrateRecommendationStaticState, setNavigatorContextProvider, @@ -126,7 +124,7 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.recommendation >, - // TODO: make the standaloneEngineDefinition not async since there are no search executed + // TODO: The standaloneEngineDefinition should not be async since no request is sent to the API standaloneEngineDefinition: { build: build(SolutionType.standalone), fetchStaticState: fetchStaticState(SolutionType.standalone), @@ -150,8 +148,8 @@ export function defineCommerceEngine< // standaloneSearchBox: defineStandaloneSearchBox({ // options: {redirectionUrl: 'rest'}, // }), -// facets: defineContext(), -// searchParam: defineParameterManager({search: false}), +// // facets: defineContext(), +// // searchParam: defineParameterManager({search: false}), // trending: defineRecommendations({ // options: {slotId: 'ttt'}, // }), @@ -162,14 +160,11 @@ export function defineCommerceEngine< // }); // // TODO: should have a way to select which recommendation to fetch -// const r = await standaloneEngineDefinition.fetchStaticState({ -// controllers: {searchParam: {initialState: {parameters: {q: 'test'}}}}, -// }); +// const r = await standaloneEngineDefinition.fetchStaticState() + +// const b = await recommendationEngineDefinition.build(['']) -// const b = await recommendationEngineDefinition.fetchStaticState([ -// 'trending', -// ]); -// const b = await recommendationEngineDefinition.fetchStaticState.fromBuildResult({buildResult:{controllers:{}}}) +// const b = await recommendationEngineDefinition.fetchStaticState.fromBuildResult({}) // // b.controllers.; // const a = await searchEngineDefinition.fetchStaticState(); // TODO: fix typing if controller is set to {search: false} diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts index fcf386f7fa7..09d1b84c038 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts @@ -19,15 +19,10 @@ import { } from '../types/common.js'; import { BuildParameters, + CommerceControllerDefinitionsMap, EngineDefinitionOptions, } from '../types/core-engine.js'; -// TODO: rename -export interface RecommendationExtraOptions { - // TODO: rename - count: number; -} - /** * The SSR commerce engine. */ @@ -130,19 +125,11 @@ function buildSSRCommerceEngine( } export const buildFactory = - < - TControllerDefinitions extends ControllerDefinitionsMap< - SSRCommerceEngine, - Controller - >, - >( + ( controllerDefinitions: TControllerDefinitions, options: CommerceEngineDefinitionOptions ) => - ( - solutionType: T, - solutionTypeOptions?: RecommendationExtraOptions - ) => + (solutionType: T) => async (...[buildOptions]: BuildParameters) => { const logger = buildLogger(options.loggerOptions); if (!options.navigatorContextProvider) { @@ -150,15 +137,18 @@ export const buildFactory = '[WARNING] Missing navigator context in server-side code. Make sure to set it with `setNavigatorContextProvider` before calling fetchStaticState()' ); } + const engine = buildSSRCommerceEngine( solutionType, buildOptions && 'extend' in buildOptions && buildOptions?.extend ? await buildOptions.extend(options) : options, - solutionType === SolutionType.recommendation - ? solutionTypeOptions?.count || 0 + solutionType === SolutionType.recommendation && + Array.isArray(buildOptions) + ? buildOptions.length : 0 ); + const controllers = buildControllerDefinitions({ definitionsMap: (controllerDefinitions ?? {}) as TControllerDefinitions, engine, diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts similarity index 92% rename from packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts rename to packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts index ba7bfeaf985..2141cda25ea 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/hydrate-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts @@ -12,7 +12,7 @@ import { CommerceEngineDefinitionOptions, } from './build-factory.js'; -export const hydrateStaticStateFactory: < +export const hydratedStaticStateFactory: < TControllerDefinitions extends CommerceControllerDefinitionsMap, >( controllerDefinitions: TControllerDefinitions, @@ -36,7 +36,7 @@ export const hydrateStaticStateFactory: < ...(params as BuildParameters) ); const staticStateBuild = - await hydrateStaticStateFactory( + await hydratedStaticStateFactory( controllerDefinitions, options )(solutionType); @@ -57,7 +57,6 @@ export const hydrateStaticStateFactory: < }, ] = params; - // TODO: should be only one searchAction for search and listing searchActions.forEach((action) => { engine.dispatch(action); }); diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts similarity index 85% rename from packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts rename to packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts index e2fbf28a605..cd4f1b9755c 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrate-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts @@ -13,7 +13,7 @@ import { CommerceEngineDefinitionOptions, } from './build-factory.js'; -export function hydrateRecommendationStaticStateFactory< +export function hydratedRecommendationStaticStateFactory< TControllerDefinitions extends CommerceControllerDefinitionsMap, >( controllerDefinitions: TControllerDefinitions, @@ -24,13 +24,13 @@ export function hydrateRecommendationStaticStateFactory< const solutionTypeBuild = await buildFactory( controllerDefinitions, options - )(SolutionType.recommendation, {count: 1111}); // TODO: FIXME: + )(SolutionType.recommendation); const buildResult = (await solutionTypeBuild( ...(params as BuildParameters) - )) as BuildResult; // TODO: check if can remove the cast + )) as BuildResult; - const staticState = await hydrateRecommendationStaticStateFactory( + const staticState = await hydratedRecommendationStaticStateFactory( controllerDefinitions, options ).fromBuildResult({ diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts index 39065944f4e..11949733709 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -53,18 +53,18 @@ export function fetchRecommendationStaticStateFactory< const solutionTypeBuild = await buildFactory( controllerDefinitions, options - )(SolutionType.recommendation, {count: 1111}); // TODO: FIXME: + )(SolutionType.recommendation); const buildResult = (await solutionTypeBuild( - ...params - )) as BuildResult; // TODO: check if can remove the cast + allowedRecommendationKeys + )) as BuildResult; const staticState = await fetchRecommendationStaticStateFactory( controllerDefinitions, options ).fromBuildResult({ buildResult, - recommendationControllerKeys: allowedRecommendationKeys, + allowedRecommendationKeys, }); return staticState; }, @@ -75,14 +75,14 @@ export function fetchRecommendationStaticStateFactory< const [ { buildResult: {engine, controllers}, - recommendationControllerKeys, + allowedRecommendationKeys, }, ] = params; filterRecommendationControllers( controllers, controllerDefinitions ?? {} - ).refresh(recommendationControllerKeys); + ).refresh(allowedRecommendationKeys); const searchActions = await Promise.all( engine.waitForRequestCompletedAction() diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts index 10d7f8de969..448c822bb7d 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts @@ -19,7 +19,6 @@ import { CommerceEngineDefinitionOptions, } from './build-factory.js'; -// TODO: this is not a factory. either make it a factory or rename it export const fetchStaticStateFactory: < TControllerDefinitions extends CommerceControllerDefinitionsMap, >( @@ -45,7 +44,6 @@ export const fetchStaticStateFactory: < )(solutionType); const staticState = await staticStateBuild.fromBuildResult({ buildResult, - // recommendationControllerKeys // TODO: is missing }); return staticState; }, @@ -68,7 +66,6 @@ export const fetchStaticStateFactory: < break; } - // TODO: should be only one searchAction for search and listing const searchActions = await Promise.all( engine.waitForRequestCompletedAction() ); diff --git a/packages/headless/src/app/ssr-engine/types/from-build-result.ts b/packages/headless/src/app/ssr-engine/types/from-build-result.ts index 43212747352..ce380b391de 100644 --- a/packages/headless/src/app/ssr-engine/types/from-build-result.ts +++ b/packages/headless/src/app/ssr-engine/types/from-build-result.ts @@ -11,10 +11,12 @@ export interface FromBuildResultOptions< buildResult: EngineDefinitionBuildResult; /** * An optional array of keys representing the recommendation controllers to refresh. - * If a recommendation key defined in your engine definition is present in this list, a recommendation query will be triggered against the API. + * If a recommendation key defined in your engine definition is present in this list, the associate recommendation controller + * will query the API. + * * This is applicable only if the engine is a recommendation engine. */ - recommendationControllerKeys?: (keyof TControllers)[]; + allowedRecommendationKeys?: (keyof TControllers)[]; } export interface FromBuildResult< diff --git a/packages/headless/src/test/mock-engine-v2.ts b/packages/headless/src/test/mock-engine-v2.ts index dd43748f7a7..866548e7200 100644 --- a/packages/headless/src/test/mock-engine-v2.ts +++ b/packages/headless/src/test/mock-engine-v2.ts @@ -3,7 +3,7 @@ import {pino, Logger} from 'pino'; import {vi, Mock} from 'vitest'; import {CaseAssistEngine} from '../app/case-assist-engine/case-assist-engine.js'; import {CommerceEngine} from '../app/commerce-engine/commerce-engine.js'; -import {SSRCommerceEngine} from '../app/commerce-engine/commerce-engine.ssr.js'; +import {SSRCommerceEngine} from '../app/commerce-ssr-engine/factories/build-factory.js'; import type {CoreEngine, CoreEngineNext} from '../app/engine.js'; import {InsightEngine} from '../app/insight-engine/insight-engine.js'; import {defaultNodeJSNavigatorContextProvider} from '../app/navigatorContextProvider.js'; From 9b049fdac11da58f9580ca96ef2ce2b8cff33673 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sat, 16 Nov 2024 23:48:59 -0500 Subject: [PATCH 16/48] adjust factory params and throw error on bad engine definition --- .../src/app/commerce-engine/commerce-engine.ssr.ts | 10 +++++----- .../app/commerce-ssr-engine/factories/build-factory.ts | 2 +- .../factories/hydrated-state-factory.ts | 4 ++-- .../factories/recommendation-hydrated-state-factory.ts | 2 +- .../factories/recommendation-static-state-factory.ts | 2 +- .../factories/static-state-factory.ts | 8 ++++++-- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 7ef14861c7c..56ce7a08b2b 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -78,27 +78,27 @@ export function defineCommerceEngine< }; const build = buildFactory( - controllerDefinitions!, // TODO: find a way to get rid of the ! + controllerDefinitions, getOptions() ); const fetchStaticState = fetchStaticStateFactory( - controllerDefinitions!, + controllerDefinitions, getOptions() ); const hydrateStaticState = hydratedStaticStateFactory( - controllerDefinitions!, + controllerDefinitions, getOptions() ); const fetchRecommendationStaticState = fetchRecommendationStaticStateFactory( - controllerDefinitions!, + controllerDefinitions, getOptions() ); const hydrateRecommendationStaticState = hydratedRecommendationStaticStateFactory( - controllerDefinitions!, + controllerDefinitions, getOptions() ); diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts index 09d1b84c038..f9bc6535353 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts @@ -126,7 +126,7 @@ function buildSSRCommerceEngine( export const buildFactory = ( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ) => (solutionType: T) => diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts index 2141cda25ea..b004b174ea6 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/hydrated-state-factory.ts @@ -15,13 +15,13 @@ import { export const hydratedStaticStateFactory: < TControllerDefinitions extends CommerceControllerDefinitionsMap, >( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ) => ( solutionType: SolutionType ) => HydrateStaticStateFunction = ( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ) => (solutionType: SolutionType) => diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts index cd4f1b9755c..321880e1432 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-hydrated-state-factory.ts @@ -16,7 +16,7 @@ import { export function hydratedRecommendationStaticStateFactory< TControllerDefinitions extends CommerceControllerDefinitionsMap, >( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ): HydrateStaticStateFunction { return composeFunction( diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts index 11949733709..3bb87a8ed09 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -22,7 +22,7 @@ import { export function fetchRecommendationStaticStateFactory< TControllerDefinitions extends CommerceControllerDefinitionsMap, >( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ): FetchStaticStateFunction { type ControllerDefinitionKeys = keyof Controllers; diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts index 448c822bb7d..5ee3776eb31 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts @@ -22,13 +22,13 @@ import { export const fetchStaticStateFactory: < TControllerDefinitions extends CommerceControllerDefinitionsMap, >( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ) => ( solutionType: SolutionType ) => FetchStaticStateFunction = ( - controllerDefinitions: TControllerDefinitions, + controllerDefinitions: TControllerDefinitions | undefined, options: CommerceEngineDefinitionOptions ) => (solutionType: SolutionType) => @@ -64,6 +64,10 @@ export const fetchStaticStateFactory: < case SolutionType.search: buildSearch(engine).executeFirstSearch(); break; + case SolutionType.recommendation: + throw new Error( + 'You are using the wrong engine definition. For recommendations, use the recommendation engine definition instead' + ); } const searchActions = await Promise.all( From 84e5c53397009743deda20411852e741ce71dac2 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sun, 17 Nov 2024 00:42:26 -0500 Subject: [PATCH 17/48] do not ask props for disabled controllers --- .../commerce-engine/commerce-engine.ssr.ts | 19 ++++++++------- .../src/app/commerce-ssr-engine/common.ts | 8 +++---- .../recommendation-static-state-factory.ts | 3 ++- .../app/commerce-ssr-engine/types/build.ts | 20 ++++++++++++---- .../app/commerce-ssr-engine/types/common.ts | 24 +++++++++++++++++-- .../commerce-ssr-engine/types/core-engine.ts | 5 ++-- .../types/fetch-static-state.ts | 20 ++++++++++++---- .../types/hydrate-static-state.ts | 18 +++++++++++--- .../ssr-engine/types/fetch-static-state.ts | 2 +- 9 files changed, 89 insertions(+), 30 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 56ce7a08b2b..e4f63467236 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -137,19 +137,20 @@ export function defineCommerceEngine< }; } -// Sandbox +/* Sandbox */ // const { // recommendationEngineDefinition, // searchEngineDefinition, // standaloneEngineDefinition, +// listingEngineDefinition // } = defineCommerceEngine({ // configuration: getSampleCommerceEngineConfiguration(), // controllers: { -// standaloneSearchBox: defineStandaloneSearchBox({ -// options: {redirectionUrl: 'rest'}, -// }), -// // facets: defineContext(), -// // searchParam: defineParameterManager({search: false}), +// // standaloneSearchBox: defineStandaloneSearchBox({ +// // options: {redirectionUrl: 'rest'}, +// // }), +// // context: defineContext(), +// searchParam: defineParameterManager({search: false}), // trending: defineRecommendations({ // options: {slotId: 'ttt'}, // }), @@ -160,11 +161,11 @@ export function defineCommerceEngine< // }); // // TODO: should have a way to select which recommendation to fetch -// const r = await standaloneEngineDefinition.fetchStaticState() +// const r = await listingEngineDefinition.hydrateStaticState({}) // TODO: now do the same with hydration -// const b = await recommendationEngineDefinition.build(['']) +// const b = await standaloneEngineDefinition.fetchStaticState() -// const b = await recommendationEngineDefinition.fetchStaticState.fromBuildResult({}) +// const b = await recommendationEngineDefinition.fetchStaticState(['popular']) // // b.controllers.; // const a = await searchEngineDefinition.fetchStaticState(); // TODO: fix typing if controller is set to {search: false} diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 7575ee88b31..d2d4b7e60c0 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,4 +1,5 @@ import {UnknownAction} from '@reduxjs/toolkit'; +import {Logger} from 'pino'; import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; import {RecommendationsDefinitionMeta} from '../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; @@ -144,7 +145,8 @@ export function filterRecommendationControllers< TControllerDefinitions extends ControllerDefinitionsMap, >( controllers: Record, // TODO: or InferControllersMapFromDefinition - controllerDefinitions: TControllerDefinitions + controllerDefinitions: TControllerDefinitions, + logger: Logger ) { const slotIdSet = new Set(); @@ -160,7 +162,7 @@ export function filterRecommendationControllers< }; const warnDuplicateRecommendation = (slotId: string, productId?: string) => { - console.warn( + logger.warn( 'Multiple recommendation controllers found for the same slotId and productId', {slotId, productId} ); @@ -195,8 +197,6 @@ export function filterRecommendationControllers< if (whitelist === undefined) { return; } - // TODO: FIND a better way - // TODO: do not refresh multiple recommendation controllers for the same slotId and productId const isRecommendationController = (key: string) => name.includes(key) && whitelist.includes(key); diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts index 3bb87a8ed09..3432e762a78 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -81,7 +81,8 @@ export function fetchRecommendationStaticStateFactory< filterRecommendationControllers( controllers, - controllerDefinitions ?? {} + controllerDefinitions ?? {}, + logger ).refresh(allowedRecommendationKeys); const searchActions = await Promise.all( diff --git a/packages/headless/src/app/commerce-ssr-engine/types/build.ts b/packages/headless/src/app/commerce-ssr-engine/types/build.ts index a3b5fe1a238..3333d066720 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/build.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/build.ts @@ -1,13 +1,17 @@ +import type {Controller} from '../../../controllers/controller/headless-controller.js'; import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, ControllersPropsMap, EngineDefinitionBuildResult, - EngineDefinitionControllersPropsOption, OptionsExtender, OptionsTuple, } from '../../ssr-engine/types/common.js'; -import {SolutionType} from './common.js'; +import { + ControllerDefinitionsMap, + EngineDefinitionControllersPropsOption, + SolutionType, +} from './common.js'; export interface BuildOptions { extend?: OptionsExtender; @@ -18,6 +22,10 @@ export type Build< TEngineOptions, TControllersMap extends ControllersMap, TControllersProps extends ControllersPropsMap, + TControllersDefinitionsMap extends ControllerDefinitionsMap< + TEngine, + Controller + >, TSolutionType extends SolutionType, > = TSolutionType extends SolutionType.recommendation ? { @@ -25,7 +33,7 @@ export type Build< * Initializes an engine and controllers from the definition. */ ( - c: (keyof TControllersMap)[] + controllers: (keyof TControllersMap)[] ): Promise>; } : { @@ -35,7 +43,11 @@ export type Build< ( ...params: OptionsTuple< BuildOptions & - EngineDefinitionControllersPropsOption + EngineDefinitionControllersPropsOption< + TControllersDefinitionsMap, + TControllersProps, + TSolutionType + > > ): Promise>; }; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index 74cad63a186..0f6b6a7bb94 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -10,14 +10,14 @@ import type { InferControllerPropsMapFromDefinitions, ControllerStaticStateMap, EngineDefinitionBuildResult, - EngineDefinitionControllersPropsOption, HydratedState, OptionsTuple, + ControllersPropsMap, + HasKeys, } from '../../ssr-engine/types/common.js'; export type { EngineDefinitionBuildResult, - EngineDefinitionControllersPropsOption, HydratedState, OptionsTuple, InferControllerStaticStateFromController, @@ -149,6 +149,26 @@ export type InferControllerStaticStateMapFromDefinitionsWithSolutionType< >; }; +export type EngineDefinitionControllersPropsOption< + TControllers extends ControllerDefinitionsMap< + CoreEngine | CoreEngineNext, + Controller + >, + TControllersPropsMap extends ControllersPropsMap, + TSolutionType extends SolutionType, +> = { + [K in keyof TControllers as HasKey< + TControllers[K], + TSolutionType + > extends never + ? never + : K]: HasKeys extends false + ? {} + : { + controllers: TControllersPropsMap; + }; +}; + export interface ControllerDefinitionOption { /** * Whether the controller will be used in a product listing context. diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 37eba9acadd..4b5730db093 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -26,8 +26,6 @@ import { HydrateStaticStateOptions, } from './hydrate-static-state.js'; -// TODO: why not use the one for commerce - export type { FromBuildResult, FromBuildResultOptions, @@ -69,6 +67,7 @@ export interface EngineDefinition< TSolutionType >, InferControllerPropsMapFromDefinitions, + TControllers, TSolutionType >; /** @@ -79,6 +78,7 @@ export interface EngineDefinition< InferControllersMapFromDefinition, UnknownAction, InferControllerPropsMapFromDefinitions, + TControllers, TSolutionType >; /** @@ -89,6 +89,7 @@ export interface EngineDefinition< TEngineOptions, InferControllersMapFromDefinition, InferControllerPropsMapFromDefinitions, + TControllers, TSolutionType >; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index 9bc2dbf0699..2469152c61c 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -1,12 +1,16 @@ import type {UnknownAction} from '@reduxjs/toolkit'; +import type {Controller} from '../../../controllers/controller/headless-controller.js'; import {SolutionType} from '../../commerce-ssr-engine/types/common.js'; -import type {EngineStaticState} from '../../commerce-ssr-engine/types/common.js'; +import type { + ControllerDefinitionsMap, + EngineDefinitionControllersPropsOption, + EngineStaticState, +} from '../../commerce-ssr-engine/types/common.js'; import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, ControllersPropsMap, ControllerStaticStateMap, - EngineDefinitionControllersPropsOption, OptionsTuple, } from '../../ssr-engine/types/common.js'; import type {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; @@ -19,6 +23,10 @@ export type FetchStaticState< TSearchAction extends UnknownAction, TControllersStaticState extends ControllerStaticStateMap, TControllersProps extends ControllersPropsMap, + TControllersDefinitionsMap extends ControllerDefinitionsMap< + TEngine, + Controller + >, TSolutionType extends SolutionType, > = TSolutionType extends SolutionType.recommendation ? { @@ -28,7 +36,7 @@ export type FetchStaticState< * Useful for static generation and server-side rendering. */ ( - controllers: Array // TODO: make the array unique + controllers: Array ): Promise>; fromBuildResult: FromBuildResult< @@ -47,7 +55,11 @@ export type FetchStaticState< ( ...params: OptionsTuple< FetchStaticStateOptions & - EngineDefinitionControllersPropsOption + EngineDefinitionControllersPropsOption< + TControllersDefinitionsMap, + TControllersProps, + TSolutionType + > > ): Promise>; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts index 34669305f9d..0896a068a03 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -1,14 +1,18 @@ import type {UnknownAction} from '@reduxjs/toolkit'; +import type {Controller} from '../../../controllers/controller/headless-controller.js'; import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, ControllersPropsMap, - EngineDefinitionControllersPropsOption, HydratedState, OptionsTuple, } from '../../ssr-engine/types/common.js'; import type {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; -import {SolutionType} from './common.js'; +import { + ControllerDefinitionsMap, + EngineDefinitionControllersPropsOption, + SolutionType, +} from './common.js'; export interface HydrateStaticStateOptions { searchActions: TSearchAction[]; @@ -19,6 +23,10 @@ export type HydrateStaticState< TControllers extends ControllersMap, TSearchAction extends UnknownAction, TControllersProps extends ControllersPropsMap, + TControllersDefinitionsMap extends ControllerDefinitionsMap< + TEngine, + Controller + >, TSolutionType extends SolutionType, > = TSolutionType extends SolutionType.recommendation ? { @@ -47,7 +55,11 @@ export type HydrateStaticState< ( ...params: OptionsTuple< HydrateStaticStateOptions & - EngineDefinitionControllersPropsOption + EngineDefinitionControllersPropsOption< + TControllersDefinitionsMap, + TControllersProps, + TSolutionType + > > ): Promise>; diff --git a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts index ad02be7dba8..bc0ade2d47a 100644 --- a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts @@ -53,7 +53,7 @@ export type FetchStaticStateWithList< * Useful for static generation and server-side rendering. */ ( - c: (keyof TControllers)[], + controllers: (keyof TControllers)[], ...params: OptionsTuple< FetchStaticStateOptions & EngineDefinitionControllersPropsOption From 018c0be01999d7896def20a77e64c0cce5e62d14 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sun, 17 Nov 2024 00:43:21 -0500 Subject: [PATCH 18/48] remove comments --- .../commerce-engine/commerce-engine.ssr.ts | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index e4f63467236..a82fc09ceb6 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -2,13 +2,6 @@ * Utility functions to be used for Commerce Server Side Rendering. */ import type {Controller} from '../../controllers/controller/headless-controller.js'; -// import { -// defineContext, -// defineParameterManager, -// defineRecommendations, -// defineStandaloneSearchBox, -// getSampleCommerceEngineConfiguration, -// } from '../../ssr-commerce.index.js'; import { buildFactory, CommerceEngineDefinitionOptions, @@ -89,13 +82,11 @@ export function defineCommerceEngine< controllerDefinitions, getOptions() ); - const fetchRecommendationStaticState = fetchRecommendationStaticStateFactory( controllerDefinitions, getOptions() ); - const hydrateRecommendationStaticState = hydratedRecommendationStaticStateFactory( controllerDefinitions, @@ -136,37 +127,3 @@ export function defineCommerceEngine< >, }; } - -/* Sandbox */ -// const { -// recommendationEngineDefinition, -// searchEngineDefinition, -// standaloneEngineDefinition, -// listingEngineDefinition -// } = defineCommerceEngine({ -// configuration: getSampleCommerceEngineConfiguration(), -// controllers: { -// // standaloneSearchBox: defineStandaloneSearchBox({ -// // options: {redirectionUrl: 'rest'}, -// // }), -// // context: defineContext(), -// searchParam: defineParameterManager({search: false}), -// trending: defineRecommendations({ -// options: {slotId: 'ttt'}, -// }), -// popular: defineRecommendations({ -// options: {slotId: 'ppp'}, -// }), -// }, -// }); - -// // TODO: should have a way to select which recommendation to fetch -// const r = await listingEngineDefinition.hydrateStaticState({}) // TODO: now do the same with hydration - -// const b = await standaloneEngineDefinition.fetchStaticState() - -// const b = await recommendationEngineDefinition.fetchStaticState(['popular']) -// // b.controllers.; - -// const a = await searchEngineDefinition.fetchStaticState(); // TODO: fix typing if controller is set to {search: false} -// // a.controllers.; // TODO: should throw an error since it's not defined in search From a7200d87f37deb07b20eca55a3489b746ed14731 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sun, 17 Nov 2024 00:47:44 -0500 Subject: [PATCH 19/48] update warning message --- .../app/commerce-ssr-engine/factories/static-state-factory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts index 5ee3776eb31..b152e585798 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts @@ -66,7 +66,7 @@ export const fetchStaticStateFactory: < break; case SolutionType.recommendation: throw new Error( - 'You are using the wrong engine definition. For recommendations, use the recommendation engine definition instead' + '[WARNING] Invalid engine definition. For recommendations, use the recommendation engine definition instead' ); } From 785951f84a4399371a146894d6d5faf78c5e0402 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Sun, 17 Nov 2024 00:49:43 -0500 Subject: [PATCH 20/48] fix export --- packages/headless/src/ssr-commerce.index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/headless/src/ssr-commerce.index.ts b/packages/headless/src/ssr-commerce.index.ts index 7c35d848638..aac2508d21c 100644 --- a/packages/headless/src/ssr-commerce.index.ts +++ b/packages/headless/src/ssr-commerce.index.ts @@ -69,7 +69,7 @@ export type { CommerceEngineDefinitionOptions, - SSRCommerceEngine, + SSRCommerceEngine as CommerceEngine, } from './app/commerce-ssr-engine/factories/build-factory.js'; export type {Unsubscribe, Middleware} from '@reduxjs/toolkit'; From f342da5932de12cbccfe69dc0e5ee5a7ae13e943 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Mon, 18 Nov 2024 07:13:55 +0100 Subject: [PATCH 21/48] clean PR --- .../src/app/commerce-ssr-engine/common.ts | 72 ----------------- .../recommendation-static-state-factory.ts | 78 ++++++++++++++++++- .../app/commerce-ssr-engine/types/common.ts | 2 - .../ssr-engine/types/fetch-static-state.ts | 29 ------- .../headless-core-parameter-manager.ssr.ts | 1 - packages/headless/src/utils/utils.ts | 19 ++--- .../app/(listing)/[category]/page.tsx | 17 +--- .../app/_components/pages/recommendation.tsx | 50 ------------ .../components/pages/product-page.tsx | 2 + .../components/providers/listing-provider.tsx | 4 +- .../providers/recommendation-provider.tsx | 60 -------------- .../lib/commerce-engine-config.ts | 14 ---- .../lib/commerce-engine.ts | 8 -- 13 files changed, 88 insertions(+), 268 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx delete mode 100644 packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index d2d4b7e60c0..377b79424d5 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -1,7 +1,4 @@ import {UnknownAction} from '@reduxjs/toolkit'; -import {Logger} from 'pino'; -import {Recommendations} from '../../controllers/commerce/recommendations/headless-recommendations.js'; -import {RecommendationsDefinitionMeta} from '../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; import {Controller} from '../../controllers/controller/headless-controller.js'; import {InvalidControllerDefinition} from '../../utils/errors.js'; import {clone, filterObject, mapObject} from '../../utils/utils.js'; @@ -139,72 +136,3 @@ export function ensureAtLeastOneSolutionType( throw new InvalidControllerDefinition(); } } - -export function filterRecommendationControllers< - TEngine extends CoreEngine | CoreEngineNext, - TControllerDefinitions extends ControllerDefinitionsMap, ->( - controllers: Record, // TODO: or InferControllersMapFromDefinition - controllerDefinitions: TControllerDefinitions, - logger: Logger -) { - const slotIdSet = new Set(); - - const isRecommendationDefinition = < - C extends ControllerDefinition, - >( - controllerDefinition: C - ): controllerDefinition is C & RecommendationsDefinitionMeta => { - return ( - 'recommendation' in controllerDefinition && - controllerDefinition.recommendation === true - ); - }; - - const warnDuplicateRecommendation = (slotId: string, productId?: string) => { - logger.warn( - 'Multiple recommendation controllers found for the same slotId and productId', - {slotId, productId} - ); - }; - - const filtered = Object.entries(controllerDefinitions).filter( - ([_, value]) => { - if (!isRecommendationDefinition(value)) { - return false; - } - const {slotId, productId} = value.options; - const key = `${slotId}${productId || ''}`; - if (slotIdSet.has(key)) { - warnDuplicateRecommendation(slotId, productId); - return false; - } - slotIdSet.add(key); - return true; - } - ); - - const name = filtered.map(([name, _]) => name); - - return { - /** - * Go through all the controllers passed in argument and only refresh recommendation controllers. - * - * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. - * @param controllerNames - A list of all recommendation controllers to refresh - */ - refresh(whitelist?: string[]) { - if (whitelist === undefined) { - return; - } - const isRecommendationController = (key: string) => - name.includes(key) && whitelist.includes(key); - - Object.entries(controllers) - .filter(([key, _]) => isRecommendationController(key)) - .forEach(([_, controller]) => - (controller as Recommendations).refresh?.() - ); - }, - }; -} diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts index 3432e762a78..2d58a08c4ba 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -1,8 +1,15 @@ import {UnknownAction} from '@reduxjs/toolkit'; +import {Logger} from 'pino'; +import {Recommendations} from '../../../controllers/commerce/recommendations/headless-recommendations.js'; +import {RecommendationsDefinitionMeta} from '../../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; +import {Controller} from '../../../controllers/controller/headless-controller.js'; +import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {buildLogger} from '../../logger.js'; import {composeFunction} from '../../ssr-engine/common.js'; -import {createStaticState, filterRecommendationControllers} from '../common.js'; +import {createStaticState} from '../common.js'; import { + ControllerDefinition, + ControllerDefinitionsMap, EngineStaticState, InferControllerStaticStateMapFromDefinitionsWithSolutionType, SolutionType, @@ -103,3 +110,72 @@ export function fetchRecommendationStaticStateFactory< } ); } + +function filterRecommendationControllers< + TEngine extends CoreEngine | CoreEngineNext, + TControllerDefinitions extends ControllerDefinitionsMap, +>( + controllers: Record, // TODO: or InferControllersMapFromDefinition + controllerDefinitions: TControllerDefinitions, + logger: Logger +) { + const slotIdSet = new Set(); + + const isRecommendationDefinition = < + C extends ControllerDefinition, + >( + controllerDefinition: C + ): controllerDefinition is C & RecommendationsDefinitionMeta => { + return ( + 'recommendation' in controllerDefinition && + controllerDefinition.recommendation === true + ); + }; + + const warnDuplicateRecommendation = (slotId: string, productId?: string) => { + logger.warn( + 'Multiple recommendation controllers found for the same slotId and productId', + {slotId, productId} + ); + }; + + const filtered = Object.entries(controllerDefinitions).filter( + ([_, value]) => { + if (!isRecommendationDefinition(value)) { + return false; + } + const {slotId, productId} = value.options; + const key = `${slotId}${productId || ''}`; + if (slotIdSet.has(key)) { + warnDuplicateRecommendation(slotId, productId); + return false; + } + slotIdSet.add(key); + return true; + } + ); + + const name = filtered.map(([name, _]) => name); + + return { + /** + * Go through all the controllers passed in argument and only refresh recommendation controllers. + * + * @param controllers - A record of all controllers where the key is the controller name and the value is the controller instance. + * @param controllerNames - A list of all recommendation controllers to refresh + */ + refresh(whitelist?: string[]) { + if (whitelist === undefined) { + return; + } + const isRecommendationController = (key: string) => + name.includes(key) && whitelist.includes(key); + + Object.entries(controllers) + .filter(([key, _]) => isRecommendationController(key)) + .forEach(([_, controller]) => + (controller as Recommendations).refresh?.() + ); + }, + }; +} diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index 0f6b6a7bb94..087b02b50d2 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -296,7 +296,6 @@ export type SubControllerDefinitionWithoutProps< : TDefinition extends {listing: false; search: false} ? InvalidControllerDefinition : never; -// TODO: add recommendation type here! export type SubControllerDefinitionWithProps< TController extends Controller, @@ -311,4 +310,3 @@ export type SubControllerDefinitionWithProps< : TDefinition extends {listing: false; search: false} ? InvalidControllerDefinition : never; -// TODO: add recommendation type here! diff --git a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts index bc0ade2d47a..a1f87a7781a 100644 --- a/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/ssr-engine/types/fetch-static-state.ts @@ -38,32 +38,3 @@ export type FetchStaticState< EngineStaticState >; }; - -// TODO: find a better name -export type FetchStaticStateWithList< - TEngine extends CoreEngine | CoreEngineNext, - TControllers extends ControllersMap, - TSearchAction extends UnknownAction, - TControllersStaticState extends ControllerStaticStateMap, - TControllersProps extends ControllersPropsMap, -> = { - /** - * Executes only the initial search for a given configuration, then returns a resumable snapshot of engine state along with the state of the controllers. - * - * Useful for static generation and server-side rendering. - */ - ( - controllers: (keyof TControllers)[], - ...params: OptionsTuple< - FetchStaticStateOptions & - EngineDefinitionControllersPropsOption - > - ): Promise>; - - fromBuildResult: FromBuildResult< - TEngine, - TControllers, - FetchStaticStateOptions, - EngineStaticState - >; -}; diff --git a/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts b/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts index 4b783b7453b..2a4917db7d0 100644 --- a/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts @@ -46,7 +46,6 @@ export function defineParameterManager< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { - // TODO: we do now want to have to set the props if we set standalone to false... same thing with other components like cart ...options, buildWithProps: (engine, props, solutionType) => { if (solutionType === SolutionType.listing) { diff --git a/packages/headless/src/utils/utils.ts b/packages/headless/src/utils/utils.ts index ee2ec13dd87..99af274dd43 100644 --- a/packages/headless/src/utils/utils.ts +++ b/packages/headless/src/utils/utils.ts @@ -162,13 +162,17 @@ export function createWaitForActionMiddleware( function isRecommendationActionPayload

( action: unknown ): action is PayloadAction { - // TODO: clean that thing!! - if (typeof action === 'object' && action !== null && 'meta' in action) { + if (action === null || action === undefined) { + return false; + } + + if (typeof action === 'object' && 'meta' in action) { return ( (action as PayloadAction).meta ?.arg?.slotId !== undefined ); } + return false; } @@ -177,7 +181,6 @@ export function createWaitForActionMiddlewareForRecommendation< >( isDesiredAction: (action: unknown) => action is TAction, memo: Set - //TODO:: this will not work for non recommendation action ): {promise: Promise; middleware: Middleware} { const {promise, resolve} = createDeferredPromise(); let hasBeenResolved = false; @@ -186,22 +189,12 @@ export function createWaitForActionMiddlewareForRecommendation< const middleware: Middleware = () => (next) => (action) => { next(action); - // if (isDesiredAction(action) && isRecommendationActionPayload(action)) { - // console.log(''); - // console.log('slotId:', action.meta.arg.slotId); - // console.log( - // 'condition: ', - // hasBeenResolved, - // memo.has(action.meta.arg.slotId) - // ); - // } if ( isDesiredAction(action) && !hasBeenResolved && isRecommendationActionPayload(action) && !hasSlotBeenProcessed(action.meta.arg.slotId) ) { - // console.log(' --- RESOLVE ---'); hasBeenResolved = true; memo.add(action.meta.arg.slotId); resolve(action); diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index 269a0d5b346..740a6869fa6 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -6,15 +6,11 @@ import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; import ListingProvider from '@/components/providers/listing-provider'; -import RecommendationProvider from '@/components/providers/recommendation-provider'; import Recommendations from '@/components/recommendation-list'; import Sort from '@/components/sort'; import StandaloneSearchBox from '@/components/standalone-search-box'; import Summary from '@/components/summary'; -import { - listingEngineDefinition, - recommendationEngineDefinition, -} from '@/lib/commerce-engine'; +import {listingEngineDefinition} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; @@ -58,10 +54,6 @@ export default async function Listing({params}: {params: {category: string}}) { }, }); - const recStaticState = await recommendationEngineDefinition.fetchStaticState([ - 'popularBoughtRecs', - ]); - return (

- - - +
diff --git a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx b/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx deleted file mode 100644 index e3c7d60fcc5..00000000000 --- a/packages/samples/headless-ssr-commerce/app/_components/pages/recommendation.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import {NavigatorContext} from '@coveo/headless/ssr-commerce'; -import {useEffect, useState} from 'react'; -import { - RecommendationStaticState, - RecommendationHydratedState, - recommendationEngineDefinition, -} from '../../_lib/commerce-engine'; -import {Recommendations} from '../recommendation-list'; - -export default function Recommendation({ - staticState, - navigatorContext, -}: { - staticState: RecommendationStaticState; - navigatorContext: NavigatorContext; -}) { - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - recommendationEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - recommendationEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - return ( - <> - - - - ); -} diff --git a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx index 91b90767b71..7dd0e40d8a7 100644 --- a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx +++ b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx @@ -46,6 +46,8 @@ export default function ProductPage(props: IProductPageProps) { }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); + + // TODO: use recommendationEngineDefinition for engines controllers }); }, [staticState]); diff --git a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx index 72641370628..46e777db338 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx @@ -38,9 +38,7 @@ export default function ListingProvider({ }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - // controllers.popularBoughtRecs.refresh(); // FIXME: does not work + // TODO: use recommendationEngineDefinition for recommendation controllers }); }, [staticState]); diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx deleted file mode 100644 index fe80184f3a9..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client'; - -import { - recommendationEngineDefinition, - RecommendationHydratedState, - RecommendationStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; - -interface RecommendationPageProps { - staticState: RecommendationStaticState; - navigatorContext: NavigatorContext; -} - -export default function RecommendationProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - recommendationEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - recommendationEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) // TODO: need to pass the search actions!!!! - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - <>{children} - - ); - } else { - return ( - - {/* // TODO: Add KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. - Type 'bigint' is not assignable to type 'ReactNode'.*/} - <>{children} - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts index 7cb742017ae..e4545e20d9f 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts @@ -47,20 +47,6 @@ export default { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, }), - popularBoughtRecs_DUPLICATE: defineRecommendations({ - // TODO: support option to run only on specific - options: { - slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', - }, - }), - // TODO: [x] check for invalid slotId => it will reject as expected - // TODO: [ ] check for duplicate slotId - // TODO: encounter for multiple recommendations with same slot id - // popwularBoughtRecs: defineRecommendations({ - // options: { - // slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30172', - // }, - // }), cart: defineCart(), searchBox: defineSearchBox(), context: defineContext(), diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts index a2a7ed771f5..91ea7d56473 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts @@ -11,7 +11,6 @@ export const { listingEngineDefinition, searchEngineDefinition, standaloneEngineDefinition, - recommendationEngineDefinition, useEngine, } = engineDefinition; @@ -55,10 +54,3 @@ export type StandaloneStaticState = InferStaticState< export type StandaloneHydratedState = InferHydratedState< typeof standaloneEngineDefinition >; - -export type RecommendationStaticState = InferStaticState< - typeof recommendationEngineDefinition ->; -export type RecommendationHydratedState = InferHydratedState< - typeof recommendationEngineDefinition ->; From 677fd451c1b88b4507157a7e55b2272fdef9df13 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Mon, 18 Nov 2024 08:25:59 +0100 Subject: [PATCH 22/48] clean controller build condition --- .../src/app/commerce-ssr-engine/common.ts | 35 +++++-------------- .../recommendation-static-state-factory.ts | 2 +- .../headless-core-breadcrumb-manager.ssr.ts | 2 ++ .../headless-commerce-facet-generator.ssr.ts | 2 ++ .../headless-core-commerce-pagination.ssr.ts | 2 ++ .../headless-core-parameter-manager.ssr.ts | 2 ++ .../sort/headless-core-commerce-sort.ssr.ts | 2 ++ .../headless-sub-controller.ssr.ts | 2 ++ .../core/summary/headless-core-summary.ssr.ts | 2 ++ .../product-list/headless-product-list.ssr.ts | 2 ++ 10 files changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/common.ts b/packages/headless/src/app/commerce-ssr-engine/common.ts index 377b79424d5..27892b9f6ea 100644 --- a/packages/headless/src/app/commerce-ssr-engine/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/common.ts @@ -80,34 +80,17 @@ export function buildControllerDefinitions< TSolutionType > { const controllerMap = mapObject(definitionsMap, (definition, key) => { - const unavailableInSearchSolutionType = - 'search' in definition && - definition['search'] === false && - solutionType === SolutionType['search']; - - const unavailableInListingSolutionType = - 'listing' in definition && - definition['listing'] === false && - solutionType === SolutionType['listing']; - - const unavailableInStandaloneSolutionType = - solutionType === SolutionType['standalone'] && 'standalone' in definition - ? definition['standalone'] === false - : false; - - const unavailabeInRecs = - // TODO: use this disjunction pattern for all other conditions - (solutionType === SolutionType['recommendation'] && - !('recommendation' in definition)) || - ('recommendation' in definition && - definition['recommendation'] === false && - solutionType === SolutionType['recommendation']); + const unavailableInSolutionType = (type: SolutionType) => + (!(type in definition) && solutionType === SolutionType[type]) || + (type in definition && + definition[type as keyof typeof definition] === false && + solutionType === SolutionType[type]); if ( - unavailableInSearchSolutionType || - unavailableInListingSolutionType || - unavailableInStandaloneSolutionType || - unavailabeInRecs + unavailableInSolutionType(SolutionType.search) || + unavailableInSolutionType(SolutionType.listing) || + unavailableInSolutionType(SolutionType.standalone) || + unavailableInSolutionType(SolutionType.recommendation) ) { return null; } diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts index 2d58a08c4ba..e0bb3646a9f 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -115,7 +115,7 @@ function filterRecommendationControllers< TEngine extends CoreEngine | CoreEngineNext, TControllerDefinitions extends ControllerDefinitionsMap, >( - controllers: Record, // TODO: or InferControllersMapFromDefinition + controllers: Record, controllerDefinitions: TControllerDefinitions, logger: Logger ) { diff --git a/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ssr.ts b/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ssr.ts index 6557cfc3c12..8416e622351 100644 --- a/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/breadcrumb-manager/headless-core-breadcrumb-manager.ssr.ts @@ -25,6 +25,8 @@ export function defineBreadcrumbManager< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + listing: true, + search: true, ...options, build: (engine, solutionType) => solutionType === SolutionType.listing diff --git a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts index d008d0039b8..d68d9c8b07a 100644 --- a/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.ts @@ -113,6 +113,8 @@ export function defineFacetGenerator< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + listing: true, + search: true, ...options, build: (engine, solutionType) => buildFacetGenerator(engine, {props: {solutionType: solutionType!}}), diff --git a/packages/headless/src/controllers/commerce/core/pagination/headless-core-commerce-pagination.ssr.ts b/packages/headless/src/controllers/commerce/core/pagination/headless-core-commerce-pagination.ssr.ts index 10e29dd8a9d..0fe04c67d38 100644 --- a/packages/headless/src/controllers/commerce/core/pagination/headless-core-commerce-pagination.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/pagination/headless-core-commerce-pagination.ssr.ts @@ -28,6 +28,8 @@ export function definePagination< >(props?: PaginationProps & TOptions) { ensureAtLeastOneSolutionType(props); return { + listing: true, + search: true, ...props, build: (engine, solutionType) => solutionType === SolutionType.listing diff --git a/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts b/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts index 2a4917db7d0..fa85bfd07f0 100644 --- a/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/parameter-manager/headless-core-parameter-manager.ssr.ts @@ -46,6 +46,8 @@ export function defineParameterManager< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + listing: true, + search: true, ...options, buildWithProps: (engine, props, solutionType) => { if (solutionType === SolutionType.listing) { diff --git a/packages/headless/src/controllers/commerce/core/sort/headless-core-commerce-sort.ssr.ts b/packages/headless/src/controllers/commerce/core/sort/headless-core-commerce-sort.ssr.ts index 24743cd3ef9..126497a8580 100644 --- a/packages/headless/src/controllers/commerce/core/sort/headless-core-commerce-sort.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/sort/headless-core-commerce-sort.ssr.ts @@ -24,6 +24,8 @@ export function defineSort< >(props?: SortProps & TOptions) { ensureAtLeastOneSolutionType(props); return { + listing: true, + search: true, ...props, build: (engine, solutionType) => solutionType === SolutionType.listing diff --git a/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ssr.ts b/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ssr.ts index 066a3f38690..356d4caf5cf 100644 --- a/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/sub-controller/headless-sub-controller.ssr.ts @@ -17,6 +17,8 @@ export function defineQuerySummary< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + listing: true, + search: true, ...options, build: (engine, solutionType) => solutionType === SolutionType.listing diff --git a/packages/headless/src/controllers/commerce/core/summary/headless-core-summary.ssr.ts b/packages/headless/src/controllers/commerce/core/summary/headless-core-summary.ssr.ts index d8f410d8d4c..85e788de36a 100644 --- a/packages/headless/src/controllers/commerce/core/summary/headless-core-summary.ssr.ts +++ b/packages/headless/src/controllers/commerce/core/summary/headless-core-summary.ssr.ts @@ -32,6 +32,8 @@ export function defineSummary< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + listing: true, + search: true, ...options, build: (engine, solutionType) => solutionType === SolutionType.listing diff --git a/packages/headless/src/controllers/commerce/product-list/headless-product-list.ssr.ts b/packages/headless/src/controllers/commerce/product-list/headless-product-list.ssr.ts index df540b83c34..d8c559f0137 100644 --- a/packages/headless/src/controllers/commerce/product-list/headless-product-list.ssr.ts +++ b/packages/headless/src/controllers/commerce/product-list/headless-product-list.ssr.ts @@ -29,6 +29,8 @@ export function defineProductList< >(options?: TOptions) { ensureAtLeastOneSolutionType(options); return { + listing: true, + search: true, ...options, build: (engine, solutionType) => solutionType === SolutionType.listing From 87d07920f1edfe4c7dd9afe8023adedfef90945d Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Mon, 18 Nov 2024 08:33:00 +0100 Subject: [PATCH 23/48] create commerce build result type --- .../commerce-ssr-engine/types/core-engine.ts | 6 ++-- .../types/fetch-static-state.ts | 2 +- .../types/from-build-result.ts | 32 +++++++++++++++++++ .../types/hydrate-static-state.ts | 2 +- .../src/app/ssr-engine/types/build.ts | 12 ------- .../app/ssr-engine/types/from-build-result.ts | 10 ------ 6 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 4b5730db093..5c8d107c6f3 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -4,10 +4,7 @@ import {CommerceEngineDefinition} from '../../commerce-engine/commerce-engine.ss import {EngineConfiguration} from '../../engine-configuration.js'; import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {NavigatorContextProvider} from '../../navigatorContextProvider.js'; -import type { - FromBuildResult, - FromBuildResultOptions, -} from '../../ssr-engine/types/from-build-result.js'; +import type {FromBuildResultOptions} from '../../ssr-engine/types/from-build-result.js'; import {SSRCommerceEngine} from '../factories/build-factory.js'; import {Build, BuildOptions} from './build.js'; import { @@ -21,6 +18,7 @@ import { FetchStaticState, FetchStaticStateOptions, } from './fetch-static-state.js'; +import {FromBuildResult} from './from-build-result.js'; import { HydrateStaticState, HydrateStaticStateOptions, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index 2469152c61c..71bafbc5cd6 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -13,7 +13,7 @@ import type { ControllerStaticStateMap, OptionsTuple, } from '../../ssr-engine/types/common.js'; -import type {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; +import {FromBuildResult} from './from-build-result.js'; export type FetchStaticStateOptions = {}; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts new file mode 100644 index 00000000000..e9c5933b9cf --- /dev/null +++ b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts @@ -0,0 +1,32 @@ +import {CoreEngine, CoreEngineNext} from '../../engine.js'; +import {ControllersMap} from '../../ssr-engine/types/common.js'; +import {EngineDefinitionBuildResult} from './common.js'; + +export interface FromBuildResultOptions< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, +> { + /** + * The build result of the engine + */ + buildResult: EngineDefinitionBuildResult; + /** + * An optional array of keys representing the recommendation controllers to refresh. + * If a recommendation key defined in your engine definition is present in this list, the associate recommendation controller + * will query the API. + * + * This is applicable only if the engine is a recommendation engine. + */ + allowedRecommendationKeys?: (keyof TControllers)[]; +} + +export interface FromBuildResult< + TEngine extends CoreEngine | CoreEngineNext, + TControllers extends ControllersMap, + TOptions, + TReturn, +> { + ( + options: FromBuildResultOptions & TOptions + ): Promise; +} diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts index 0896a068a03..d45c07d6af2 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -7,12 +7,12 @@ import type { HydratedState, OptionsTuple, } from '../../ssr-engine/types/common.js'; -import type {FromBuildResult} from '../../ssr-engine/types/from-build-result.js'; import { ControllerDefinitionsMap, EngineDefinitionControllersPropsOption, SolutionType, } from './common.js'; +import {FromBuildResult} from './from-build-result.js'; export interface HydrateStaticStateOptions { searchActions: TSearchAction[]; diff --git a/packages/headless/src/app/ssr-engine/types/build.ts b/packages/headless/src/app/ssr-engine/types/build.ts index 62b11178821..e7dcabba4dd 100644 --- a/packages/headless/src/app/ssr-engine/types/build.ts +++ b/packages/headless/src/app/ssr-engine/types/build.ts @@ -12,18 +12,6 @@ export interface BuildOptions { extend?: OptionsExtender; } -export interface BuildWithForRecommendations< - TEngine extends CoreEngine | CoreEngineNext, - TControllersMap extends ControllersMap, -> { - /** - * Initializes an engine and controllers from the definition. - */ - ( - controllers: (keyof TControllersMap)[] - ): Promise>; -} - export interface Build< TEngine extends CoreEngine | CoreEngineNext, TEngineOptions, diff --git a/packages/headless/src/app/ssr-engine/types/from-build-result.ts b/packages/headless/src/app/ssr-engine/types/from-build-result.ts index ce380b391de..fab3bd03029 100644 --- a/packages/headless/src/app/ssr-engine/types/from-build-result.ts +++ b/packages/headless/src/app/ssr-engine/types/from-build-result.ts @@ -5,17 +5,7 @@ export interface FromBuildResultOptions< TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllersMap, > { - /** - * The build result of the engine - */ buildResult: EngineDefinitionBuildResult; - /** - * An optional array of keys representing the recommendation controllers to refresh. - * If a recommendation key defined in your engine definition is present in this list, the associate recommendation controller - * will query the API. - * - * This is applicable only if the engine is a recommendation engine. - */ allowedRecommendationKeys?: (keyof TControllers)[]; } From 16c2b5e0a7984d3a979454c435168e4f82442524 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Mon, 18 Nov 2024 08:44:28 +0100 Subject: [PATCH 24/48] remove unnecessary error --- .../app/commerce-ssr-engine/factories/static-state-factory.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts index b152e585798..fa79338be79 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/static-state-factory.ts @@ -64,10 +64,6 @@ export const fetchStaticStateFactory: < case SolutionType.search: buildSearch(engine).executeFirstSearch(); break; - case SolutionType.recommendation: - throw new Error( - '[WARNING] Invalid engine definition. For recommendations, use the recommendation engine definition instead' - ); } const searchActions = await Promise.all( From dbffb512ec5c9d8f3b5784efd3f8f110a35fe270 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Mon, 18 Nov 2024 09:55:45 +0100 Subject: [PATCH 25/48] revert EngineDefinitionControllersPropsOption --- .../app/commerce-ssr-engine/types/build.ts | 12 +--------- .../app/commerce-ssr-engine/types/common.ts | 24 ++----------------- .../commerce-ssr-engine/types/core-engine.ts | 3 --- .../types/fetch-static-state.ts | 12 +--------- .../types/hydrate-static-state.ts | 12 +--------- 5 files changed, 5 insertions(+), 58 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/types/build.ts b/packages/headless/src/app/commerce-ssr-engine/types/build.ts index 3333d066720..8bf2fe0bd2b 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/build.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/build.ts @@ -1,4 +1,3 @@ -import type {Controller} from '../../../controllers/controller/headless-controller.js'; import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, @@ -8,7 +7,6 @@ import type { OptionsTuple, } from '../../ssr-engine/types/common.js'; import { - ControllerDefinitionsMap, EngineDefinitionControllersPropsOption, SolutionType, } from './common.js'; @@ -22,10 +20,6 @@ export type Build< TEngineOptions, TControllersMap extends ControllersMap, TControllersProps extends ControllersPropsMap, - TControllersDefinitionsMap extends ControllerDefinitionsMap< - TEngine, - Controller - >, TSolutionType extends SolutionType, > = TSolutionType extends SolutionType.recommendation ? { @@ -43,11 +37,7 @@ export type Build< ( ...params: OptionsTuple< BuildOptions & - EngineDefinitionControllersPropsOption< - TControllersDefinitionsMap, - TControllersProps, - TSolutionType - > + EngineDefinitionControllersPropsOption > ): Promise>; }; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index 087b02b50d2..2c18f95a23d 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -10,14 +10,14 @@ import type { InferControllerPropsMapFromDefinitions, ControllerStaticStateMap, EngineDefinitionBuildResult, + EngineDefinitionControllersPropsOption, HydratedState, OptionsTuple, - ControllersPropsMap, - HasKeys, } from '../../ssr-engine/types/common.js'; export type { EngineDefinitionBuildResult, + EngineDefinitionControllersPropsOption, HydratedState, OptionsTuple, InferControllerStaticStateFromController, @@ -149,26 +149,6 @@ export type InferControllerStaticStateMapFromDefinitionsWithSolutionType< >; }; -export type EngineDefinitionControllersPropsOption< - TControllers extends ControllerDefinitionsMap< - CoreEngine | CoreEngineNext, - Controller - >, - TControllersPropsMap extends ControllersPropsMap, - TSolutionType extends SolutionType, -> = { - [K in keyof TControllers as HasKey< - TControllers[K], - TSolutionType - > extends never - ? never - : K]: HasKeys extends false - ? {} - : { - controllers: TControllersPropsMap; - }; -}; - export interface ControllerDefinitionOption { /** * Whether the controller will be used in a product listing context. diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 5c8d107c6f3..89038e891f1 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -65,7 +65,6 @@ export interface EngineDefinition< TSolutionType >, InferControllerPropsMapFromDefinitions, - TControllers, TSolutionType >; /** @@ -76,7 +75,6 @@ export interface EngineDefinition< InferControllersMapFromDefinition, UnknownAction, InferControllerPropsMapFromDefinitions, - TControllers, TSolutionType >; /** @@ -87,7 +85,6 @@ export interface EngineDefinition< TEngineOptions, InferControllersMapFromDefinition, InferControllerPropsMapFromDefinitions, - TControllers, TSolutionType >; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index 71bafbc5cd6..16bc1aa0155 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -1,8 +1,6 @@ import type {UnknownAction} from '@reduxjs/toolkit'; -import type {Controller} from '../../../controllers/controller/headless-controller.js'; import {SolutionType} from '../../commerce-ssr-engine/types/common.js'; import type { - ControllerDefinitionsMap, EngineDefinitionControllersPropsOption, EngineStaticState, } from '../../commerce-ssr-engine/types/common.js'; @@ -23,10 +21,6 @@ export type FetchStaticState< TSearchAction extends UnknownAction, TControllersStaticState extends ControllerStaticStateMap, TControllersProps extends ControllersPropsMap, - TControllersDefinitionsMap extends ControllerDefinitionsMap< - TEngine, - Controller - >, TSolutionType extends SolutionType, > = TSolutionType extends SolutionType.recommendation ? { @@ -55,11 +49,7 @@ export type FetchStaticState< ( ...params: OptionsTuple< FetchStaticStateOptions & - EngineDefinitionControllersPropsOption< - TControllersDefinitionsMap, - TControllersProps, - TSolutionType - > + EngineDefinitionControllersPropsOption > ): Promise>; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts index d45c07d6af2..92797528f9f 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -1,5 +1,4 @@ import type {UnknownAction} from '@reduxjs/toolkit'; -import type {Controller} from '../../../controllers/controller/headless-controller.js'; import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, @@ -8,7 +7,6 @@ import type { OptionsTuple, } from '../../ssr-engine/types/common.js'; import { - ControllerDefinitionsMap, EngineDefinitionControllersPropsOption, SolutionType, } from './common.js'; @@ -23,10 +21,6 @@ export type HydrateStaticState< TControllers extends ControllersMap, TSearchAction extends UnknownAction, TControllersProps extends ControllersPropsMap, - TControllersDefinitionsMap extends ControllerDefinitionsMap< - TEngine, - Controller - >, TSolutionType extends SolutionType, > = TSolutionType extends SolutionType.recommendation ? { @@ -55,11 +49,7 @@ export type HydrateStaticState< ( ...params: OptionsTuple< HydrateStaticStateOptions & - EngineDefinitionControllersPropsOption< - TControllersDefinitionsMap, - TControllersProps, - TSolutionType - > + EngineDefinitionControllersPropsOption > ): Promise>; From 961c28e1260ee18da25e9ffbee3abbfb13d9bd94 Mon Sep 17 00:00:00 2001 From: ylakhdar Date: Mon, 18 Nov 2024 10:36:47 +0100 Subject: [PATCH 26/48] update UT --- .../headless-react/src/ssr-commerce/commerce-engine.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/headless-react/src/ssr-commerce/commerce-engine.test.tsx b/packages/headless-react/src/ssr-commerce/commerce-engine.test.tsx index e9b5c2fb56f..0d32a09560a 100644 --- a/packages/headless-react/src/ssr-commerce/commerce-engine.test.tsx +++ b/packages/headless-react/src/ssr-commerce/commerce-engine.test.tsx @@ -49,6 +49,7 @@ describe('Headless react SSR utils', () => { listingEngineDefinition, searchEngineDefinition, standaloneEngineDefinition, + recommendationEngineDefinition, ...rest } = defineCommerceEngine({configuration: sampleConfig}); const { From c58b3ae29c7152ab79cf9b22df2b43360673a3f1 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:58:28 -0500 Subject: [PATCH 27/48] Add recs to sample --- .../app/(listing)/[category]/page.tsx | 21 ++++++- .../headless-ssr-commerce/app/cart/page.tsx | 16 ++++- .../headless-ssr-commerce/app/search/page.tsx | 7 --- .../components/pages/product-page.tsx | 4 -- .../components/providers/listing-provider.tsx | 1 - .../providers/recommendation-provider.tsx | 60 +++++++++++++++++++ .../popular-bought.tsx} | 14 +++-- .../recommendations/popular-viewed.tsx | 40 +++++++++++++ .../lib/commerce-engine-config.ts | 4 +- .../lib/commerce-engine.ts | 12 +++- 10 files changed, 155 insertions(+), 24 deletions(-) create mode 100644 packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx rename packages/samples/headless-ssr-commerce/components/{recommendation-list.tsx => recommendations/popular-bought.tsx} (67%) create mode 100644 packages/samples/headless-ssr-commerce/components/recommendations/popular-viewed.tsx diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index 740a6869fa6..ec0c9bae99e 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -6,11 +6,16 @@ import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; import ListingProvider from '@/components/providers/listing-provider'; -import Recommendations from '@/components/recommendation-list'; +import RecommendationProvider from '@/components/providers/recommendation-provider'; +import PopularBought from '@/components/recommendations/popular-bought'; +import PopularViewed from '@/components/recommendations/popular-viewed'; import Sort from '@/components/sort'; import StandaloneSearchBox from '@/components/standalone-search-box'; import Summary from '@/components/summary'; -import {listingEngineDefinition} from '@/lib/commerce-engine'; +import { + listingEngineDefinition, + recommendationEngineDefinition, +} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; @@ -54,6 +59,10 @@ export default async function Listing({params}: {params: {category: string}}) { }, }); + const recsStaticState = await recommendationEngineDefinition.fetchStaticState( + ['popularBought', 'popularViewed'] + ); + return (
- + + + +
diff --git a/packages/samples/headless-ssr-commerce/app/cart/page.tsx b/packages/samples/headless-ssr-commerce/app/cart/page.tsx index 9713f817bb4..bc994c0db1e 100644 --- a/packages/samples/headless-ssr-commerce/app/cart/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/cart/page.tsx @@ -1,8 +1,13 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import Cart from '@/components/cart'; import ContextDropdown from '@/components/context-dropdown'; +import RecommendationProvider from '@/components/providers/recommendation-provider'; import SearchProvider from '@/components/providers/search-provider'; -import {searchEngineDefinition} from '@/lib/commerce-engine'; +import PopularBought from '@/components/recommendations/popular-bought'; +import { + recommendationEngineDefinition, + searchEngineDefinition, +} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; @@ -30,6 +35,9 @@ export default async function Search() { }, }); + const recsStaticState = await recommendationEngineDefinition.fetchStaticState( + ['popularBought'] + ); return ( + + + ); diff --git a/packages/samples/headless-ssr-commerce/app/search/page.tsx b/packages/samples/headless-ssr-commerce/app/search/page.tsx index 6458b16d347..c3917a7bce0 100644 --- a/packages/samples/headless-ssr-commerce/app/search/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/search/page.tsx @@ -4,7 +4,6 @@ import ContextDropdown from '@/components/context-dropdown'; import FacetGenerator from '@/components/facets/facet-generator'; import ProductList from '@/components/product-list'; import SearchProvider from '@/components/providers/search-provider'; -import Recommendations from '@/components/recommendation-list'; import SearchBox from '@/components/search-box'; import ShowMore from '@/components/show-more'; import Summary from '@/components/summary'; @@ -60,12 +59,6 @@ export default async function Search() { > */} - -
- {/* popularBoughtRecs */} - {/* TODO: KIT-3503: need to revisit the way recommendations are added*/} - -
); diff --git a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx index 7dd0e40d8a7..79232674ef2 100644 --- a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx +++ b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx @@ -8,7 +8,6 @@ import { import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; import {useSearchParams} from 'next/navigation'; import {useEffect, useState} from 'react'; -import Recommendations from '../recommendation-list'; interface IProductPageProps { staticState: StandaloneStaticState; @@ -46,8 +45,6 @@ export default function ProductPage(props: IProductPageProps) { }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); - - // TODO: use recommendationEngineDefinition for engines controllers }); }, [staticState]); @@ -63,7 +60,6 @@ export default function ProductPage(props: IProductPageProps) { {name} ({productId}) - ${price}


- ); } diff --git a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx index 46e777db338..6b253a54c16 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx @@ -38,7 +38,6 @@ export default function ListingProvider({ }) .then(({engine, controllers}) => { setHydratedState({engine, controllers}); - // TODO: use recommendationEngineDefinition for recommendation controllers }); }, [staticState]); diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx new file mode 100644 index 00000000000..ef4c0cd07ce --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { + recommendationEngineDefinition, + RecommendationHydratedState, + RecommendationStaticState, +} from '@/lib/commerce-engine'; +import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; +import {PropsWithChildren, useEffect, useState} from 'react'; + +interface RecommendationProviderProps { + staticState: RecommendationStaticState; + navigatorContext: NavigatorContext; +} + +export default function RecommendationProvider({ + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + // Setting the navigator context provider also in client-side before hydrating the application + recommendationEngineDefinition.setNavigatorContextProvider( + () => navigatorContext + ); + + useEffect(() => { + recommendationEngineDefinition + .hydrateStaticState({ + searchActions: staticState.searchActions, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + <>{children} + + ); + } else { + return ( + + {/* // TODO: Add KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. + Type 'bigint' is not assignable to type 'ReactNode'.*/} + <>{children} + + ); + } +} diff --git a/packages/samples/headless-ssr-commerce/components/recommendation-list.tsx b/packages/samples/headless-ssr-commerce/components/recommendations/popular-bought.tsx similarity index 67% rename from packages/samples/headless-ssr-commerce/components/recommendation-list.tsx rename to packages/samples/headless-ssr-commerce/components/recommendations/popular-bought.tsx index 5a62113537d..427fceef362 100644 --- a/packages/samples/headless-ssr-commerce/components/recommendation-list.tsx +++ b/packages/samples/headless-ssr-commerce/components/recommendations/popular-bought.tsx @@ -1,12 +1,12 @@ 'use client'; -import {usePopularBoughtRecs} from '@/lib/commerce-engine'; +import {usePopularBought} from '@/lib/commerce-engine'; import {Product} from '@coveo/headless-react/ssr-commerce'; +import Image from 'next/image'; import {useRouter} from 'next/navigation'; -export default function Recommendations() { - // TODO: KIT-3503: refresh recs server side - const {state, methods} = usePopularBoughtRecs(); +export default function PopularBought() { + const {state, methods} = usePopularBought(); const router = useRouter(); @@ -25,6 +25,12 @@ export default function Recommendations() {
  • ))} diff --git a/packages/samples/headless-ssr-commerce/components/recommendations/popular-viewed.tsx b/packages/samples/headless-ssr-commerce/components/recommendations/popular-viewed.tsx new file mode 100644 index 00000000000..cbd7cdc7129 --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/recommendations/popular-viewed.tsx @@ -0,0 +1,40 @@ +'use client'; + +import {usePopularViewed} from '@/lib/commerce-engine'; +import {Product} from '@coveo/headless-react/ssr-commerce'; +import Image from 'next/image'; +import {useRouter} from 'next/navigation'; + +export default function PopularViewed() { + const {state, methods} = usePopularViewed(); + + const router = useRouter(); + + const onProductClick = (product: Product) => { + methods?.interactiveProduct({options: {product}}).select(); + router.push( + `/products/${product.ec_product_id}?name=${product.ec_name}&price=${product.ec_price}` + ); + }; + + return ( + <> +
      +

      {state.headline}

      + {state.products.map((product) => ( +
    • + +
    • + ))} +
    + + ); +} diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts index e4545e20d9f..feb0703f666 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine-config.ts @@ -37,12 +37,12 @@ export default { controllers: { summary: defineSummary(), productList: defineProductList(), - popularViewedRecs: defineRecommendations({ + popularViewed: defineRecommendations({ options: { slotId: 'd73afbd2-8521-4ee6-a9b8-31f064721e73', }, }), - popularBoughtRecs: defineRecommendations({ + popularBought: defineRecommendations({ options: { slotId: 'af4fb7ba-6641-4b67-9cf9-be67e9f30174', }, diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts index 91ea7d56473..ad6db361dff 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts @@ -10,6 +10,7 @@ export const engineDefinition = defineCommerceEngine(engineConfig); export const { listingEngineDefinition, searchEngineDefinition, + recommendationEngineDefinition, standaloneEngineDefinition, useEngine, } = engineDefinition; @@ -22,8 +23,8 @@ export const { useInstantProducts, useNotifyTrigger, usePagination, - usePopularBoughtRecs, - usePopularViewedRecs, + usePopularBought, + usePopularViewed, useProductView, useQueryTrigger, useRecentQueriesList, @@ -48,6 +49,13 @@ export type SearchHydratedState = InferHydratedState< typeof searchEngineDefinition >; +export type RecommendationStaticState = InferStaticState< + typeof recommendationEngineDefinition +>; +export type RecommendationHydratedState = InferHydratedState< + typeof recommendationEngineDefinition +>; + export type StandaloneStaticState = InferStaticState< typeof standaloneEngineDefinition >; From b8e6e16059606b6931eb74669c1a529303f44113 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:22:17 -0500 Subject: [PATCH 28/48] no core engine --- .../commerce-ssr-engine/factories/build-factory.ts | 2 +- .../recommendation-static-state-factory.ts | 6 ++---- .../src/app/commerce-ssr-engine/types/build.ts | 11 +++++++---- .../app/commerce-ssr-engine/types/core-engine.ts | 3 --- .../types/fetch-static-state.ts | 4 ---- .../commerce-ssr-engine/types/from-build-result.ts | 14 ++++---------- .../types/hydrate-static-state.ts | 13 +++++-------- 7 files changed, 19 insertions(+), 34 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts index f9bc6535353..b084d2d8545 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/build-factory.ts @@ -34,7 +34,7 @@ export interface SSRCommerceEngine extends CommerceEngine { } export type CommerceEngineDefinitionOptions< - TControllers extends ControllerDefinitionsMap, + TControllers extends ControllerDefinitionsMap, > = EngineDefinitionOptions; function isListingFetchCompletedAction(action: unknown): action is Action { diff --git a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts index e0bb3646a9f..6299c3d8cb8 100644 --- a/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts +++ b/packages/headless/src/app/commerce-ssr-engine/factories/recommendation-static-state-factory.ts @@ -3,7 +3,6 @@ import {Logger} from 'pino'; import {Recommendations} from '../../../controllers/commerce/recommendations/headless-recommendations.js'; import {RecommendationsDefinitionMeta} from '../../../controllers/commerce/recommendations/headless-recommendations.ssr.js'; import {Controller} from '../../../controllers/controller/headless-controller.js'; -import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {buildLogger} from '../../logger.js'; import {composeFunction} from '../../ssr-engine/common.js'; import {createStaticState} from '../common.js'; @@ -112,8 +111,7 @@ export function fetchRecommendationStaticStateFactory< } function filterRecommendationControllers< - TEngine extends CoreEngine | CoreEngineNext, - TControllerDefinitions extends ControllerDefinitionsMap, + TControllerDefinitions extends ControllerDefinitionsMap, >( controllers: Record, controllerDefinitions: TControllerDefinitions, @@ -122,7 +120,7 @@ function filterRecommendationControllers< const slotIdSet = new Set(); const isRecommendationDefinition = < - C extends ControllerDefinition, + C extends ControllerDefinition, >( controllerDefinition: C ): controllerDefinition is C & RecommendationsDefinitionMeta => { diff --git a/packages/headless/src/app/commerce-ssr-engine/types/build.ts b/packages/headless/src/app/commerce-ssr-engine/types/build.ts index 8bf2fe0bd2b..ddbe2a29265 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/build.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/build.ts @@ -1,4 +1,3 @@ -import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, ControllersPropsMap, @@ -6,6 +5,7 @@ import type { OptionsExtender, OptionsTuple, } from '../../ssr-engine/types/common.js'; +import {SSRCommerceEngine} from '../factories/build-factory.js'; import { EngineDefinitionControllersPropsOption, SolutionType, @@ -16,7 +16,6 @@ export interface BuildOptions { } export type Build< - TEngine extends CoreEngine | CoreEngineNext, TEngineOptions, TControllersMap extends ControllersMap, TControllersProps extends ControllersPropsMap, @@ -28,7 +27,9 @@ export type Build< */ ( controllers: (keyof TControllersMap)[] - ): Promise>; + ): Promise< + EngineDefinitionBuildResult + >; } : { /** @@ -39,5 +40,7 @@ export type Build< BuildOptions & EngineDefinitionControllersPropsOption > - ): Promise>; + ): Promise< + EngineDefinitionBuildResult + >; }; diff --git a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts index 45db41f5269..2d7083e8e4a 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/core-engine.ts @@ -52,7 +52,6 @@ export interface EngineDefinition< * Fetches the static state on the server side using your engine definition. */ fetchStaticState: FetchStaticState< - SSRCommerceEngine, InferControllersMapFromDefinition, UnknownAction, InferControllerStaticStateMapFromDefinitionsWithSolutionType< @@ -66,7 +65,6 @@ export interface EngineDefinition< * Fetches the hydrated state on the client side using your engine definition and the static state. */ hydrateStaticState: HydrateStaticState< - SSRCommerceEngine, InferControllersMapFromDefinition, UnknownAction, InferControllerPropsMapFromDefinitions, @@ -76,7 +74,6 @@ export interface EngineDefinition< * Builds an engine and its controllers from an engine definition. */ build: Build< - SSRCommerceEngine, TEngineOptions, InferControllersMapFromDefinition, InferControllerPropsMapFromDefinitions, diff --git a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts index 16bc1aa0155..ec73f32afd9 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/fetch-static-state.ts @@ -4,7 +4,6 @@ import type { EngineDefinitionControllersPropsOption, EngineStaticState, } from '../../commerce-ssr-engine/types/common.js'; -import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, ControllersPropsMap, @@ -16,7 +15,6 @@ import {FromBuildResult} from './from-build-result.js'; export type FetchStaticStateOptions = {}; export type FetchStaticState< - TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllersMap, TSearchAction extends UnknownAction, TControllersStaticState extends ControllerStaticStateMap, @@ -34,7 +32,6 @@ export type FetchStaticState< ): Promise>; fromBuildResult: FromBuildResult< - TEngine, TControllers, FetchStaticStateOptions, EngineStaticState @@ -54,7 +51,6 @@ export type FetchStaticState< ): Promise>; fromBuildResult: FromBuildResult< - TEngine, TControllers, FetchStaticStateOptions, EngineStaticState diff --git a/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts index e9c5933b9cf..b8cdace3642 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/from-build-result.ts @@ -1,15 +1,12 @@ -import {CoreEngine, CoreEngineNext} from '../../engine.js'; import {ControllersMap} from '../../ssr-engine/types/common.js'; +import {SSRCommerceEngine} from '../factories/build-factory.js'; import {EngineDefinitionBuildResult} from './common.js'; -export interface FromBuildResultOptions< - TEngine extends CoreEngine | CoreEngineNext, - TControllers extends ControllersMap, -> { +export interface FromBuildResultOptions { /** * The build result of the engine */ - buildResult: EngineDefinitionBuildResult; + buildResult: EngineDefinitionBuildResult; /** * An optional array of keys representing the recommendation controllers to refresh. * If a recommendation key defined in your engine definition is present in this list, the associate recommendation controller @@ -21,12 +18,9 @@ export interface FromBuildResultOptions< } export interface FromBuildResult< - TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllersMap, TOptions, TReturn, > { - ( - options: FromBuildResultOptions & TOptions - ): Promise; + (options: FromBuildResultOptions & TOptions): Promise; } diff --git a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts index 92797528f9f..55196960e04 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/hydrate-static-state.ts @@ -1,11 +1,11 @@ import type {UnknownAction} from '@reduxjs/toolkit'; -import type {CoreEngine, CoreEngineNext} from '../../engine.js'; import type { ControllersMap, ControllersPropsMap, HydratedState, OptionsTuple, } from '../../ssr-engine/types/common.js'; +import {SSRCommerceEngine} from '../factories/build-factory.js'; import { EngineDefinitionControllersPropsOption, SolutionType, @@ -17,7 +17,6 @@ export interface HydrateStaticStateOptions { } export type HydrateStaticState< - TEngine extends CoreEngine | CoreEngineNext, TControllers extends ControllersMap, TSearchAction extends UnknownAction, TControllersProps extends ControllersPropsMap, @@ -31,13 +30,12 @@ export type HydrateStaticState< */ ( ...params: OptionsTuple> - ): Promise>; + ): Promise>; fromBuildResult: FromBuildResult< - TEngine, TControllers, HydrateStaticStateOptions, - HydratedState + HydratedState >; } : { @@ -51,12 +49,11 @@ export type HydrateStaticState< HydrateStaticStateOptions & EngineDefinitionControllersPropsOption > - ): Promise>; + ): Promise>; fromBuildResult: FromBuildResult< - TEngine, TControllers, HydrateStaticStateOptions, - HydratedState + HydratedState >; }; From 3f93d5150c1474f1baba8220443787c32f879b1a Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:39:30 -0500 Subject: [PATCH 29/48] add todo --- .../headless/src/app/commerce-engine/commerce-engine.ssr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts index 5e835bf8f9f..69e82a409f3 100644 --- a/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts +++ b/packages/headless/src/app/commerce-engine/commerce-engine.ssr.ts @@ -110,7 +110,7 @@ export function defineCommerceEngine< TControllerDefinitions, SolutionType.recommendation >, - // TODO: The standaloneEngineDefinition should not be async since no request is sent to the API + // TODO KIT-3738 : The standaloneEngineDefinition should not be async since no request is sent to the API standaloneEngineDefinition: { build: build(SolutionType.standalone), fetchStaticState: fetchStaticState(SolutionType.standalone), From 8c37023b63ffa42b58dcbe16c47388d09f11ce07 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:07:46 -0500 Subject: [PATCH 30/48] working recs --- .../src/ssr-commerce/common.tsx | 1 + .../headless-react/src/ssr-commerce/index.ts | 5 + .../src/ssr-commerce/providers.tsx | 261 ++++++++++++++++++ .../app/(listing)/[category]/page.tsx | 2 +- .../components/provider.tsx | 24 ++ .../components/providers/providers.tsx | 23 ++ 6 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 packages/headless-react/src/ssr-commerce/providers.tsx create mode 100644 packages/samples/headless-ssr-commerce/components/provider.tsx create mode 100644 packages/samples/headless-ssr-commerce/components/providers/providers.tsx diff --git a/packages/headless-react/src/ssr-commerce/common.tsx b/packages/headless-react/src/ssr-commerce/common.tsx index 62d90fd9774..fcc5ca689fc 100644 --- a/packages/headless-react/src/ssr-commerce/common.tsx +++ b/packages/headless-react/src/ssr-commerce/common.tsx @@ -33,6 +33,7 @@ function isHydratedStateContext< return 'engine' in ctx; } +// TODO: Make this error when the controller is not found and it becomes "empty" function buildControllerHook< TControllers extends ControllerDefinitionsMap, TKey extends keyof TControllers, diff --git a/packages/headless-react/src/ssr-commerce/index.ts b/packages/headless-react/src/ssr-commerce/index.ts index 28dd0a131a4..45901d23ddf 100644 --- a/packages/headless-react/src/ssr-commerce/index.ts +++ b/packages/headless-react/src/ssr-commerce/index.ts @@ -1,4 +1,9 @@ export {defineCommerceEngine} from './commerce-engine.js'; +export { + ProviderWithRecommendation, + ProviderWithListing, + buildProviderWithDefinition, +} from './providers.js'; export type {ReactCommerceEngineDefinition} from './commerce-engine.js'; export {MissingEngineProviderError} from '../errors.js'; export * from '@coveo/headless/ssr-commerce'; diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx new file mode 100644 index 00000000000..d6379c22be0 --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -0,0 +1,261 @@ +'use client'; + +import { + InferHydratedState, + InferStaticState, + NavigatorContext, +} from '@coveo/headless/ssr-commerce'; +import {PropsWithChildren, useEffect, useState} from 'react'; +import {defineCommerceEngine} from './commerce-engine.js'; + +interface RecommendationProviderProps { + recommendationEngineDefinition: ReturnType< + typeof defineCommerceEngine + >['recommendationEngineDefinition']; + staticState: InferStaticState< + ReturnType['recommendationEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} + +function RecommendationProvider({ + recommendationEngineDefinition, + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + type RecommendationHydratedState = InferHydratedState< + typeof recommendationEngineDefinition + >; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + recommendationEngineDefinition.setNavigatorContextProvider( + () => navigatorContext + ); + + useEffect(() => { + recommendationEngineDefinition + .hydrateStaticState({ + searchActions: staticState.searchActions, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } +} + +interface WithRecommendationProps { + staticState: InferStaticState< + ReturnType['recommendationEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} + +export function ProviderWithRecommendation( + recommendationEngineDefinition: unknown +) { + return function WrappedRecommendationProvider({ + staticState, + navigatorContext, + children, + }: PropsWithChildren) { + const castedDefinition = recommendationEngineDefinition as ReturnType< + typeof defineCommerceEngine + >['recommendationEngineDefinition']; + + return ( + + {children} + + ); + }; +} + +interface ListingProviderProps { + listingEngineDefinition: unknown; + staticState: InferStaticState< + ReturnType['listingEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} + +function ListingProvider({ + listingEngineDefinition, + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + const definition = listingEngineDefinition as good; + + type good = ReturnType< + typeof defineCommerceEngine + >['listingEngineDefinition']; + + type RecommendationHydratedState = InferHydratedState; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + definition.setNavigatorContextProvider(() => navigatorContext); + + useEffect(() => { + definition + .hydrateStaticState({ + searchActions: staticState.searchActions, + controllers: staticState.controllers, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } +} + +interface WithListingProps { + staticState: InferStaticState< + ReturnType['listingEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} + +export function ProviderWithListing(listingEngineDefinition: unknown) { + return function WrappedRecommendationProvider({ + staticState, + navigatorContext, + children, + }: PropsWithChildren) { + return ( + + {children} + + ); + }; +} + +interface ProviderProps { + definition: + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition']; + staticState: InferStaticState< + ReturnType['recommendationEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} + +// Build a agnostic provider +// keep the same exact code except for the useEffect +// figure out the useEffect in another function +// this function will have to understand whether the engine is listing, recommendation, search or standalone. +function Provider({ + definition, + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + type RecommendationHydratedState = InferHydratedState; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + definition.setNavigatorContextProvider(() => navigatorContext); + + useEffect(() => { + definition + .hydrateStaticState({ + searchActions: staticState.searchActions, + + controllers: staticState.controllers, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } +} + +export function buildProviderWithDefinition( + definition: + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] +) { + return function WrappedProvider({ + staticState, + navigatorContext, + children, + }: PropsWithChildren) { + return ( + + {children} + + ); + }; +} diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index ec0c9bae99e..e66fac7a267 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -6,7 +6,7 @@ import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; import ListingProvider from '@/components/providers/listing-provider'; -import RecommendationProvider from '@/components/providers/recommendation-provider'; +import {RecommendationProvider} from '@/components/providers/providers'; import PopularBought from '@/components/recommendations/popular-bought'; import PopularViewed from '@/components/recommendations/popular-viewed'; import Sort from '@/components/sort'; diff --git a/packages/samples/headless-ssr-commerce/components/provider.tsx b/packages/samples/headless-ssr-commerce/components/provider.tsx new file mode 100644 index 00000000000..502b0dd9e56 --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/provider.tsx @@ -0,0 +1,24 @@ +/* eslint-disable */ + +// @ts-ignore +const withFunction = (WrappedComponent) => { + // @ts-ignore + return (props) => { + const {userFunction} = props; + + if (!userFunction) { + throw new Error('You must provide a userFunction'); + } + + return ; + }; +}; + +// @ts-ignore +const MyComponent = ({userFunction}) => ( + +); + +export const MyProvider = ({children}: {children: React.ReactNode}) => { + return
    {children}
    ; +}; diff --git a/packages/samples/headless-ssr-commerce/components/providers/providers.tsx b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx new file mode 100644 index 00000000000..08d023d935f --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { + listingEngineDefinition, + recommendationEngineDefinition, +} from '@/lib/commerce-engine'; +import {buildProviderWithDefinition} from '@coveo/headless-react/ssr-commerce'; + +export const ListingProvider = buildProviderWithDefinition( + listingEngineDefinition +); + +export const SearchProvider = buildProviderWithDefinition( + listingEngineDefinition +); + +export const RecommendationProvider = buildProviderWithDefinition( + recommendationEngineDefinition +); + +export const StandaloneProvider = buildProviderWithDefinition( + listingEngineDefinition +); From 5fb7c0199e45c855f24ea7d58879f4b579ea4f23 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:45:25 -0500 Subject: [PATCH 31/48] trying with a bunch of generics https://coveord.atlassian.net/browse/KIT-3734 --- .../src/ssr-commerce/providers.tsx | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index d6379c22be0..67d911568b9 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -1,9 +1,13 @@ 'use client'; import { + CommerceEngineDefinition, + Controller, + ControllerDefinitionsMap, InferHydratedState, InferStaticState, NavigatorContext, + SolutionType, } from '@coveo/headless/ssr-commerce'; import {PropsWithChildren, useEffect, useState} from 'react'; import {defineCommerceEngine} from './commerce-engine.js'; @@ -177,12 +181,29 @@ export function ProviderWithListing(listingEngineDefinition: unknown) { }; } -interface ProviderProps { - definition: - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition']; +interface ProviderProps< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> { + definition: CommerceEngineDefinition & { + StaticStateProvider: React.FC< + React.PropsWithChildren<{ + controllers: import('@coveo/headless/ssr-commerce').InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >; + }> + >; + HydratedStateProvider: React.FC< + React.PropsWithChildren<{ + engine: import('@coveo/headless/ssr-commerce').CommerceEngine; + controllers: import('@coveo/headless/ssr-commerce').InferControllersMapFromDefinition< + TControllers, + TSolutionType + >; + }> + >; + }; staticState: InferStaticState< ReturnType['recommendationEngineDefinition'] >; @@ -193,12 +214,15 @@ interface ProviderProps { // keep the same exact code except for the useEffect // figure out the useEffect in another function // this function will have to understand whether the engine is listing, recommendation, search or standalone. -function Provider({ +function Provider< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>({ definition, staticState, navigatorContext, children, -}: PropsWithChildren) { +}: PropsWithChildren>) { type RecommendationHydratedState = InferHydratedState; const [hydratedState, setHydratedState] = useState< RecommendationHydratedState | undefined @@ -236,12 +260,29 @@ function Provider({ } } -export function buildProviderWithDefinition( - definition: - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] +export function buildProviderWithDefinition< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>( + definition: CommerceEngineDefinition & { + StaticStateProvider: React.FC< + React.PropsWithChildren<{ + controllers: import('@coveo/headless/ssr-commerce').InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >; + }> + >; + HydratedStateProvider: React.FC< + React.PropsWithChildren<{ + engine: import('@coveo/headless/ssr-commerce').CommerceEngine; + controllers: import('@coveo/headless/ssr-commerce').InferControllersMapFromDefinition< + TControllers, + TSolutionType + >; + }> + >; + } ) { return function WrappedProvider({ staticState, From 9f06f72b564b7fee079d77a86f16c011d966ac3a Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:45:56 -0500 Subject: [PATCH 32/48] Revert "trying with a bunch of generics" This reverts commit 5fb7c0199e45c855f24ea7d58879f4b579ea4f23. --- .../src/ssr-commerce/providers.tsx | 69 ++++--------------- 1 file changed, 14 insertions(+), 55 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index 67d911568b9..d6379c22be0 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -1,13 +1,9 @@ 'use client'; import { - CommerceEngineDefinition, - Controller, - ControllerDefinitionsMap, InferHydratedState, InferStaticState, NavigatorContext, - SolutionType, } from '@coveo/headless/ssr-commerce'; import {PropsWithChildren, useEffect, useState} from 'react'; import {defineCommerceEngine} from './commerce-engine.js'; @@ -181,29 +177,12 @@ export function ProviderWithListing(listingEngineDefinition: unknown) { }; } -interface ProviderProps< - TControllers extends ControllerDefinitionsMap, - TSolutionType extends SolutionType, -> { - definition: CommerceEngineDefinition & { - StaticStateProvider: React.FC< - React.PropsWithChildren<{ - controllers: import('@coveo/headless/ssr-commerce').InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >; - }> - >; - HydratedStateProvider: React.FC< - React.PropsWithChildren<{ - engine: import('@coveo/headless/ssr-commerce').CommerceEngine; - controllers: import('@coveo/headless/ssr-commerce').InferControllersMapFromDefinition< - TControllers, - TSolutionType - >; - }> - >; - }; +interface ProviderProps { + definition: + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition']; staticState: InferStaticState< ReturnType['recommendationEngineDefinition'] >; @@ -214,15 +193,12 @@ interface ProviderProps< // keep the same exact code except for the useEffect // figure out the useEffect in another function // this function will have to understand whether the engine is listing, recommendation, search or standalone. -function Provider< - TControllers extends ControllerDefinitionsMap, - TSolutionType extends SolutionType, ->({ +function Provider({ definition, staticState, navigatorContext, children, -}: PropsWithChildren>) { +}: PropsWithChildren) { type RecommendationHydratedState = InferHydratedState; const [hydratedState, setHydratedState] = useState< RecommendationHydratedState | undefined @@ -260,29 +236,12 @@ function Provider< } } -export function buildProviderWithDefinition< - TControllers extends ControllerDefinitionsMap, - TSolutionType extends SolutionType, ->( - definition: CommerceEngineDefinition & { - StaticStateProvider: React.FC< - React.PropsWithChildren<{ - controllers: import('@coveo/headless/ssr-commerce').InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >; - }> - >; - HydratedStateProvider: React.FC< - React.PropsWithChildren<{ - engine: import('@coveo/headless/ssr-commerce').CommerceEngine; - controllers: import('@coveo/headless/ssr-commerce').InferControllersMapFromDefinition< - TControllers, - TSolutionType - >; - }> - >; - } +export function buildProviderWithDefinition( + definition: + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] ) { return function WrappedProvider({ staticState, From f4177f5fe135f2a93664dbe8c515d68d2a350aac Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:10:00 -0500 Subject: [PATCH 33/48] weird stuff --- .../src/ssr-commerce/providers.tsx | 52 +++++++++++++++---- .../providers/standalone-provider.tsx | 2 +- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index d6379c22be0..73f6cba8936 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -177,12 +177,16 @@ export function ProviderWithListing(listingEngineDefinition: unknown) { }; } -interface ProviderProps { - definition: +interface ProviderProps< + T extends Omit< | ReturnType['recommendationEngineDefinition'] | ReturnType['listingEngineDefinition'] | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition']; + | ReturnType['standaloneEngineDefinition'], + 'StaticStateProvider' // Omit 'StaticStateProvider' or any other properties you don't want + >, +> { + definition: T; staticState: InferStaticState< ReturnType['recommendationEngineDefinition'] >; @@ -193,12 +197,20 @@ interface ProviderProps { // keep the same exact code except for the useEffect // figure out the useEffect in another function // this function will have to understand whether the engine is listing, recommendation, search or standalone. -function Provider({ +function Provider< + T extends Omit< + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'], + 'StaticStateProvider' // Omit 'StaticStateProvider' or any other properties you don't want + >, +>({ definition, staticState, navigatorContext, children, -}: PropsWithChildren) { +}: PropsWithChildren>) { type RecommendationHydratedState = InferHydratedState; const [hydratedState, setHydratedState] = useState< RecommendationHydratedState | undefined @@ -228,21 +240,39 @@ function Provider({ ); } else { + const castedDefinition = definition as unknown aseitherDefinition; + return ( - + {children} - + ); } } -export function buildProviderWithDefinition( - definition: +type eitherDefinition = + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition']; + +type HasStaticStateProvider = T extends { + StaticStateProvider: React.FC>; +} + ? T + : never; + +export function buildProviderWithDefinition< + T extends Omit< | ReturnType['recommendationEngineDefinition'] | ReturnType['listingEngineDefinition'] | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] -) { + | ReturnType['standaloneEngineDefinition'], + 'StaticStateProvider' // Omit 'StaticStateProvider' or any other properties you don't want + >, +>(definition: T) { return function WrappedProvider({ staticState, navigatorContext, diff --git a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx index a32a78d16ed..14b9da59d2a 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx @@ -30,7 +30,7 @@ export default function StandaloneProvider({ useEffect(() => { standaloneEngineDefinition .hydrateStaticState({ - searchAction: staticState.searchAction, + searchActions: staticState.searchActions, controllers: { cart: { initialState: {items: staticState.controllers.cart.state.items}, From 2e08ec00c806b2438afbb26fc931b730d82e958b Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:10:11 -0500 Subject: [PATCH 34/48] Revert "weird stuff" This reverts commit f4177f5fe135f2a93664dbe8c515d68d2a350aac. --- .../src/ssr-commerce/providers.tsx | 52 ++++--------------- .../providers/standalone-provider.tsx | 2 +- 2 files changed, 12 insertions(+), 42 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index 73f6cba8936..d6379c22be0 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -177,16 +177,12 @@ export function ProviderWithListing(listingEngineDefinition: unknown) { }; } -interface ProviderProps< - T extends Omit< +interface ProviderProps { + definition: | ReturnType['recommendationEngineDefinition'] | ReturnType['listingEngineDefinition'] | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'], - 'StaticStateProvider' // Omit 'StaticStateProvider' or any other properties you don't want - >, -> { - definition: T; + | ReturnType['standaloneEngineDefinition']; staticState: InferStaticState< ReturnType['recommendationEngineDefinition'] >; @@ -197,20 +193,12 @@ interface ProviderProps< // keep the same exact code except for the useEffect // figure out the useEffect in another function // this function will have to understand whether the engine is listing, recommendation, search or standalone. -function Provider< - T extends Omit< - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'], - 'StaticStateProvider' // Omit 'StaticStateProvider' or any other properties you don't want - >, ->({ +function Provider({ definition, staticState, navigatorContext, children, -}: PropsWithChildren>) { +}: PropsWithChildren) { type RecommendationHydratedState = InferHydratedState; const [hydratedState, setHydratedState] = useState< RecommendationHydratedState | undefined @@ -240,39 +228,21 @@ function Provider< ); } else { - const castedDefinition = definition as unknown aseitherDefinition; - return ( - + {children} - + ); } } -type eitherDefinition = - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition']; - -type HasStaticStateProvider = T extends { - StaticStateProvider: React.FC>; -} - ? T - : never; - -export function buildProviderWithDefinition< - T extends Omit< +export function buildProviderWithDefinition( + definition: | ReturnType['recommendationEngineDefinition'] | ReturnType['listingEngineDefinition'] | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'], - 'StaticStateProvider' // Omit 'StaticStateProvider' or any other properties you don't want - >, ->(definition: T) { + | ReturnType['standaloneEngineDefinition'] +) { return function WrappedProvider({ staticState, navigatorContext, diff --git a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx index 14b9da59d2a..a32a78d16ed 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx @@ -30,7 +30,7 @@ export default function StandaloneProvider({ useEffect(() => { standaloneEngineDefinition .hydrateStaticState({ - searchActions: staticState.searchActions, + searchAction: staticState.searchAction, controllers: { cart: { initialState: {items: staticState.controllers.cart.state.items}, From f79746fd59b462f08ee72a3ecd17871d6e9bc1a4 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:36:44 -0500 Subject: [PATCH 35/48] clean https://coveord.atlassian.net/browse/KIT-3734 --- .../src/ssr-commerce/providers.tsx | 328 +++++++++--------- 1 file changed, 170 insertions(+), 158 deletions(-) diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index d6379c22be0..7d7981aafae 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -8,174 +8,174 @@ import { import {PropsWithChildren, useEffect, useState} from 'react'; import {defineCommerceEngine} from './commerce-engine.js'; -interface RecommendationProviderProps { - recommendationEngineDefinition: ReturnType< - typeof defineCommerceEngine - >['recommendationEngineDefinition']; - staticState: InferStaticState< - ReturnType['recommendationEngineDefinition'] - >; - navigatorContext: NavigatorContext; -} +// interface RecommendationProviderProps { +// recommendationEngineDefinition: ReturnType< +// typeof defineCommerceEngine +// >['recommendationEngineDefinition']; +// staticState: InferStaticState< +// ReturnType['recommendationEngineDefinition'] +// >; +// navigatorContext: NavigatorContext; +// } -function RecommendationProvider({ - recommendationEngineDefinition, - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - type RecommendationHydratedState = InferHydratedState< - typeof recommendationEngineDefinition - >; - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); +// function RecommendationProvider({ +// recommendationEngineDefinition, +// staticState, +// navigatorContext, +// children, +// }: PropsWithChildren) { +// type RecommendationHydratedState = InferHydratedState< +// typeof recommendationEngineDefinition +// >; +// const [hydratedState, setHydratedState] = useState< +// RecommendationHydratedState | undefined +// >(undefined); - recommendationEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); +// recommendationEngineDefinition.setNavigatorContextProvider( +// () => navigatorContext +// ); - useEffect(() => { - recommendationEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); +// useEffect(() => { +// recommendationEngineDefinition +// .hydrateStaticState({ +// searchActions: staticState.searchActions, +// }) +// .then(({engine, controllers}) => { +// setHydratedState({engine, controllers}); +// }); +// }, [staticState]); - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} +// if (hydratedState) { +// return ( +// +// {children} +// +// ); +// } else { +// return ( +// +// {children} +// +// ); +// } +// } -interface WithRecommendationProps { - staticState: InferStaticState< - ReturnType['recommendationEngineDefinition'] - >; - navigatorContext: NavigatorContext; -} +// interface WithRecommendationProps { +// staticState: InferStaticState< +// ReturnType['recommendationEngineDefinition'] +// >; +// navigatorContext: NavigatorContext; +// } -export function ProviderWithRecommendation( - recommendationEngineDefinition: unknown -) { - return function WrappedRecommendationProvider({ - staticState, - navigatorContext, - children, - }: PropsWithChildren) { - const castedDefinition = recommendationEngineDefinition as ReturnType< - typeof defineCommerceEngine - >['recommendationEngineDefinition']; +// interface WithListingProps { +// staticState: InferStaticState< +// ReturnType['listingEngineDefinition'] +// >; +// navigatorContext: NavigatorContext; +// } - return ( - - {children} - - ); - }; -} +// export function ProviderWithRecommendation( +// recommendationEngineDefinition: unknown +// ) { +// return function WrappedRecommendationProvider({ +// staticState, +// navigatorContext, +// children, +// }: PropsWithChildren) { +// const castedDefinition = recommendationEngineDefinition as ReturnType< +// typeof defineCommerceEngine +// >['recommendationEngineDefinition']; -interface ListingProviderProps { - listingEngineDefinition: unknown; - staticState: InferStaticState< - ReturnType['listingEngineDefinition'] - >; - navigatorContext: NavigatorContext; -} +// return ( +// +// {children} +// +// ); +// }; +// } -function ListingProvider({ - listingEngineDefinition, - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const definition = listingEngineDefinition as good; +// interface ListingProviderProps { +// listingEngineDefinition: unknown; +// staticState: InferStaticState< +// ReturnType['listingEngineDefinition'] +// >; +// navigatorContext: NavigatorContext; +// } - type good = ReturnType< - typeof defineCommerceEngine - >['listingEngineDefinition']; +// function ListingProvider({ +// listingEngineDefinition, +// staticState, +// navigatorContext, +// children, +// }: PropsWithChildren) { +// const definition = listingEngineDefinition as good; - type RecommendationHydratedState = InferHydratedState; - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); +// type good = ReturnType< +// typeof defineCommerceEngine +// >['listingEngineDefinition']; - definition.setNavigatorContextProvider(() => navigatorContext); +// type RecommendationHydratedState = InferHydratedState; +// const [hydratedState, setHydratedState] = useState< +// RecommendationHydratedState | undefined +// >(undefined); - useEffect(() => { - definition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: staticState.controllers, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); +// definition.setNavigatorContextProvider(() => navigatorContext); - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} +// useEffect(() => { +// definition +// .hydrateStaticState({ +// searchActions: staticState.searchActions, +// controllers: staticState.controllers, +// }) +// .then(({engine, controllers}) => { +// setHydratedState({engine, controllers}); +// }); +// }, [staticState]); -interface WithListingProps { - staticState: InferStaticState< - ReturnType['listingEngineDefinition'] - >; - navigatorContext: NavigatorContext; -} +// if (hydratedState) { +// return ( +// +// {children} +// +// ); +// } else { +// return ( +// +// {children} +// +// ); +// } +// } -export function ProviderWithListing(listingEngineDefinition: unknown) { - return function WrappedRecommendationProvider({ - staticState, - navigatorContext, - children, - }: PropsWithChildren) { - return ( - - {children} - - ); - }; -} +// export function ProviderWithListing(listingEngineDefinition: unknown) { +// return function WrappedRecommendationProvider({ +// staticState, +// navigatorContext, +// children, +// }: PropsWithChildren) { +// return ( +// +// {children} +// +// ); +// }; +// } interface ProviderProps { definition: @@ -184,15 +184,14 @@ interface ProviderProps { | ReturnType['searchEngineDefinition'] | ReturnType['standaloneEngineDefinition']; staticState: InferStaticState< - ReturnType['recommendationEngineDefinition'] + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] >; navigatorContext: NavigatorContext; } -// Build a agnostic provider -// keep the same exact code except for the useEffect -// figure out the useEffect in another function -// this function will have to understand whether the engine is listing, recommendation, search or standalone. function Provider({ definition, staticState, @@ -236,6 +235,19 @@ function Provider({ } } +interface WithDefinitionProps { + staticState: InferStaticState< + | ReturnType['listingEngineDefinition'] + | ReturnType['recommendationEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} + +/** + * TODO MONDAY WHAT I NEED TO DO HERE IS ADD A GENERIC THAT TAKES IN THE CONTROLLER AND MAKE IT AFFECT THE STATICSTATEPROVIDER TYPE OF THE DEFINITION TYPE + */ export function buildProviderWithDefinition( definition: | ReturnType['recommendationEngineDefinition'] @@ -247,7 +259,7 @@ export function buildProviderWithDefinition( staticState, navigatorContext, children, - }: PropsWithChildren) { + }: PropsWithChildren) { return ( Date: Mon, 25 Nov 2024 10:42:07 -0500 Subject: [PATCH 36/48] deep types --- package-lock.json | 6 - .../src/ssr-commerce/providers.tsx | 208 +++++++++++------- .../components/provider.tsx | 24 -- 3 files changed, 132 insertions(+), 106 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/components/provider.tsx diff --git a/package-lock.json b/package-lock.json index 12841a886c5..c3424180a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21024,12 +21024,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "devOptional": true - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index 7d7981aafae..c4970984376 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -1,11 +1,19 @@ 'use client'; import { + CommerceEngine, + Controller, + ControllerDefinitionsMap, + InferControllersMapFromDefinition, + InferControllerStaticStateMapFromDefinitionsWithSolutionType, InferHydratedState, InferStaticState, NavigatorContext, + NavigatorContextProvider, + SolutionType, } from '@coveo/headless/ssr-commerce'; import {PropsWithChildren, useEffect, useState} from 'react'; +import {SSRCommerceEngine} from '../../../headless/dist/definitions/app/commerce-ssr-engine/factories/build-factory.js'; import {defineCommerceEngine} from './commerce-engine.js'; // interface RecommendationProviderProps { @@ -177,97 +185,145 @@ import {defineCommerceEngine} from './commerce-engine.js'; // }; // } -interface ProviderProps { - definition: - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition']; - staticState: InferStaticState< - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] - >; - navigatorContext: NavigatorContext; -} +// interface ProviderProps { +// definition: GenericDefinition; +// staticState: GenericStaticState; +// navigatorContext: NavigatorContext; +// } -function Provider({ - definition, - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - type RecommendationHydratedState = InferHydratedState; - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - definition.setNavigatorContextProvider(() => navigatorContext); - - useEffect(() => { - definition - .hydrateStaticState({ - searchActions: staticState.searchActions, - - controllers: staticState.controllers, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} +// function Provider({ +// definition, +// staticState, +// navigatorContext, +// children, +// }: PropsWithChildren) { +// type RecommendationHydratedState = InferHydratedState; +// const [hydratedState, setHydratedState] = useState< +// RecommendationHydratedState | undefined +// >(undefined); + +// definition.setNavigatorContextProvider(() => navigatorContext); + +// useEffect(() => { +// definition +// .hydrateStaticState({ +// searchActions: staticState.searchActions, + +// controllers: staticState.controllers, +// }) +// .then(({engine, controllers}) => { +// setHydratedState({engine, controllers}); +// }); +// }, [staticState]); + +// if (hydratedState) { +// return ( +// +// {children} +// +// ); +// } else { +// return ( +// +// {children} +// +// ); +// } +// } -interface WithDefinitionProps { - staticState: InferStaticState< - | ReturnType['listingEngineDefinition'] - | ReturnType['recommendationEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] - >; +interface WithDefinitionProps< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> { + staticState: InferStaticState>; navigatorContext: NavigatorContext; } +type GenericDefinition< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +> = { + StaticStateProvider: ( + props: PropsWithChildren<{ + controllers: InferControllerStaticStateMapFromDefinitionsWithSolutionType< + TControllers, + TSolutionType + >; + }> + ) => JSX.Element; + + HydratedStateProvider: ( + props: PropsWithChildren<{ + engine: CommerceEngine; + controllers: InferControllersMapFromDefinition< + TControllers, + TSolutionType + >; + }> + ) => JSX.Element; + setNavigatorContextProvider: ( + navigatorContextProvider: NavigatorContextProvider + ) => void; + hydrateStaticState: (args: { + controllers: TControllers; + searchActions: unknown; + }) => Promise<{engine: SSRCommerceEngine; controllers: TControllers}>; +}; +// Repeat for other definitions + /** * TODO MONDAY WHAT I NEED TO DO HERE IS ADD A GENERIC THAT TAKES IN THE CONTROLLER AND MAKE IT AFFECT THE STATICSTATEPROVIDER TYPE OF THE DEFINITION TYPE */ -export function buildProviderWithDefinition( - definition: - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] -) { +export function buildProviderWithDefinition< + TControllers extends ControllerDefinitionsMap, + TSolutionType extends SolutionType, +>(definition: GenericDefinition) { return function WrappedProvider({ staticState, navigatorContext, children, }: PropsWithChildren) { + type RecommendationHydratedState = InferHydratedState; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + // Set navigator context provider + useEffect(() => { + definition.setNavigatorContextProvider(() => navigatorContext); + }, [navigatorContext]); + + // Hydrate static state + useEffect(() => { + definition + .hydrateStaticState({ + searchActions: staticState.searchActions, + controllers: staticState.controllers, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + // Render based on hydrated state + if (hydratedState) { + return ( + + {children} + + ); + } + return ( - + {children} - + ); }; } diff --git a/packages/samples/headless-ssr-commerce/components/provider.tsx b/packages/samples/headless-ssr-commerce/components/provider.tsx deleted file mode 100644 index 502b0dd9e56..00000000000 --- a/packages/samples/headless-ssr-commerce/components/provider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable */ - -// @ts-ignore -const withFunction = (WrappedComponent) => { - // @ts-ignore - return (props) => { - const {userFunction} = props; - - if (!userFunction) { - throw new Error('You must provide a userFunction'); - } - - return ; - }; -}; - -// @ts-ignore -const MyComponent = ({userFunction}) => ( - -); - -export const MyProvider = ({children}: {children: React.ReactNode}) => { - return
    {children}
    ; -}; From f19b86b7841bac3881860d034279e7776208a795 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:42:13 -0500 Subject: [PATCH 37/48] Revert "deep types" This reverts commit 745b70dccf75cf0c0ab07a44d80868ca071a5508. --- package-lock.json | 6 + .../src/ssr-commerce/providers.tsx | 208 +++++++----------- .../components/provider.tsx | 24 ++ 3 files changed, 106 insertions(+), 132 deletions(-) create mode 100644 packages/samples/headless-ssr-commerce/components/provider.tsx diff --git a/package-lock.json b/package-lock.json index c3424180a04..12841a886c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21024,6 +21024,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "devOptional": true + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index c4970984376..7d7981aafae 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -1,19 +1,11 @@ 'use client'; import { - CommerceEngine, - Controller, - ControllerDefinitionsMap, - InferControllersMapFromDefinition, - InferControllerStaticStateMapFromDefinitionsWithSolutionType, InferHydratedState, InferStaticState, NavigatorContext, - NavigatorContextProvider, - SolutionType, } from '@coveo/headless/ssr-commerce'; import {PropsWithChildren, useEffect, useState} from 'react'; -import {SSRCommerceEngine} from '../../../headless/dist/definitions/app/commerce-ssr-engine/factories/build-factory.js'; import {defineCommerceEngine} from './commerce-engine.js'; // interface RecommendationProviderProps { @@ -185,145 +177,97 @@ import {defineCommerceEngine} from './commerce-engine.js'; // }; // } -// interface ProviderProps { -// definition: GenericDefinition; -// staticState: GenericStaticState; -// navigatorContext: NavigatorContext; -// } - -// function Provider({ -// definition, -// staticState, -// navigatorContext, -// children, -// }: PropsWithChildren) { -// type RecommendationHydratedState = InferHydratedState; -// const [hydratedState, setHydratedState] = useState< -// RecommendationHydratedState | undefined -// >(undefined); - -// definition.setNavigatorContextProvider(() => navigatorContext); - -// useEffect(() => { -// definition -// .hydrateStaticState({ -// searchActions: staticState.searchActions, - -// controllers: staticState.controllers, -// }) -// .then(({engine, controllers}) => { -// setHydratedState({engine, controllers}); -// }); -// }, [staticState]); - -// if (hydratedState) { -// return ( -// -// {children} -// -// ); -// } else { -// return ( -// -// {children} -// -// ); -// } -// } - -interface WithDefinitionProps< - TControllers extends ControllerDefinitionsMap, - TSolutionType extends SolutionType, -> { - staticState: InferStaticState>; +interface ProviderProps { + definition: + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition']; + staticState: InferStaticState< + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] + >; navigatorContext: NavigatorContext; } -type GenericDefinition< - TControllers extends ControllerDefinitionsMap, - TSolutionType extends SolutionType, -> = { - StaticStateProvider: ( - props: PropsWithChildren<{ - controllers: InferControllerStaticStateMapFromDefinitionsWithSolutionType< - TControllers, - TSolutionType - >; - }> - ) => JSX.Element; +function Provider({ + definition, + staticState, + navigatorContext, + children, +}: PropsWithChildren) { + type RecommendationHydratedState = InferHydratedState; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + definition.setNavigatorContextProvider(() => navigatorContext); + + useEffect(() => { + definition + .hydrateStaticState({ + searchActions: staticState.searchActions, + + controllers: staticState.controllers, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + {children} + + ); + } else { + return ( + + {children} + + ); + } +} - HydratedStateProvider: ( - props: PropsWithChildren<{ - engine: CommerceEngine; - controllers: InferControllersMapFromDefinition< - TControllers, - TSolutionType - >; - }> - ) => JSX.Element; - setNavigatorContextProvider: ( - navigatorContextProvider: NavigatorContextProvider - ) => void; - hydrateStaticState: (args: { - controllers: TControllers; - searchActions: unknown; - }) => Promise<{engine: SSRCommerceEngine; controllers: TControllers}>; -}; -// Repeat for other definitions +interface WithDefinitionProps { + staticState: InferStaticState< + | ReturnType['listingEngineDefinition'] + | ReturnType['recommendationEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] + >; + navigatorContext: NavigatorContext; +} /** * TODO MONDAY WHAT I NEED TO DO HERE IS ADD A GENERIC THAT TAKES IN THE CONTROLLER AND MAKE IT AFFECT THE STATICSTATEPROVIDER TYPE OF THE DEFINITION TYPE */ -export function buildProviderWithDefinition< - TControllers extends ControllerDefinitionsMap, - TSolutionType extends SolutionType, ->(definition: GenericDefinition) { +export function buildProviderWithDefinition( + definition: + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition'] +) { return function WrappedProvider({ staticState, navigatorContext, children, }: PropsWithChildren) { - type RecommendationHydratedState = InferHydratedState; - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - // Set navigator context provider - useEffect(() => { - definition.setNavigatorContextProvider(() => navigatorContext); - }, [navigatorContext]); - - // Hydrate static state - useEffect(() => { - definition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: staticState.controllers, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - // Render based on hydrated state - if (hydratedState) { - return ( - - {children} - - ); - } - return ( - + {children} - +
    ); }; } diff --git a/packages/samples/headless-ssr-commerce/components/provider.tsx b/packages/samples/headless-ssr-commerce/components/provider.tsx new file mode 100644 index 00000000000..502b0dd9e56 --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/provider.tsx @@ -0,0 +1,24 @@ +/* eslint-disable */ + +// @ts-ignore +const withFunction = (WrappedComponent) => { + // @ts-ignore + return (props) => { + const {userFunction} = props; + + if (!userFunction) { + throw new Error('You must provide a userFunction'); + } + + return ; + }; +}; + +// @ts-ignore +const MyComponent = ({userFunction}) => ( + +); + +export const MyProvider = ({children}: {children: React.ReactNode}) => { + return
    {children}
    ; +}; From 329f417d4b465d75e86abdfff385d8d73ea387cc Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:10:01 -0500 Subject: [PATCH 38/48] feat(headless-react): simply create providers with definition https://coveord.atlassian.net/browse/KIT-3734 --- package-lock.json | 6 - .../headless-react/src/ssr-commerce/index.ts | 6 +- .../src/ssr-commerce/providers.tsx | 326 +++++------------- .../app/(listing)/[category]/page.tsx | 6 +- .../headless-ssr-commerce/app/cart/page.tsx | 10 +- .../app/products/[productId]/page.tsx | 2 +- .../headless-ssr-commerce/app/search/page.tsx | 2 +- .../components/provider.tsx | 24 -- .../components/providers/listing-provider.tsx | 67 ---- .../components/providers/providers.tsx | 6 +- .../providers/recommendation-provider.tsx | 60 ---- .../components/providers/search-provider.tsx | 72 ---- .../providers/standalone-provider.tsx | 72 ---- 13 files changed, 95 insertions(+), 564 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/components/provider.tsx delete mode 100644 packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx delete mode 100644 packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx delete mode 100644 packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx delete mode 100644 packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx diff --git a/package-lock.json b/package-lock.json index 12841a886c5..c3424180a04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21024,12 +21024,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "devOptional": true - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", diff --git a/packages/headless-react/src/ssr-commerce/index.ts b/packages/headless-react/src/ssr-commerce/index.ts index 45901d23ddf..bb5b63a2699 100644 --- a/packages/headless-react/src/ssr-commerce/index.ts +++ b/packages/headless-react/src/ssr-commerce/index.ts @@ -1,9 +1,5 @@ export {defineCommerceEngine} from './commerce-engine.js'; -export { - ProviderWithRecommendation, - ProviderWithListing, - buildProviderWithDefinition, -} from './providers.js'; +export {buildProviderWithDefinition} from './providers.js'; export type {ReactCommerceEngineDefinition} from './commerce-engine.js'; export {MissingEngineProviderError} from '../errors.js'; export * from '@coveo/headless/ssr-commerce'; diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx index 7d7981aafae..6b9e15388d9 100644 --- a/packages/headless-react/src/ssr-commerce/providers.tsx +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -8,266 +8,96 @@ import { import {PropsWithChildren, useEffect, useState} from 'react'; import {defineCommerceEngine} from './commerce-engine.js'; -// interface RecommendationProviderProps { -// recommendationEngineDefinition: ReturnType< -// typeof defineCommerceEngine -// >['recommendationEngineDefinition']; -// staticState: InferStaticState< -// ReturnType['recommendationEngineDefinition'] -// >; -// navigatorContext: NavigatorContext; -// } - -// function RecommendationProvider({ -// recommendationEngineDefinition, -// staticState, -// navigatorContext, -// children, -// }: PropsWithChildren) { -// type RecommendationHydratedState = InferHydratedState< -// typeof recommendationEngineDefinition -// >; -// const [hydratedState, setHydratedState] = useState< -// RecommendationHydratedState | undefined -// >(undefined); - -// recommendationEngineDefinition.setNavigatorContextProvider( -// () => navigatorContext -// ); - -// useEffect(() => { -// recommendationEngineDefinition -// .hydrateStaticState({ -// searchActions: staticState.searchActions, -// }) -// .then(({engine, controllers}) => { -// setHydratedState({engine, controllers}); -// }); -// }, [staticState]); - -// if (hydratedState) { -// return ( -// -// {children} -// -// ); -// } else { -// return ( -// -// {children} -// -// ); -// } -// } - -// interface WithRecommendationProps { -// staticState: InferStaticState< -// ReturnType['recommendationEngineDefinition'] -// >; -// navigatorContext: NavigatorContext; -// } - -// interface WithListingProps { -// staticState: InferStaticState< -// ReturnType['listingEngineDefinition'] -// >; -// navigatorContext: NavigatorContext; -// } - -// export function ProviderWithRecommendation( -// recommendationEngineDefinition: unknown -// ) { -// return function WrappedRecommendationProvider({ -// staticState, -// navigatorContext, -// children, -// }: PropsWithChildren) { -// const castedDefinition = recommendationEngineDefinition as ReturnType< -// typeof defineCommerceEngine -// >['recommendationEngineDefinition']; - -// return ( -// -// {children} -// -// ); -// }; -// } - -// interface ListingProviderProps { -// listingEngineDefinition: unknown; -// staticState: InferStaticState< -// ReturnType['listingEngineDefinition'] -// >; -// navigatorContext: NavigatorContext; -// } - -// function ListingProvider({ -// listingEngineDefinition, -// staticState, -// navigatorContext, -// children, -// }: PropsWithChildren) { -// const definition = listingEngineDefinition as good; - -// type good = ReturnType< -// typeof defineCommerceEngine -// >['listingEngineDefinition']; - -// type RecommendationHydratedState = InferHydratedState; -// const [hydratedState, setHydratedState] = useState< -// RecommendationHydratedState | undefined -// >(undefined); - -// definition.setNavigatorContextProvider(() => navigatorContext); - -// useEffect(() => { -// definition -// .hydrateStaticState({ -// searchActions: staticState.searchActions, -// controllers: staticState.controllers, -// }) -// .then(({engine, controllers}) => { -// setHydratedState({engine, controllers}); -// }); -// }, [staticState]); - -// if (hydratedState) { -// return ( -// -// {children} -// -// ); -// } else { -// return ( -// -// {children} -// -// ); -// } -// } - -// export function ProviderWithListing(listingEngineDefinition: unknown) { -// return function WrappedRecommendationProvider({ -// staticState, -// navigatorContext, -// children, -// }: PropsWithChildren) { -// return ( -// -// {children} -// -// ); -// }; -// } - -interface ProviderProps { - definition: - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition']; - staticState: InferStaticState< - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] - >; - navigatorContext: NavigatorContext; -} - -function Provider({ - definition, - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - type RecommendationHydratedState = InferHydratedState; - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - definition.setNavigatorContextProvider(() => navigatorContext); - - useEffect(() => { - definition - .hydrateStaticState({ - searchActions: staticState.searchActions, - - controllers: staticState.controllers, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} - interface WithDefinitionProps { - staticState: InferStaticState< - | ReturnType['listingEngineDefinition'] - | ReturnType['recommendationEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] - >; + staticState: InferStaticState; navigatorContext: NavigatorContext; } -/** - * TODO MONDAY WHAT I NEED TO DO HERE IS ADD A GENERIC THAT TAKES IN THE CONTROLLER AND MAKE IT AFFECT THE STATICSTATEPROVIDER TYPE OF THE DEFINITION TYPE - */ -export function buildProviderWithDefinition( - definition: - | ReturnType['recommendationEngineDefinition'] - | ReturnType['listingEngineDefinition'] - | ReturnType['searchEngineDefinition'] - | ReturnType['standaloneEngineDefinition'] -) { +type LooseDefinition = { + setNavigatorContextProvider: unknown; + build: unknown; + hydrateStaticState: unknown; + fetchStaticState: unknown; + HydratedStateProvider: unknown; + StaticStateProvider: unknown; +}; + +type RealDefinition = + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition']; + +export function buildProviderWithDefinition(looseDefinition: LooseDefinition) { return function WrappedProvider({ staticState, navigatorContext, children, }: PropsWithChildren) { + const definition = looseDefinition as RealDefinition; + type RecommendationHydratedState = InferHydratedState; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + definition.setNavigatorContextProvider(() => navigatorContext); + + useEffect(() => { + const {searchActions, controllers} = staticState; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hydrateControllers: Record = {}; + + if ('cart' in controllers) { + hydrateControllers.cart = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initialState: {items: (controllers as any).cart.state.items}, + }; + } + + if ('context' in controllers) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hydrateControllers.context = (controllers as any).context.state; + } + + definition + .hydrateStaticState({ + searchActions, + controllers: { + ...controllers, + ...hydrateControllers, + }, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + {children} + + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const StaticStateProviderWithAnyControllers = (looseDefinition as any) + .StaticStateProvider as React.ComponentType<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + controllers: any; + children: React.ReactNode; + }>; + return ( - {children} - + ); }; } diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index e66fac7a267..c5cefdea131 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -5,8 +5,10 @@ import ContextDropdown from '@/components/context-dropdown'; import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; -import ListingProvider from '@/components/providers/listing-provider'; -import {RecommendationProvider} from '@/components/providers/providers'; +import { + ListingProvider, + RecommendationProvider, +} from '@/components/providers/providers'; import PopularBought from '@/components/recommendations/popular-bought'; import PopularViewed from '@/components/recommendations/popular-viewed'; import Sort from '@/components/sort'; diff --git a/packages/samples/headless-ssr-commerce/app/cart/page.tsx b/packages/samples/headless-ssr-commerce/app/cart/page.tsx index bc994c0db1e..0b3907c4e1f 100644 --- a/packages/samples/headless-ssr-commerce/app/cart/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/cart/page.tsx @@ -1,8 +1,10 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import Cart from '@/components/cart'; import ContextDropdown from '@/components/context-dropdown'; -import RecommendationProvider from '@/components/providers/recommendation-provider'; -import SearchProvider from '@/components/providers/search-provider'; +import { + ListingProvider, + RecommendationProvider, +} from '@/components/providers/providers'; import PopularBought from '@/components/recommendations/popular-bought'; import { recommendationEngineDefinition, @@ -39,7 +41,7 @@ export default async function Search() { ['popularBought'] ); return ( - @@ -53,7 +55,7 @@ export default async function Search() { - + ); } diff --git a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx index 2208ae33475..793cbf7d858 100644 --- a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx @@ -1,7 +1,7 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import ContextDropdown from '@/components/context-dropdown'; import ProductPage from '@/components/pages/product-page'; -import StandaloneProvider from '@/components/providers/standalone-provider'; +import {StandaloneProvider} from '@/components/providers/providers'; import StandaloneSearchBox from '@/components/standalone-search-box'; import {searchEngineDefinition} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; diff --git a/packages/samples/headless-ssr-commerce/app/search/page.tsx b/packages/samples/headless-ssr-commerce/app/search/page.tsx index c3917a7bce0..50b32f3310e 100644 --- a/packages/samples/headless-ssr-commerce/app/search/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/search/page.tsx @@ -3,7 +3,7 @@ import BreadcrumbManager from '@/components/breadcrumb-manager'; import ContextDropdown from '@/components/context-dropdown'; import FacetGenerator from '@/components/facets/facet-generator'; import ProductList from '@/components/product-list'; -import SearchProvider from '@/components/providers/search-provider'; +import {SearchProvider} from '@/components/providers/providers'; import SearchBox from '@/components/search-box'; import ShowMore from '@/components/show-more'; import Summary from '@/components/summary'; diff --git a/packages/samples/headless-ssr-commerce/components/provider.tsx b/packages/samples/headless-ssr-commerce/components/provider.tsx deleted file mode 100644 index 502b0dd9e56..00000000000 --- a/packages/samples/headless-ssr-commerce/components/provider.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable */ - -// @ts-ignore -const withFunction = (WrappedComponent) => { - // @ts-ignore - return (props) => { - const {userFunction} = props; - - if (!userFunction) { - throw new Error('You must provide a userFunction'); - } - - return ; - }; -}; - -// @ts-ignore -const MyComponent = ({userFunction}) => ( - -); - -export const MyProvider = ({children}: {children: React.ReactNode}) => { - return
    {children}
    ; -}; diff --git a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx deleted file mode 100644 index 918919891fe..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import { - listingEngineDefinition, - ListingHydratedState, - ListingStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; -import {HydrationMetadata} from '../hydration-metadata'; - -interface ListingPageProps { - staticState: ListingStaticState; - navigatorContext: NavigatorContext; -} - -export default function ListingProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - ListingHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - listingEngineDefinition.setNavigatorContextProvider(() => navigatorContext); - - useEffect(() => { - listingEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - - ); - } else { - return ( - - {children} - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/providers.tsx b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx index 08d023d935f..a48aeec58d2 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/providers.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx @@ -3,6 +3,8 @@ import { listingEngineDefinition, recommendationEngineDefinition, + searchEngineDefinition, + standaloneEngineDefinition, } from '@/lib/commerce-engine'; import {buildProviderWithDefinition} from '@coveo/headless-react/ssr-commerce'; @@ -11,7 +13,7 @@ export const ListingProvider = buildProviderWithDefinition( ); export const SearchProvider = buildProviderWithDefinition( - listingEngineDefinition + searchEngineDefinition ); export const RecommendationProvider = buildProviderWithDefinition( @@ -19,5 +21,5 @@ export const RecommendationProvider = buildProviderWithDefinition( ); export const StandaloneProvider = buildProviderWithDefinition( - listingEngineDefinition + standaloneEngineDefinition ); diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx deleted file mode 100644 index ef4c0cd07ce..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client'; - -import { - recommendationEngineDefinition, - RecommendationHydratedState, - RecommendationStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; - -interface RecommendationProviderProps { - staticState: RecommendationStaticState; - navigatorContext: NavigatorContext; -} - -export default function RecommendationProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - recommendationEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - recommendationEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - <>{children} - - ); - } else { - return ( - - {/* // TODO: Add KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. - Type 'bigint' is not assignable to type 'ReactNode'.*/} - <>{children} - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx deleted file mode 100644 index d8726cdca2a..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import { - SearchHydratedState, - SearchStaticState, - searchEngineDefinition, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; -import {HydrationMetadata} from '../hydration-metadata'; - -interface SearchPageProps { - staticState: SearchStaticState; - navigatorContext: NavigatorContext; -} - -export default function SearchProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - SearchHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext); - - useEffect(() => { - searchEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - // controllers.popularBoughtRecs.refresh(); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - - ); - } else { - return ( - - {children} - - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx deleted file mode 100644 index a32a78d16ed..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import { - StandaloneHydratedState, - StandaloneStaticState, - standaloneEngineDefinition, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; - -interface StandalonePageProps { - staticState: StandaloneStaticState; - navigatorContext: NavigatorContext; -} - -export default function StandaloneProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - StandaloneHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - standaloneEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - standaloneEngineDefinition - .hydrateStaticState({ - searchAction: staticState.searchAction, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - // controllers.popularBoughtRecs.refresh(); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {/* // TODO: KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. - Type 'bigint' is not assignable to type 'ReactNode'.*/} - <>{children} - - ); - } else { - return ( - - {/* // TODO: KIT-3701: Type 'React.ReactNode' is not assignable to type 'import(".../node_modules/@types/react/index").ReactNode'. - Type 'bigint' is not assignable to type 'ReactNode'.*/} - <>{children} - - ); - } -} From 09c16bf1c1479238c05b178836ef1080baa1d029 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:19:14 -0500 Subject: [PATCH 39/48] add TODO https://coveord.atlassian.net/browse/KIT-3734 --- packages/headless-react/src/ssr-commerce/common.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/headless-react/src/ssr-commerce/common.tsx b/packages/headless-react/src/ssr-commerce/common.tsx index fcc5ca689fc..64ea0a07e82 100644 --- a/packages/headless-react/src/ssr-commerce/common.tsx +++ b/packages/headless-react/src/ssr-commerce/common.tsx @@ -33,7 +33,7 @@ function isHydratedStateContext< return 'engine' in ctx; } -// TODO: Make this error when the controller is not found and it becomes "empty" +// TODO: https://coveord.atlassian.net/browse/KIT-3756 Add error when the controller is not found and it becomes "empty" function buildControllerHook< TControllers extends ControllerDefinitionsMap, TKey extends keyof TControllers, From 3f1fd0f2bb1f8704e33dc667d50eac1d96389006 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:23:22 -0500 Subject: [PATCH 40/48] delete useless itnerfaces https://coveord.atlassian.net/browse/KIT-3734 --- .../components/hydration-metadata.tsx | 21 ++++++++---- .../lib/commerce-engine.ts | 32 +------------------ 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx b/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx index 58efd70d0ae..6ebe8120d67 100644 --- a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx +++ b/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx @@ -1,18 +1,27 @@ 'use client'; import { - ListingHydratedState, - ListingStaticState, - SearchHydratedState, - SearchStaticState, + listingEngineDefinition, + searchEngineDefinition, } from '@/lib/commerce-engine'; +import { + InferHydratedState, + InferStaticState, +} from '@coveo/headless-react/ssr-commerce'; import {FunctionComponent} from 'react'; export interface HydrationMetadataProps { - staticState: SearchStaticState | ListingStaticState; - hydratedState?: SearchHydratedState | ListingHydratedState; + staticState: + | InferStaticState + | InferStaticState; + hydratedState?: + | InferHydratedState + | InferHydratedState; } +// This component displays metadata about the hydration state of the page. +// IMPORTANT: It was created for testing our package. +// You should not use this component anywhere. export const HydrationMetadata: FunctionComponent = ({ staticState, hydratedState, diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts index ad6db361dff..ef7b21019a3 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts @@ -1,8 +1,4 @@ -import { - defineCommerceEngine, - InferStaticState, - InferHydratedState, -} from '@coveo/headless-react/ssr-commerce'; +import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce'; import engineConfig from './commerce-engine-config'; export const engineDefinition = defineCommerceEngine(engineConfig); @@ -36,29 +32,3 @@ export const { useFacetGenerator, useBreadcrumbManager, } = engineDefinition.controllers; - -export type ListingStaticState = InferStaticState< - typeof listingEngineDefinition ->; -export type ListingHydratedState = InferHydratedState< - typeof listingEngineDefinition ->; - -export type SearchStaticState = InferStaticState; -export type SearchHydratedState = InferHydratedState< - typeof searchEngineDefinition ->; - -export type RecommendationStaticState = InferStaticState< - typeof recommendationEngineDefinition ->; -export type RecommendationHydratedState = InferHydratedState< - typeof recommendationEngineDefinition ->; - -export type StandaloneStaticState = InferStaticState< - typeof standaloneEngineDefinition ->; -export type StandaloneHydratedState = InferHydratedState< - typeof standaloneEngineDefinition ->; From 4cc0ed608b5b2c8cff62317b76ff1470a1916cd4 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:42:04 -0500 Subject: [PATCH 41/48] fix product page and fix standalone provider https://coveord.atlassian.net/browse/KIT-3734 --- .../headless-instant-products.ssr.ts | 5 +- .../headless-recent-queries-list.ssr.ts | 5 +- .../headless-ssr-commerce/app/cart/page.tsx | 10 +-- .../app/products/[productId]/page.tsx | 20 +++--- .../components/pages/product-page.tsx | 65 ------------------- 5 files changed, 23 insertions(+), 82 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/components/pages/product-page.tsx diff --git a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts index 0d5fe0cb290..e51099e17e1 100644 --- a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts +++ b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts @@ -1,4 +1,4 @@ -import {SearchAndListingControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { InstantProducts, InstantProductsProps, @@ -12,7 +12,7 @@ export type { export type {InstantProducts, InstantProductsProps}; export interface InstantProductsDefinition - extends SearchAndListingControllerDefinitionWithoutProps {} + extends UniversalControllerDefinitionWithoutProps {} /** * Defines the `InstantProducts` controller for the purpose of server-side rendering. @@ -28,6 +28,7 @@ export function defineInstantProducts( return { listing: true, search: true, + standalone: true, build: (engine) => buildInstantProducts(engine, props), }; } diff --git a/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts b/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts index 72aac7cee78..cb072bedb86 100644 --- a/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts +++ b/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts @@ -1,4 +1,4 @@ -import {SearchAndListingControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { RecentQueriesList, RecentQueriesListProps, @@ -13,7 +13,7 @@ export type { export type {RecentQueriesList, RecentQueriesListProps}; export interface RecentQueriesListDefinition - extends SearchAndListingControllerDefinitionWithoutProps {} + extends UniversalControllerDefinitionWithoutProps {} /** * Defines the `RecentQueriesList` controller for the purpose of server-side rendering. @@ -30,6 +30,7 @@ export function defineRecentQueriesList( return { search: true, listing: true, + standalone: true, build: (engine) => buildRecentQueriesList(engine, props), }; } diff --git a/packages/samples/headless-ssr-commerce/app/cart/page.tsx b/packages/samples/headless-ssr-commerce/app/cart/page.tsx index 0b3907c4e1f..5a448a8a870 100644 --- a/packages/samples/headless-ssr-commerce/app/cart/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/cart/page.tsx @@ -2,10 +2,11 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import Cart from '@/components/cart'; import ContextDropdown from '@/components/context-dropdown'; import { - ListingProvider, RecommendationProvider, + StandaloneProvider, } from '@/components/providers/providers'; import PopularBought from '@/components/recommendations/popular-bought'; +import StandaloneSearchBox from '@/components/standalone-search-box'; import { recommendationEngineDefinition, searchEngineDefinition, @@ -41,12 +42,13 @@ export default async function Search() { ['popularBought'] ); return ( - -
    +
    +
    - + ); } diff --git a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx index 793cbf7d858..e0a8cb8db28 100644 --- a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx @@ -1,18 +1,18 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import ContextDropdown from '@/components/context-dropdown'; -import ProductPage from '@/components/pages/product-page'; import {StandaloneProvider} from '@/components/providers/providers'; import StandaloneSearchBox from '@/components/standalone-search-box'; import {searchEngineDefinition} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; -import {Suspense} from 'react'; export default async function ProductDescriptionPage({ params, + searchParams, }: { params: {productId: string}; + searchParams: Promise<{[key: string]: string | string[] | undefined}>; }) { // Sets the navigator context provider to use the newly created `navigatorContext` before fetching the app static state const navigatorContext = new NextJsNavigatorContext(headers()); @@ -35,6 +35,11 @@ export default async function ProductDescriptionPage({ }, }, }); + + const resolvedSearchParams = await searchParams; + const price = Number(resolvedSearchParams.price) ?? NaN; + const name = resolvedSearchParams.name ?? params.productId; + return ( Product description page - Loading...

    }> - -
    +

    + {name} ({params.productId}) - ${price} +

    +
    ); } diff --git a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx deleted file mode 100644 index 79232674ef2..00000000000 --- a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import { - standaloneEngineDefinition, - StandaloneHydratedState, - StandaloneStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {useSearchParams} from 'next/navigation'; -import {useEffect, useState} from 'react'; - -interface IProductPageProps { - staticState: StandaloneStaticState; - navigatorContext: NavigatorContext; - productId: string; -} - -export default function ProductPage(props: IProductPageProps) { - const [hydratedState, setHydratedState] = useState< - StandaloneHydratedState | undefined - >(undefined); - - const {staticState, navigatorContext, productId} = props; - - const searchParams = useSearchParams(); - - const price = Number(searchParams.get('price')) ?? NaN; - const name = searchParams.get('name') ?? productId; - - // Setting the navigator context provider also in client-side before hydrating the application - standaloneEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - standaloneEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - const viewController = hydratedState?.controllers.productView; - - useEffect(() => { - viewController?.view({name, productId, price}); - }, [viewController, productId, name, price]); - - return ( - <> -

    - {name} ({productId}) - ${price} -

    -
    - - ); -} From 4a139b11f5fd107ac96648b004eee22d4a61ddd5 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:46:03 -0500 Subject: [PATCH 42/48] remove TODO https://coveord.atlassian.net/browse/KIT-3734 --- packages/headless-react/src/ssr-commerce/common.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/headless-react/src/ssr-commerce/common.tsx b/packages/headless-react/src/ssr-commerce/common.tsx index 64ea0a07e82..62d90fd9774 100644 --- a/packages/headless-react/src/ssr-commerce/common.tsx +++ b/packages/headless-react/src/ssr-commerce/common.tsx @@ -33,7 +33,6 @@ function isHydratedStateContext< return 'engine' in ctx; } -// TODO: https://coveord.atlassian.net/browse/KIT-3756 Add error when the controller is not found and it becomes "empty" function buildControllerHook< TControllers extends ControllerDefinitionsMap, TKey extends keyof TControllers, From 652917dd997ec85fd95fed247d493152c98f58ea Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:52:53 -0500 Subject: [PATCH 43/48] add comments https://coveord.atlassian.net/browse/KIT-3734 --- .../headless-ssr-commerce/components/providers/providers.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/samples/headless-ssr-commerce/components/providers/providers.tsx b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx index a48aeec58d2..0df7111416e 100644 --- a/packages/samples/headless-ssr-commerce/components/providers/providers.tsx +++ b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx @@ -8,18 +8,22 @@ import { } from '@/lib/commerce-engine'; import {buildProviderWithDefinition} from '@coveo/headless-react/ssr-commerce'; +// Wraps listing pages to provide context for listing-specific hooks export const ListingProvider = buildProviderWithDefinition( listingEngineDefinition ); +// Wraps search pages to provide context for search-specific hooks export const SearchProvider = buildProviderWithDefinition( searchEngineDefinition ); +// Wraps recommendations, whether in a standalone, search, or listing page export const RecommendationProvider = buildProviderWithDefinition( recommendationEngineDefinition ); +// Used for components that don’t require triggering a search or product fetch (e.g., cart pages, standalone search box) export const StandaloneProvider = buildProviderWithDefinition( standaloneEngineDefinition ); From 2a2aaee23e45e87137850ffd547ec12fcd5c3826 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:33:35 -0500 Subject: [PATCH 44/48] UniversalControllerDefinitionWithoutProps -> NonRecommendationControllerDefinitionWithoutProps https://coveord.atlassian.net/browse/KIT-3734 --- .../headless/src/app/commerce-ssr-engine/types/common.ts | 4 ++-- .../controllers/commerce/context/cart/headless-cart.ssr.ts | 7 +++++-- .../controllers/commerce/context/headless-context.ssr.ts | 7 +++++-- .../headless-field-suggestions-generator.ssr.ts | 4 ++-- .../instant-products/headless-instant-products.ssr.ts | 4 ++-- .../commerce/product-view/headless-product-view.ssr.ts | 4 ++-- .../headless-recent-queries-list.ssr.ts | 4 ++-- .../headless-standalone-search-box.ssr.ts | 4 ++-- 8 files changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index 918cba55ade..fe449f46df9 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -210,11 +210,11 @@ export type RecommendationOnlyControllerDefinitionWithProps< > = ControllerDefinitionWithProps & RecommendationOnlyController; -export type UniversalControllerDefinitionWithoutProps< +export type NonRecommendationControllerDefinitionWithoutProps< TController extends Controller, > = ControllerDefinitionWithoutProps & UniversalController; -export type UniversalControllerDefinitionWithProps< +export type NonRecommendationControllerDefinitionWithProps< TController extends Controller, TProps, > = ControllerDefinitionWithProps & UniversalController; diff --git a/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts b/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts index 903c64708c6..ac1c12864ea 100644 --- a/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts +++ b/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithProps} from '../../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithProps} from '../../../../app/commerce-ssr-engine/types/common.js'; import {Cart, buildCart, CartInitialState} from './headless-cart.js'; export type {CartState, CartItem, CartProps} from './headless-cart.js'; @@ -9,7 +9,10 @@ export interface CartBuildProps { } export interface CartDefinition - extends UniversalControllerDefinitionWithProps {} + extends NonRecommendationControllerDefinitionWithProps< + Cart, + CartBuildProps + > {} /** * Defines a `Cart` controller instance. diff --git a/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts b/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts index 3743e9d83b0..dbacda42a17 100644 --- a/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts +++ b/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { Context, buildContext, @@ -11,7 +11,10 @@ export type {ContextState, Context, ContextProps} from './headless-context.js'; export type {View, UserLocation, ContextOptions}; export interface ContextDefinition - extends UniversalControllerDefinitionWithProps {} + extends NonRecommendationControllerDefinitionWithProps< + Context, + ContextOptions + > {} /** * Defines a `Context` controller instance. diff --git a/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts b/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts index c339b8d4762..ee89bd6b5bc 100644 --- a/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts +++ b/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { FieldSuggestionsGenerator, buildFieldSuggestionsGenerator, @@ -17,7 +17,7 @@ export type {GeneratedFieldSuggestionsControllers} from './headless-field-sugges export type {FieldSuggestionsGenerator}; export interface FieldSuggestionsGeneratorDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `FieldSuggestionsGenerator` controller for the purpose of server-side rendering. diff --git a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts index 1d2b1c88681..bf6941c388e 100644 --- a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts +++ b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { InstantProducts, InstantProductsProps, @@ -12,7 +12,7 @@ export type { export type {InstantProducts, InstantProductsProps}; export interface InstantProductsDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `InstantProducts` controller for the purpose of server-side rendering. diff --git a/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts b/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts index 5882b96c6bb..8a7210c3fe6 100644 --- a/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts +++ b/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts @@ -1,5 +1,5 @@ import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine.js'; -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { buildController, Controller, @@ -10,7 +10,7 @@ import { } from './headless-product-view.js'; export interface ProductViewDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines a `ProductView` controller instance. diff --git a/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts b/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts index cb072bedb86..9db774096f1 100644 --- a/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts +++ b/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { RecentQueriesList, RecentQueriesListProps, @@ -13,7 +13,7 @@ export type { export type {RecentQueriesList, RecentQueriesListProps}; export interface RecentQueriesListDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `RecentQueriesList` controller for the purpose of server-side rendering. diff --git a/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts b/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts index c7d5c780426..f992138e3d6 100644 --- a/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts +++ b/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import {StandaloneSearchBoxProps} from '../../standalone-search-box/headless-standalone-search-box.js'; import { StandaloneSearchBox, @@ -10,7 +10,7 @@ export type {StandaloneSearchBoxState} from './headless-standalone-search-box.js export type {StandaloneSearchBox, StandaloneSearchBoxProps}; export interface StandaloneSearchBoxDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `StandaloneSearchBox` controller for the purpose of server-side rendering. From 6a353489ac33552bdf4b34ce507aeb9fded5c387 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:37:33 -0500 Subject: [PATCH 45/48] text clearer https://coveord.atlassian.net/browse/KIT-3734 --- .../headless-ssr-commerce/components/hydration-metadata.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx b/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx index 6ebe8120d67..9c1d1016f16 100644 --- a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx +++ b/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx @@ -21,7 +21,7 @@ export interface HydrationMetadataProps { // This component displays metadata about the hydration state of the page. // IMPORTANT: It was created for testing our package. -// You should not use this component anywhere. +// You won't have to create this component yourself. This is just an internal tool. export const HydrationMetadata: FunctionComponent = ({ staticState, hydratedState, From e41d2cab4aa0f5f4358d2acc5a815741b406055c Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:16:33 -0500 Subject: [PATCH 46/48] fix build https://coveord.atlassian.net/browse/KIT-3734 --- .../providers/recommendation-provider.tsx | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx deleted file mode 100644 index 6ea9c228236..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import { - recommendationEngineDefinition, - RecommendationHydratedState, - RecommendationStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; - -interface RecommendationProviderProps { - staticState: RecommendationStaticState; - navigatorContext: NavigatorContext; -} - -export default function RecommendationProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - recommendationEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - recommendationEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} From f454e9b4debf2eebb961bea93f57c21cdfacf04c Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:28:13 -0500 Subject: [PATCH 47/48] remove metadata https://coveord.atlassian.net/browse/KIT-3734 --- package-lock.json | 280 ------------------ .../components/hydration-metadata.tsx | 60 ---- .../e2e/cart/cart.spec.ts | 67 ----- .../e2e/listing/listing.spec.ts | 79 ----- .../e2e/search/search.spec.ts | 83 ------ .../headless-ssr-commerce/package.json | 1 - 6 files changed, 570 deletions(-) delete mode 100644 packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx diff --git a/package-lock.json b/package-lock.json index c35d6a417ca..001dba45bb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48174,13 +48174,6 @@ "win32" ] }, - "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -51210,26 +51203,6 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "6.1.61", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.61.tgz", - "integrity": "sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.61" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.61", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.61.tgz", - "integrity": "sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==", - "dev": true, - "license": "MIT" - }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -65637,7 +65610,6 @@ "@types/react-dom": "18.3.0", "eslint": "8.57", "eslint-config-next": "14.2.5", - "jsdom": "25.0.1", "typescript": "5.4.5" } }, @@ -65656,61 +65628,6 @@ "node": ">=18" } }, - "packages/samples/headless-ssr-commerce/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/cssstyle": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rrweb-cssom": "^0.7.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "packages/samples/headless-ssr-commerce/node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -65725,95 +65642,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "packages/samples/headless-ssr-commerce/node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.1.0", - "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^2.11.2" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "packages/samples/headless-ssr-commerce/node_modules/nwsapi": { - "version": "2.2.13", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", - "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", - "dev": true, - "license": "MIT" - }, "packages/samples/headless-ssr-commerce/node_modules/playwright": { "version": "1.45.3", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", @@ -65844,114 +65672,6 @@ "node": ">=18" } }, - "packages/samples/headless-ssr-commerce/node_modules/tough-cookie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", - "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "packages/samples/headless-ssr-commerce/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "packages/samples/headless-ssr/app-router": { "name": "@coveo/headless-ssr-samples-app-router", "version": "0.0.0", diff --git a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx b/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx deleted file mode 100644 index 9c1d1016f16..00000000000 --- a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx +++ /dev/null @@ -1,60 +0,0 @@ -'use client'; - -import { - listingEngineDefinition, - searchEngineDefinition, -} from '@/lib/commerce-engine'; -import { - InferHydratedState, - InferStaticState, -} from '@coveo/headless-react/ssr-commerce'; -import {FunctionComponent} from 'react'; - -export interface HydrationMetadataProps { - staticState: - | InferStaticState - | InferStaticState; - hydratedState?: - | InferHydratedState - | InferHydratedState; -} - -// This component displays metadata about the hydration state of the page. -// IMPORTANT: It was created for testing our package. -// You won't have to create this component yourself. This is just an internal tool. -export const HydrationMetadata: FunctionComponent = ({ - staticState, - hydratedState, -}) => { - return ( - <> -
    - Hydrated:{' '} - -
    - - Rendered page with{' '} - { - (hydratedState ?? staticState).controllers.productList.state.products - .length - }{' '} - products - -
    - Items in cart:{' '} - {(hydratedState ?? staticState).controllers.cart.state.items.length} -
    -
    - Rendered on{' '} - - {new Date().toISOString()} - -
    - - ); -}; diff --git a/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts b/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts index 34ed42720ae..9513a9d5f92 100644 --- a/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts +++ b/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts @@ -1,4 +1,3 @@ -import {JSDOM} from 'jsdom'; import {test, expect} from './cart.fixture'; test.describe('default', () => { @@ -221,69 +220,3 @@ test.describe('default', () => { }); }); }); - -test.describe('ssr', () => { - const numItemsInCart = 0; // Define the numResults variable - const numItemsInCartMsg = `Items in cart: ${numItemsInCart}`; - - test(`renders page in SSR as expected`, async ({page}) => { - const responsePromise = page.waitForResponse('**/cart'); - await page.goto('/cart'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - expect(dom.window.document.querySelector('#cart-msg')?.textContent).toBe( - numItemsInCartMsg - ); - - expect(dom.window.document.querySelectorAll('ul#cart li').length).toBe( - numItemsInCart - ); - expect( - ( - dom.window.document.querySelector( - '#hydrated-indicator' - ) as HTMLInputElement - )?.checked - ).toBe(false); - }); - - test(`renders page in CSR as expected`, async ({page, cart, hydrated}) => { - await page.goto('/cart'); - await expect(hydrated.hydratedCartMessage).toHaveText(numItemsInCartMsg); - - expect(await cart.items.all()).toHaveLength(numItemsInCart); - - expect(await hydrated.hydratedIndicator).toBe(true); - }); - - test('renders product list in SSR and then in CSR', async ({ - page, - cart, - hydrated, - }) => { - const responsePromise = page.waitForResponse('**/cart'); - await page.goto('/cart'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - const ssrTimestamp = Date.parse( - dom.window.document.querySelector('#timestamp')!.textContent || '' - ); - - const hydratedTimestamp = Date.parse( - (await hydrated.hydratedTimestamp.textContent()) || '' - ); - - expect(ssrTimestamp).not.toBeNaN(); - await expect(hydrated.hydratedCartMessage).toHaveText(numItemsInCartMsg); - expect(await cart.items.all()).toHaveLength(numItemsInCart); - expect(hydratedTimestamp).toBeGreaterThan(ssrTimestamp); - }); -}); diff --git a/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts b/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts index 551f677eb74..64bed901937 100644 --- a/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts +++ b/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts @@ -1,4 +1,3 @@ -import {JSDOM} from 'jsdom'; import {test, expect} from './listing.fixture'; test.describe('default', () => { @@ -76,81 +75,3 @@ test.describe('default', () => { }); }); }); - -test.describe('ssr', () => { - const numResults = 9; // Define the numResults variable - const numResultsMsg = `Rendered page with ${numResults} products`; - - test(`renders page in SSR as expected`, async ({page}) => { - const responsePromise = page.waitForResponse('**/surf-accessories'); - await page.goto('/surf-accessories'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - expect( - dom.window.document.querySelector('#hydrated-msg')?.textContent - ).toBe(numResultsMsg); - - expect( - dom.window.document.querySelectorAll('[aria-label="Product List"] li') - .length - ).toBe(numResults); - - expect( - (dom.window.document.querySelector('#sorts-select') as HTMLSelectElement) - .selectedOptions[0]?.textContent - ).toBe('Relevance'); - - expect( - ( - dom.window.document.querySelector( - '#hydrated-indicator' - ) as HTMLInputElement - )?.checked - ).toBe(false); - }); - - test(`renders page in CSR as expected`, async ({ - page, - search, - sort, - hydrated, - }) => { - await page.goto('/surf-accessories'); - - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - expect(await search.productItems).toHaveLength(numResults); - expect(await sort.selectedOption.textContent()).toBe('Relevance'); - expect(await hydrated.hydratedIndicator).toBe(true); - }); - - test('renders product list in SSR and then in CSR', async ({ - page, - search, - hydrated, - }) => { - const responsePromise = page.waitForResponse('**/surf-accessories'); - await page.goto('/surf-accessories'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - const ssrTimestamp = Date.parse( - dom.window.document.querySelector('#timestamp')!.textContent || '' - ); - - const hydratedTimestamp = Date.parse( - (await hydrated.hydratedTimestamp.textContent()) || '' - ); - - expect(ssrTimestamp).not.toBeNaN(); - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - expect(await search.productItems).toHaveLength(numResults); - expect(hydratedTimestamp).toBeGreaterThan(ssrTimestamp); - }); -}); diff --git a/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts b/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts index fddee69787d..6c1ec777157 100644 --- a/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts +++ b/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts @@ -1,4 +1,3 @@ -import {JSDOM} from 'jsdom'; import {test, expect} from './search.fixture'; test.describe('default', () => { @@ -112,85 +111,3 @@ test.describe('default', () => { }); }); }); -test.describe('ssr', () => { - const numResults = 9; // Define the numResults variable - const numResultsMsg = `Rendered page with ${numResults} products`; - - test(`renders page in SSR as expected`, async ({page}) => { - const responsePromise = page.waitForResponse('**/search'); - await page.goto('/search'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - expect( - dom.window.document.querySelector('#hydrated-msg')?.textContent - ).toBe(numResultsMsg); - - expect( - dom.window.document.querySelectorAll('[aria-label="Product List"] li') - .length - ).toBe(numResults); - expect( - ( - dom.window.document.querySelector( - '#hydrated-indicator' - ) as HTMLInputElement - )?.checked - ).toBe(false); - }); - - test(`renders page in CSR as expected`, async ({page, search, hydrated}) => { - await page.goto('/search'); - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - - expect(await search.productItems).toHaveLength(numResults); - expect(await hydrated.hydratedIndicator).toBe(true); - }); - - test('renders product list in SSR and then in CSR', async ({ - page, - search, - hydrated, - }) => { - const responsePromise = page.waitForResponse('**/search'); - await page.goto('/search'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - const ssrTimestamp = Date.parse( - dom.window.document.querySelector('#timestamp')!.textContent || '' - ); - - const hydratedTimestamp = Date.parse( - (await hydrated.hydratedTimestamp.textContent()) || '' - ); - - expect(ssrTimestamp).not.toBeNaN(); - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - expect(await search.productItems).toHaveLength(numResults); - expect(hydratedTimestamp).toBeGreaterThan(ssrTimestamp); - }); - - test('after submitting a query, should change results', async ({ - page, - search, - facet, - }) => { - await page.goto('/search'); - const initialProducts = await search.productList.textContent(); - - await search.searchBox.fill('shoes'); - await search.searchButton.click(); - - await facet.facetLoading.waitFor({state: 'visible'}); - await facet.facetLoading.waitFor({state: 'hidden'}); - - expect(await search.productList.textContent()).not.toEqual(initialProducts); - }); -}); diff --git a/packages/samples/headless-ssr-commerce/package.json b/packages/samples/headless-ssr-commerce/package.json index 718070a2460..1d64c81e5a4 100644 --- a/packages/samples/headless-ssr-commerce/package.json +++ b/packages/samples/headless-ssr-commerce/package.json @@ -23,7 +23,6 @@ "@playwright/test": "1.45.3", "eslint": "8.57", "eslint-config-next": "14.2.5", - "jsdom": "25.0.1", "typescript": "5.4.5" } } From ae9bbae92cb01d6db519c5ad8a69147c59b4ec61 Mon Sep 17 00:00:00 2001 From: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com> Date: Wed, 27 Nov 2024 08:47:26 -0500 Subject: [PATCH 48/48] Update atomic-external.e2e.ts https://coveord.atlassian.net/browse/KIT-3734 --- .../search/atomic-external/e2e/atomic-external.e2e.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts b/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts index 34dc7594eb0..0bedc4f4807 100644 --- a/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts +++ b/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts @@ -18,11 +18,12 @@ test.describe('when modifying state of a component (search box) that is a child await external.searchBox.press('Enter'); }); - test("other components' state under the same atomic-external should be affected", async ({ - external, - }) => { - await expect(external.querySummary).toHaveText(/hello/); - }); + test.fixme( + "other components' state under the same atomic-external should be affected", + async ({external}) => { + await expect(external.querySummary).toHaveText(/hello/); + } + ); test.fixme( "other components' state under the linked atomic-search-interface should be affected",