embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
232 lines (205 loc) • 7.22 kB
JavaScript
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
class ASTModifier {
parseFile(content, isTypeScript = false) {
return parser.parse(content, {
sourceType: 'module',
plugins: [
'jsx',
isTypeScript && 'typescript',
'decorators-legacy',
'dynamicImport',
'classProperties',
'optionalChaining',
'nullishCoalescingOperator'
].filter(Boolean)
});
}
async modifyLayout(layoutPath, content, isTypeScript, isAppRouter = true) {
const ast = this.parseFile(content, isTypeScript);
let modified = false;
let needsScriptImport = false;
// Check if Script is already imported from next/script
let hasScriptImport = false;
traverse(ast, {
ImportDeclaration(path) {
if (path.node.source.value === 'next/script') {
hasScriptImport = true;
}
}
});
traverse(ast, {
// Find the default export function
ExportDefaultDeclaration(path) {
const declaration = path.node.declaration;
if (t.isFunctionDeclaration(declaration) ||
t.isArrowFunctionExpression(declaration) ||
t.isFunctionExpression(declaration)) {
// Find the return statement with JSX
traverse(declaration, {
ReturnStatement(returnPath) {
const jsxElement = returnPath.node.argument;
if (t.isJSXElement(jsxElement) || t.isJSXFragment(jsxElement)) {
// Inject Embedia chat before closing body tag
const result = this.injectEmbediaChat(jsxElement, isAppRouter);
modified = result.modified;
needsScriptImport = result.needsScriptImport && !hasScriptImport;
}
}
}, path.scope);
}
}
});
if (modified) {
// Add Script import if needed
if (needsScriptImport && isAppRouter) {
this.addScriptImport(ast);
}
const output = generate(ast, {
retainLines: true,
comments: true
});
return output.code;
}
throw new Error('Could not modify layout file - unable to find suitable injection point');
}
injectEmbediaChat(jsxElement, isAppRouter) {
// Find the body element
const bodyElement = this.findJSXElement(jsxElement, 'body');
if (bodyElement) {
// Create the Embedia integration nodes
const embediaNodes = isAppRouter
? this.createEmbediaNodesAppRouter()
: this.createEmbediaNodesPagesRouter();
// Add nodes before closing body tag
bodyElement.children.push(...embediaNodes);
return { modified: true, needsScriptImport: isAppRouter };
}
return { modified: false, needsScriptImport: false };
}
createEmbediaNodesAppRouter() {
// Create div for chat root
const chatRoot = t.jsxElement(
t.jsxOpeningElement(
t.jsxIdentifier('div'),
[t.jsxAttribute(t.jsxIdentifier('id'), t.stringLiteral('embedia-chat-root'))],
true
),
null,
[],
true
);
// Create Script component for integration
const scriptElement = t.jsxElement(
t.jsxOpeningElement(
t.jsxIdentifier('Script'),
[
t.jsxAttribute(
t.jsxIdentifier('id'),
t.stringLiteral('embedia-chat-integration')
),
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;
if (EmbediaChat && // Simplified web component logic
const container = document.getElementById('embedia-chat-root');
if (container) {
const root = window.
}
}
}).catch(console.error);
}
`,
cooked: `
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) {
const root = window.
}
}
}).catch(console.error);
}
`
})
], [])
)
])
)
)
],
true
),
null,
[],
true
);
// Add comment for clarity
const comment = t.jsxExpressionContainer(
t.jsxEmptyExpression()
);
comment.innerComments = [{
type: 'CommentBlock',
value: ' Embedia Chat Integration '
}];
return [comment, chatRoot, scriptElement];
}
createEmbediaNodesPagesRouter() {
// For pages router, we'll use a different approach with useEffect
// This will be handled differently in the pages router integration
return [];
}
findJSXElement(node, elementName) {
if (t.isJSXElement(node) &&
t.isJSXIdentifier(node.openingElement.name, { name: elementName })) {
return node;
}
if (node.children) {
for (const child of node.children) {
if (t.isJSXElement(child)) {
const found = this.findJSXElement(child, elementName);
if (found) return found;
}
}
}
return null;
}
addScriptImport(ast) {
// Find existing imports
let lastImportIndex = -1;
ast.program.body.forEach((node, index) => {
if (t.isImportDeclaration(node)) {
lastImportIndex = index;
}
});
// Create Script import
const scriptImport = t.importDeclaration(
[t.importDefaultSpecifier(t.identifier('Script'))],
t.stringLiteral('next/script')
);
// Add after last import or at the beginning
if (lastImportIndex >= 0) {
ast.program.body.splice(lastImportIndex + 1, 0, scriptImport);
} else {
ast.program.body.unshift(scriptImport);
}
}
}
module.exports = ASTModifier;