@replohq/sdk 2.9.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/_vendor/replo-utils/lib/url.d.ts +10 -3
  2. package/lib/buildMetadata.js +3 -3
  3. package/loaders/collection-loader.d.ts +4 -1
  4. package/loaders/collection-loader.js +5 -9
  5. package/loaders/collection-products-loader.d.ts +4 -1
  6. package/loaders/collection-products-loader.js +5 -9
  7. package/loaders/contentful-entries-loader.d.ts +4 -1
  8. package/loaders/contentful-entries-loader.js +2 -6
  9. package/loaders/contentful-entry-loader.d.ts +4 -1
  10. package/loaders/contentful-entry-loader.js +2 -6
  11. package/loaders/invoke-loader-action.d.ts +2 -1
  12. package/loaders/invoke-loader-action.js +2 -4
  13. package/loaders/invoke-loader-server.js +8 -4
  14. package/loaders/invoke-loader.d.ts +11 -3
  15. package/loaders/invoke-loader.js +6 -5
  16. package/loaders/judgeme-review-aggregate-loader.d.ts +4 -1
  17. package/loaders/judgeme-review-aggregate-loader.js +5 -11
  18. package/loaders/judgeme-reviews-loader.d.ts +4 -1
  19. package/loaders/judgeme-reviews-loader.js +2 -6
  20. package/loaders/junip-review-aggregate-loader.d.ts +4 -1
  21. package/loaders/junip-review-aggregate-loader.js +5 -11
  22. package/loaders/junip-reviews-loader.d.ts +4 -1
  23. package/loaders/junip-reviews-loader.js +2 -6
  24. package/loaders/kaching-bundles-loader.d.ts +4 -1
  25. package/loaders/kaching-bundles-loader.js +5 -6
  26. package/loaders/levanta-product-loader.d.ts +4 -1
  27. package/loaders/levanta-product-loader.js +5 -9
  28. package/loaders/loader-output.d.ts +28 -0
  29. package/loaders/loader-output.js +122 -0
  30. package/loaders/loader-outputs.d.ts +55 -0
  31. package/loaders/loader-outputs.js +0 -0
  32. package/loaders/loox-review-aggregate-loader.d.ts +4 -1
  33. package/loaders/loox-review-aggregate-loader.js +5 -11
  34. package/loaders/loox-reviews-loader.d.ts +4 -1
  35. package/loaders/loox-reviews-loader.js +5 -9
  36. package/loaders/loyaltylion-earning-rules-loader.js +5 -11
  37. package/loaders/loyaltylion-points-for-price-loader.js +5 -11
  38. package/loaders/loyaltylion-rewards-loader.js +5 -9
  39. package/loaders/loyaltylion-tiers-loader.js +5 -9
  40. package/loaders/metaobject-loader.d.ts +4 -1
  41. package/loaders/metaobject-loader.js +2 -6
  42. package/loaders/metaobjects-loader.d.ts +4 -1
  43. package/loaders/metaobjects-loader.js +2 -6
  44. package/loaders/okendo-review-aggregate-loader.d.ts +4 -1
  45. package/loaders/okendo-review-aggregate-loader.js +5 -9
  46. package/loaders/okendo-reviews-loader.d.ts +4 -1
  47. package/loaders/okendo-reviews-loader.js +2 -6
  48. package/loaders/product-loader.d.ts +4 -1
  49. package/loaders/product-loader.js +2 -6
  50. package/loaders/rebuy-recommendations-loader.d.ts +4 -1
  51. package/loaders/rebuy-recommendations-loader.js +5 -9
  52. package/loaders/reviews-io-review-aggregate-loader.d.ts +4 -1
  53. package/loaders/reviews-io-review-aggregate-loader.js +2 -8
  54. package/loaders/reviews-io-reviews-loader.d.ts +4 -1
  55. package/loaders/reviews-io-reviews-loader.js +2 -8
  56. package/loaders/shopify-integration-details-loader.d.ts +4 -1
  57. package/loaders/shopify-integration-details-loader.js +5 -11
  58. package/loaders/smile-earning-rules-loader.d.ts +4 -1
  59. package/loaders/smile-earning-rules-loader.js +5 -9
  60. package/loaders/smile-rewards-loader.d.ts +4 -1
  61. package/loaders/smile-rewards-loader.js +5 -9
  62. package/loaders/smile-vip-tiers-loader.d.ts +4 -1
  63. package/loaders/smile-vip-tiers-loader.js +5 -9
  64. package/loaders/statsig-experiment-loader.d.ts +4 -1
  65. package/loaders/statsig-experiment-loader.js +2 -6
  66. package/loaders/yotpo-review-aggregate-loader.d.ts +4 -1
  67. package/loaders/yotpo-review-aggregate-loader.js +5 -11
  68. package/loaders/yotpo-reviews-loader.d.ts +4 -1
  69. package/loaders/yotpo-reviews-loader.js +2 -6
  70. package/package.json +4 -4
  71. package/size-budget/fixture/app/fixture-pdp/page.d.ts +1 -1
  72. package/size-budget/fixture/app/fixture-pdp/product-details.d.ts +1 -0
@@ -1,8 +1,14 @@
1
+ type UrlSearchParamValues = Record<string, string | number | boolean | string[] | null | undefined>;
1
2
  /**
2
- * Builds a URLSearchParams from a record, omitting undefined values. Array
3
- * values are appended as repeated keys; scalars are stringified.
3
+ * Builds a URLSearchParams from a record, omitting null and undefined values.
4
+ * Array values are appended as repeated keys; scalars are stringified.
4
5
  */
5
- export declare function buildUrlSearchParams(values: Record<string, string | number | boolean | string[] | undefined>): URLSearchParams;
6
+ export declare function buildUrlSearchParams(values: UrlSearchParamValues): URLSearchParams;
7
+ /**
8
+ * Appends a query string built by `buildUrlSearchParams` to a path, leaving
9
+ * the path bare when every value is omitted.
10
+ */
11
+ export declare function pathWithSearchParams(path: string, values: UrlSearchParamValues): string;
6
12
  export declare function isValidHttpUrl(url: string, options?: {
7
13
  allowBlockedHostnames?: boolean;
8
14
  allowedHostnames?: string[];
@@ -72,3 +78,4 @@ export declare function sanitizePublicHttpUrl(value: string | null, { preservePr
72
78
  }): string | null;
73
79
  export declare function isPublicHttpsUrl(value: string): boolean;
74
80
  export declare function safeHost(url: string): string | null;
81
+ export {};
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "branch": "HEAD",
5
- "commit": "9b8b9783f1040bce19d91ce9adc8385204be0b93",
6
- "builtAt": "2026-10-26T04:07:50.620Z"
5
+ "commit": "96a07333d34af59c46f8874351df866542e7c2e9",
6
+ "builtAt": "2026-10-26T22:54:01.502Z"
7
7
  };
@@ -1,8 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { CollectionLoaderArgs } from "./collection-loader-args";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
3
5
  import type { FullCollection } from "./loader-schemas";
6
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["SHOPIFY_COLLECTION"];
4
7
  type CollectionLoaderProps = CollectionLoaderArgs & {
5
- loaderKey: string;
8
+ loaderKey: LoaderKeyProp<LoaderKey>;
6
9
  children: (data: FullCollection) => ReactNode;
7
10
  fallback?: ReactNode;
8
11
  };
@@ -13,15 +13,11 @@ function CollectionLoaderInner({
13
13
  }) {
14
14
  const queryResult = useSuspenseQuery({
15
15
  queryKey: [loaderKey, args],
16
- queryFn: async () => {
17
- const [loadedData, schemas] = await Promise.all([
18
- invokeLoader({
19
- loaderKey,
20
- args
21
- }),
22
- import("./loader-schemas")
23
- ]);
24
- return schemas.fullCollectionSchema.parse(loadedData);
16
+ queryFn: () => {
17
+ return invokeLoader({
18
+ loaderKey,
19
+ args
20
+ });
25
21
  }
26
22
  });
27
23
  useReportPreviewPlaceholderRender(queryResult.data);
@@ -1,8 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { CollectionProductsLoaderArgs } from "./collection-products-loader-args";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
3
5
  import type { PaginatedProducts } from "./loader-schemas";
6
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["SHOPIFY_COLLECTION_PRODUCTS"];
4
7
  type CollectionProductsLoaderProps = CollectionProductsLoaderArgs & {
5
- loaderKey: string;
8
+ loaderKey: LoaderKeyProp<LoaderKey>;
6
9
  children: (data: PaginatedProducts) => ReactNode;
7
10
  fallback?: ReactNode;
8
11
  };
@@ -13,15 +13,11 @@ function CollectionProductsLoaderInner({
13
13
  }) {
14
14
  const queryResult = useSuspenseQuery({
15
15
  queryKey: [loaderKey, args],
16
- queryFn: async () => {
17
- const [loadedData, schemas] = await Promise.all([
18
- invokeLoader({
19
- loaderKey,
20
- args
21
- }),
22
- import("./loader-schemas")
23
- ]);
24
- return schemas.paginatedProductsSchema.parse(loadedData);
16
+ queryFn: () => {
17
+ return invokeLoader({
18
+ loaderKey,
19
+ args
20
+ });
25
21
  }
26
22
  });
27
23
  useReportPreviewPlaceholderRender(queryResult.data);
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
3
+ import type { LoaderKeyProp } from "./loader-outputs";
2
4
  import type { ContentfulEntriesLoaderOutput } from "./loader-schemas";
5
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["CONTENTFUL_ENTRIES"];
3
6
  type ContentfulEntriesLoaderProps = {
4
- loaderKey: string;
7
+ loaderKey: LoaderKeyProp<LoaderKey>;
5
8
  spaceId: string;
6
9
  contentType: string;
7
10
  environmentId?: string;
@@ -32,12 +32,8 @@ function ContentfulEntriesLoaderInner({
32
32
  }) {
33
33
  const { data } = useSuspenseQuery({
34
34
  queryKey: [loaderKey, args],
35
- queryFn: async () => {
36
- const [loadedData, schemas] = await Promise.all([
37
- invokeLoader({ loaderKey, args }),
38
- import("./loader-schemas")
39
- ]);
40
- return schemas.contentfulEntriesLoaderOutputSchema.parse(loadedData);
35
+ queryFn: () => {
36
+ return invokeLoader({ loaderKey, args });
41
37
  }
42
38
  });
43
39
  return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
3
+ import type { LoaderKeyProp } from "./loader-outputs";
2
4
  import type { ContentfulEntryLoaderOutput } from "./loader-schemas";
5
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["CONTENTFUL_ENTRY"];
3
6
  type ContentfulEntryLoaderProps = {
4
- loaderKey: string;
7
+ loaderKey: LoaderKeyProp<LoaderKey>;
5
8
  spaceId: string;
6
9
  entryId: string;
7
10
  environmentId?: string;
@@ -26,12 +26,8 @@ function ContentfulEntryLoaderInner({
26
26
  }) {
27
27
  const { data } = useSuspenseQuery({
28
28
  queryKey: [loaderKey, args],
29
- queryFn: async () => {
30
- const [loadedData, schemas] = await Promise.all([
31
- invokeLoader({ loaderKey, args }),
32
- import("./loader-schemas")
33
- ]);
34
- return schemas.contentfulEntryLoaderOutputSchema.parse(loadedData);
29
+ queryFn: () => {
30
+ return invokeLoader({ loaderKey, args });
35
31
  }
36
32
  });
37
33
  return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
@@ -3,7 +3,8 @@
3
3
  * Called from client-side loader components when React Query needs to
4
4
  * fetch data in the browser (e.g., client-side navigation, cache miss).
5
5
  * This runs on the server — Next.js handles the client/server boundary
6
- * automatically.
6
+ * automatically. The output is validated here, before it crosses to the
7
+ * browser.
7
8
  */
8
9
  export declare function invokeLoaderAction({ loaderKey, args, }: {
9
10
  loaderKey: string;
@@ -1,12 +1,10 @@
1
1
  "use server";
2
- import { getCanopyApiGateway } from "../gateways/canopy-api";
2
+ import { fetchValidatedLoaderOutput } from "./loader-output";
3
3
  async function invokeLoaderAction({
4
4
  loaderKey,
5
5
  args
6
6
  }) {
7
- const gateway = await getCanopyApiGateway();
8
- const response = await gateway.invokeLoader({ loaderKey, args });
9
- return response.data;
7
+ return fetchValidatedLoaderOutput({ loaderKey, args });
10
8
  }
11
9
  export {
12
10
  invokeLoaderAction
@@ -1,9 +1,9 @@
1
1
  import { unstable_cache } from "next/cache";
2
2
  import {
3
3
  CanopyApiGatewayError,
4
- getCanopyApiGateway,
5
4
  shouldIncludeInactiveProducts
6
5
  } from "../gateways/canopy-api";
6
+ import { fetchValidatedLoaderOutput, LoaderOutputError } from "./loader-output";
7
7
  import { resolvePreviewPlaceholderData } from "./preview-placeholder";
8
8
  const DEFAULT_REVALIDATE_SECONDS = 60;
9
9
  class LoaderDataNotFoundError extends CanopyApiGatewayError {
@@ -46,11 +46,15 @@ async function invokeLoaderServer({
46
46
  });
47
47
  const cachedInvocation = await unstable_cache(
48
48
  async () => {
49
- const gateway = await getCanopyApiGateway();
50
49
  try {
51
- const response = await gateway.invokeLoader({ loaderKey, args });
52
- return { kind: "data", data: response.data };
50
+ return {
51
+ kind: "data",
52
+ data: await fetchValidatedLoaderOutput({ loaderKey, args })
53
+ };
53
54
  } catch (error) {
55
+ if (error instanceof LoaderOutputError) {
56
+ return { kind: "data", data: error.unvalidatedOutput };
57
+ }
54
58
  if (error instanceof CanopyApiGatewayError && error.statusCode === 404) {
55
59
  return { kind: "notFound" };
56
60
  }
@@ -1,3 +1,5 @@
1
+ import type { DataLoaderKey } from "./loader-keys";
2
+ import type { LoaderKeyProp, LoaderOutput } from "./loader-outputs";
1
3
  import { CanopyError } from "../lib/canopy-error";
2
4
  /** Scoped error for loader invocation failures. */
3
5
  export declare class LoaderInvokeError extends CanopyError {
@@ -7,6 +9,12 @@ export declare class LoaderInvokeError extends CanopyError {
7
9
  * In the browser, dynamically imports a server action so React 19 never sees
8
10
  * the "use server" reference during render.
9
11
  *
12
+ * `Key` names the loader the caller expects, and the result is typed from it.
13
+ * It is not checked here: `loaderKey` can be any string a site passed to a
14
+ * component, and the server validates the output against the schema
15
+ * registered for the key actually sent (`loader-output.ts`), so the browser
16
+ * never downloads Zod or the schemas. Callers pass `Key` explicitly.
17
+ *
10
18
  * Next defines `process.env.NEXT_RUNTIME` as "" in browser bundles, so the
11
19
  * bundler drops the server branch and never emits the gateway, its env
12
20
  * handling, or its Zod schemas as a client chunk. `typeof window` is not
@@ -15,7 +23,7 @@ export declare class LoaderInvokeError extends CanopyError {
15
23
  * In an editor iframe previewing an unfilled dynamic route, returns sample
16
24
  * data instead of fetching — see `preview-placeholder.ts`.
17
25
  */
18
- export declare function invokeLoader({ loaderKey, args, }: {
19
- loaderKey: string;
26
+ export declare function invokeLoader<Key extends DataLoaderKey>(params: {
27
+ loaderKey: LoaderKeyProp<Key>;
20
28
  args: Record<string, unknown>;
21
- }): Promise<unknown>;
29
+ }): Promise<LoaderOutput<Key>>;
@@ -2,7 +2,7 @@ import { CanopyError } from "../lib/canopy-error";
2
2
  import { resolvePreviewPlaceholderData } from "./preview-placeholder";
3
3
  class LoaderInvokeError extends CanopyError {
4
4
  }
5
- async function invokeLoader({
5
+ async function invokeUntypedLoader({
6
6
  loaderKey,
7
7
  args
8
8
  }) {
@@ -12,10 +12,8 @@ async function invokeLoader({
12
12
  }
13
13
  try {
14
14
  if (process.env.NEXT_RUNTIME) {
15
- const { getCanopyApiGateway } = await import("../gateways/canopy-api");
16
- const gateway = await getCanopyApiGateway();
17
- const response = await gateway.invokeLoader({ loaderKey, args });
18
- return response.data;
15
+ const { fetchValidatedLoaderOutput } = await import("./loader-output");
16
+ return await fetchValidatedLoaderOutput({ loaderKey, args });
19
17
  }
20
18
  const { invokeLoaderAction } = await import("./invoke-loader-action");
21
19
  return await invokeLoaderAction({ loaderKey, args });
@@ -30,6 +28,9 @@ async function invokeLoader({
30
28
  });
31
29
  }
32
30
  }
31
+ async function invokeLoader(params) {
32
+ return await invokeUntypedLoader(params);
33
+ }
33
34
  export {
34
35
  LoaderInvokeError,
35
36
  invokeLoader
@@ -1,9 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { z } from "zod";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
3
5
  import type { judgemeProductReviewAggregateLoaderOutputSchema } from "./loader-schemas";
6
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["JUDGEME_PRODUCT_REVIEW_AGGREGATE"];
4
7
  type JudgemeReviewAggregateData = z.infer<typeof judgemeProductReviewAggregateLoaderOutputSchema>;
5
8
  type JudgemeReviewAggregateLoaderProps = {
6
- loaderKey: string;
9
+ loaderKey: LoaderKeyProp<LoaderKey>;
7
10
  productId: string;
8
11
  children: (data: JudgemeReviewAggregateData) => ReactNode;
9
12
  fallback?: ReactNode;
@@ -11,17 +11,11 @@ function JudgemeReviewAggregateLoaderInner({
11
11
  }) {
12
12
  const { data } = useSuspenseQuery({
13
13
  queryKey: [loaderKey, { productId }],
14
- queryFn: async () => {
15
- const [loadedData, schemas] = await Promise.all([
16
- invokeLoader({
17
- loaderKey,
18
- args: { productId }
19
- }),
20
- import("./loader-schemas")
21
- ]);
22
- return schemas.judgemeProductReviewAggregateLoaderOutputSchema.parse(
23
- loadedData
24
- );
14
+ queryFn: () => {
15
+ return invokeLoader({
16
+ loaderKey,
17
+ args: { productId }
18
+ });
25
19
  }
26
20
  });
27
21
  return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
@@ -1,9 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { z } from "zod";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
3
5
  import type { judgemeProductReviewsLoaderOutputSchema } from "./loader-schemas";
6
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["JUDGEME_PRODUCT_REVIEWS"];
4
7
  type JudgemeReviewsData = z.infer<typeof judgemeProductReviewsLoaderOutputSchema>;
5
8
  type JudgemeReviewsLoaderProps = {
6
- loaderKey: string;
9
+ loaderKey: LoaderKeyProp<LoaderKey>;
7
10
  productId: string;
8
11
  perPage?: number;
9
12
  page?: number;
@@ -25,12 +25,8 @@ function JudgemeReviewsLoaderInner({
25
25
  const args = buildReviewsArgs({ productId, perPage, page });
26
26
  const { data } = useSuspenseQuery({
27
27
  queryKey: [loaderKey, args],
28
- queryFn: async () => {
29
- const [loadedData, schemas] = await Promise.all([
30
- invokeLoader({ loaderKey, args }),
31
- import("./loader-schemas")
32
- ]);
33
- return schemas.judgemeProductReviewsLoaderOutputSchema.parse(loadedData);
28
+ queryFn: () => {
29
+ return invokeLoader({ loaderKey, args });
34
30
  }
35
31
  });
36
32
  return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
@@ -1,9 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { z } from "zod";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
3
5
  import type { junipProductReviewAggregateLoaderOutputSchema } from "./loader-schemas";
6
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["JUNIP_PRODUCT_REVIEW_AGGREGATE"];
4
7
  type JunipReviewAggregateData = z.infer<typeof junipProductReviewAggregateLoaderOutputSchema>;
5
8
  type JunipReviewAggregateLoaderProps = {
6
- loaderKey: string;
9
+ loaderKey: LoaderKeyProp<LoaderKey>;
7
10
  productId: string;
8
11
  children: (data: JunipReviewAggregateData) => ReactNode;
9
12
  fallback?: ReactNode;
@@ -11,17 +11,11 @@ function JunipReviewAggregateLoaderInner({
11
11
  }) {
12
12
  const { data } = useSuspenseQuery({
13
13
  queryKey: [loaderKey, { productId }],
14
- queryFn: async () => {
15
- const [loadedData, schemas] = await Promise.all([
16
- invokeLoader({
17
- loaderKey,
18
- args: { productId }
19
- }),
20
- import("./loader-schemas")
21
- ]);
22
- return schemas.junipProductReviewAggregateLoaderOutputSchema.parse(
23
- loadedData
24
- );
14
+ queryFn: () => {
15
+ return invokeLoader({
16
+ loaderKey,
17
+ args: { productId }
18
+ });
25
19
  }
26
20
  });
27
21
  return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
@@ -1,9 +1,12 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { z } from "zod";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
3
5
  import type { junipProductReviewsLoaderOutputSchema } from "./loader-schemas";
6
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["JUNIP_PRODUCT_REVIEWS"];
4
7
  type JunipReviewsData = z.infer<typeof junipProductReviewsLoaderOutputSchema>;
5
8
  type JunipReviewsLoaderProps = {
6
- loaderKey: string;
9
+ loaderKey: LoaderKeyProp<LoaderKey>;
7
10
  productId: string;
8
11
  perPage?: number;
9
12
  children: (data: JunipReviewsData) => ReactNode;
@@ -19,12 +19,8 @@ function JunipReviewsLoaderInner({
19
19
  const args = buildReviewsArgs({ productId, perPage });
20
20
  const { data } = useSuspenseQuery({
21
21
  queryKey: [loaderKey, args],
22
- queryFn: async () => {
23
- const [loadedData, schemas] = await Promise.all([
24
- invokeLoader({ loaderKey, args }),
25
- import("./loader-schemas")
26
- ]);
27
- return schemas.junipProductReviewsLoaderOutputSchema.parse(loadedData);
22
+ queryFn: () => {
23
+ return invokeLoader({ loaderKey, args });
28
24
  }
29
25
  });
30
26
  return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { AddToCartItem } from "../cart/hooks/use-add-to-cart";
3
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
4
+ import type { LoaderKeyProp } from "./loader-outputs";
5
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["SHOPIFY_KACHING_BUNDLES"];
3
6
  type KachingBundlesLoaderProps = {
4
- loaderKey: string;
7
+ loaderKey: LoaderKeyProp<LoaderKey>;
5
8
  productId: string;
6
9
  dealBlockId?: string;
7
10
  country?: string;
@@ -15,12 +15,11 @@ function KachingBundlesLoaderInner({
15
15
  const [lines, setLines] = useState([]);
16
16
  const { data } = useSuspenseQuery({
17
17
  queryKey: [loaderKey, args],
18
- queryFn: async () => {
19
- const [loadedData, schemas] = await Promise.all([
20
- invokeLoader({ loaderKey, args }),
21
- import("./loader-schemas")
22
- ]);
23
- return schemas.shopifyKachingBundlesLoaderOutputSchema.parse(loadedData);
18
+ queryFn: () => {
19
+ return invokeLoader({
20
+ loaderKey,
21
+ args
22
+ });
24
23
  }
25
24
  });
26
25
  if (data.bundles === null) {
@@ -1,7 +1,10 @@
1
1
  import type { ReactNode } from "react";
2
+ import type { DATA_LOADER_KEYS } from "./loader-keys";
3
+ import type { LoaderKeyProp } from "./loader-outputs";
2
4
  import type { LevantaProductFull } from "./loader-schemas";
5
+ type LoaderKey = (typeof DATA_LOADER_KEYS)["LEVANTA_PRODUCT"];
3
6
  type LevantaProductLoaderProps = {
4
- loaderKey: string;
7
+ loaderKey: LoaderKeyProp<LoaderKey>;
5
8
  asin: string;
6
9
  marketplace?: string;
7
10
  children: (data: LevantaProductFull) => ReactNode;
@@ -12,15 +12,11 @@ function LevantaProductLoaderInner({
12
12
  }) {
13
13
  const queryResult = useSuspenseQuery({
14
14
  queryKey: [loaderKey, { asin, marketplace }],
15
- queryFn: async () => {
16
- const [loadedData, schemas] = await Promise.all([
17
- invokeLoader({
18
- loaderKey,
19
- args: { asin, marketplace }
20
- }),
21
- import("./loader-schemas")
22
- ]);
23
- return schemas.levantaProductFullSchema.parse(loadedData);
15
+ queryFn: () => {
16
+ return invokeLoader({
17
+ loaderKey,
18
+ args: { asin, marketplace }
19
+ });
24
20
  }
25
21
  });
26
22
  return /* @__PURE__ */ jsx(Fragment, { children: children(queryResult.data) });
@@ -0,0 +1,28 @@
1
+ import type { DataLoaderKey } from "./loader-keys";
2
+ import * as z from "zod";
3
+ import { CanopyError } from "../lib/canopy-error";
4
+ /**
5
+ * A loader returned data that does not match its output contract. Carries the
6
+ * unvalidated output so a caller that fails soft can still serve it.
7
+ */
8
+ export declare class LoaderOutputError extends CanopyError {
9
+ readonly unvalidatedOutput: unknown;
10
+ constructor({ loaderKey, unvalidatedOutput, zodError, }: {
11
+ loaderKey: DataLoaderKey;
12
+ unvalidatedOutput: unknown;
13
+ zodError: z.ZodError;
14
+ });
15
+ }
16
+ /**
17
+ * Invokes a loader through the canopy-api gateway and validates its output
18
+ * against the loader's contract. The parsed value drops fields the contract
19
+ * does not declare. Output of a loader key the SDK has no contract for passes
20
+ * through unchanged.
21
+ *
22
+ * Malformed output is logged with the loader key and the failing paths, then
23
+ * rejected with a `LoaderOutputError`; callers decide whether to fail soft.
24
+ */
25
+ export declare function fetchValidatedLoaderOutput({ loaderKey, args, }: {
26
+ loaderKey: string;
27
+ args: Record<string, unknown>;
28
+ }): Promise<unknown>;
@@ -0,0 +1,122 @@
1
+ import * as z from "zod";
2
+ import { getCanopyApiGateway } from "../gateways/canopy-api";
3
+ import { CanopyError } from "../lib/canopy-error";
4
+ import {
5
+ okendoReviewAggregateOutputSchema,
6
+ okendoReviewsOutputSchema,
7
+ rebuyRecommendationsOutputSchema,
8
+ smileEarningRulesOutputSchema,
9
+ smileRewardsOutputSchema,
10
+ smileVipTiersOutputSchema
11
+ } from "./integration-loader-schemas";
12
+ import { DATA_LOADER_KEYS } from "./loader-keys";
13
+ import {
14
+ contentfulEntriesLoaderOutputSchema,
15
+ contentfulEntryLoaderOutputSchema,
16
+ fullCollectionSchema,
17
+ fullProductSchema,
18
+ judgemeProductReviewAggregateLoaderOutputSchema,
19
+ judgemeProductReviewsLoaderOutputSchema,
20
+ junipProductReviewAggregateLoaderOutputSchema,
21
+ junipProductReviewsLoaderOutputSchema,
22
+ levantaProductFullSchema,
23
+ looxProductReviewAggregateLoaderOutputSchema,
24
+ looxProductReviewsLoaderOutputSchema,
25
+ loyaltyLionEarningRulesLoaderOutputSchema,
26
+ loyaltyLionPointsForPriceLoaderOutputSchema,
27
+ loyaltyLionRewardsLoaderOutputSchema,
28
+ loyaltyLionTiersLoaderOutputSchema,
29
+ paginatedProductsSchema,
30
+ reviewsIoProductReviewAggregateLoaderOutputSchema,
31
+ reviewsIoProductReviewsLoaderOutputSchema,
32
+ shopifyIntegrationDetailsLoaderOutputSchema,
33
+ shopifyKachingBundlesLoaderOutputSchema,
34
+ shopifyMetaobjectLoaderOutputSchema,
35
+ shopifyMetaobjectsLoaderOutputSchema,
36
+ statsigExperimentLoaderOutputSchema,
37
+ yotpoProductReviewAggregateLoaderOutputSchema,
38
+ yotpoProductReviewsLoaderOutputSchema
39
+ } from "./loader-schemas";
40
+ const LOADER_OUTPUT_SCHEMAS = {
41
+ [DATA_LOADER_KEYS.SHOPIFY_INTEGRATION_DETAILS]: shopifyIntegrationDetailsLoaderOutputSchema,
42
+ [DATA_LOADER_KEYS.SHOPIFY_PRODUCT]: fullProductSchema,
43
+ [DATA_LOADER_KEYS.SHOPIFY_COLLECTION]: fullCollectionSchema,
44
+ [DATA_LOADER_KEYS.SHOPIFY_COLLECTION_PRODUCTS]: paginatedProductsSchema,
45
+ [DATA_LOADER_KEYS.SHOPIFY_METAOBJECT]: shopifyMetaobjectLoaderOutputSchema,
46
+ [DATA_LOADER_KEYS.SHOPIFY_METAOBJECTS]: shopifyMetaobjectsLoaderOutputSchema,
47
+ [DATA_LOADER_KEYS.SHOPIFY_KACHING_BUNDLES]: shopifyKachingBundlesLoaderOutputSchema,
48
+ [DATA_LOADER_KEYS.REPLO_PRODUCT]: fullProductSchema,
49
+ [DATA_LOADER_KEYS.OKENDO_PRODUCT_REVIEWS]: okendoReviewsOutputSchema,
50
+ [DATA_LOADER_KEYS.OKENDO_PRODUCT_REVIEW_AGGREGATE]: okendoReviewAggregateOutputSchema,
51
+ [DATA_LOADER_KEYS.REBUY_RECOMMENDATIONS]: rebuyRecommendationsOutputSchema,
52
+ [DATA_LOADER_KEYS.LEVANTA_PRODUCT]: levantaProductFullSchema,
53
+ [DATA_LOADER_KEYS.SMILE_IO_VIP_TIERS]: smileVipTiersOutputSchema,
54
+ [DATA_LOADER_KEYS.SMILE_IO_EARNING_RULES]: smileEarningRulesOutputSchema,
55
+ [DATA_LOADER_KEYS.SMILE_IO_REWARDS]: smileRewardsOutputSchema,
56
+ [DATA_LOADER_KEYS.LOYALTYLION_EARNING_RULES]: loyaltyLionEarningRulesLoaderOutputSchema,
57
+ [DATA_LOADER_KEYS.LOYALTYLION_REWARDS]: loyaltyLionRewardsLoaderOutputSchema,
58
+ [DATA_LOADER_KEYS.LOYALTYLION_TIERS]: loyaltyLionTiersLoaderOutputSchema,
59
+ [DATA_LOADER_KEYS.LOYALTYLION_POINTS_FOR_PRICE]: loyaltyLionPointsForPriceLoaderOutputSchema,
60
+ [DATA_LOADER_KEYS.YOTPO_PRODUCT_REVIEWS]: yotpoProductReviewsLoaderOutputSchema,
61
+ [DATA_LOADER_KEYS.YOTPO_PRODUCT_REVIEW_AGGREGATE]: yotpoProductReviewAggregateLoaderOutputSchema,
62
+ [DATA_LOADER_KEYS.REVIEWS_IO_PRODUCT_REVIEWS]: reviewsIoProductReviewsLoaderOutputSchema,
63
+ [DATA_LOADER_KEYS.REVIEWS_IO_PRODUCT_REVIEW_AGGREGATE]: reviewsIoProductReviewAggregateLoaderOutputSchema,
64
+ [DATA_LOADER_KEYS.LOOX_PRODUCT_REVIEWS]: looxProductReviewsLoaderOutputSchema,
65
+ [DATA_LOADER_KEYS.LOOX_PRODUCT_REVIEW_AGGREGATE]: looxProductReviewAggregateLoaderOutputSchema,
66
+ [DATA_LOADER_KEYS.JUDGEME_PRODUCT_REVIEWS]: judgemeProductReviewsLoaderOutputSchema,
67
+ [DATA_LOADER_KEYS.JUDGEME_PRODUCT_REVIEW_AGGREGATE]: judgemeProductReviewAggregateLoaderOutputSchema,
68
+ [DATA_LOADER_KEYS.JUNIP_PRODUCT_REVIEWS]: junipProductReviewsLoaderOutputSchema,
69
+ [DATA_LOADER_KEYS.JUNIP_PRODUCT_REVIEW_AGGREGATE]: junipProductReviewAggregateLoaderOutputSchema,
70
+ [DATA_LOADER_KEYS.STATSIG_EXPERIMENT]: statsigExperimentLoaderOutputSchema,
71
+ [DATA_LOADER_KEYS.CONTENTFUL_ENTRY]: contentfulEntryLoaderOutputSchema,
72
+ [DATA_LOADER_KEYS.CONTENTFUL_ENTRIES]: contentfulEntriesLoaderOutputSchema
73
+ };
74
+ const dataLoaderKeySchema = z.enum(DATA_LOADER_KEYS);
75
+ class LoaderOutputError extends CanopyError {
76
+ unvalidatedOutput;
77
+ constructor({
78
+ loaderKey,
79
+ unvalidatedOutput,
80
+ zodError
81
+ }) {
82
+ super({
83
+ message: `Loader ${loaderKey} returned data that does not match its output schema: ${zodError.message}`,
84
+ cause: zodError,
85
+ additionalData: { loaderKey }
86
+ });
87
+ this.unvalidatedOutput = unvalidatedOutput;
88
+ }
89
+ }
90
+ async function fetchValidatedLoaderOutput({
91
+ loaderKey,
92
+ args
93
+ }) {
94
+ const gateway = await getCanopyApiGateway();
95
+ const { data } = await gateway.invokeLoader({ loaderKey, args });
96
+ const knownKey = dataLoaderKeySchema.safeParse(loaderKey);
97
+ if (!knownKey.success) {
98
+ return data;
99
+ }
100
+ const result = LOADER_OUTPUT_SCHEMAS[knownKey.data].safeParse(data);
101
+ if (result.success) {
102
+ return result.data;
103
+ }
104
+ console.error(
105
+ `[Replo] Loader ${knownKey.data} returned data that does not match its output schema`,
106
+ {
107
+ loaderKey: knownKey.data,
108
+ issues: result.error.issues.map((issue) => {
109
+ return { path: issue.path.join("."), message: issue.message };
110
+ })
111
+ }
112
+ );
113
+ throw new LoaderOutputError({
114
+ loaderKey: knownKey.data,
115
+ unvalidatedOutput: data,
116
+ zodError: result.error
117
+ });
118
+ }
119
+ export {
120
+ LoaderOutputError,
121
+ fetchValidatedLoaderOutput
122
+ };