/* eslint-disable max-len */ import * as path from 'path' import { execSync } from 'child_process' import fs from 'fs' import colors from 'colors' import requireFromString from 'require-from-string' import lineByLine from 'n-readlines' import * as ts from 'typescript' import { IndexState } from '../types' import { esbuildNodeVersion, execConfig, log, tickLog, } from '../helpers' export const cleanDistFolder = async () => { fs.rmSync('./dist/', { recursive: true, force: true }) fs.mkdirSync('./dist/') } export const getIndexFileEndpoints = async () => { const indexFile = './API/index.ts' if (!fs.existsSync(indexFile)) { console.info('Error >> No Index file. This is a fundamental issue of wappi. Please undo the deletion of this file.') process.exit(0) } const indexStringContents = fs.readFileSync(indexFile, { encoding: 'utf8', flag: 'r' }) const indexContents = requireFromString(indexStringContents) const endpoints = Object.keys(indexContents.Endpoints) const validEndpoints = endpoints.filter((e: string) => !e.includes('Core')) const endpointLogList = endpoints.join('\r\n') const endpointPaths = endpoints.map((endpoint) => { const x = endpoint.split('_') return `${x[0]}/${x[1]}/${x[2]}.${x[3]}.${x[4]}` }) return { validEndpoints, endpoints, endpointLogList, endpointPaths, } } export const installNodeModules = async () => { // Check If Node Modules Exist // If they don't, install them // copy the packageJson and install only production packages fs.cpSync('./package.json', './dist/package.json') const output = execSync( 'cd dist && npm install --omit=dev', execConfig, ) console.info(output) } export const buildAPIFolder = async (state: IndexState) => { const startTime = new Date() const { endpointLogList } = await getIndexFileEndpoints() const apiFolderCount: number = parseInt(execSync('ls -l ./API/ | grep -c ^d', execConfig), 10) if (apiFolderCount < 3) console.info(colors.red("\nYou haven't added any endpoints yet")) const showLogs = state.devMode ? ' --log-level=verbose' : ' --log-level=silent' const buildAPI = execSync( `esbuild ./API/*/*/*/index.ts --bundle --outdir=./dist/API/ --platform=node --target=${esbuildNodeVersion} --minify-whitespace --packages=external${showLogs};`, execConfig, ) log(buildAPI, 'green', state) const buildIndex = execSync( `esbuild ./API/index.ts --outfile=./dist/API/index.js${showLogs};`, execConfig, ) log(buildIndex, 'green', state) log('\nEndpoints Built:', 'green', state) const elapsedSeconds = (new Date().getTime() - startTime.getTime()) tickLog(`Compiled ${apiFolderCount} endpoints in ${elapsedSeconds}ms`, 'green', 'white', state, true) log(endpointLogList, 'green', state) } const wappiTempServerPath = './dist/temp/server' export const copyTemplateServerFilesToDistTemp = async () => { if (fs.existsSync(wappiTempServerPath)) fs.rmSync(wappiTempServerPath, { recursive: true }) fs.mkdirSync('./dist/temp') fs.mkdirSync('./dist/temp/server') fs.cpSync(`${__dirname}/Templates/Server`, './dist/temp/server/', { recursive: true }) } export const buildServer = async () => { const external = '"--external:@prisma/client"' execSync( `esbuild ./dist/temp/server/ServerUtils/*/index.ts --bundle --outdir=./dist/server/ServerUtils --platform=node --target=${esbuildNodeVersion} --minify-whitespace --log-level=error ${external}`, execConfig, ) execSync( `esbuild ./dist/temp/server/Config/* --bundle --outdir=./dist/server/Config --platform=node --target=${esbuildNodeVersion} --minify-whitespace --log-level=error ${external}`, execConfig, ) execSync( `esbuild ./dist/temp/server/*.ts --bundle --outdir=./dist/server/ --platform=node --target=${esbuildNodeVersion} --minify-whitespace --log-level=error ${external}`, execConfig, ) } export const buildPrismaFiles = async () => { fs.cpSync('./prisma/schema.prisma', './dist/server/schema.prisma') execSync('npx prisma generate', execConfig) execSync('npx prisma generate --schema=./dist/server/schema.prisma', execConfig) } export const cleanup = async () => { if (fs.existsSync(wappiTempServerPath)) fs.rmSync(wappiTempServerPath, { recursive: true }) } export const copyDotEnvFile = async () => { if (!fs.existsSync('./.env')) { console.info(colors.red('\nERROR:: \nNo `.env` file found')) console.info(colors.yellow('\nFix: \nCopy the `.env.example` and change the name of it to `.env`')) process.exit(1) } fs.copyFileSync('./.env', './dist/server/.env') } export const copyWappiConfigFile = async () => fs.copyFileSync('./wappi.json', './dist/server/wappi.json') export const functionNameToPath = (functionName: string) => { const f = functionName.split('_') return `${f[0]}/${f[1]}/${f[2]}.${f[3]}.${f[4]}` } const readTypesFile = async (endpoint: string, typesFilePath: string): Promise => { // eslint-disable-next-line new-cap const liner = new lineByLine(typesFilePath) let fileContents = '' let line: boolean | Buffer const state: any = { successResponse: [`export interface ${endpoint}__SuccessResponse {`], successResponseStart: false, successResponseAdded: false, errorResponse: [`export interface ${endpoint}__ErrorResponse {`], errorResponseStart: false, errorResponseAdded: false, body: [`export interface ${endpoint}__Body {`], bodyStart: false, bodyAdded: false, } // eslint-disable-next-line no-cond-assign while (line = liner.next()) { const lineText = line.toString('ascii') const lineToAdd = `${lineText}` if (lineText === 'export interface SuccessResponse {') { state.successResponseStart = true } else if (state.successResponseStart) { state.successResponse.push(lineToAdd) } if (lineText === 'export interface ErrorResponse {') { state.errorResponseStart = true } else if (state.errorResponseStart) { state.errorResponse.push(lineToAdd) } if (lineText === 'export interface EndPointBody {') { state.bodyStart = true } else if (state.bodyStart) { state.body.push(lineToAdd) } if (lineText === 'export interface EndPointBody {}') { fileContents += `${state.body.join('\n')}}\n\n` } if (lineText === '}') { if (!state.successResponseAdded && state.successResponseStart) { fileContents += `${state.successResponse.join('\n')}\n\n` state.successResponseAdded = true state.successResponseStart = false } if (!state.errorResponseAdded && state.errorResponseStart) { fileContents += `${state.errorResponse.join('\n')}\n\n` state.errorResponseAdded = true state.errorResponseStart = false } if (!state.bodyAdded && state.bodyStart) { fileContents += `${state.body.join('\n')}\n\n` state.bodyAdded = true state.bodyStart = false } } } return fileContents } type ExtractedType = { key: string type: string } const extractAllTypeDefinitions = async (filePath: string): Promise => { const fileContent = await fs.readFileSync(filePath, 'utf8') // Create a SourceFile const sourceFile = ts.createSourceFile( filePath, fileContent, ts.ScriptTarget.Latest, true, ) const types: ExtractedType[] = [] const visit = (node: ts.Node) => { // Define node types to extract if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) { const name = node.name.getText(sourceFile) const start = node.getStart(sourceFile, false) const end = node.getEnd() const typeText = fileContent.substring(start, end) types.push({ key: name, type: typeText, }) } ts.forEachChild(node, visit) } // Start AST traversal from the root node visit(sourceFile) return types } // const extractAllTypeDefinitions = async (filePath: string): Promise => { // try { // // Read the file content asynchronously // const content = fs.readFileSync(filePath, 'utf8') // // Define a regex pattern to match type, interface, or enum definitions // // This pattern looks for "export interface", "export type", or "export enum" followed by the name, and captures until it reaches a closing brace. // // It assumes that the definitions are not nested and are well-formed. // const pattern = /(?:export )?(interface|type|enum) (\w+)(?: extends [\w<>,\s]+)? \{[\s\S]*?\n\}/gm // let match: any[] | null // const types: ExtractedType[] = [] // // eslint-disable-next-line no-cond-assign // while ((match = pattern.exec(content)) !== null) { // types.push({ // key: match[2], // The type, interface, or enum name // type: match[0], // The full type definition as a string // }) // } // console.log('types', types) // if (types.length === 0) { // console.info(`No type definitions found in ${filePath}`) // return [] // } // return types // } catch (error) { // console.error('Error reading file or extracting types:', error) // throw error // } // } type TypeDefinition = { key: string; type: string; }; type TypeDefinitionObj = { key: string; type: string; count: number; }; type TypeDefinitionObject = { [key: string]: TypeDefinitionObj; }; const sortDependantTypesInOrder = (types: TypeDefinition[]): TypeDefinitionObj[] => { // Define the keys to exclude from the final result const mainWappiKeys = new Set(['EndPointBody', 'SuccessResponse', 'ErrorResponse']) const allWappiKeys = new Set([...mainWappiKeys, 'Response', 'CreateLogProps', 'DB', 'PrismaDB', 'WappiBaseNoAuthBody', 'WappiBaseAuthenticatedBody', 'Body']) // 1. Create a new object with every type const newTypes: TypeDefinitionObject = types.reduce((acc, type) => { acc[type.key] = { ...type, count: 0 } // Initialize count to 0 for each type return acc }, {} as TypeDefinitionObject) // 2. Iterate over each type definition and count references types.forEach((type) => { // Check each type for references in the other types types.forEach((otherType) => { // Create a regex to find references to 'type.key' that respects word boundaries const regex = new RegExp(`\\b${type.key}\\b`, 'g') const matches = (otherType.type.match(regex) || []).length if (matches) { newTypes[type.key].count += 1 } }) }) // 3. Delete all the wappi types from the object. As they are handled differently Object.keys(newTypes).forEach((key) => { if (allWappiKeys.has(key)) { delete newTypes[key] } }) // 4. Turn it back into an array. const typesArray: TypeDefinitionObj[] = Object.values(newTypes) // Function to check if `typeA` depends on `typeB` function dependsOn(typeA: TypeDefinitionObj, typeB: TypeDefinitionObj): boolean { return typeA.type.includes(typeB.key) } // Perform a simplistic "topological sort" based on dependencies // This is a naive approach and may not work for complex dependency graphs or handle cycles function sortTypesByDependency(typesToBeSorted: TypeDefinitionObj[]): TypeDefinitionObj[] { const sorted: TypeDefinitionObj[] = [] const visited: Record = {} function visit(type: TypeDefinitionObj) { if (visited[type.key]) return visited[type.key] = true // Find and visit all types that this type depends on first typesToBeSorted.forEach((depType) => { if (dependsOn(type, depType)) { visit(depType) } }) // After visiting dependencies, add this type to the sorted list sorted.push(type) } typesToBeSorted.forEach(visit) return sorted } // 5. Sort the object of imported types, to order them in dependency order. const sortedTypes = sortTypesByDependency(typesArray) return sortedTypes } const findErrorTypeInFile = (fileContents: string, errorType: string) => { const lines = fileContents.split('\n') let chunk: string[] = [] let foundChunk = false let errorChunk: string[] = [] lines.forEach((line: string) => { chunk.push(line) if (line.includes(errorType)) { foundChunk = true } else if (line === '}' && foundChunk) { errorChunk = chunk foundChunk = false chunk = [] } else if (line === '}') { chunk = [] } }) const response = errorChunk.join('\n') return response } const validationErrorHelp = (error: any, fileContents: string) => { if (error.includes('no-undef')) { const onlyError = error.split('\n').filter((o: string) => o.includes('no-undef')) const errorTypes = onlyError.map((eslintError: string) => eslintError.split("'")[1]) const errorChunks = errorTypes.map((type: string) => findErrorTypeInFile(fileContents, type)) console.info(colors.red.bold('\nThere is a types linting issue.')) console.info(colors.red.underline.bold('FIX THE FOLLOWING TYPES 🔨\n')) errorChunks.forEach((chunk: string) => { const chunkLines = chunk.split('\n') chunkLines.forEach((line: string) => { let isErrorLine = false errorTypes.forEach((errorType: string) => { if (line.includes(errorType)) isErrorLine = true }) if (isErrorLine) { console.info(colors.red(line)) } else { console.info(colors.yellow(line)) } }) }) } } async function findTypesTsFiles(dir: string, foundFiles: string[] = []): Promise { try { // Read the contents of the current directory const items = fs.readdirSync(dir, { withFileTypes: true }) // Await all recursive checks simultaneously for better performance await Promise.all(items.map(async (item) => { // Construct the full path of the current item const fullPath = path.join(dir, item.name) if (item.isDirectory()) { // If the item is a directory, recurse into it await findTypesTsFiles(fullPath, foundFiles) } else if (item.isFile() && item.name === 'types.ts') { // If the item is the target file, add it to the foundFiles array foundFiles.push(fullPath) } })) return foundFiles } catch (error) { console.error('Error reading directory:', error) throw error } } const buildTypeMappings = (endpoints) => { // Build the type mappings for wappi-fe let fileContents = '// wappi-fe TYPE MAPPINGS. TO MAKE THINGS FREAKING AWESOME!\n' // Create a BodyTypeMapping for every endpoint fileContents += '\nexport interface BodyTypeMapping {\n' endpoints.forEach((endpoint) => { fileContents += ` '${endpoint}': ${endpoint}__Body;\n` }) fileContents += '}\n' // Create a SuccessTypeMapping for every endpoint fileContents += '\nexport interface SuccessTypeMapping {\n' endpoints.forEach((endpoint) => { fileContents += ` '${endpoint}': ${endpoint}__SuccessResponse;\n` }) fileContents += '}\n' // Create a ErrorTypeMapping for every endpoint fileContents += '\nexport interface ErrorTypeMapping {\n' endpoints.forEach((endpoint) => { fileContents += ` '${endpoint}': ${endpoint}__ErrorResponse;\n` }) fileContents += '}\n' return fileContents } type PrismaEnumType = { key: string; type: string; }; const extractPrismaEnumsToTypes = (filePath: string): PrismaEnumType[] => { try { // Read the schema file content const content = fs.readFileSync(filePath, 'utf8') // Regular expression to match Prisma enum definitions const enumRegex = /enum (\w+) {\s*([^}]+)\s*}/g let match const types: PrismaEnumType[] = [] // eslint-disable-next-line no-cond-assign while ((match = enumRegex.exec(content)) !== null) { const enumName = match[1] // Split enum values correctly by newline or space, then trim and filter out empty lines const enumBodyLines = match[2].split(/\r?\n/) const enumValues = enumBodyLines .map((line) => { // Remove inline comments const commentIndex = line.indexOf('//') return commentIndex !== -1 ? line.substring(0, commentIndex) : line }) .map((val) => val.trim()) // Trim whitespace .filter((val) => val.length > 0) // Filter out empty lines // Construct the TypeScript type string correctly, excluding comments const typeString = `export type ${enumName} = '${enumValues.join("' | '")}';` types.push({ key: enumName, type: typeString }) } return types } catch (error) { console.error('Error reading file or extracting enums:', error) throw error } } export const buildTypesExport = async (state: IndexState, publicEndpoints?: boolean) => { // foreach Endpoint, pull the types out of the types file // Get all the EndPointBody types and add them to the namespace. // Make this file publicly available. // In the wappi-fe -> When running dev, pull this file into the filesystem. log('CI Pipeline - Installing all Node Modules', 'grey', state) const indexFile = './API/index.ts' if (!fs.existsSync(indexFile)) { console.info('Error >> No Index file. This is a fundamental issue of wappi. Please undo the deletion of this file.') return } const indexStringContents = fs.readFileSync(indexFile, { encoding: 'utf8', flag: 'r' }) const indexContents = requireFromString(indexStringContents) let endpoints = Object.keys(indexContents.Endpoints) const hasPublicEndpoints = !!indexContents.PublicApiEndpoint if (publicEndpoints && hasPublicEndpoints) { endpoints = Object.keys(indexContents.PublicApiEndpoint) } if (publicEndpoints && !hasPublicEndpoints) return let fileContents = '' const eslintIgnoresForTypesFile = [ '/* eslint-disable @typescript-eslint/no-empty-interface */\n', ] eslintIgnoresForTypesFile.forEach((ignore) => { fileContents += ignore }) const processFiles = async () => { let processFileChain: any = Promise.resolve() const allTypes: ExtractedType[] = [] // 1. Generate types from the prisma.schema file processFileChain.then(() => extractPrismaEnumsToTypes('prisma/schema.prisma')) .then((prismaTypes: any) => { allTypes.push(...prismaTypes) }) // 2. Get all the imported types from all the types files. const allTypesFileLocations = await findTypesTsFiles('API') allTypesFileLocations.forEach((file) => { processFileChain = processFileChain .then(() => extractAllTypeDefinitions(file)) .then((fileTypesArray) => { allTypes.push(...fileTypesArray) }) }) // 3. Get an array of ordered type dependencies ready to be added to the file contents. processFileChain.then(() => { fileContents += '\n// DEPENDENCY AND PRISMA ENUM TYPES\n' }) .then(() => sortDependantTypesInOrder(allTypes)) .then((orderedArrayOfDependentTypes: TypeDefinitionObj[]) => { // 3.1. Add Types to file contents orderedArrayOfDependentTypes.forEach((typeObj: TypeDefinitionObj) => { fileContents += `\n${typeObj.type}\n` }) }).then(() => { fileContents += '\n// WAPPI ENDPOINT TYPES\n\n' }) // 4. Process the types files as normal. endpoints.forEach((endpoint) => { processFileChain = processFileChain.then(() => { const typesFilePath = `./API/${functionNameToPath(endpoint)}/types.ts` if (!fs.existsSync(typesFilePath)) { console.info(`\n${colors.red.bold(`\nERROR:: Missing types.ts file in endpoint: API/${functionNameToPath(endpoint)}`)}\n\n`) console.info('Solutions:\n') console.info('1. Check that the endpoint exists. It\'s in the API/index.ts file.') console.info(' If it shouldn\'t be there, delete the line:') console.info(`\n ${endpoint}: true,\n`) console.info('2. Check the endpoint has a types file.') console.info(' If not? Add one.') console.info(' With the same format as all the other endpoint types files\n') process.exit(1) return '' } return readTypesFile(endpoint, typesFilePath) }).then((contents: string) => { fileContents += contents }) }) // 5. Create the typeMappings for Body, SuccessResponse & ErrorResponse processFileChain = processFileChain.then(() => { const contents = buildTypeMappings(endpoints) fileContents += contents }) return processFileChain } await processFiles() // remove last line of the file contents let typesFileName = './dist/API/types.ts' if (publicEndpoints) { typesFileName = './dist/API/publicTypes.ts' } fs.writeFileSync(typesFileName, `${fileContents.trim()}\n`) try { execSync( 'npx eslint ./dist/API/types.ts', execConfig, ) } catch (error) { const err = error.stdout.toString() validationErrorHelp(err, fileContents) } }