UNPKG

embedia

Version:

Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys

305 lines (270 loc) 9.4 kB
const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generate = require('@babel/generator').default; const t = require('@babel/types'); /** * ImprovedASTModifier - Simpler, more robust layout modification * Focuses on app router layout files with better error handling */ class ImprovedASTModifier { parseFile(content, isTypeScript = false) { try { return parser.parse(content, { sourceType: 'module', plugins: [ 'jsx', isTypeScript && 'typescript', isTypeScript && 'tsx', 'decorators-legacy', 'dynamicImport', 'classProperties', 'optionalChaining', 'nullishCoalescingOperator', 'exportDefaultFrom' ].filter(Boolean) }); } catch (error) { throw new Error(`Failed to parse file: ${error.message}`); } } async modifyAppRouterLayout(content, isTypeScript) { const ast = this.parseFile(content, isTypeScript); let modified = false; let hasScriptImport = false; let layoutFunctionName = null; // Step 1: Check for Script import traverse(ast, { ImportDeclaration(path) { if (path.node.source.value === 'next/script') { const specifiers = path.node.specifiers; hasScriptImport = specifiers.some(spec => t.isImportDefaultSpecifier(spec) && spec.local.name === 'Script' ); } } }); // Step 2: Find the RootLayout function traverse(ast, { // Handle: export default function RootLayout ExportDefaultDeclaration(path) { const declaration = path.node.declaration; if (t.isFunctionDeclaration(declaration)) { layoutFunctionName = declaration.id?.name || 'RootLayout'; modified = modifyLayoutFunction(declaration); } }, // Handle: function RootLayout ... export default RootLayout FunctionDeclaration(path) { if (path.node.id?.name?.includes('Layout')) { layoutFunctionName = path.node.id.name; // Check if this function is exported const binding = path.scope.getBinding(layoutFunctionName); if (binding?.referencePaths.some(ref => { const parent = ref.findParent(p => t.isExportDefaultDeclaration(p.node)); return parent != null; })) { modified = modifyLayoutFunction(path.node); } } }, // Handle: const RootLayout = () => ... VariableDeclarator(path) { if (path.node.id?.name?.includes('Layout') && (t.isArrowFunctionExpression(path.node.init) || t.isFunctionExpression(path.node.init))) { layoutFunctionName = path.node.id.name; modified = modifyLayoutFunction(path.node.init); } } }); if (!modified) { throw new Error('Could not find layout function to modify'); } // Step 3: Add Script import if needed if (!hasScriptImport) { addScriptImport(ast); } // Generate the modified code const output = generate(ast, { retainLines: false, // Don't retain lines for cleaner output comments: true, jsescOption: { quotes: 'single' } }); return output.code; // Helper function to modify layout function function modifyLayoutFunction(functionNode) { let foundBody = false; traverse(functionNode, { JSXElement(path) { const node = path.node; // Look for body element if (t.isJSXIdentifier(node.openingElement.name, { name: 'body' })) { foundBody = true; // Check if already has embedia-chat-root const hasEmbedia = node.children.some(child => { if (t.isJSXElement(child)) { const id = child.openingElement.attributes.find(attr => t.isJSXAttribute(attr) && attr.name.name === 'id' ); return id?.value?.value === 'embedia-chat-root'; } return false; }); if (!hasEmbedia) { // Add comment const comment = t.jsxText('\n {/* Embedia Chat Integration */}\n '); // Add div const chatDiv = t.jsxElement( t.jsxOpeningElement( t.jsxIdentifier('div'), [t.jsxAttribute(t.jsxIdentifier('id'), t.stringLiteral('embedia-chat-root'))], true ), null, [], true ); // Add Script const scriptElement = createScriptElement(); // Insert before closing body tag node.children.splice(-1, 0, comment, chatDiv, t.jsxText('\n '), scriptElement, t.jsxText('\n ')); path.stop(); // Stop traversing once we've made changes } } } }, functionNode); return foundBody; } // Helper to create Script element function createScriptElement() { return t.jsxElement( t.jsxOpeningElement( t.jsxIdentifier('Script'), [ t.jsxAttribute( t.jsxIdentifier('id'), t.stringLiteral('embedia-chat-script') ), t.jsxAttribute( t.jsxIdentifier('strategy'), t.stringLiteral('afterInteractive') ), t.jsxAttribute( t.jsxIdentifier('dangerouslySetInnerHTML'), t.jsxExpressionContainer( t.objectExpression([ t.objectProperty( t.identifier('__html'), t.templateLiteral([ t.templateElement({ raw: ` if (typeof window !== 'undefined') { import('/components/generated/embedia-chat/index.js').then((module) => { const EmbediaChat = module.default || module.EmbediaChat; // Simplified mounting logic }).catch(console.error); } `, cooked: ` if (typeof window !== 'undefined') { import('/components/generated/embedia-chat/index.js').then((module) => { const EmbediaChat = module.default || module.EmbediaChat; // Simplified mounting logic }).catch(console.error); } ` }) ], []) ) ]) ) ) ], true ), null, [], true ); } // Helper to add Script import function addScriptImport(ast) { let lastImportIndex = -1; let hasReactImport = false; ast.program.body.forEach((node, index) => { if (t.isImportDeclaration(node)) { lastImportIndex = index; if (node.source.value === 'react') { hasReactImport = true; } } }); const scriptImport = t.importDeclaration( [t.importDefaultSpecifier(t.identifier('Script'))], t.stringLiteral('next/script') ); if (lastImportIndex >= 0) { ast.program.body.splice(lastImportIndex + 1, 0, scriptImport); } else { ast.program.body.unshift(scriptImport); } } } /** * Create a simpler integration approach using client component */ createSimpleIntegration() { return `'use client' import { useEffect } from 'react' export default function EmbediaChatLoader() { useEffect(() => { if (typeof window !== 'undefined') { import('/components/generated/embedia-chat/index.js').then((module) => { const EmbediaChat = module.default || module.EmbediaChat; if (EmbediaChat && // Simplified web component logic const container = document.getElementById('embedia-chat-root'); if (container && !container.hasChildNodes()) { const root = window. } } }).catch(console.error); } }, []); return <div id="embedia-chat-root" />; }`; } /** * Generate manual integration instructions with simpler approach */ getSimpleManualInstructions() { return `To integrate Embedia Chat, you have two options: Option 1: Add to your layout.js/tsx file: 1. Import the loader component at the top: import EmbediaChatLoader from '@/components/EmbediaChatLoader' 2. Add before closing </body> tag: <EmbediaChatLoader /> Option 2: Direct integration in layout: 1. Import Script at the top: import Script from 'next/script' 2. Add before closing </body> tag: <embedia-chatbot></embedia-chatbot> <Script id="embedia-chat-ast-loader" strategy="afterInteractive" dangerouslySetInnerHTML={{ __html: \` import('/components/generated/embedia-chat/index.js').then((module) => { const EmbediaChat = module.default; const container = document.getElementById('embedia-chat-root'); if (container) { const root = window. } }); \` }} />`; } } module.exports = ImprovedASTModifier;