import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator' import type { DesignerDocument } from '@/components/visualDesigner/types' import type { CustomComponent } from '@/proxy/developerKit/models' import type React from 'react' export type RuntimeComponent = React.ComponentType> export interface CompiledBundle { registry: Record errors: Record } /** * Minimal surface of `@babel/standalone` used here. Declared locally so the * (multi megabyte) package stays out of the static import graph. */ export interface BabelLike { transform: (code: string, options: Record) => { code?: string | null } } const BABEL_PRESETS = ['react', 'typescript'] const RESERVED_WORDS = new Set([ 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', ]) /** Identifiers the generated bundle owns; scope entries may not shadow them. */ const INTERNAL_IDENTIFIERS = new Set(['componentRegistry', '__errors', '__resolve', '__define']) const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/ export const isUsableScopeKey = (key: string) => IDENTIFIER_PATTERN.test(key) && !RESERVED_WORDS.has(key) && !INTERNAL_IDENTIFIERS.has(key) export const toErrorMessage = (error: unknown) => error instanceof Error ? error.message : String(error) /** * Builds the scope injected into every runtime component. Everything the UI kit * exports is exposed automatically, so a newly added UI component is usable from * custom components without touching this file. */ export const buildRuntimeScope = (modules: Record) => { const scope: Record = {} for (const [key, value] of Object.entries(modules)) { if (value === undefined || !isUsableScopeKey(key)) continue scope[key] = value } return scope } export const parseComponentDependencies = (raw?: string | null): string[] => { if (!raw?.trim()) return [] const value = raw.trim() try { if (value.startsWith('[')) { const parsed: unknown = JSON.parse(value) return Array.isArray(parsed) ? parsed.filter((item): item is string => !!item) : [] } } catch (error) { console.warn('Component dependencies could not be parsed:', error) return [] } return value .split(',') .map((item) => item.trim()) .filter(Boolean) } /** * Visual designer documents are the source of truth when present; otherwise the * hand written code is used. */ export const getComponentRuntimeCode = (component: CustomComponent) => { if (!component) return '' try { const props: unknown = component.props ? JSON.parse(component.props) : null const document = (props as { visualDesigner?: DesignerDocument } | null)?.visualDesigner if ( document?.version === 1 && document.sourceMode === 'visual' && Array.isArray(document.nodes) ) { return generateDesignerCode(component.name, document) } } catch (error) { console.warn(`Visual designer schema could not be read for ${component.name}:`, error) } return component.code } /** * Stored components run inside a shared function scope. Imports are supplied by * that scope and therefore cannot remain as nested ESM declarations. */ export const normalizeComponentCodeForBundle = (code: string) => code .replace(/^\s*import\s+(?:(?:type\s+)?[\s\S]*?\s+from\s+)?['"][^'"\r\n]+['"]\s*;?\s*$/gm, '') // Re-export statements have no meaning inside the shared scope. .replace(/^\s*export\s+(?:\*|\{[\s\S]*?\})\s*(?:from\s*['"][^'"\r\n]+['"])?\s*;?\s*$/gm, '') .replace(/\bexport\s+default\s+/g, '') // `export const Foo = ...` keeps its declaration, loses the modifier. .replace( /^(\s*)export\s+(?=(?:default\s+)?(?:async\s+)?(?:const|let|var|function|class)\b)/gm, '$1', ) .trim() export const toJavaScriptIdentifier = (value: string) => { const identifier = value.replace(/[^A-Za-z0-9_$]/g, '_') return /^[A-Za-z_$]/.test(identifier) ? identifier : `Component_${identifier}` } /** Finds the identifier the stored code assigns the component to. */ export const extractComponentInfo = (code: string, defaultName = 'DynamicComponent') => { const patterns = [ /const\s+([A-Za-z]\w*)\s*:\s*React\.FC/, /export\s+default\s+([A-Za-z]\w*)/, /function\s+([A-Za-z]\w*)/, /const\s+([A-Za-z]\w*)\s*=/, /class\s+([A-Za-z]\w*)/, ] for (const pattern of patterns) { const match = code.match(pattern) // `export default function Foo` would otherwise yield the `function` keyword. if (match && !RESERVED_WORDS.has(match[1])) return match[1] } return defaultName } /** * Cheap fingerprint of everything that influences compilation. Used to skip * recompilation when unrelated fields (description, audit data) change. */ export const getCompilationSignature = (components: CustomComponent[]) => components .filter((component) => component.isActive) .map((component) => `${component.id}|${component.name}|${component.code}|${component.props}`) .join('') interface ComponentSibling { name: string referenceName: string } interface ComponentBundleEntry extends ComponentSibling { nameCapitalized: string internalName: string transpiledCode: string } const transpileComponents = (components: CustomComponent[], Babel: BabelLike) => { const entries: ComponentBundleEntry[] = [] const siblings: ComponentSibling[] = [] const errors: Record = {} for (const component of components) { const name = component.name const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1) const referenceName = toJavaScriptIdentifier(nameCapitalized) const runtimeCode = getComponentRuntimeCode(component) // Recorded even when compilation fails, so components referencing a broken // sibling render a placeholder instead of throwing a ReferenceError. siblings.push({ name, referenceName }) try { // Transpiling per component (instead of one big pass) keeps a single // syntax error from taking down every other custom component. const transpiled = Babel.transform(normalizeComponentCodeForBundle(runtimeCode), { presets: BABEL_PRESETS, filename: `${referenceName}.tsx`, // The output is inlined into a function body, so any surviving module // syntax must fail here (attributable to this component) instead of // breaking the shared bundle at evaluation time. sourceType: 'script', }).code if (!transpiled) throw new Error('Boş derleme çıktısı') entries.push({ name, nameCapitalized, referenceName, internalName: extractComponentInfo(runtimeCode, referenceName), transpiledCode: transpiled, }) } catch (error) { errors[name] = toErrorMessage(error) console.error(`Custom component derlenemedi: ${name}`, error) } } return { entries, siblings, errors } } const buildBundleSource = ( entries: ComponentBundleEntry[], siblings: ComponentSibling[], scopeKeys: string[], seedErrors: Record, ) => { const definitions = entries .map((entry) => { const siblingNames = new Set() const siblingBindings = siblings .filter((other) => other.name !== entry.name) .filter((other) => { if (other.referenceName === entry.internalName || siblingNames.has(other.referenceName)) { return false } siblingNames.add(other.referenceName) return true }) // Resolved through the registry (not a captured variable) so definition // order never matters and partially failed bundles still cross-render. .map((other) => `const ${other.referenceName} = __resolve(${JSON.stringify(other.name)});`) .join('\n ') return ` try { __define(${JSON.stringify(entry.name)}, ${JSON.stringify(entry.nameCapitalized)}, function() { ${siblingBindings} ${entry.transpiledCode} return ${entry.internalName}; }); } catch (error) { __errors[${JSON.stringify(entry.name)}] = (error && error.message) || String(error); console.error('Custom component yüklenemedi: ${entry.name}', error); }` }) .join('\n') return ` (function(${scopeKeys.join(', ')}) { const { useState, useEffect, useCallback, useMemo, useRef, useReducer, useContext, createContext, memo, Fragment } = React; const componentRegistry = {}; const __errors = ${JSON.stringify(seedErrors)}; const __resolve = (name) => (props) => { const Resolved = componentRegistry[name]; if (Resolved) return React.createElement(Resolved, props); return React.createElement( 'div', { className: 'text-sm text-red-600' }, __errors[name] ? name + ': ' + __errors[name] : 'Component not found: ' + name, ); }; const __define = (name, capitalized, factory) => { const Component = factory(); componentRegistry[name] = Component; componentRegistry[capitalized] = Component; }; ${definitions} return { registry: componentRegistry, errors: __errors }; }) ` } /** * Compiles every active component into a single cross-referencing registry. * Failures are isolated per component: a broken component reports an error while * the rest keep working. */ export const compileComponentBundle = ( components: CustomComponent[], scope: Record, Babel: BabelLike, ): CompiledBundle => { const { entries, siblings, errors } = transpileComponents(components, Babel) if (!entries.length) return { registry: {}, errors } const scopeKeys = Object.keys(scope) const source = buildBundleSource(entries, siblings, scopeKeys, errors) try { // The bundle is a function expression whose parameters are the scope keys, // so the injected modules are ordinary locals inside every component. // Parenthesised on one line with `return`: a newline here would trigger // automatic semicolon insertion and yield `undefined`. const factory = new Function(`return (${source})`)() as (...args: unknown[]) => CompiledBundle const bundle = factory(...scopeKeys.map((key) => scope[key])) return { registry: bundle.registry, errors: { ...errors, ...bundle.errors } } } catch (error) { console.error('Custom component paketi çalıştırılamadı:', error) const message = toErrorMessage(error) for (const entry of entries) errors[entry.name] = message return { registry: {}, errors } } }