2023-12-13 10:22:02 +01:00
|
|
|
import * as p from '@clack/prompts'
|
|
|
|
import { spinner } from '@clack/prompts'
|
2024-04-26 10:15:40 +02:00
|
|
|
import {
|
|
|
|
existsSync,
|
|
|
|
mkdirSync,
|
|
|
|
readFileSync,
|
|
|
|
readdirSync,
|
|
|
|
writeFileSync,
|
|
|
|
} from 'fs'
|
2023-12-13 10:22:02 +01:00
|
|
|
import { join } from 'path'
|
|
|
|
import prettier from 'prettier'
|
|
|
|
import { spawn } from 'child_process'
|
|
|
|
import builderPackageJson from '../../../apps/builder/package.json'
|
|
|
|
|
|
|
|
const slugRegex = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
|
|
|
|
|
|
const prettierRc = {
|
|
|
|
trailingComma: 'es5',
|
|
|
|
tabWidth: 2,
|
|
|
|
semi: false,
|
|
|
|
singleQuote: true,
|
|
|
|
} as const
|
|
|
|
|
|
|
|
type PromptResult = {
|
|
|
|
name: string
|
|
|
|
id: string
|
|
|
|
auth: 'apiKey' | 'encryptedData' | 'none'
|
|
|
|
camelCaseId: string
|
|
|
|
}
|
|
|
|
|
|
|
|
const main = async () => {
|
|
|
|
p.intro('Create a new Typebot integration block')
|
|
|
|
const { name, id } = await p.group(
|
|
|
|
{
|
|
|
|
name: () =>
|
|
|
|
p.text({
|
|
|
|
message: 'Integration name?',
|
|
|
|
placeholder: 'Short name like: Sheets, Analytics, Cal.com',
|
|
|
|
}),
|
|
|
|
id: ({ results }) =>
|
|
|
|
p.text({
|
|
|
|
message:
|
|
|
|
'Integration ID? (should be a slug like: cal-com, openai...)',
|
|
|
|
placeholder: 'my-integration',
|
|
|
|
initialValue: slugify(results.name ?? ''),
|
|
|
|
validate: (val) => {
|
|
|
|
if (!slugRegex.test(val))
|
|
|
|
return 'Invalid ID. Must be a slug, like: "google-sheets".'
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
onCancel: () => {
|
|
|
|
p.cancel('Operation cancelled.')
|
|
|
|
process.exit(0)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
const auth = (await p.select({
|
|
|
|
message: 'Does this integration require authentication to work?',
|
|
|
|
options: [
|
|
|
|
{ value: 'apiKey', label: 'API key or token' },
|
|
|
|
{ value: 'encryptedData', label: 'Custom encrypted data' },
|
|
|
|
{ value: 'none', label: 'None' },
|
|
|
|
],
|
|
|
|
})) as 'apiKey' | 'encryptedData' | 'none'
|
|
|
|
|
|
|
|
const prompt: PromptResult = {
|
|
|
|
name,
|
|
|
|
id: id as string,
|
|
|
|
auth,
|
|
|
|
camelCaseId: camelize(id as string),
|
|
|
|
}
|
|
|
|
|
|
|
|
const s = spinner()
|
|
|
|
s.start('Creating files...')
|
|
|
|
const newBlockPath = join(process.cwd(), `../blocks/${prompt.camelCaseId}`)
|
2024-04-26 10:15:40 +02:00
|
|
|
if (existsSync(newBlockPath) && readdirSync(newBlockPath).length > 1) {
|
2023-12-13 10:22:02 +01:00
|
|
|
s.stop('Creating files...')
|
|
|
|
p.log.error(
|
|
|
|
`An integration with the ID "${prompt.id}" already exists. Please choose a different ID.`
|
|
|
|
)
|
|
|
|
process.exit(1)
|
|
|
|
}
|
2024-04-26 10:15:40 +02:00
|
|
|
if (!existsSync(newBlockPath)) mkdirSync(newBlockPath)
|
2023-12-13 10:22:02 +01:00
|
|
|
await createPackageJson(newBlockPath, prompt)
|
|
|
|
await createTsConfig(newBlockPath)
|
|
|
|
await createIndexFile(newBlockPath, prompt)
|
|
|
|
await createLogoFile(newBlockPath, prompt)
|
|
|
|
if (prompt.auth !== 'none') await createAuthFile(newBlockPath, prompt)
|
2024-03-18 16:09:19 +01:00
|
|
|
await createSchemasFile(newBlockPath, prompt)
|
2024-03-19 10:38:55 +01:00
|
|
|
await addBlockToRepository(prompt)
|
2023-12-13 10:22:02 +01:00
|
|
|
s.stop('Creating files...')
|
|
|
|
s.start('Installing dependencies...')
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
const ls = spawn('pnpm', ['install'])
|
|
|
|
ls.stderr.on('data', (data) => {
|
|
|
|
reject(data)
|
|
|
|
})
|
|
|
|
ls.on('error', (error) => {
|
|
|
|
reject(error)
|
|
|
|
})
|
|
|
|
ls.on('close', () => {
|
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
s.stop('Installing dependencies...')
|
|
|
|
p.outro(
|
|
|
|
`Done! 🎉 Head over to packages/forge/blocks/${prompt.camelCaseId} and start coding!`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const slugify = (str: string): string => {
|
|
|
|
return String(str)
|
|
|
|
.normalize('NFKD') // split accented characters into their base characters and diacritical marks
|
|
|
|
.replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block.
|
|
|
|
.trim() // trim leading or trailing whitespace
|
|
|
|
.toLowerCase() // convert to lowercase
|
|
|
|
.replace(/[^a-z0-9\. -]/g, '') // remove non-alphanumeric characters
|
|
|
|
.replace(/[\s\.]+/g, '-') // replace spaces and dots with hyphens
|
|
|
|
.replace(/-+/g, '-') // remove consecutive hyphens
|
|
|
|
}
|
|
|
|
|
|
|
|
const camelize = (str: string) =>
|
|
|
|
str.replace(/-([a-z])/g, function (g) {
|
|
|
|
return g[1].toUpperCase()
|
|
|
|
})
|
|
|
|
|
|
|
|
const capitalize = (str: string) => {
|
|
|
|
const [fst] = str
|
|
|
|
|
|
|
|
return `${fst.toUpperCase()}${str.slice(1)}`
|
|
|
|
}
|
|
|
|
|
|
|
|
const createIndexFile = async (
|
|
|
|
path: string,
|
|
|
|
{ id, camelCaseId, name, auth }: PromptResult
|
|
|
|
) => {
|
|
|
|
const camelCaseName = camelize(id as string)
|
|
|
|
writeFileSync(
|
|
|
|
join(path, 'index.ts'),
|
|
|
|
await prettier.format(
|
|
|
|
`import { createBlock } from '@typebot.io/forge'
|
|
|
|
import { ${capitalize(camelCaseId)}Logo } from './logo'
|
|
|
|
${auth !== 'none' ? `import { auth } from './auth'` : ''}
|
|
|
|
|
2024-03-18 16:09:19 +01:00
|
|
|
export const ${camelCaseName}Block = createBlock({
|
2023-12-13 10:22:02 +01:00
|
|
|
id: '${id}',
|
|
|
|
name: '${name}',
|
|
|
|
tags: [],
|
|
|
|
LightLogo: ${capitalize(camelCaseName)}Logo,${auth !== 'none' ? `auth,` : ''}
|
|
|
|
actions: [],
|
|
|
|
})
|
|
|
|
`,
|
|
|
|
{ parser: 'typescript', ...prettierRc }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const createPackageJson = async (path: string, { id }: { id: unknown }) => {
|
|
|
|
writeFileSync(
|
|
|
|
join(path, 'package.json'),
|
|
|
|
await prettier.format(
|
|
|
|
JSON.stringify({
|
|
|
|
name: `@typebot.io/${id}-block`,
|
|
|
|
version: '1.0.0',
|
|
|
|
description: '',
|
|
|
|
main: 'index.ts',
|
|
|
|
keywords: [],
|
2024-05-23 10:42:23 +02:00
|
|
|
license: 'AGPL-3.0-or-later',
|
2023-12-13 10:22:02 +01:00
|
|
|
devDependencies: {
|
|
|
|
'@typebot.io/forge': 'workspace:*',
|
|
|
|
'@typebot.io/tsconfig': 'workspace:*',
|
|
|
|
'@types/react': builderPackageJson.devDependencies['@types/react'],
|
|
|
|
typescript: builderPackageJson.devDependencies['typescript'],
|
|
|
|
'@typebot.io/lib': 'workspace:*',
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
{ parser: 'json', ...prettierRc }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const createTsConfig = async (path: string) => {
|
|
|
|
writeFileSync(
|
|
|
|
join(path, 'tsconfig.json'),
|
|
|
|
await prettier.format(
|
|
|
|
JSON.stringify({
|
|
|
|
extends: '@typebot.io/tsconfig/base.json',
|
|
|
|
include: ['**/*.ts', '**/*.tsx'],
|
|
|
|
exclude: ['node_modules'],
|
|
|
|
compilerOptions: {
|
|
|
|
lib: ['ESNext', 'DOM'],
|
|
|
|
noEmit: true,
|
2024-03-19 10:38:55 +01:00
|
|
|
jsx: 'preserve',
|
|
|
|
jsxImportSource: 'react',
|
2023-12-13 10:22:02 +01:00
|
|
|
},
|
|
|
|
}),
|
|
|
|
{ parser: 'json', ...prettierRc }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const createLogoFile = async (
|
|
|
|
path: string,
|
|
|
|
{ camelCaseId }: { camelCaseId: string }
|
|
|
|
) => {
|
|
|
|
writeFileSync(
|
|
|
|
join(path, 'logo.tsx'),
|
|
|
|
await prettier.format(
|
2024-03-19 10:38:55 +01:00
|
|
|
`/** @jsxImportSource react */
|
2023-12-13 10:22:02 +01:00
|
|
|
|
|
|
|
export const ${capitalize(
|
|
|
|
camelCaseId
|
|
|
|
)}Logo = (props: React.SVGProps<SVGSVGElement>) => <svg></svg>
|
|
|
|
`,
|
|
|
|
{ parser: 'typescript', ...prettierRc }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const createAuthFile = async (
|
|
|
|
path: string,
|
|
|
|
{ name, auth }: { name: string; auth: 'apiKey' | 'encryptedData' | 'none' }
|
|
|
|
) =>
|
|
|
|
writeFileSync(
|
|
|
|
join(path, 'auth.ts'),
|
|
|
|
await prettier.format(
|
|
|
|
`import { option, AuthDefinition } from '@typebot.io/forge'
|
|
|
|
|
|
|
|
export const auth = {
|
|
|
|
type: 'encryptedCredentials',
|
|
|
|
name: '${name} account',
|
|
|
|
${
|
|
|
|
auth === 'apiKey'
|
|
|
|
? `schema: option.object({
|
|
|
|
apiKey: option.string.layout({
|
|
|
|
label: 'API key',
|
|
|
|
isRequired: true,
|
2024-01-25 16:35:16 +01:00
|
|
|
inputType: 'password',
|
2023-12-13 10:22:02 +01:00
|
|
|
helperText:
|
|
|
|
'You can generate an API key [here](<INSERT_URL>).',
|
2024-03-15 14:28:12 +01:00
|
|
|
withVariableButton: false,
|
|
|
|
isDebounceDisabled: true,
|
2023-12-13 10:22:02 +01:00
|
|
|
}),
|
|
|
|
}),`
|
|
|
|
: ''
|
|
|
|
}
|
|
|
|
} satisfies AuthDefinition`,
|
|
|
|
{ parser: 'typescript', ...prettierRc }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2024-03-19 10:38:55 +01:00
|
|
|
const addBlockToRepository = async ({
|
2024-04-26 10:15:40 +02:00
|
|
|
auth,
|
2024-03-19 10:38:55 +01:00
|
|
|
camelCaseId,
|
|
|
|
id,
|
|
|
|
}: {
|
2024-04-26 10:15:40 +02:00
|
|
|
auth: string
|
2024-03-19 10:38:55 +01:00
|
|
|
camelCaseId: string
|
|
|
|
id: string
|
|
|
|
}) => {
|
|
|
|
const schemasPath = await addBlockToRepoPackageJson(id)
|
|
|
|
await addBlockToRepoDefinitions(schemasPath, camelCaseId, id)
|
|
|
|
await addBlockToRepoConstants(schemasPath, id)
|
|
|
|
await addBlockToRepoSchemas(schemasPath, camelCaseId, id)
|
2024-04-26 10:15:40 +02:00
|
|
|
if (auth !== 'none')
|
|
|
|
await addBlockToCredentialsRepoSchemas(schemasPath, camelCaseId, id)
|
2024-03-19 10:38:55 +01:00
|
|
|
}
|
|
|
|
|
2024-03-18 16:09:19 +01:00
|
|
|
const createSchemasFile = async (
|
|
|
|
path: string,
|
|
|
|
{
|
|
|
|
id,
|
2024-07-16 15:11:48 +02:00
|
|
|
auth,
|
2024-03-18 16:09:19 +01:00
|
|
|
}: { id: string; name: string; auth: 'apiKey' | 'encryptedData' | 'none' }
|
|
|
|
) => {
|
|
|
|
const camelCaseName = camelize(id as string)
|
|
|
|
writeFileSync(
|
|
|
|
join(path, 'schemas.ts'),
|
|
|
|
await prettier.format(
|
|
|
|
`// Do not edit this file manually
|
2024-07-16 15:11:48 +02:00
|
|
|
import { ${
|
|
|
|
auth !== 'none' ? 'parseBlockCredentials,' : ''
|
|
|
|
} parseBlockSchema } from '@typebot.io/forge'
|
|
|
|
import { ${camelCaseName}Block } from '.'${
|
|
|
|
auth !== 'none' ? `\nimport { auth } from './auth'` : ''
|
|
|
|
}
|
2024-03-18 16:09:19 +01:00
|
|
|
|
|
|
|
export const ${camelCaseName}BlockSchema = parseBlockSchema(${camelCaseName}Block)
|
2024-07-16 15:11:48 +02:00
|
|
|
${
|
|
|
|
auth !== 'none'
|
|
|
|
? `export const ${camelCaseName}CredentialsSchema = parseBlockCredentials(${camelCaseName}Block.id, auth.schema)`
|
|
|
|
: ''
|
|
|
|
}`,
|
2024-03-18 16:09:19 +01:00
|
|
|
{ parser: 'typescript', ...prettierRc }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-19 10:38:55 +01:00
|
|
|
async function addBlockToRepoDefinitions(
|
|
|
|
schemasPath: string,
|
|
|
|
camelCaseId: string,
|
|
|
|
id: string
|
|
|
|
) {
|
|
|
|
const existingDefinitionsData = readFileSync(
|
|
|
|
join(schemasPath, 'definitions.ts')
|
|
|
|
).toString()
|
|
|
|
const importStatement = `import { ${camelCaseId}Block } from '@typebot.io/${id}-block'`
|
|
|
|
const objectEntry = ` [${camelCaseId}Block.id]: ${camelCaseId}Block,`
|
|
|
|
const nextLineImportIndex = existingDefinitionsData.indexOf(
|
|
|
|
'\n',
|
|
|
|
existingDefinitionsData.lastIndexOf('import')
|
|
|
|
)
|
|
|
|
|
|
|
|
const newObjectEntryIndex = existingDefinitionsData.lastIndexOf(',') + 1
|
|
|
|
|
|
|
|
const newDefinitionsData = `${existingDefinitionsData.slice(
|
|
|
|
0,
|
|
|
|
nextLineImportIndex
|
|
|
|
)}
|
|
|
|
${importStatement}
|
|
|
|
${existingDefinitionsData.slice(nextLineImportIndex, newObjectEntryIndex)}
|
|
|
|
${objectEntry}
|
|
|
|
${existingDefinitionsData.slice(newObjectEntryIndex)}`
|
|
|
|
|
|
|
|
writeFileSync(
|
|
|
|
join(schemasPath, 'definitions.ts'),
|
|
|
|
await prettier.format(newDefinitionsData, {
|
|
|
|
parser: 'typescript',
|
|
|
|
...prettierRc,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function addBlockToRepoSchemas(
|
|
|
|
schemasPath: string,
|
|
|
|
camelCaseId: string,
|
|
|
|
id: string
|
|
|
|
) {
|
|
|
|
const existingDefinitionsData = readFileSync(
|
|
|
|
join(schemasPath, 'schemas.ts')
|
|
|
|
).toString()
|
|
|
|
const importStatement = `import { ${camelCaseId}Block } from '@typebot.io/${id}-block'
|
|
|
|
import { ${camelCaseId}BlockSchema } from '@typebot.io/${id}-block/schemas'`
|
|
|
|
const objectEntry = ` [${camelCaseId}Block.id]: ${camelCaseId}BlockSchema,`
|
|
|
|
const nextLineImportIndex = existingDefinitionsData.indexOf(
|
|
|
|
'\n',
|
|
|
|
existingDefinitionsData.lastIndexOf('import')
|
|
|
|
)
|
|
|
|
|
|
|
|
const newObjectEntryIndex = existingDefinitionsData.lastIndexOf(',') + 1
|
|
|
|
|
|
|
|
const newDefinitionsData = `${existingDefinitionsData.slice(
|
|
|
|
0,
|
|
|
|
nextLineImportIndex
|
|
|
|
)}
|
|
|
|
${importStatement}
|
|
|
|
${existingDefinitionsData.slice(nextLineImportIndex, newObjectEntryIndex)}
|
|
|
|
${objectEntry}
|
|
|
|
${existingDefinitionsData.slice(newObjectEntryIndex)}`
|
|
|
|
|
|
|
|
writeFileSync(
|
|
|
|
join(schemasPath, 'schemas.ts'),
|
|
|
|
await prettier.format(newDefinitionsData, {
|
|
|
|
parser: 'typescript',
|
|
|
|
...prettierRc,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-04-26 10:15:40 +02:00
|
|
|
async function addBlockToCredentialsRepoSchemas(
|
|
|
|
schemasPath: string,
|
|
|
|
camelCaseId: string,
|
|
|
|
id: string
|
|
|
|
) {
|
|
|
|
const existingSchemasData = readFileSync(
|
|
|
|
join(schemasPath, 'credentials.ts')
|
|
|
|
).toString()
|
|
|
|
const importStatement = `import { ${camelCaseId}Block } from '@typebot.io/${id}-block'
|
|
|
|
import { ${camelCaseId}CredentialsSchema } from '@typebot.io/${id}-block/schemas'`
|
|
|
|
const objectEntry = ` [${camelCaseId}Block.id]: ${camelCaseId}CredentialsSchema,`
|
|
|
|
const nextLineImportIndex = existingSchemasData.indexOf(
|
|
|
|
'\n',
|
|
|
|
existingSchemasData.lastIndexOf('import')
|
|
|
|
)
|
|
|
|
|
|
|
|
const newObjectEntryIndex = existingSchemasData.lastIndexOf(',') + 1
|
|
|
|
|
|
|
|
const newDefinitionsData = `${existingSchemasData.slice(
|
|
|
|
0,
|
|
|
|
nextLineImportIndex
|
|
|
|
)}
|
|
|
|
${importStatement}
|
|
|
|
${existingSchemasData.slice(nextLineImportIndex, newObjectEntryIndex)}
|
|
|
|
${objectEntry}
|
|
|
|
${existingSchemasData.slice(newObjectEntryIndex)}`
|
|
|
|
|
|
|
|
writeFileSync(
|
|
|
|
join(schemasPath, 'credentials.ts'),
|
|
|
|
await prettier.format(newDefinitionsData, {
|
|
|
|
parser: 'typescript',
|
|
|
|
...prettierRc,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-03-19 10:38:55 +01:00
|
|
|
async function addBlockToRepoConstants(schemasPath: string, id: string) {
|
|
|
|
const existingDefinitionsData = readFileSync(
|
|
|
|
join(schemasPath, 'constants.ts')
|
|
|
|
).toString()
|
|
|
|
|
|
|
|
writeFileSync(
|
|
|
|
join(schemasPath, 'constants.ts'),
|
|
|
|
await prettier.format(
|
|
|
|
existingDefinitionsData.replace(
|
|
|
|
`] as const satisfies ForgedBlock['type'][]`,
|
|
|
|
`'${id}'] as const satisfies ForgedBlock['type'][]`
|
|
|
|
),
|
|
|
|
{
|
|
|
|
parser: 'typescript',
|
|
|
|
...prettierRc,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function addBlockToRepoPackageJson(id: string) {
|
|
|
|
const schemasPath = join(process.cwd(), `../repository`)
|
|
|
|
const packageJson = require(join(schemasPath, 'package.json'))
|
|
|
|
packageJson.devDependencies[`@typebot.io/${id}-block`] = 'workspace:*'
|
|
|
|
writeFileSync(
|
|
|
|
join(schemasPath, 'package.json'),
|
|
|
|
await prettier.format(JSON.stringify(packageJson, null, 2), {
|
|
|
|
parser: 'json',
|
|
|
|
...prettierRc,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
return schemasPath
|
|
|
|
}
|
|
|
|
|
2023-12-13 10:22:02 +01:00
|
|
|
main()
|
|
|
|
.then()
|
|
|
|
.catch((err) => {
|
|
|
|
console.error(err)
|
|
|
|
process.exit(1)
|
|
|
|
})
|