@nosto/nosto-react 1.0.0 → 2.1.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.
@@ -1,7 +1,8 @@
1
- import React, { useEffect, isValidElement, useState, useRef } from "react"
2
- import { NostoContext } from "./context"
3
- import { createRoot, Root } from "react-dom/client"
4
- import { NostoClient, Recommendation } from "../types"
1
+ import { isValidElement } from "react"
2
+ import { NostoContext, RecommendationComponent } from "../context"
3
+ import { useLoadClientScript } from "../hooks"
4
+ import type { ReactElement } from "react"
5
+ import { ScriptLoadOptions } from "../hooks/scriptLoader"
5
6
 
6
7
  /**
7
8
  * @group Components
@@ -19,7 +20,10 @@ export interface NostoProviderProps {
19
20
  * Indicates an url of a server
20
21
  */
21
22
  host?: string
22
- children: React.ReactElement
23
+ /**
24
+ * children
25
+ */
26
+ children: ReactElement | ReactElement[]
23
27
  /**
24
28
  * Indicates if merchant uses multiple currencies
25
29
  */
@@ -27,9 +31,7 @@ export interface NostoProviderProps {
27
31
  /**
28
32
  * Recommendation component which holds nostoRecommendation object
29
33
  */
30
- recommendationComponent?: React.ReactElement<{
31
- nostoRecommendation: Recommendation
32
- }>
34
+ recommendationComponent?: RecommendationComponent
33
35
  /**
34
36
  * Enables Shopify markets with language and market id
35
37
  */
@@ -37,6 +39,14 @@ export interface NostoProviderProps {
37
39
  language?: string
38
40
  marketId?: string | number
39
41
  }
42
+ /**
43
+ * Load nosto script (should be false if loading the script outside of nosto-react)
44
+ */
45
+ loadScript?: boolean
46
+ /**
47
+ * Custom script loader
48
+ */
49
+ scriptLoader?: (scriptSrc: string, options?: ScriptLoadOptions) => Promise<void>
40
50
  }
41
51
 
42
52
  /**
@@ -64,143 +74,23 @@ export default function NostoProvider(props: NostoProviderProps) {
64
74
  const {
65
75
  account,
66
76
  multiCurrency = false,
67
- host,
68
77
  children,
69
78
  recommendationComponent,
70
- shopifyMarkets,
71
79
  } = props
72
- const [clientScriptLoadedState, setClientScriptLoadedState] = React.useState(false)
73
- const clientScriptLoaded = React.useMemo(() => clientScriptLoadedState, [clientScriptLoadedState])
74
80
 
75
81
  // Pass currentVariation as empty string if multiCurrency is disabled
76
82
  const currentVariation = multiCurrency ? props.currentVariation : ""
77
83
 
78
- // Set responseMode for loading campaigns:
79
- const responseMode = isValidElement(recommendationComponent) ? "JSON_ORIGINAL" : "HTML"
80
-
81
- // RecommendationComponent for client-side rendering:
82
- function RecommendationComponentWrapper(props: { nostoRecommendation: Recommendation }) {
83
- return React.cloneElement(recommendationComponent!, {
84
- // eslint-disable-next-line react/prop-types
85
- nostoRecommendation: props.nostoRecommendation,
86
- })
87
- }
88
-
89
- // custom hook for rendering campaigns (CSR/SSR):
90
- const [pageType, setPageType] = useState("")
91
- function useRenderCampaigns(type: string = "") {
92
- const placementRefs = useRef<Record<string, Root>>({})
93
- useEffect(() => {
94
- if (pageType !== type) {
95
- setPageType(type)
96
- }
97
- }, [])
98
-
99
- const pageTypeUpdated = type === pageType
100
-
101
- function renderCampaigns(
102
- data: {
103
- recommendations: Record<string, Recommendation>
104
- campaigns: {
105
- recommendations: Record<string, Recommendation>
106
- }
107
- },
108
- api: NostoClient
109
- ) {
110
- if (responseMode == "HTML") {
111
- // inject content campaigns as usual:
112
- api.placements.injectCampaigns(data.recommendations)
113
- } else {
114
- // render recommendation component into placements:
115
- const recommendations = data.campaigns.recommendations
116
- for (const key in recommendations) {
117
- const recommendation = recommendations[key]
118
- const placementSelector = "#" + key
119
- const placement = () => document.querySelector(placementSelector)
120
-
121
- if (placement()) {
122
- if (!placementRefs.current[key]) placementRefs.current[key] = createRoot(placement()!)
123
- const root = placementRefs.current[key]!
124
- root.render(
125
- <RecommendationComponentWrapper
126
- nostoRecommendation={recommendation}
127
- ></RecommendationComponentWrapper>
128
- )
129
- }
130
- }
131
- }
132
- }
133
- return { renderCampaigns, pageTypeUpdated }
84
+ if (recommendationComponent && !isValidElement(recommendationComponent)) {
85
+ throw new Error(
86
+ "The recommendationComponent prop must be a valid React element. Please provide a valid React element."
87
+ )
134
88
  }
135
89
 
136
- useEffect(() => {
137
- if (!window.nostojs) {
138
- window.nostojs = (cb: (api: NostoClient) => void) => {
139
- (window.nostojs.q = window.nostojs.q || []).push(cb)
140
- }
141
- window.nostojs(api => api.setAutoLoad(false))
142
- }
143
-
144
- if (!document.querySelectorAll("[nosto-client-script]").length && !shopifyMarkets) {
145
- const script = document.createElement("script")
146
- script.type = "text/javascript"
147
- script.src = "//" + (host || "connect.nosto.com") + "/include/" + account
148
- script.async = true
149
- script.setAttribute("nosto-client-script", "")
150
-
151
- script.onload = () => {
152
- if (typeof jest !== "undefined") {
153
- window.nosto?.reload({
154
- site: "localhost",
155
- })
156
- }
157
- setClientScriptLoadedState(true)
158
- }
159
- document.body.appendChild(script)
160
- }
161
-
162
- // Enable Shopify markets functionality:
163
- if (shopifyMarkets) {
164
- const existingScript = document.querySelector("[nosto-client-script]")
165
- const nostoSandbox = document.querySelector("#nosto-sandbox")
166
-
167
- if (
168
- !existingScript ||
169
- existingScript?.getAttribute("nosto-language") !== shopifyMarkets?.language ||
170
- existingScript?.getAttribute("nosto-market-id") !== shopifyMarkets?.marketId
171
- ) {
172
- if (clientScriptLoadedState) {
173
- setClientScriptLoadedState(false)
174
- }
175
-
176
- existingScript?.parentNode?.removeChild(existingScript)
177
- nostoSandbox?.parentNode?.removeChild(nostoSandbox)
178
-
179
- const script = document.createElement("script")
180
- script.type = "text/javascript"
181
- script.src =
182
- "//" +
183
- (host || "connect.nosto.com") +
184
- `/script/shopify/market/nosto.js?merchant=${account}&market=${
185
- shopifyMarkets.marketId || ""
186
- }&locale=${shopifyMarkets?.language?.toLowerCase() || ""}`
187
- script.async = true
188
- script.setAttribute("nosto-client-script", "")
189
- script.setAttribute("nosto-language", shopifyMarkets?.language || "")
190
- script.setAttribute("nosto-market-id", String(shopifyMarkets?.marketId))
90
+ // Set responseMode for loading campaigns:
91
+ const responseMode = recommendationComponent ? "JSON_ORIGINAL" : "HTML"
191
92
 
192
- script.onload = () => {
193
- if (typeof jest !== "undefined") {
194
- window.nosto?.reload({
195
- site: "localhost",
196
- })
197
- }
198
- setClientScriptLoadedState(true)
199
- }
200
- document.body.appendChild(script)
201
- }
202
- }
203
- }, [clientScriptLoadedState, shopifyMarkets])
93
+ const { clientScriptLoaded } = useLoadClientScript(props)
204
94
 
205
95
  return (
206
96
  <NostoContext.Provider
@@ -209,9 +99,7 @@ export default function NostoProvider(props: NostoProviderProps) {
209
99
  clientScriptLoaded,
210
100
  currentVariation,
211
101
  responseMode,
212
- recommendationComponent,
213
- useRenderCampaigns,
214
- pageType,
102
+ recommendationComponent
215
103
  }}
216
104
  >
217
105
  {children}
@@ -1,5 +1,4 @@
1
- import { useNostoContext } from "./context"
2
- import { useNostoApi } from "../utils/hooks"
1
+ import { useRenderCampaigns, useNostoApi } from "../hooks"
3
2
 
4
3
  /**
5
4
  * You can personalise your search pages by using the NostoSearch component.
@@ -26,9 +25,7 @@ import { useNostoApi } from "../utils/hooks"
26
25
  */
27
26
  export default function NostoSearch(props: { query: string; placements?: string[] }) {
28
27
  const { query, placements } = props
29
- const { recommendationComponent, useRenderCampaigns } = useNostoContext()
30
-
31
- const { renderCampaigns, pageTypeUpdated } = useRenderCampaigns("search")
28
+ const { renderCampaigns } = useRenderCampaigns()
32
29
 
33
30
  useNostoApi(
34
31
  async (api) => {
@@ -36,9 +33,9 @@ export default function NostoSearch(props: { query: string; placements?: string[
36
33
  .viewSearch(query)
37
34
  .setPlacements(placements || api.placements.getPlacements())
38
35
  .load()
39
- renderCampaigns(data, api)
36
+ renderCampaigns(data)
40
37
  },
41
- [query, recommendationComponent, pageTypeUpdated]
38
+ [query]
42
39
  )
43
40
  return null
44
41
  }
@@ -1,7 +1,6 @@
1
- import { useNostoContext } from "./context"
1
+ import { useNostoContext, useDeepCompareEffect } from "../hooks"
2
2
  import { Cart, Customer } from "../types"
3
3
  import { snakeize } from "../utils/snakeize"
4
- import { useDeepCompareEffect } from "../utils/hooks"
5
4
 
6
5
  /**
7
6
  * Nosto React requires that you pass it the details of current cart contents and the details of the currently logged-in customer, if any, on every route change.
@@ -0,0 +1,3 @@
1
+ export function isNostoLoaded() {
2
+ return typeof window.nosto !== "undefined"
3
+ }
@@ -7,8 +7,5 @@ export { default as NostoSearch } from "./NostoSearch"
7
7
  export { default as NostoOrder } from "./NostoOrder"
8
8
  export { default as NostoHome } from "./NostoHome"
9
9
  export { default as NostoPlacement } from "./NostoPlacement"
10
- export { default as NostoProvider } from "./NostoProvider"
11
-
12
- export { NostoContext, useNostoContext } from "./context"
13
- export type { NostoContextType } from "./context"
10
+ export { default as NostoProvider, type NostoProviderProps } from "./NostoProvider"
14
11
  export { default as NostoSession } from "./NostoSession"
package/src/context.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { createContext, ReactElement } from "react"
2
+ import { Recommendation, RenderMode } from "./types"
3
+
4
+ type AnyFunction = (...args: unknown[]) => unknown
5
+
6
+ export type RecommendationComponent = ReactElement<{
7
+ nostoRecommendation: Recommendation
8
+ }>
9
+
10
+ /**
11
+ * @group Types
12
+ */
13
+ export interface NostoContextType {
14
+ account: string
15
+ clientScriptLoaded: boolean
16
+ currentVariation?: string
17
+ renderFunction?: AnyFunction
18
+ responseMode: RenderMode
19
+ recommendationComponent?: RecommendationComponent
20
+ }
21
+
22
+ /**
23
+ * @group Essential Functions
24
+ */
25
+ export const NostoContext = createContext<NostoContextType>({
26
+ account: "",
27
+ currentVariation: "",
28
+ responseMode: "HTML",
29
+ clientScriptLoaded: false
30
+ })
31
+
@@ -0,0 +1,5 @@
1
+ export { useDeepCompareEffect } from "./useDeepCompareEffect"
2
+ export { useNostoApi } from "./useNostoApi"
3
+ export { useNostoContext } from "./useNostoContext"
4
+ export { useRenderCampaigns } from "./useRenderCampaigns"
5
+ export { useLoadClientScript } from "./useLoadClientScript"
@@ -0,0 +1,30 @@
1
+ export default function scriptLoader(scriptSrc: string, options?: ScriptLoadOptions): Promise<void> {
2
+ return new Promise((resolve, reject) => {
3
+ const script = document.createElement("script")
4
+ script.type = "text/javascript"
5
+ script.src = scriptSrc
6
+ script.async = true
7
+ script.onload = () => resolve()
8
+ script.onerror = () => reject()
9
+ Object.entries(options?.attributes ?? {}).forEach(([k, v]) => script.setAttribute(k, v))
10
+ if (options?.position === "head") {
11
+ document.head.appendChild(script)
12
+ } else {
13
+ document.body.appendChild(script)
14
+ }
15
+ })
16
+ }
17
+
18
+ /**
19
+ * @group Types
20
+ */
21
+ export type ScriptLoadOptions = {
22
+ /**
23
+ * Indicates the position of the script, default is "body"
24
+ */
25
+ position?: "head" | "body"
26
+ /**
27
+ * Indicates the attributes of the script element
28
+ */
29
+ attributes?: Record<string, string>
30
+ }
@@ -0,0 +1,21 @@
1
+ import { useEffect, useRef, useMemo } from "react"
2
+ import { deepCompare } from "../utils/compare"
3
+
4
+ export function useDeepCompareEffect(
5
+ callback: Parameters<typeof useEffect>[0],
6
+ dependencies: Parameters<typeof useEffect>[1]
7
+ ): ReturnType<typeof useEffect> {
8
+ return useEffect(callback, useDeepCompareMemoize(dependencies))
9
+ }
10
+
11
+ function useDeepCompareMemoize<T>(value: T) {
12
+ const ref = useRef<T>(value);
13
+ const signalRef = useRef<number>(0)
14
+
15
+ if (!deepCompare(value, ref.current)) {
16
+ ref.current = value
17
+ signalRef.current += 1
18
+ }
19
+
20
+ return useMemo(() => ref.current, [signalRef.current])
21
+ }
@@ -0,0 +1,84 @@
1
+ import { useState, useEffect } from "react"
2
+ import { isNostoLoaded } from "../components/helpers"
3
+ import type { NostoClient } from "../types"
4
+ import type { NostoProviderProps } from "../components/NostoProvider"
5
+ import scriptLoaderFn from "./scriptLoader"
6
+
7
+ type NostoScriptProps = Pick<NostoProviderProps, "account" | "host" | "shopifyMarkets" | "loadScript" | "scriptLoader">
8
+
9
+ export function useLoadClientScript(props: NostoScriptProps) {
10
+ const { host = "connect.nosto.com", scriptLoader = scriptLoaderFn, account, shopifyMarkets, loadScript = true } = props
11
+ const [clientScriptLoaded, setClientScriptLoaded] = useState(false)
12
+
13
+ useEffect(() => {
14
+ function scriptOnload() {
15
+ // Override for production scripts to work in unit tests
16
+ if ("nostoReactTest" in window) {
17
+ window.nosto?.reload({
18
+ site: "localhost"
19
+ })
20
+ }
21
+ setClientScriptLoaded(true)
22
+ }
23
+
24
+ // Create and append script element
25
+ async function injectScriptElement(urlPartial: string, extraAttributes: Record<string, string> = {}) {
26
+ const scriptSrc = `//${host}${urlPartial}`
27
+ const attributes = { "nosto-client-script": "", ...extraAttributes }
28
+ await scriptLoader(scriptSrc, { attributes })
29
+ scriptOnload()
30
+ }
31
+
32
+ function prepareShopifyMarketsScript() {
33
+ const existingScript = document.querySelector("[nosto-client-script]")
34
+
35
+ const marketId = String(shopifyMarkets?.marketId || "")
36
+ const language = shopifyMarkets?.language || ""
37
+
38
+ const attributeMismatch =
39
+ existingScript?.getAttribute("nosto-language") !== language ||
40
+ existingScript?.getAttribute("nosto-market-id") !== marketId
41
+
42
+ if (!existingScript || attributeMismatch) {
43
+ if (clientScriptLoaded) {
44
+ setClientScriptLoaded(false)
45
+ }
46
+
47
+ const nostoSandbox = document.querySelector("#nosto-sandbox")
48
+
49
+ existingScript?.parentNode?.removeChild(existingScript)
50
+ nostoSandbox?.parentNode?.removeChild(nostoSandbox)
51
+
52
+ const urlPartial =
53
+ `/script/shopify/market/nosto.js?merchant=${account}&market=${marketId}&locale=${language.toLowerCase()}`
54
+ injectScriptElement(urlPartial, { "nosto-language": language, "nosto-market-id": marketId })
55
+ }
56
+ }
57
+
58
+ // Load Nosto API stub
59
+ if (!window.nostojs) {
60
+ window.nostojs = (cb: (api: NostoClient) => void) => {
61
+ (window.nostojs.q = window.nostojs.q || []).push(cb)
62
+ }
63
+ window.nostojs(api => api.setAutoLoad(false))
64
+ }
65
+
66
+ if (!loadScript) {
67
+ window.nosto ? scriptOnload() : window.nostojs(scriptOnload)
68
+ return
69
+ }
70
+
71
+ // Load Nosto client script if not already loaded externally
72
+ if (!isNostoLoaded() && !shopifyMarkets) {
73
+ const urlPartial = `/include/${account}`
74
+ injectScriptElement(urlPartial)
75
+ }
76
+
77
+ // Load Shopify Markets scripts
78
+ if (shopifyMarkets) {
79
+ prepareShopifyMarketsScript()
80
+ }
81
+ }, [shopifyMarkets?.marketId, shopifyMarkets?.language])
82
+
83
+ return { clientScriptLoaded }
84
+ }
@@ -0,0 +1,22 @@
1
+ import { DependencyList, useEffect } from "react"
2
+ import { useNostoContext } from "./useNostoContext"
3
+ import { NostoClient } from "../types"
4
+ import { useDeepCompareEffect } from "./useDeepCompareEffect"
5
+
6
+ export function useNostoApi(
7
+ cb: (api: NostoClient) => void,
8
+ deps?: DependencyList,
9
+ flags?: { deep?: boolean }
10
+ ): void {
11
+ const { clientScriptLoaded, currentVariation, responseMode } = useNostoContext()
12
+ const useEffectFn = flags?.deep ? useDeepCompareEffect : useEffect
13
+
14
+ useEffectFn(() => {
15
+ if (clientScriptLoaded) {
16
+ window.nostojs(api => {
17
+ api.defaultSession().setVariation(currentVariation!).setResponseMode(responseMode)
18
+ cb(api)
19
+ })
20
+ }
21
+ }, [clientScriptLoaded, currentVariation, responseMode, ...(deps ?? [])])
22
+ }
@@ -0,0 +1,12 @@
1
+ import { useContext } from "react"
2
+ import { NostoContext, NostoContextType } from "../context"
3
+
4
+ /**
5
+ * A hook that allows you to access the NostoContext and retrieve Nosto-related data from it in React components.
6
+ *
7
+ * @group Essential Functions
8
+ */
9
+ export function useNostoContext(): NostoContextType {
10
+ return useContext(NostoContext)
11
+ }
12
+
@@ -0,0 +1,59 @@
1
+ import { cloneElement, useRef } from "react"
2
+ import { createRoot, Root } from "react-dom/client"
3
+ import { ActionResponse, Recommendation } from "../types"
4
+ import { useNostoContext } from "./useNostoContext"
5
+ import { RecommendationComponent } from "../context"
6
+
7
+ // RecommendationComponent for client-side rendering:
8
+ function RecommendationComponentWrapper(props: {
9
+ recommendationComponent: RecommendationComponent,
10
+ nostoRecommendation: Recommendation }) {
11
+
12
+ return cloneElement(props.recommendationComponent, {
13
+ // eslint-disable-next-line react/prop-types
14
+ nostoRecommendation: props.nostoRecommendation,
15
+ })
16
+ }
17
+
18
+ function injectCampaigns(data: ActionResponse) {
19
+ if (!window.nostojs) {
20
+ throw new Error("Nosto has not yet been initialized")
21
+ }
22
+ window.nostojs(api => {
23
+ api.placements.injectCampaigns(data.recommendations)
24
+ })
25
+ }
26
+
27
+ export function useRenderCampaigns() {
28
+ const { responseMode, recommendationComponent } = useNostoContext()
29
+ const placementRefs = useRef<Record<string, Root>>({})
30
+
31
+ if (responseMode == "HTML") {
32
+ return { renderCampaigns: injectCampaigns }
33
+ }
34
+
35
+ function renderCampaigns(data: ActionResponse) {
36
+ // render recommendation component into placements:
37
+ const recommendations = data.campaigns?.recommendations ?? {}
38
+ for (const key in recommendations) {
39
+ const recommendation = recommendations[key] as Recommendation
40
+ const placementSelector = "#" + key
41
+ const placementElement = document.querySelector(placementSelector)
42
+
43
+ if (placementElement) {
44
+ if (!placementRefs.current[key]) {
45
+ placementRefs.current[key] = createRoot(placementElement)
46
+ }
47
+ const root = placementRefs.current[key]!
48
+ root.render(
49
+ <RecommendationComponentWrapper
50
+ recommendationComponent={recommendationComponent!}
51
+ nostoRecommendation={recommendation}
52
+ ></RecommendationComponentWrapper>
53
+ )
54
+ }
55
+ }
56
+ }
57
+
58
+ return { renderCampaigns }
59
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,5 @@
1
1
  export type { Cart, Customer, Product, Order, Recommendation } from "./types"
2
2
  export * from "./components"
3
+ export { type ScriptLoadOptions } from "./hooks/scriptLoader"
4
+ export { NostoContext, type NostoContextType } from "./context"
5
+ export { useNostoContext } from "./hooks/useNostoContext"
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ declare global {
17
17
  export interface NostoClient {
18
18
  setAutoLoad(autoload: boolean): void
19
19
  defaultSession(): Session
20
+ listen(event: string, callback: (data: unknown) => void): void
20
21
  placements: {
21
22
  getPlacements(): string[]
22
23
  injectCampaigns(recommendations: Record<string, unknown>): void
@@ -28,7 +29,7 @@ export interface NostoClient {
28
29
  */
29
30
  export interface Recommendation {
30
31
  result_id: string
31
- products: Product[]
32
+ products: PushedProduct[]
32
33
  result_type: string
33
34
  title: string
34
35
  div_id: string
@@ -36,6 +37,77 @@ export interface Recommendation {
36
37
  params: unknown
37
38
  }
38
39
 
40
+ export interface PushedProduct {
41
+ age_group?: string
42
+ alternate_image_urls: string[]
43
+ availability: string
44
+ brand?: string
45
+ category: string[]
46
+ category_id: string[]
47
+ condition?: string
48
+ custom_fields: { [index: string]: string }
49
+ date_published?: Date
50
+ description?: string
51
+ gender?: string
52
+ google_category?: string
53
+ gtin?: string
54
+ image_url?: string
55
+ inventory_level?: number
56
+ list_price?: number
57
+ name: string
58
+ parent_category_id: string[]
59
+ price: number
60
+ price_currency_code: string
61
+ product_id: string
62
+ rating_value?: number
63
+ review_count?: number
64
+ skus: PushedProductSKU[]
65
+ source_updated?: Date
66
+ supplier_cost?: number
67
+ tags1: string[]
68
+ tags2: string[]
69
+ tags3: string[]
70
+ thumb_url?: string
71
+ unit_pricing_base_measure?: number
72
+ unit_pricing_measure?: number
73
+ unit_pricing_unit?: string
74
+ update_received?: Date
75
+ url: string
76
+ variation_id?: string
77
+ variations: { [index: string]: PushedVariation }
78
+ }
79
+
80
+ export interface PushedProductSKU extends NostoSku { }
81
+
82
+ export interface PushedVariation extends NostoVariant { }
83
+
84
+
85
+ export interface NostoSku extends Sku {
86
+ inventory_level?: number
87
+ }
88
+
89
+ export interface NostoVariant {
90
+ availability: string
91
+ available: boolean
92
+ discounted: boolean
93
+ list_price?: number
94
+ price: number
95
+ price_currency_code: string
96
+ price_text?: string
97
+ }
98
+
99
+ export interface Sku {
100
+ availability: string
101
+ custom_fields: { [index: string]: string }
102
+ gtin?: string
103
+ id: string
104
+ image_url?: string
105
+ list_price?: number
106
+ name: string
107
+ price: number
108
+ url?: string
109
+ }
110
+
39
111
  // copied from client script d.ts export
40
112
  declare const eventTypes: readonly ["vp", "lp", "dp", "rp", "bp", "vc", "or", "is", "cp", "ec", "es", "gc", "src", "cpr", "pl", "cc", "con"]
41
113
  declare type EventType = typeof eventTypes[number]