2
0

Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View 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)

View 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]
}, [])
}

View 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
}

View File

@ -0,0 +1 @@
export const hasVariable = (str: string): boolean => /\{\{(.*?)\}\}/g.test(str)

View 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,
}
})

View 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:*"
}
}

View 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
}
}

View 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)
}

View 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
}

View 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, '&lt;').replace(/>/g, '&gt;')
return JSON.stringify(value).replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

View 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),
}
})

View 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
}

View 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,
]
}