first commit
This commit is contained in:
55
vite-plugins/ErrorOverlay.js
Normal file
55
vite-plugins/ErrorOverlay.js
Normal file
@@ -0,0 +1,55 @@
|
||||
// Make HTMLElement available in non-browser environments
|
||||
const { HTMLElement = class {} } = globalThis;
|
||||
|
||||
export class ErrorOverlay extends HTMLElement {
|
||||
static getOverlayHTML() {
|
||||
return `
|
||||
<div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
close() {
|
||||
this.parentNode?.removeChild(this);
|
||||
}
|
||||
|
||||
static sendErrorToParent(error, title, details, componentName) {
|
||||
// Send error to parent using framewire
|
||||
if (globalThis.window?.parent) {
|
||||
|
||||
try {
|
||||
globalThis.window.parent?.postMessage({
|
||||
type: "app_error",
|
||||
error: { title, details, componentName, originalError: error }
|
||||
}, "*");
|
||||
} catch (error) {
|
||||
console.warn('Failed to send error to iframe parent:', error?.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(error) {
|
||||
super(error)
|
||||
|
||||
const stack = error?.stack;
|
||||
let componentName = stack?.match(/at\s+(\w+)\s+\(eval/)?.[1];
|
||||
if (componentName === 'eval') {
|
||||
componentName = null;
|
||||
}
|
||||
const title = componentName ? `in ${componentName}: ${error.message?.toString()}` : error.message?.toString();
|
||||
const details = JSON.stringify(error);
|
||||
|
||||
// Call editor frame with the error (via post message)
|
||||
ErrorOverlay.sendErrorToParent(error, title, details, componentName);
|
||||
|
||||
// Create the overlay element using HTML template
|
||||
const overlay = document.createElement('div');
|
||||
overlay.innerHTML = ErrorOverlay.getOverlayHTML();
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// vite/react-plugin transpiles classes with _SomeClass, so we need to replace all _ErrorOverlay with ErrorOverlay
|
||||
export const errorOverlayCode = ErrorOverlay.toString().replaceAll('_ErrorOverlay', 'ErrorOverlay');
|
||||
17
vite-plugins/error-overlay-plugin.js
Normal file
17
vite-plugins/error-overlay-plugin.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { errorOverlayCode } from './ErrorOverlay.js';
|
||||
|
||||
|
||||
export function errorOverlayPlugin() {
|
||||
return {
|
||||
name: 'error-overlay',
|
||||
transform(code, id, opts = {}) {
|
||||
if (opts?.ssr) return;
|
||||
|
||||
if (!id.includes('vite/dist/client/client.mjs')) return;
|
||||
|
||||
console.log("🔥 [custom-error-overlay] patching overlay", id);
|
||||
|
||||
return code.replace('class ErrorOverlay', errorOverlayCode + '\nclass OldErrorOverlay');
|
||||
}
|
||||
}
|
||||
}
|
||||
248
vite-plugins/visual-edit-plugin.js
Normal file
248
vite-plugins/visual-edit-plugin.js
Normal file
@@ -0,0 +1,248 @@
|
||||
import { parse } from '@babel/parser';
|
||||
import { default as traverse } from '@babel/traverse';
|
||||
import { default as generate } from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
|
||||
// Helper function to check if JSX element contains dynamic content
|
||||
function checkIfElementHasDynamicContent(jsxElement) {
|
||||
let hasDynamicContent = false;
|
||||
|
||||
// Helper function to check if any node contains dynamic patterns
|
||||
function checkNodeForDynamicContent(node) {
|
||||
// JSX expressions like {variable}, {func()}, {obj.prop}
|
||||
if (t.isJSXExpressionContainer(node)) {
|
||||
const expression = node.expression;
|
||||
|
||||
// Skip empty expressions {}
|
||||
if (t.isJSXEmptyExpression(expression)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Any non-literal expression is considered dynamic
|
||||
if (!t.isLiteral(expression)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Template literals with expressions `Hello ${name}`
|
||||
if (t.isTemplateLiteral(node) && node.expressions.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Member expressions like props.title, state.value
|
||||
if (t.isMemberExpression(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Function calls like getData(), format()
|
||||
if (t.isCallExpression(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Conditional expressions like condition ? "yes" : "no"
|
||||
if (t.isConditionalExpression(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Identifier references (could be props, state, variables)
|
||||
if (t.isIdentifier(node)) {
|
||||
// Common dynamic identifiers
|
||||
const dynamicNames = ['props', 'state', 'data', 'item', 'value', 'text', 'content'];
|
||||
if (dynamicNames.some(name => node.name.includes(name))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recursively traverse all child nodes
|
||||
function traverseNode(node) {
|
||||
if (checkNodeForDynamicContent(node)) {
|
||||
hasDynamicContent = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Recursively check child nodes
|
||||
Object.keys(node).forEach(key => {
|
||||
const value = node[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(child => {
|
||||
if (child && typeof child === 'object' && child.type) {
|
||||
traverseNode(child);
|
||||
}
|
||||
});
|
||||
} else if (value && typeof value === 'object' && value.type) {
|
||||
traverseNode(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check all children of the JSX element
|
||||
jsxElement.children.forEach(child => {
|
||||
if (hasDynamicContent) return; // Early exit if already found dynamic content
|
||||
traverseNode(child);
|
||||
});
|
||||
|
||||
return hasDynamicContent;
|
||||
}
|
||||
|
||||
export function visualEditPlugin() {
|
||||
return {
|
||||
name: 'visual-edit-transform',
|
||||
enforce: 'pre',
|
||||
order: 'pre',
|
||||
// Inject Tailwind CDN for visual editing capabilities
|
||||
transformIndexHtml(html) {
|
||||
// Inject the Tailwind CSS CDN script right before the closing </head> tag
|
||||
const tailwindScript = ` <!-- Tailwind CSS CDN for visual editing -->\n <script src="https://cdn.tailwindcss.com"></script>\n `;
|
||||
return html.replace('</head>', tailwindScript + '</head>');
|
||||
},
|
||||
transform(code, id) {
|
||||
// Skip node_modules and visual-edit-agent itself
|
||||
if (id.includes('node_modules') || id.includes('visual-edit-agent')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Process JS/JSX/TS/TSX files
|
||||
if (!id.match(/\.(jsx?|tsx?)$/)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[Visual Edit Plugin] Processing file: ${id}`);
|
||||
|
||||
// Extract filename from path, preserving pages/ or components/ structure
|
||||
const pathParts = id.split('/');
|
||||
let filename;
|
||||
|
||||
// Check if this is a pages or components file
|
||||
if (id.includes('/pages/')) {
|
||||
const pagesIndex = pathParts.findIndex(part => part === 'pages');
|
||||
if (pagesIndex >= 0 && pagesIndex < pathParts.length - 1) {
|
||||
// Get all parts from 'pages' to the file, preserving nested structure
|
||||
const relevantParts = pathParts.slice(pagesIndex, pathParts.length);
|
||||
const lastPart = relevantParts[relevantParts.length - 1];
|
||||
// Remove file extension from the last part
|
||||
relevantParts[relevantParts.length - 1] = lastPart.includes('.') ? lastPart.split('.')[0] : lastPart;
|
||||
filename = relevantParts.join('/');
|
||||
} else {
|
||||
filename = pathParts[pathParts.length - 1];
|
||||
if (filename.includes('.')) {
|
||||
filename = filename.split('.')[0];
|
||||
}
|
||||
}
|
||||
} else if (id.includes('/components/')) {
|
||||
const componentsIndex = pathParts.findIndex(part => part === 'components');
|
||||
if (componentsIndex >= 0 && componentsIndex < pathParts.length - 1) {
|
||||
// Get all parts from 'components' to the file, preserving nested structure
|
||||
const relevantParts = pathParts.slice(componentsIndex, pathParts.length);
|
||||
const lastPart = relevantParts[relevantParts.length - 1];
|
||||
// Remove file extension from the last part
|
||||
relevantParts[relevantParts.length - 1] = lastPart.includes('.') ? lastPart.split('.')[0] : lastPart;
|
||||
filename = relevantParts.join('/');
|
||||
} else {
|
||||
filename = pathParts[pathParts.length - 1];
|
||||
if (filename.includes('.')) {
|
||||
filename = filename.split('.')[0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other files (like layout), just use the filename
|
||||
filename = pathParts[pathParts.length - 1];
|
||||
if (filename.includes('.')) {
|
||||
filename = filename.split('.')[0];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse the code into an AST
|
||||
const ast = parse(code, {
|
||||
sourceType: 'module',
|
||||
plugins: [
|
||||
'jsx',
|
||||
'typescript',
|
||||
'decorators-legacy',
|
||||
'classProperties',
|
||||
'objectRestSpread',
|
||||
'functionBind',
|
||||
'exportDefaultFrom',
|
||||
'exportNamespaceFrom',
|
||||
'dynamicImport',
|
||||
'nullishCoalescingOperator',
|
||||
'optionalChaining',
|
||||
'asyncGenerators',
|
||||
'bigInt',
|
||||
'optionalCatchBinding',
|
||||
'throwExpressions'
|
||||
],
|
||||
});
|
||||
|
||||
// Traverse the AST and add source location and dynamic content attributes to JSX elements
|
||||
let elementsProcessed = 0;
|
||||
traverse.default(ast, {
|
||||
JSXElement(path) {
|
||||
const jsxElement = path.node;
|
||||
const openingElement = jsxElement.openingElement;
|
||||
|
||||
// Skip fragments
|
||||
if (t.isJSXFragment(jsxElement)) return;
|
||||
|
||||
// Skip if already has source location attribute
|
||||
const hasSourceLocation = openingElement.attributes.some(attr =>
|
||||
t.isJSXAttribute(attr) &&
|
||||
t.isJSXIdentifier(attr.name) &&
|
||||
attr.name.name === 'data-source-location'
|
||||
);
|
||||
|
||||
if (hasSourceLocation) return;
|
||||
|
||||
// Get line and column from AST node location
|
||||
const { line, column } = openingElement.loc?.start || { line: 1, column: 0 };
|
||||
|
||||
// Create the source location attribute
|
||||
const sourceLocationAttr = t.jsxAttribute(
|
||||
t.jsxIdentifier('data-source-location'),
|
||||
t.stringLiteral(`${filename}:${line}:${column}`)
|
||||
);
|
||||
|
||||
// Check if element has dynamic content
|
||||
const isDynamic = checkIfElementHasDynamicContent(jsxElement);
|
||||
|
||||
// Create the dynamic content attribute
|
||||
const dynamicContentAttr = t.jsxAttribute(
|
||||
t.jsxIdentifier('data-dynamic-content'),
|
||||
t.stringLiteral(isDynamic ? 'true' : 'false')
|
||||
);
|
||||
|
||||
// Add both attributes to the beginning of the attributes array
|
||||
openingElement.attributes.unshift(sourceLocationAttr, dynamicContentAttr);
|
||||
elementsProcessed++;
|
||||
}
|
||||
});
|
||||
|
||||
// Generate the code back from the AST
|
||||
const result = generate.default(ast, {
|
||||
compact: false,
|
||||
concise: false,
|
||||
retainLines: true
|
||||
});
|
||||
|
||||
console.log(`[Visual Edit Plugin] Successfully transformed ${id} - processed ${elementsProcessed} JSX elements`);
|
||||
|
||||
return {
|
||||
code: result.code,
|
||||
map: null
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to add source location to JSX:', error);
|
||||
return {
|
||||
code: code, // Return original code on failure
|
||||
map: null
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user