2
0

(webhook) Add client execution option

This commit is contained in:
Baptiste Arnaud
2023-05-26 09:20:22 +02:00
parent 084a17ffc8
commit 75f9da0a4f
23 changed files with 426 additions and 306 deletions

View File

@@ -30,6 +30,7 @@ import { getDeepKeys } from '../helpers/getDeepKeys'
import { QueryParamsInputs, HeadersInputs } from './KeyValueInputs' import { QueryParamsInputs, HeadersInputs } from './KeyValueInputs'
import { DataVariableInputs } from './ResponseMappingInputs' import { DataVariableInputs } from './ResponseMappingInputs'
import { VariableForTestInputs } from './VariableForTestInputs' import { VariableForTestInputs } from './VariableForTestInputs'
import { SwitchWithRelatedSettings } from '@/components/SwitchWithRelatedSettings'
type Props = { type Props = {
blockId: string blockId: string
@@ -52,32 +53,31 @@ export const WebhookAdvancedConfigForm = ({
const [responseKeys, setResponseKeys] = useState<string[]>([]) const [responseKeys, setResponseKeys] = useState<string[]>([])
const { showToast } = useToast() const { showToast } = useToast()
const handleMethodChange = (method: HttpMethod) => const updateMethod = (method: HttpMethod) =>
onWebhookChange({ ...webhook, method }) onWebhookChange({ ...webhook, method })
const handleQueryParamsChange = (queryParams: KeyValue[]) => const updateQueryParams = (queryParams: KeyValue[]) =>
onWebhookChange({ ...webhook, queryParams }) onWebhookChange({ ...webhook, queryParams })
const handleHeadersChange = (headers: KeyValue[]) => const updateHeaders = (headers: KeyValue[]) =>
onWebhookChange({ ...webhook, headers }) onWebhookChange({ ...webhook, headers })
const handleBodyChange = (body: string) => const updateBody = (body: string) => onWebhookChange({ ...webhook, body })
onWebhookChange({ ...webhook, body })
const handleVariablesChange = (variablesForTest: VariableForTest[]) => const updateVariablesForTest = (variablesForTest: VariableForTest[]) =>
onOptionsChange({ ...options, variablesForTest }) onOptionsChange({ ...options, variablesForTest })
const handleResponseMappingChange = ( const updateResponseVariableMapping = (
responseVariableMapping: ResponseVariableMapping[] responseVariableMapping: ResponseVariableMapping[]
) => onOptionsChange({ ...options, responseVariableMapping }) ) => onOptionsChange({ ...options, responseVariableMapping })
const handleAdvancedConfigChange = (isAdvancedConfig: boolean) => const updateAdvancedConfig = (isAdvancedConfig: boolean) =>
onOptionsChange({ ...options, isAdvancedConfig }) onOptionsChange({ ...options, isAdvancedConfig })
const handleBodyFormStateChange = (isCustomBody: boolean) => const updateIsCustomBody = (isCustomBody: boolean) =>
onOptionsChange({ ...options, isCustomBody }) onOptionsChange({ ...options, isCustomBody })
const handleTestRequestClick = async () => { const executeTestRequest = async () => {
if (!typebot || !webhook) return if (!typebot || !webhook) return
setIsTestResponseLoading(true) setIsTestResponseLoading(true)
await Promise.all([updateWebhook(webhook.id, webhook), save()]) await Promise.all([updateWebhook(webhook.id, webhook), save()])
@@ -96,6 +96,9 @@ export const WebhookAdvancedConfigForm = ({
setIsTestResponseLoading(false) setIsTestResponseLoading(false)
} }
const updateIsExecutedOnClient = (isExecutedOnClient: boolean) =>
onOptionsChange({ ...options, isExecutedOnClient })
const ResponseMappingInputs = useMemo( const ResponseMappingInputs = useMemo(
() => () =>
function Component(props: TableListItemProps<ResponseVariableMapping>) { function Component(props: TableListItemProps<ResponseVariableMapping>) {
@@ -106,18 +109,22 @@ export const WebhookAdvancedConfigForm = ({
return ( return (
<> <>
<SwitchWithLabel <SwitchWithRelatedSettings
label="Advanced configuration" label="Advanced configuration"
initialValue={options.isAdvancedConfig ?? true} initialValue={options.isAdvancedConfig ?? true}
onCheckChange={handleAdvancedConfigChange} onCheckChange={updateAdvancedConfig}
>
<SwitchWithLabel
label="Execute on client"
moreInfoContent="If enabled, the webhook will be executed on the client. It means it will be executed in the browser of your visitor. Make sure to enable CORS and do not expose sensitive data."
initialValue={options.isExecutedOnClient ?? false}
onCheckChange={updateIsExecutedOnClient}
/> />
{(options.isAdvancedConfig ?? true) && (
<>
<HStack justify="space-between"> <HStack justify="space-between">
<Text>Method:</Text> <Text>Method:</Text>
<DropdownList <DropdownList
currentItem={webhook.method as HttpMethod} currentItem={webhook.method as HttpMethod}
onItemSelect={handleMethodChange} onItemSelect={updateMethod}
items={Object.values(HttpMethod)} items={Object.values(HttpMethod)}
/> />
</HStack> </HStack>
@@ -130,7 +137,7 @@ export const WebhookAdvancedConfigForm = ({
<AccordionPanel pt="4"> <AccordionPanel pt="4">
<TableList<KeyValue> <TableList<KeyValue>
initialItems={webhook.queryParams} initialItems={webhook.queryParams}
onItemsChange={handleQueryParamsChange} onItemsChange={updateQueryParams}
Item={QueryParamsInputs} Item={QueryParamsInputs}
addLabel="Add a param" addLabel="Add a param"
/> />
@@ -144,7 +151,7 @@ export const WebhookAdvancedConfigForm = ({
<AccordionPanel pt="4"> <AccordionPanel pt="4">
<TableList<KeyValue> <TableList<KeyValue>
initialItems={webhook.headers} initialItems={webhook.headers}
onItemsChange={handleHeadersChange} onItemsChange={updateHeaders}
Item={HeadersInputs} Item={HeadersInputs}
addLabel="Add a value" addLabel="Add a value"
/> />
@@ -159,13 +166,13 @@ export const WebhookAdvancedConfigForm = ({
<SwitchWithLabel <SwitchWithLabel
label="Custom body" label="Custom body"
initialValue={options.isCustomBody ?? true} initialValue={options.isCustomBody ?? true}
onCheckChange={handleBodyFormStateChange} onCheckChange={updateIsCustomBody}
/> />
{(options.isCustomBody ?? true) && ( {(options.isCustomBody ?? true) && (
<CodeEditor <CodeEditor
defaultValue={webhook.body ?? ''} defaultValue={webhook.body ?? ''}
lang="json" lang="json"
onChange={handleBodyChange} onChange={updateBody}
debounceTimeout={0} debounceTimeout={0}
/> />
)} )}
@@ -181,18 +188,17 @@ export const WebhookAdvancedConfigForm = ({
initialItems={ initialItems={
options?.variablesForTest ?? { byId: {}, allIds: [] } options?.variablesForTest ?? { byId: {}, allIds: [] }
} }
onItemsChange={handleVariablesChange} onItemsChange={updateVariablesForTest}
Item={VariableForTestInputs} Item={VariableForTestInputs}
addLabel="Add an entry" addLabel="Add an entry"
/> />
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
</> </SwitchWithRelatedSettings>
)}
{webhook.url && ( {webhook.url && (
<Button <Button
onClick={handleTestRequestClick} onClick={executeTestRequest}
colorScheme="blue" colorScheme="blue"
isLoading={isTestResponseLoading} isLoading={isTestResponseLoading}
> >
@@ -211,7 +217,7 @@ export const WebhookAdvancedConfigForm = ({
<AccordionPanel pt="4"> <AccordionPanel pt="4">
<TableList<ResponseVariableMapping> <TableList<ResponseVariableMapping>
initialItems={options.responseVariableMapping} initialItems={options.responseVariableMapping}
onItemsChange={handleResponseMappingChange} onItemsChange={updateResponseVariableMapping}
Item={ResponseMappingInputs} Item={ResponseMappingInputs}
addLabel="Add an entry" addLabel="Add an entry"
/> />

View File

@@ -71,8 +71,7 @@ if (window.$chatwoot) {
export const executeChatwootBlock = ( export const executeChatwootBlock = (
{ typebot, result }: SessionState, { typebot, result }: SessionState,
block: ChatwootBlock, block: ChatwootBlock
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
const chatwootCode = const chatwootCode =
block.options.task === 'Close widget' block.options.task === 'Close widget'
@@ -88,7 +87,6 @@ export const executeChatwootBlock = (
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
clientSideActions: [ clientSideActions: [
{ {
lastBubbleBlockId,
chatwoot: { chatwoot: {
scriptToExecute: { scriptToExecute: {
content: parseVariables(typebot.variables, { fieldToParse: 'id' })( content: parseVariables(typebot.variables, { fieldToParse: 'id' })(

View File

@@ -5,8 +5,7 @@ import { GoogleAnalyticsBlock, SessionState } from '@typebot.io/schemas'
export const executeGoogleAnalyticsBlock = ( export const executeGoogleAnalyticsBlock = (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: GoogleAnalyticsBlock, block: GoogleAnalyticsBlock
lastBubbleBlockId?: string
): ExecuteIntegrationResponse => { ): ExecuteIntegrationResponse => {
const googleAnalytics = deepParseVariables(variables)(block.options) const googleAnalytics = deepParseVariables(variables)(block.options)
return { return {
@@ -19,7 +18,6 @@ export const executeGoogleAnalyticsBlock = (
? Number(googleAnalytics.value) ? Number(googleAnalytics.value)
: undefined, : undefined,
}, },
lastBubbleBlockId,
}, },
], ],
} }

View File

@@ -4,7 +4,6 @@ import {
ZapierBlock, ZapierBlock,
MakeComBlock, MakeComBlock,
PabblyConnectBlock, PabblyConnectBlock,
VariableWithUnknowValue,
SessionState, SessionState,
Webhook, Webhook,
Typebot, Typebot,
@@ -17,17 +16,23 @@ import {
KeyValue, KeyValue,
ReplyLog, ReplyLog,
ResultInSession, ResultInSession,
ExecutableWebhook,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { stringify } from 'qs' import { stringify } from 'qs'
import { byId, omit } from '@typebot.io/lib' import { omit } from '@typebot.io/lib'
import { parseAnswers } from '@typebot.io/lib/results' import { parseAnswers } from '@typebot.io/lib/results'
import got, { Method, Headers, HTTPError } from 'got' import got, { Method, HTTPError, OptionsInit } from 'got'
import { parseSampleResult } from './parseSampleResult' import { parseSampleResult } from './parseSampleResult'
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog' import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog' import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { updateVariables } from '@/features/variables/updateVariables'
import { parseVariables } from '@/features/variables/parseVariables' import { parseVariables } from '@/features/variables/parseVariables'
import { resumeWebhookExecution } from './resumeWebhookExecution'
type ParsedWebhook = ExecutableWebhook & {
basicAuth: { username?: string; password?: string }
isJson: boolean
}
export const executeWebhookBlock = async ( export const executeWebhookBlock = async (
state: SessionState, state: SessionState,
@@ -51,70 +56,34 @@ export const executeWebhookBlock = async (
return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] } return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
} }
const preparedWebhook = prepareWebhookAttributes(webhook, block.options) const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const webhookResponse = await executeWebhook({ typebot })( const parsedWebhook = await parseWebhookAttributes(
preparedWebhook, typebot,
typebot.variables,
block.groupId, block.groupId,
result result
) )(preparedWebhook)
const status = webhookResponse.statusCode.toString() if (!parsedWebhook) {
const isError = status.startsWith('4') || status.startsWith('5')
if (isError) {
log = { log = {
status: 'error', status: 'error',
description: `Webhook returned error: ${webhookResponse.data}`, description: `Couldn't parse webhook attributes`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
} }
result && result &&
(await saveErrorLog({ (await saveErrorLog({
resultId: result.id, resultId: result.id,
message: log.description, message: log.description,
details: log.details,
})) }))
} else { return { outgoingEdgeId: block.outgoingEdgeId, logs: [log] }
log = {
status: 'success',
description: `Webhook executed successfully!`,
details: JSON.stringify(webhookResponse.data, null, 2).substring(0, 1000),
} }
result && if (block.options.isExecutedOnClient)
(await saveSuccessLog({
resultId: result.id,
message: log.description,
details: JSON.stringify(webhookResponse.data, null, 2).substring(
0,
1000
),
}))
}
const newVariables = block.options.responseVariableMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = typebot.variables.find(byId(varMapping.variableId))
if (!existingVariable) return newVariables
const func = Function(
'data',
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
)
try {
const value: unknown = func(webhookResponse)
return [...newVariables, { ...existingVariable, value }]
} catch (err) {
return newVariables
}
}, [])
if (newVariables.length > 0) {
const newSessionState = await updateVariables(state)(newVariables)
return { return {
outgoingEdgeId: block.outgoingEdgeId, outgoingEdgeId: block.outgoingEdgeId,
newSessionState, clientSideActions: [
{
webhookToExecute: parsedWebhook,
},
],
} }
} const webhookResponse = await executeWebhook(parsedWebhook, result)
return resumeWebhookExecution(state, block)(webhookResponse)
return { outgoingEdgeId: block.outgoingEdgeId, logs: log ? [log] : undefined }
} }
const prepareWebhookAttributes = ( const prepareWebhookAttributes = (
@@ -131,19 +100,15 @@ const prepareWebhookAttributes = (
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body) const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
export const executeWebhook = const parseWebhookAttributes =
({ typebot }: Pick<SessionState, 'typebot'>) => (
async ( typebot: SessionState['typebot'],
webhook: Webhook,
variables: Variable[],
groupId: string, groupId: string,
result: ResultInSession result: ResultInSession
): Promise<WebhookResponse> => { ) =>
if (!webhook.url || !webhook.method) async (webhook: Webhook): Promise<ParsedWebhook | undefined> => {
return { if (!webhook.url || !webhook.method) return
statusCode: 400, const { variables } = typebot
data: { message: `Webhook doesn't have url or method` },
}
const basicAuth: { username?: string; password?: string } = {} const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers.findIndex( const basicAuthHeaderIdx = webhook.headers.findIndex(
(h) => (h) =>
@@ -161,13 +126,11 @@ export const executeWebhook =
webhook.headers.splice(basicAuthHeaderIdx, 1) webhook.headers.splice(basicAuthHeaderIdx, 1)
} }
const headers = convertKeyValueTableToObject(webhook.headers, variables) as const headers = convertKeyValueTableToObject(webhook.headers, variables) as
| Headers | ExecutableWebhook['headers']
| undefined | undefined
const queryParams = stringify( const queryParams = stringify(
convertKeyValueTableToObject(webhook.queryParams, variables) convertKeyValueTableToObject(webhook.queryParams, variables)
) )
const contentType = headers ? headers['Content-Type'] : undefined
const bodyContent = await getBodyContent( const bodyContent = await getBodyContent(
typebot, typebot,
[] []
@@ -186,23 +149,38 @@ export const executeWebhook =
) )
: { data: undefined, isJson: false } : { data: undefined, isJson: false }
const request = { return {
url: parseVariables(variables)( url: parseVariables(variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '') webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
), ),
method: webhook.method as Method, basicAuth,
method: webhook.method,
headers, headers,
...basicAuth, body,
isJson,
}
}
export const executeWebhook = async (
webhook: ParsedWebhook,
result: ResultInSession
): Promise<WebhookResponse> => {
const { headers, url, method, basicAuth, body, isJson } = webhook
const contentType = headers ? headers['Content-Type'] : undefined
const request = {
url,
method: method as Method,
headers,
...(basicAuth ?? {}),
json: json:
!contentType?.includes('x-www-form-urlencoded') && body && isJson !contentType?.includes('x-www-form-urlencoded') && body && isJson
? body ? body
: undefined, : undefined,
form: form:
contentType?.includes('x-www-form-urlencoded') && body contentType?.includes('x-www-form-urlencoded') && body ? body : undefined,
? body body: body && !isJson ? (body as string) : undefined,
: undefined, } satisfies OptionsInit
body: body && !isJson ? body : undefined,
}
try { try {
const response = await got(request.url, omit(request, 'url')) const response = await got(request.url, omit(request, 'url'))
await saveSuccessLog({ await saveSuccessLog({

View File

@@ -0,0 +1,87 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/saveErrorLog'
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
import { parseVariables } from '@/features/variables/parseVariables'
import { updateVariables } from '@/features/variables/updateVariables'
import { byId } from '@typebot.io/lib'
import {
MakeComBlock,
PabblyConnectBlock,
VariableWithUnknowValue,
WebhookBlock,
ZapierBlock,
} from '@typebot.io/schemas'
import { ReplyLog, SessionState } from '@typebot.io/schemas/features/chat'
export const resumeWebhookExecution =
(
state: SessionState,
block: WebhookBlock | ZapierBlock | MakeComBlock | PabblyConnectBlock
) =>
async (response: {
statusCode: number
data?: unknown
}): Promise<ExecuteIntegrationResponse> => {
const { typebot, result } = state
let log: ReplyLog | undefined
const status = response.statusCode.toString()
const isError = status.startsWith('4') || status.startsWith('5')
if (isError) {
log = {
status: 'error',
description: `Webhook returned error: ${response.data}`,
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
}
result &&
(await saveErrorLog({
resultId: result.id,
message: log.description,
details: log.details,
}))
} else {
log = {
status: 'success',
description: `Webhook executed successfully!`,
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
}
result &&
(await saveSuccessLog({
resultId: result.id,
message: log.description,
details: JSON.stringify(response.data, null, 2).substring(0, 1000),
}))
}
const newVariables = block.options.responseVariableMapping.reduce<
VariableWithUnknowValue[]
>((newVariables, varMapping) => {
if (!varMapping?.bodyPath || !varMapping.variableId) return newVariables
const existingVariable = typebot.variables.find(
byId(varMapping.variableId)
)
if (!existingVariable) return newVariables
const func = Function(
'data',
`return data.${parseVariables(typebot.variables)(varMapping?.bodyPath)}`
)
try {
const value: unknown = func(response)
return [...newVariables, { ...existingVariable, value }]
} catch (err) {
return newVariables
}
}, [])
if (newVariables.length > 0) {
const newSessionState = await updateVariables(state)(newVariables)
return {
outgoingEdgeId: block.outgoingEdgeId,
newSessionState,
}
}
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: log ? [log] : undefined,
}
}

View File

@@ -5,15 +5,13 @@ import { ExecuteLogicResponse } from '@/features/chat/types'
export const executeRedirect = ( export const executeRedirect = (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: RedirectBlock, block: RedirectBlock
lastBubbleBlockId?: string
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId } if (!block.options?.url) return { outgoingEdgeId: block.outgoingEdgeId }
const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url)) const formattedUrl = sanitizeUrl(parseVariables(variables)(block.options.url))
return { return {
clientSideActions: [ clientSideActions: [
{ {
lastBubbleBlockId,
redirect: { url: formattedUrl, isNewTab: block.options.isNewTab }, redirect: { url: formattedUrl, isNewTab: block.options.isNewTab },
}, },
], ],

View File

@@ -6,8 +6,7 @@ import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas'
export const executeScript = ( export const executeScript = (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: ScriptBlock, block: ScriptBlock
lastBubbleBlockId?: string
): ExecuteLogicResponse => { ): ExecuteLogicResponse => {
if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId } if (!block.options.content) return { outgoingEdgeId: block.outgoingEdgeId }
@@ -21,7 +20,6 @@ export const executeScript = (
clientSideActions: [ clientSideActions: [
{ {
scriptToExecute: scriptToExecute, scriptToExecute: scriptToExecute,
lastBubbleBlockId,
}, },
], ],
} }

View File

@@ -8,8 +8,7 @@ import { parseScriptToExecuteClientSideAction } from '../script/executeScript'
export const executeSetVariable = async ( export const executeSetVariable = async (
state: SessionState, state: SessionState,
block: SetVariableBlock, block: SetVariableBlock
lastBubbleBlockId?: string
): Promise<ExecuteLogicResponse> => { ): Promise<ExecuteLogicResponse> => {
const { variables } = state.typebot const { variables } = state.typebot
if (!block.options?.variableId) if (!block.options?.variableId)
@@ -28,7 +27,6 @@ export const executeSetVariable = async (
setVariable: { setVariable: {
scriptToExecute, scriptToExecute,
}, },
lastBubbleBlockId,
}, },
], ],
} }

View File

@@ -4,8 +4,7 @@ import { SessionState, WaitBlock } from '@typebot.io/schemas'
export const executeWait = async ( export const executeWait = async (
{ typebot: { variables } }: SessionState, { typebot: { variables } }: SessionState,
block: WaitBlock, block: WaitBlock
lastBubbleBlockId?: string
): Promise<ExecuteLogicResponse> => { ): Promise<ExecuteLogicResponse> => {
if (!block.options.secondsToWaitFor) if (!block.options.secondsToWaitFor)
return { outgoingEdgeId: block.outgoingEdgeId } return { outgoingEdgeId: block.outgoingEdgeId }
@@ -19,7 +18,6 @@ export const executeWait = async (
? [ ? [
{ {
wait: { secondsToWaitFor: parsedSecondsToWaitFor }, wait: { secondsToWaitFor: parsedSecondsToWaitFor },
lastBubbleBlockId,
}, },
] ]
: undefined, : undefined,

View File

@@ -14,6 +14,7 @@ import {
ResultInSession, ResultInSession,
SessionState, SessionState,
SetVariableBlock, SetVariableBlock,
WebhookBlock,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isInputBlock, isNotDefined, byId, isDefined } from '@typebot.io/lib' import { isInputBlock, isNotDefined, byId, isDefined } from '@typebot.io/lib'
import { executeGroup } from './executeGroup' import { executeGroup } from './executeGroup'
@@ -26,6 +27,7 @@ import { updateVariables } from '@/features/variables/updateVariables'
import { parseVariables } from '@/features/variables/parseVariables' import { parseVariables } from '@/features/variables/parseVariables'
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai' import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion' import { resumeChatCompletion } from '@/features/blocks/integrations/openai/resumeChatCompletion'
import { resumeWebhookExecution } from '@/features/blocks/integrations/webhook/resumeWebhookExecution'
export const continueBotFlow = export const continueBotFlow =
(state: SessionState) => (state: SessionState) =>
@@ -60,6 +62,12 @@ export const continueBotFlow =
} }
newSessionState = await updateVariables(state)([newVariable]) newSessionState = await updateVariables(state)([newVariable])
} }
} else if (reply && block.type === IntegrationBlockType.WEBHOOK) {
const result = await resumeWebhookExecution(
state,
block
)(JSON.parse(reply))
if (result.newSessionState) newSessionState = result.newSessionState
} else if ( } else if (
isDefined(reply) && isDefined(reply) &&
block.type === IntegrationBlockType.OPEN_AI && block.type === IntegrationBlockType.OPEN_AI &&
@@ -250,7 +258,7 @@ const computeStorageUsed = async (reply: string) => {
const getOutgoingEdgeId = const getOutgoingEdgeId =
({ typebot: { variables } }: Pick<SessionState, 'typebot'>) => ({ typebot: { variables } }: Pick<SessionState, 'typebot'>) =>
( (
block: InputBlock | SetVariableBlock | OpenAIBlock, block: InputBlock | SetVariableBlock | OpenAIBlock | WebhookBlock,
reply: string | null reply: string | null
) => { ) => {
if ( if (

View File

@@ -67,9 +67,9 @@ export const executeGroup =
logs, logs,
} }
const executionResponse = isLogicBlock(block) const executionResponse = isLogicBlock(block)
? await executeLogic(newSessionState, lastBubbleBlockId)(block) ? await executeLogic(newSessionState)(block)
: isIntegrationBlock(block) : isIntegrationBlock(block)
? await executeIntegration(newSessionState, lastBubbleBlockId)(block) ? await executeIntegration(newSessionState)(block)
: null : null
if (!executionResponse) continue if (!executionResponse) continue
@@ -83,12 +83,17 @@ export const executeGroup =
) { ) {
clientSideActions = [ clientSideActions = [
...(clientSideActions ?? []), ...(clientSideActions ?? []),
...executionResponse.clientSideActions, ...executionResponse.clientSideActions.map((action) => ({
...action,
lastBubbleBlockId,
})),
] ]
if ( if (
executionResponse.clientSideActions?.find( executionResponse.clientSideActions?.find(
(action) => (action) =>
'setVariable' in action || 'streamOpenAiChatCompletion' in action 'setVariable' in action ||
'streamOpenAiChatCompletion' in action ||
'webhookToExecute' in action
) )
) { ) {
return { return {

View File

@@ -12,15 +12,15 @@ import {
import { ExecuteIntegrationResponse } from '../types' import { ExecuteIntegrationResponse } from '../types'
export const executeIntegration = export const executeIntegration =
(state: SessionState, lastBubbleBlockId?: string) => (state: SessionState) =>
async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => { async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => {
switch (block.type) { switch (block.type) {
case IntegrationBlockType.GOOGLE_SHEETS: case IntegrationBlockType.GOOGLE_SHEETS:
return executeGoogleSheetBlock(state, block) return executeGoogleSheetBlock(state, block)
case IntegrationBlockType.CHATWOOT: case IntegrationBlockType.CHATWOOT:
return executeChatwootBlock(state, block, lastBubbleBlockId) return executeChatwootBlock(state, block)
case IntegrationBlockType.GOOGLE_ANALYTICS: case IntegrationBlockType.GOOGLE_ANALYTICS:
return executeGoogleAnalyticsBlock(state, block, lastBubbleBlockId) return executeGoogleAnalyticsBlock(state, block)
case IntegrationBlockType.EMAIL: case IntegrationBlockType.EMAIL:
return executeSendEmailBlock(state, block) return executeSendEmailBlock(state, block)
case IntegrationBlockType.WEBHOOK: case IntegrationBlockType.WEBHOOK:

View File

@@ -10,21 +10,21 @@ import { executeTypebotLink } from '@/features/blocks/logic/typebotLink/executeT
import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest' import { executeAbTest } from '@/features/blocks/logic/abTest/executeAbTest'
export const executeLogic = export const executeLogic =
(state: SessionState, lastBubbleBlockId?: string) => (state: SessionState) =>
async (block: LogicBlock): Promise<ExecuteLogicResponse> => { async (block: LogicBlock): Promise<ExecuteLogicResponse> => {
switch (block.type) { switch (block.type) {
case LogicBlockType.SET_VARIABLE: case LogicBlockType.SET_VARIABLE:
return executeSetVariable(state, block, lastBubbleBlockId) return executeSetVariable(state, block)
case LogicBlockType.CONDITION: case LogicBlockType.CONDITION:
return executeCondition(state, block) return executeCondition(state, block)
case LogicBlockType.REDIRECT: case LogicBlockType.REDIRECT:
return executeRedirect(state, block, lastBubbleBlockId) return executeRedirect(state, block)
case LogicBlockType.SCRIPT: case LogicBlockType.SCRIPT:
return executeScript(state, block, lastBubbleBlockId) return executeScript(state, block)
case LogicBlockType.TYPEBOT_LINK: case LogicBlockType.TYPEBOT_LINK:
return executeTypebotLink(state, block) return executeTypebotLink(state, block)
case LogicBlockType.WAIT: case LogicBlockType.WAIT:
return executeWait(state, block, lastBubbleBlockId) return executeWait(state, block)
case LogicBlockType.JUMP: case LogicBlockType.JUMP:
return executeJumpBlock(state, block.options) return executeJumpBlock(state, block.options)
case LogicBlockType.AB_TEST: case LogicBlockType.AB_TEST:

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.0.55", "version": "0.0.56",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@@ -33,7 +33,7 @@
"eslint-plugin-solid": "0.12.1", "eslint-plugin-solid": "0.12.1",
"postcss": "8.4.23", "postcss": "8.4.23",
"react": "18.2.0", "react": "18.2.0",
"rollup": "3.23.0", "rollup": "3.20.2",
"rollup-plugin-postcss": "4.0.2", "rollup-plugin-postcss": "4.0.2",
"rollup-plugin-typescript-paths": "1.4.0", "rollup-plugin-typescript-paths": "1.4.0",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",

View File

@@ -0,0 +1,32 @@
import { getOpenAiStreamerQuery } from '@/queries/getOpenAiStreamerQuery'
import { ClientSideActionContext } from '@/types'
export const streamChat =
(context: ClientSideActionContext) =>
async (
messages: {
content?: string | undefined
role?: 'system' | 'user' | 'assistant' | undefined
}[],
{ onStreamedMessage }: { onStreamedMessage?: (message: string) => void }
) => {
const data = await getOpenAiStreamerQuery(context)(messages)
if (!data) {
return
}
const reader = data.getReader()
const decoder = new TextDecoder()
let done = false
let message = ''
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunkValue = decoder.decode(value)
message += chunkValue
onStreamedMessage?.(message)
}
return message
}

View File

@@ -0,0 +1,23 @@
import { ExecutableWebhook } from '@typebot.io/schemas'
export const executeWebhook = async (
webhookToExecute: ExecutableWebhook
): Promise<string> => {
const { url, method, body, headers } = webhookToExecute
try {
const response = await fetch(url, {
method,
body: method !== 'GET' && body ? JSON.stringify(body) : undefined,
headers,
})
const statusCode = response.status
const data = await response.json()
return JSON.stringify({ statusCode, data })
} catch (error) {
console.error(error)
return JSON.stringify({
statusCode: 500,
data: 'An error occured while executing the webhook on the client',
})
}
}

View File

@@ -22,3 +22,8 @@ export type OutgoingLog = {
description: string description: string
details?: unknown details?: unknown
} }
export type ClientSideActionContext = {
apiHost?: string
sessionId: string
}

View File

@@ -1,17 +1,14 @@
import { executeChatwoot } from '@/features/blocks/integrations/chatwoot' import { executeChatwoot } from '@/features/blocks/integrations/chatwoot'
import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics' import { executeGoogleAnalyticsBlock } from '@/features/blocks/integrations/googleAnalytics/utils/executeGoogleAnalytics'
import { streamChat } from '@/features/blocks/integrations/openai/streamChat'
import { executeRedirect } from '@/features/blocks/logic/redirect' import { executeRedirect } from '@/features/blocks/logic/redirect'
import { executeScript } from '@/features/blocks/logic/script/executeScript' import { executeScript } from '@/features/blocks/logic/script/executeScript'
import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable' import { executeSetVariable } from '@/features/blocks/logic/setVariable/executeSetVariable'
import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait' import { executeWait } from '@/features/blocks/logic/wait/utils/executeWait'
import { getOpenAiStreamerQuery } from '@/queries/getOpenAiStreamerQuery' import { executeWebhook } from '@/features/blocks/integrations/webhook/executeWebhook'
import { ClientSideActionContext } from '@/types'
import type { ChatReply } from '@typebot.io/schemas' import type { ChatReply } from '@typebot.io/schemas'
type ClientSideActionContext = {
apiHost?: string
sessionId: string
}
export const executeClientSideAction = async ( export const executeClientSideAction = async (
clientSideAction: NonNullable<ChatReply['clientSideActions']>[0], clientSideAction: NonNullable<ChatReply['clientSideActions']>[0],
context: ClientSideActionContext, context: ClientSideActionContext,
@@ -44,34 +41,8 @@ export const executeClientSideAction = async (
) )
return { replyToSend: text } return { replyToSend: text }
} }
if ('webhookToExecute' in clientSideAction) {
const response = await executeWebhook(clientSideAction.webhookToExecute)
return { replyToSend: response }
} }
const streamChat =
(context: ClientSideActionContext) =>
async (
messages: {
content?: string | undefined
role?: 'system' | 'user' | 'assistant' | undefined
}[],
{ onStreamedMessage }: { onStreamedMessage?: (message: string) => void }
) => {
const data = await getOpenAiStreamerQuery(context)(messages)
if (!data) {
return
}
const reader = data.getReader()
const decoder = new TextDecoder()
let done = false
let message = ''
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunkValue = decoder.decode(value)
message += chunkValue
onStreamedMessage?.(message)
}
return message
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.0.55", "version": "0.0.56",
"description": "React library to display typebots on your website", "description": "React library to display typebots on your website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@@ -33,7 +33,7 @@
"eslint-config-custom": "workspace:*", "eslint-config-custom": "workspace:*",
"@typebot.io/schemas": "workspace:*", "@typebot.io/schemas": "workspace:*",
"react": "18.2.0", "react": "18.2.0",
"rollup": "3.23.0", "rollup": "3.20.2",
"rollup-plugin-typescript-paths": "1.4.0", "rollup-plugin-typescript-paths": "1.4.0",
"@typebot.io/tsconfig": "workspace:*", "@typebot.io/tsconfig": "workspace:*",
"tslib": "2.5.2", "tslib": "2.5.2",

View File

@@ -19,6 +19,7 @@ export const webhookOptionsSchema = z.object({
responseVariableMapping: z.array(responseVariableMappingSchema), responseVariableMapping: z.array(responseVariableMappingSchema),
isAdvancedConfig: z.boolean().optional(), isAdvancedConfig: z.boolean().optional(),
isCustomBody: z.boolean().optional(), isCustomBody: z.boolean().optional(),
isExecutedOnClient: z.boolean().optional(),
}) })
export const webhookBlockSchema = blockBaseSchema.merge( export const webhookBlockSchema = blockBaseSchema.merge(

View File

@@ -18,6 +18,7 @@ import { answerSchema } from './answer'
import { BubbleBlockType } from './blocks/bubbles/enums' import { BubbleBlockType } from './blocks/bubbles/enums'
import { inputBlockSchemas } from './blocks/schemas' import { inputBlockSchemas } from './blocks/schemas'
import { chatCompletionMessageSchema } from './blocks/integrations/openai' import { chatCompletionMessageSchema } from './blocks/integrations/openai'
import { executableWebhookSchema } from './webhooks'
const typebotInSessionStateSchema = publicTypebotSchema.pick({ const typebotInSessionStateSchema = publicTypebotSchema.pick({
id: true, id: true,
@@ -237,6 +238,11 @@ const clientSideActionSchema = z
}), }),
}) })
) )
.or(
z.object({
webhookToExecute: executableWebhookSchema,
})
)
) )
export const chatReplySchema = z.object({ export const chatReplySchema = z.object({

View File

@@ -1,4 +1,5 @@
import { Webhook as WebhookFromPrisma } from '@typebot.io/prisma' import { Webhook as WebhookFromPrisma } from '@typebot.io/prisma'
import { z } from 'zod'
export enum HttpMethod { export enum HttpMethod {
POST = 'POST', POST = 'POST',
@@ -36,3 +37,12 @@ export const defaultWebhookAttributes: Omit<
headers: [], headers: [],
queryParams: [], queryParams: [],
} }
export const executableWebhookSchema = z.object({
url: z.string(),
headers: z.record(z.string()).optional(),
body: z.unknown().optional(),
method: z.nativeEnum(HttpMethod).optional(),
})
export type ExecutableWebhook = z.infer<typeof executableWebhookSchema>

56
pnpm-lock.yaml generated
View File

@@ -825,16 +825,16 @@ importers:
version: 7.21.5(@babel/core@7.21.8) version: 7.21.5(@babel/core@7.21.8)
'@rollup/plugin-babel': '@rollup/plugin-babel':
specifier: 6.0.3 specifier: 6.0.3
version: 6.0.3(@babel/core@7.21.8)(rollup@3.23.0) version: 6.0.3(@babel/core@7.21.8)(rollup@3.20.2)
'@rollup/plugin-node-resolve': '@rollup/plugin-node-resolve':
specifier: 15.0.2 specifier: 15.0.2
version: 15.0.2(rollup@3.23.0) version: 15.0.2(rollup@3.20.2)
'@rollup/plugin-terser': '@rollup/plugin-terser':
specifier: 0.4.3 specifier: 0.4.3
version: 0.4.3(rollup@3.23.0) version: 0.4.3(rollup@3.20.2)
'@rollup/plugin-typescript': '@rollup/plugin-typescript':
specifier: 11.1.1 specifier: 11.1.1
version: 11.1.1(rollup@3.23.0)(tslib@2.5.2)(typescript@5.0.4) version: 11.1.1(rollup@3.20.2)(tslib@2.5.2)(typescript@5.0.4)
'@typebot.io/lib': '@typebot.io/lib':
specifier: workspace:* specifier: workspace:*
version: link:../../lib version: link:../../lib
@@ -866,8 +866,8 @@ importers:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0 version: 18.2.0
rollup: rollup:
specifier: 3.23.0 specifier: 3.20.2
version: 3.23.0 version: 3.20.2
rollup-plugin-postcss: rollup-plugin-postcss:
specifier: 4.0.2 specifier: 4.0.2
version: 4.0.2(postcss@8.4.23) version: 4.0.2(postcss@8.4.23)
@@ -895,16 +895,16 @@ importers:
version: 7.21.5(@babel/core@7.21.8) version: 7.21.5(@babel/core@7.21.8)
'@rollup/plugin-babel': '@rollup/plugin-babel':
specifier: 6.0.3 specifier: 6.0.3
version: 6.0.3(@babel/core@7.21.8)(rollup@3.23.0) version: 6.0.3(@babel/core@7.21.8)(rollup@3.20.2)
'@rollup/plugin-node-resolve': '@rollup/plugin-node-resolve':
specifier: 15.0.2 specifier: 15.0.2
version: 15.0.2(rollup@3.23.0) version: 15.0.2(rollup@3.20.2)
'@rollup/plugin-terser': '@rollup/plugin-terser':
specifier: 0.4.3 specifier: 0.4.3
version: 0.4.3(rollup@3.23.0) version: 0.4.3(rollup@3.20.2)
'@rollup/plugin-typescript': '@rollup/plugin-typescript':
specifier: 11.1.1 specifier: 11.1.1
version: 11.1.1(rollup@3.23.0)(tslib@2.5.2)(typescript@5.0.4) version: 11.1.1(rollup@3.20.2)(tslib@2.5.2)(typescript@5.0.4)
'@typebot.io/js': '@typebot.io/js':
specifier: workspace:* specifier: workspace:*
version: link:../js version: link:../js
@@ -936,8 +936,8 @@ importers:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0 version: 18.2.0
rollup: rollup:
specifier: 3.23.0 specifier: 3.20.2
version: 3.23.0 version: 3.20.2
rollup-plugin-typescript-paths: rollup-plugin-typescript-paths:
specifier: 1.4.0 specifier: 1.4.0
version: 1.4.0(typescript@5.0.4) version: 1.4.0(typescript@5.0.4)
@@ -7675,7 +7675,7 @@ packages:
reselect: 4.1.7 reselect: 4.1.7
dev: false dev: false
/@rollup/plugin-babel@6.0.3(@babel/core@7.21.8)(rollup@3.23.0): /@rollup/plugin-babel@6.0.3(@babel/core@7.21.8)(rollup@3.20.2):
resolution: {integrity: sha512-fKImZKppa1A/gX73eg4JGo+8kQr/q1HBQaCGKECZ0v4YBBv3lFqi14+7xyApECzvkLTHCifx+7ntcrvtBIRcpg==} resolution: {integrity: sha512-fKImZKppa1A/gX73eg4JGo+8kQr/q1HBQaCGKECZ0v4YBBv3lFqi14+7xyApECzvkLTHCifx+7ntcrvtBIRcpg==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -7690,8 +7690,8 @@ packages:
dependencies: dependencies:
'@babel/core': 7.21.8 '@babel/core': 7.21.8
'@babel/helper-module-imports': 7.18.6 '@babel/helper-module-imports': 7.18.6
'@rollup/pluginutils': 5.0.2(rollup@3.23.0) '@rollup/pluginutils': 5.0.2(rollup@3.20.2)
rollup: 3.23.0 rollup: 3.20.2
dev: true dev: true
/@rollup/plugin-commonjs@24.0.0(rollup@2.78.0): /@rollup/plugin-commonjs@24.0.0(rollup@2.78.0):
@@ -7712,7 +7712,7 @@ packages:
rollup: 2.78.0 rollup: 2.78.0
dev: false dev: false
/@rollup/plugin-node-resolve@15.0.2(rollup@3.23.0): /@rollup/plugin-node-resolve@15.0.2(rollup@3.20.2):
resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==} resolution: {integrity: sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -7721,16 +7721,16 @@ packages:
rollup: rollup:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.23.0) '@rollup/pluginutils': 5.0.2(rollup@3.20.2)
'@types/resolve': 1.20.2 '@types/resolve': 1.20.2
deepmerge: 4.3.1 deepmerge: 4.3.1
is-builtin-module: 3.2.1 is-builtin-module: 3.2.1
is-module: 1.0.0 is-module: 1.0.0
resolve: 1.22.1 resolve: 1.22.1
rollup: 3.23.0 rollup: 3.20.2
dev: true dev: true
/@rollup/plugin-terser@0.4.3(rollup@3.23.0): /@rollup/plugin-terser@0.4.3(rollup@3.20.2):
resolution: {integrity: sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==} resolution: {integrity: sha512-EF0oejTMtkyhrkwCdg0HJ0IpkcaVg1MMSf2olHb2Jp+1mnLM04OhjpJWGma4HobiDTF0WCyViWuvadyE9ch2XA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -7739,13 +7739,13 @@ packages:
rollup: rollup:
optional: true optional: true
dependencies: dependencies:
rollup: 3.23.0 rollup: 3.20.2
serialize-javascript: 6.0.1 serialize-javascript: 6.0.1
smob: 1.1.1 smob: 1.1.1
terser: 5.17.6 terser: 5.17.6
dev: true dev: true
/@rollup/plugin-typescript@11.1.1(rollup@3.23.0)(tslib@2.5.2)(typescript@5.0.4): /@rollup/plugin-typescript@11.1.1(rollup@3.20.2)(tslib@2.5.2)(typescript@5.0.4):
resolution: {integrity: sha512-Ioir+x5Bejv72Lx2Zbz3/qGg7tvGbxQZALCLoJaGrkNXak/19+vKgKYJYM3i/fJxvsb23I9FuFQ8CUBEfsmBRg==} resolution: {integrity: sha512-Ioir+x5Bejv72Lx2Zbz3/qGg7tvGbxQZALCLoJaGrkNXak/19+vKgKYJYM3i/fJxvsb23I9FuFQ8CUBEfsmBRg==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -7758,9 +7758,9 @@ packages:
tslib: tslib:
optional: true optional: true
dependencies: dependencies:
'@rollup/pluginutils': 5.0.2(rollup@3.23.0) '@rollup/pluginutils': 5.0.2(rollup@3.20.2)
resolve: 1.22.2 resolve: 1.22.2
rollup: 3.23.0 rollup: 3.20.2
tslib: 2.5.2 tslib: 2.5.2
typescript: 5.0.4 typescript: 5.0.4
dev: true dev: true
@@ -7780,7 +7780,7 @@ packages:
rollup: 2.78.0 rollup: 2.78.0
dev: false dev: false
/@rollup/pluginutils@5.0.2(rollup@3.23.0): /@rollup/pluginutils@5.0.2(rollup@3.20.2):
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
@@ -7792,7 +7792,7 @@ packages:
'@types/estree': 1.0.0 '@types/estree': 1.0.0
estree-walker: 2.0.2 estree-walker: 2.0.2
picomatch: 2.3.1 picomatch: 2.3.1
rollup: 3.23.0 rollup: 3.20.2
dev: true dev: true
/@rushstack/eslint-patch@1.2.0: /@rushstack/eslint-patch@1.2.0:
@@ -19451,8 +19451,8 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/rollup@3.23.0: /rollup@3.20.2:
resolution: {integrity: sha512-h31UlwEi7FHihLe1zbk+3Q7z1k/84rb9BSwmBSr/XjOCEaBJ2YyedQDuM0t/kfOS0IxM+vk1/zI9XxYj9V+NJQ==} resolution: {integrity: sha512-3zwkBQl7Ai7MFYQE0y1MeQ15+9jsi7XxfrqwTb/9EK8D9C9+//EBR4M+CuA1KODRaNbFez/lWxA5vhEGZp4MUg==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'} engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
optionalDependencies: optionalDependencies:
@@ -21642,7 +21642,7 @@ packages:
esbuild: 0.17.12 esbuild: 0.17.12
postcss: 8.4.23 postcss: 8.4.23
resolve: 1.22.2 resolve: 1.22.2
rollup: 3.23.0 rollup: 3.20.2
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
dev: false dev: false