✨ Introducing The Forge (#1072)
The Forge allows anyone to easily create their own Typebot Block. Closes #380
This commit is contained in:
65
packages/variables/deepParseVariables.ts
Normal file
65
packages/variables/deepParseVariables.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import {
|
||||
defaultParseVariablesOptions,
|
||||
parseVariables,
|
||||
ParseVariablesOptions,
|
||||
} from './parseVariables'
|
||||
import { parseGuessedTypeFromString } from './parseGuessedTypeFromString'
|
||||
|
||||
type DeepParseOptions = {
|
||||
guessCorrectTypes?: boolean
|
||||
removeEmptyStrings?: boolean
|
||||
}
|
||||
|
||||
export const deepParseVariables =
|
||||
(
|
||||
variables: Variable[],
|
||||
deepParseOptions: DeepParseOptions = {
|
||||
guessCorrectTypes: false,
|
||||
removeEmptyStrings: false,
|
||||
},
|
||||
parseVariablesOptions: ParseVariablesOptions = defaultParseVariablesOptions
|
||||
) =>
|
||||
<T extends Record<string, unknown>>(object: T): T =>
|
||||
Object.keys(object).reduce<T>((newObj, key) => {
|
||||
const currentValue = object[key]
|
||||
|
||||
if (typeof currentValue === 'string') {
|
||||
const parsedVariable = parseVariables(
|
||||
variables,
|
||||
parseVariablesOptions
|
||||
)(currentValue)
|
||||
if (deepParseOptions.removeEmptyStrings && parsedVariable === '')
|
||||
return newObj
|
||||
return {
|
||||
...newObj,
|
||||
[key]: deepParseOptions.guessCorrectTypes
|
||||
? parseGuessedTypeFromString(parsedVariable)
|
||||
: parsedVariable,
|
||||
}
|
||||
}
|
||||
|
||||
if (currentValue instanceof Object && currentValue.constructor === Object)
|
||||
return {
|
||||
...newObj,
|
||||
[key]: deepParseVariables(
|
||||
variables,
|
||||
deepParseOptions,
|
||||
parseVariablesOptions
|
||||
)(currentValue as Record<string, unknown>),
|
||||
}
|
||||
|
||||
if (currentValue instanceof Array)
|
||||
return {
|
||||
...newObj,
|
||||
[key]: currentValue.map(
|
||||
deepParseVariables(
|
||||
variables,
|
||||
deepParseOptions,
|
||||
parseVariablesOptions
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return { ...newObj, [key]: currentValue }
|
||||
}, {} as T)
|
19
packages/variables/extractVariablesFromText.ts
Normal file
19
packages/variables/extractVariablesFromText.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
|
||||
export const extractVariablesFromText =
|
||||
(variables: Variable[]) =>
|
||||
(text: string): Variable[] => {
|
||||
const matches = [...text.matchAll(/\{\{(.*?)\}\}/g)]
|
||||
return matches.reduce<Variable[]>((acc, match) => {
|
||||
const variableName = match[1]
|
||||
const variable = variables.find(
|
||||
(variable) => variable.name === variableName
|
||||
)
|
||||
if (
|
||||
!variable ||
|
||||
acc.find((accVariable) => accVariable.id === variable.id)
|
||||
)
|
||||
return acc
|
||||
return [...acc, variable]
|
||||
}, [])
|
||||
}
|
12
packages/variables/findUniqueVariableValue.ts
Normal file
12
packages/variables/findUniqueVariableValue.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
|
||||
export const findUniqueVariableValue =
|
||||
(variables: Variable[]) =>
|
||||
(value: string | undefined): Variable['value'] => {
|
||||
if (!value || !value.startsWith('{{') || !value.endsWith('}}')) return null
|
||||
const variableName = value.slice(2, -2)
|
||||
const variable = variables.find(
|
||||
(variable) => variable.name === variableName
|
||||
)
|
||||
return variable?.value ?? null
|
||||
}
|
1
packages/variables/hasVariable.ts
Normal file
1
packages/variables/hasVariable.ts
Normal file
@ -0,0 +1 @@
|
||||
export const hasVariable = (str: string): boolean => /\{\{(.*?)\}\}/g.test(str)
|
17
packages/variables/injectVariablesFromExistingResult.ts
Normal file
17
packages/variables/injectVariablesFromExistingResult.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Result, Variable } from '@typebot.io/schemas'
|
||||
|
||||
export const injectVariablesFromExistingResult = (
|
||||
variables: Variable[],
|
||||
resultVariables: Result['variables']
|
||||
): Variable[] =>
|
||||
variables.map((variable) => {
|
||||
const resultVariable = resultVariables.find(
|
||||
(resultVariable) =>
|
||||
resultVariable.name === variable.name && !variable.value
|
||||
)
|
||||
if (!resultVariable) return variable
|
||||
return {
|
||||
...variable,
|
||||
value: resultVariable.value,
|
||||
}
|
||||
})
|
10
packages/variables/package.json
Normal file
10
packages/variables/package.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@typebot.io/variables",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@typebot.io/lib": "workspace:*",
|
||||
"@typebot.io/schemas": "workspace:*"
|
||||
}
|
||||
}
|
12
packages/variables/parseGuessedTypeFromString.ts
Normal file
12
packages/variables/parseGuessedTypeFromString.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const parseGuessedTypeFromString = (value: string): unknown => {
|
||||
if (value === 'undefined') return undefined
|
||||
return safeJsonParse(value)
|
||||
}
|
||||
|
||||
const safeJsonParse = (value: string): unknown => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
22
packages/variables/parseGuessedValueType.ts
Normal file
22
packages/variables/parseGuessedValueType.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
|
||||
export const parseGuessedValueType = (
|
||||
value: Variable['value']
|
||||
): string | (string | null)[] | boolean | number | null | undefined => {
|
||||
if (value === null) return null
|
||||
if (value === undefined) return undefined
|
||||
if (typeof value !== 'string') return value
|
||||
const isStartingWithZero =
|
||||
value.startsWith('0') && !value.startsWith('0.') && value.length > 1
|
||||
if (typeof value === 'string' && isStartingWithZero) return value
|
||||
const isStartingWithPlus = value.startsWith('+')
|
||||
if (typeof value === 'string' && isStartingWithPlus) return value
|
||||
if (typeof value === 'number') return value
|
||||
if (value === 'true') return true
|
||||
if (value === 'false') return false
|
||||
if (value === 'null') return null
|
||||
if (value === 'undefined') return undefined
|
||||
// isNaN works with strings
|
||||
if (isNaN(value as unknown as number)) return value
|
||||
return Number(value)
|
||||
}
|
12
packages/variables/parseVariableNumber.ts
Normal file
12
packages/variables/parseVariableNumber.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Variable } from '@typebot.io/schemas'
|
||||
import { parseGuessedValueType } from './parseGuessedValueType'
|
||||
import { parseVariables } from './parseVariables'
|
||||
|
||||
export const parseVariableNumber =
|
||||
(variables: Variable[]) =>
|
||||
(input: number | `{{${string}}}` | undefined): number | undefined => {
|
||||
if (typeof input === 'number' || input === undefined) return input
|
||||
const parsedInput = parseGuessedValueType(parseVariables(variables)(input))
|
||||
if (typeof parsedInput !== 'number') return undefined
|
||||
return parsedInput
|
||||
}
|
157
packages/variables/parseVariables.ts
Normal file
157
packages/variables/parseVariables.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
||||
import { isDefined, isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { Variable, VariableWithValue } from '@typebot.io/schemas'
|
||||
import { parseGuessedValueType } from './parseGuessedValueType'
|
||||
|
||||
export type ParseVariablesOptions = {
|
||||
fieldToParse?: 'value' | 'id'
|
||||
isInsideJson?: boolean
|
||||
takeLatestIfList?: boolean
|
||||
isInsideHtml?: boolean
|
||||
}
|
||||
|
||||
export const defaultParseVariablesOptions: ParseVariablesOptions = {
|
||||
fieldToParse: 'value',
|
||||
isInsideJson: false,
|
||||
takeLatestIfList: false,
|
||||
isInsideHtml: false,
|
||||
}
|
||||
|
||||
// {{= inline code =}}
|
||||
const inlineCodeRegex = /\{\{=(.+?)=\}\}/g
|
||||
|
||||
// {{variable}} and ${{{variable}}}
|
||||
const variableRegex = /\{\{([^{}]+)\}\}|(\$)\{\{([^{}]+)\}\}/g
|
||||
|
||||
export const parseVariables =
|
||||
(
|
||||
variables: Variable[],
|
||||
options: ParseVariablesOptions = defaultParseVariablesOptions
|
||||
) =>
|
||||
(text: string | undefined): string => {
|
||||
if (!text || text === '') return ''
|
||||
const textWithInlineCodeParsed = text.replace(
|
||||
inlineCodeRegex,
|
||||
(_full, inlineCodeToEvaluate) =>
|
||||
evaluateInlineCode(inlineCodeToEvaluate, { variables })
|
||||
)
|
||||
|
||||
return textWithInlineCodeParsed.replace(
|
||||
variableRegex,
|
||||
(_full, nameInCurlyBraces, _dollarSign, nameInTemplateLitteral) => {
|
||||
const dollarSign = (_dollarSign ?? '') as string
|
||||
const matchedVarName = nameInCurlyBraces ?? nameInTemplateLitteral
|
||||
const variable = variables.find((variable) => {
|
||||
return (
|
||||
matchedVarName === variable.name &&
|
||||
(options.fieldToParse === 'id' || isDefined(variable.value))
|
||||
)
|
||||
}) as VariableWithValue | undefined
|
||||
if (!variable) return dollarSign + ''
|
||||
if (options.fieldToParse === 'id') return dollarSign + variable.id
|
||||
const { value } = variable
|
||||
if (options.isInsideJson)
|
||||
return dollarSign + parseVariableValueInJson(value)
|
||||
const parsedValue =
|
||||
dollarSign +
|
||||
safeStringify(
|
||||
options.takeLatestIfList && Array.isArray(value)
|
||||
? value[value.length - 1]
|
||||
: value
|
||||
)
|
||||
if (!parsedValue) return dollarSign + ''
|
||||
if (options.isInsideHtml) return parseVariableValueInHtml(parsedValue)
|
||||
return parsedValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const evaluateInlineCode = (
|
||||
code: string,
|
||||
{ variables }: { variables: Variable[] }
|
||||
) => {
|
||||
const evaluating = parseVariables(variables, { fieldToParse: 'id' })(
|
||||
code.includes('return ') ? code : `return ${code}`
|
||||
)
|
||||
try {
|
||||
const func = Function(...variables.map((v) => v.id), evaluating)
|
||||
return func(...variables.map((v) => parseGuessedValueType(v.value)))
|
||||
} catch (err) {
|
||||
return parseVariables(variables)(code)
|
||||
}
|
||||
}
|
||||
|
||||
type VariableToParseInformation = {
|
||||
startIndex: number
|
||||
endIndex: number
|
||||
textToReplace: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export const getVariablesToParseInfoInText = (
|
||||
text: string,
|
||||
{
|
||||
variables,
|
||||
takeLatestIfList,
|
||||
}: { variables: Variable[]; takeLatestIfList?: boolean }
|
||||
): VariableToParseInformation[] => {
|
||||
const variablesToParseInfo: VariableToParseInformation[] = []
|
||||
const inlineCodeMatches = [...text.matchAll(inlineCodeRegex)]
|
||||
inlineCodeMatches.forEach((match) => {
|
||||
if (isNotDefined(match.index) || !match[0].length) return
|
||||
const inlineCodeToEvaluate = match[1]
|
||||
const evaluatedValue = evaluateInlineCode(inlineCodeToEvaluate, {
|
||||
variables,
|
||||
})
|
||||
variablesToParseInfo.push({
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + match[0].length,
|
||||
textToReplace: match[0],
|
||||
value:
|
||||
safeStringify(
|
||||
takeLatestIfList && Array.isArray(evaluatedValue)
|
||||
? evaluatedValue[evaluatedValue.length - 1]
|
||||
: evaluatedValue
|
||||
) ?? '',
|
||||
})
|
||||
})
|
||||
const textWithInlineCodeParsed = text.replace(
|
||||
inlineCodeRegex,
|
||||
(_full, inlineCodeToEvaluate) =>
|
||||
evaluateInlineCode(inlineCodeToEvaluate, { variables })
|
||||
)
|
||||
const variableMatches = [...textWithInlineCodeParsed.matchAll(variableRegex)]
|
||||
variableMatches.forEach((match) => {
|
||||
if (isNotDefined(match.index) || !match[0].length) return
|
||||
const matchedVarName = match[1] ?? match[3]
|
||||
const variable = variables.find((variable) => {
|
||||
return matchedVarName === variable.name && isDefined(variable.value)
|
||||
}) as VariableWithValue | undefined
|
||||
variablesToParseInfo.push({
|
||||
startIndex: match.index,
|
||||
endIndex: match.index + match[0].length,
|
||||
textToReplace: match[0],
|
||||
value:
|
||||
safeStringify(
|
||||
takeLatestIfList && Array.isArray(variable?.value)
|
||||
? variable?.value[variable?.value.length - 1]
|
||||
: variable?.value
|
||||
) ?? '',
|
||||
})
|
||||
})
|
||||
return variablesToParseInfo.sort((a, b) => a.startIndex - b.startIndex)
|
||||
}
|
||||
|
||||
const parseVariableValueInJson = (value: VariableWithValue['value']) => {
|
||||
const stringifiedValue = JSON.stringify(value)
|
||||
if (typeof value === 'string') return stringifiedValue.slice(1, -1)
|
||||
return stringifiedValue
|
||||
}
|
||||
|
||||
const parseVariableValueInHtml = (
|
||||
value: VariableWithValue['value']
|
||||
): string => {
|
||||
if (typeof value === 'string')
|
||||
return value.replace(/</g, '<').replace(/>/g, '>')
|
||||
return JSON.stringify(value).replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
15
packages/variables/prefillVariables.ts
Normal file
15
packages/variables/prefillVariables.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
||||
import { StartChatInput, Variable } from '@typebot.io/schemas'
|
||||
|
||||
export const prefillVariables = (
|
||||
variables: Variable[],
|
||||
prefilledVariables: NonNullable<StartChatInput['prefilledVariables']>
|
||||
): Variable[] =>
|
||||
variables.map((variable) => {
|
||||
const prefilledVariable = prefilledVariables[variable.name]
|
||||
if (!prefilledVariable) return variable
|
||||
return {
|
||||
...variable,
|
||||
value: safeStringify(prefilledVariable),
|
||||
}
|
||||
})
|
26
packages/variables/transformVariablesToList.ts
Normal file
26
packages/variables/transformVariablesToList.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { isNotDefined } from '@typebot.io/lib/utils'
|
||||
import { Variable, VariableWithValue } from '@typebot.io/schemas'
|
||||
|
||||
export const transformVariablesToList =
|
||||
(variables: Variable[]) =>
|
||||
(variableIds: string[]): VariableWithValue[] => {
|
||||
const newVariables = variables.reduce<VariableWithValue[]>(
|
||||
(variables, variable) => {
|
||||
if (
|
||||
!variableIds.includes(variable.id) ||
|
||||
isNotDefined(variable.value) ||
|
||||
typeof variable.value !== 'string'
|
||||
)
|
||||
return variables
|
||||
return [
|
||||
...variables,
|
||||
{
|
||||
...variable,
|
||||
value: [variable.value],
|
||||
},
|
||||
]
|
||||
},
|
||||
[]
|
||||
)
|
||||
return newVariables
|
||||
}
|
45
packages/variables/updateVariablesInSession.ts
Normal file
45
packages/variables/updateVariablesInSession.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { safeStringify } from '@typebot.io/lib/safeStringify'
|
||||
import {
|
||||
SessionState,
|
||||
VariableWithUnknowValue,
|
||||
Variable,
|
||||
} from '@typebot.io/schemas'
|
||||
|
||||
export const updateVariablesInSession =
|
||||
(state: SessionState) =>
|
||||
(newVariables: VariableWithUnknowValue[]): SessionState => ({
|
||||
...state,
|
||||
typebotsQueue: state.typebotsQueue.map((typebotInQueue, index) =>
|
||||
index === 0
|
||||
? {
|
||||
...typebotInQueue,
|
||||
typebot: {
|
||||
...typebotInQueue.typebot,
|
||||
variables: updateTypebotVariables(typebotInQueue.typebot)(
|
||||
newVariables
|
||||
),
|
||||
},
|
||||
}
|
||||
: typebotInQueue
|
||||
),
|
||||
})
|
||||
|
||||
const updateTypebotVariables =
|
||||
(typebot: { variables: Variable[] }) =>
|
||||
(newVariables: VariableWithUnknowValue[]): Variable[] => {
|
||||
const serializedNewVariables = newVariables.map((variable) => ({
|
||||
...variable,
|
||||
value: Array.isArray(variable.value)
|
||||
? variable.value.map(safeStringify)
|
||||
: safeStringify(variable.value),
|
||||
}))
|
||||
|
||||
return [
|
||||
...typebot.variables.filter((existingVariable) =>
|
||||
serializedNewVariables.every(
|
||||
(newVariable) => existingVariable.id !== newVariable.id
|
||||
)
|
||||
),
|
||||
...serializedNewVariables,
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user