embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
305 lines (270 loc) • 9.4 kB
JavaScript
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'),
[],
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.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;