diff --git a/COMPILER_FIX_SUMMARY.md b/COMPILER_FIX_SUMMARY.md new file mode 100644 index 00000000..6ef4d1b2 --- /dev/null +++ b/COMPILER_FIX_SUMMARY.md @@ -0,0 +1,157 @@ +# Correction du Compilateur : Préservation des Variables defineProps et Imports + +## Problème Identifié + +Le compilateur CanvasEngine supprimait incorrectement : +1. Les constantes déclarées avec `defineProps()` +2. Les imports non utilisés dans le script + +TypeScript considérait ces éléments comme inutilisés dans le script et les supprimait, même s'ils étaient utilisés dans le template. + +### Exemple du problème : +```vue + + + + + + +``` + +## Solution Implémentée + +Modification du fichier `packages/compiler/index.ts` pour détecter et préserver : +1. Toutes les variables déclarées avec `defineProps()` +2. Tous les imports utilisés dans le template + +### Changements Apportés + +1. **Détection améliorée** : Utilisation de regex robustes pour capturer tous les types de déclarations `defineProps` et d'imports +2. **Préservation forcée** : Ajout de références aux variables et imports détectés pour empêcher TypeScript de les supprimer +3. **Support complet** : Gestion de tous les cas d'usage (destructuration, alias, valeurs par défaut, imports mixtes, etc.) + +### Code Modifié + +```typescript +// Extract ALL variables declared with defineProps to avoid TypeScript removing them +const definePropsRegex = /(?:const|let|var)\s+([^=]+?)\s*=\s*defineProps\s*\(/g; +const definePropsVars: string[] = []; +let match; + +while ((match = definePropsRegex.exec(scriptContent)) !== null) { + const declaration = match[1].trim(); + + if (declaration.startsWith('{') && declaration.endsWith('}')) { + // Destructured variables like: const { text, value } = defineProps() + const destructuredContent = declaration.slice(1, -1); + const destructuredVars = destructuredContent.split(',').map(v => { + const cleanVar = v.trim().split(':')[0].trim(); + return cleanVar; + }); + definePropsVars.push(...destructuredVars); + } else { + // Simple variable like: const props = defineProps() + definePropsVars.push(declaration); + } +} + +// Extract ALL imports to avoid TypeScript removing them when they're used in template but not in script +const importRegex = /import\s+(?:type\s+)?([^;]+?)\s+from\s+['"]([^'"]+)['"];?/g; +const importedVars: string[] = []; +let importMatch; + +while ((importMatch = importRegex.exec(scriptContent)) !== null) { + const importClause = importMatch[1].trim(); + + // Skip type-only imports + if (importMatch[0].includes('import type')) { + continue; + } + + // Handle different import patterns + if (importClause.includes('*')) { + // Namespace import: import * as module from 'module' + const namespaceMatch = importClause.match(/\*\s+as\s+(\w+)/); + if (namespaceMatch) { + importedVars.push(namespaceMatch[1]); + } + } else if (importClause.includes('{')) { + // Named imports (possibly with default): import Default, { Named1, Named2 } from 'module' + const parts = importClause.split('{'); + + // Check for default import before the brace + const beforeBrace = parts[0].trim(); + if (beforeBrace) { + const defaultImport = beforeBrace.replace(',', '').trim(); + if (defaultImport) { + importedVars.push(defaultImport); + } + } + + // Extract named imports + const namedPart = parts[1].replace('}', ''); + const namedImports = namedPart.split(',').map(v => { + const cleanVar = v.trim().split(' as ')[0].trim(); + return cleanVar; + }).filter(v => v); + importedVars.push(...namedImports); + } else { + // Default import only: import Component from 'module' + importedVars.push(importClause); + } +} + +// Reference all defineProps variables and imported variables so TypeScript doesn't remove them +let varRefs = ''; +if (definePropsVars.length > 0) { + varRefs += `;${definePropsVars.join(';')};`; +} +if (importedVars.length > 0) { + varRefs += `;${importedVars.join(';')};`; +} +scriptContent += FLAG_COMMENT + parsedTemplate + varRefs +``` + +## Cas d'Usage Supportés + +### Variables defineProps +✅ **Déclaration simple** : `const props = defineProps()` +✅ **Destructuration** : `const { text, value } = defineProps()` +✅ **Types TypeScript** : `const { text }: { text: string } = defineProps()` +✅ **Valeurs par défaut** : `const { text = "default" } = defineProps()` +✅ **Alias** : `const { text: displayText } = defineProps()` +✅ **Déclarations multiples** : Plusieurs `defineProps` dans le même fichier +✅ **let/var** : Support de `let` et `var` en plus de `const` + +### Imports +✅ **Import nommé** : `import { Component } from 'module'` +✅ **Import par défaut** : `import Component from 'module'` +✅ **Import namespace** : `import * as Module from 'module'` +✅ **Import avec alias** : `import { Component as MyComponent } from 'module'` +✅ **Imports mixtes** : `import Default, { Named1, Named2 } from 'module'` +✅ **Imports multiples** : Plusieurs imports dans le même fichier +❌ **Imports de types** : `import type { Props } from 'types'` (ignorés, comportement correct) + +## Résultat + +Désormais, toutes les variables déclarées avec `defineProps()` ET tous les imports utilisés dans le template sont correctement préservés dans le code généré sans être supprimés par TypeScript. + +### Avant la correction : +```javascript +// Variables et imports supprimés - erreur à l'exécution +// TypeError: MyComponent is not defined +// ReferenceError: text is not defined +``` + +### Après la correction : +```javascript +import { MyComponent } from 'components'; +// ... autres imports préservés + +var text = defineProps().text; +var props = defineProps(); +// Variables et imports préservés et utilisables dans le template +``` \ No newline at end of file diff --git a/docs/public/grammar.pegjs b/docs/public/grammar.pegjs index a11e9f2f..e5968c1c 100644 --- a/docs/public/grammar.pegjs +++ b/docs/public/grammar.pegjs @@ -248,8 +248,49 @@ simpleTextPart "simple text part" } simpleDynamicPart "simple dynamic part" - = "{" _ expr:attributeValue _ "}" { - // Handle dynamic expressions like {item.name} or {@text} + = "{{" _ expr:attributeValue _ "}}" { + // Handle double brace expressions like {{ object.x }} or {{ @object.x }} or {{ @object.@x }} + if (expr.trim().match(/^(@?[a-zA-Z_][a-zA-Z0-9_]*)(\.@?[a-zA-Z_][a-zA-Z0-9_]*)*$/)) { + let foundSignal = false; + let hasLiterals = false; + + // Split by dots to handle each part separately + const parts = expr.split('.'); + const allLiterals = parts.every(part => part.trim().startsWith('@')); + + let computedValue; + + if (allLiterals) { + // All parts are literals, just remove @ prefixes + computedValue = parts.map(part => part.replace('@', '')).join('.'); + hasLiterals = true; + } else { + // Transform each part individually + computedValue = parts.map(part => { + const trimmedPart = part.trim(); + if (trimmedPart.startsWith('@')) { + hasLiterals = true; + return trimmedPart.substring(1); // Remove @ prefix for literals + } else { + // Don't transform keywords + if (['true', 'false', 'null'].includes(trimmedPart)) { + return trimmedPart; + } + foundSignal = true; + return `${trimmedPart}()`; + } + }).join('.'); + } + + if (foundSignal && !allLiterals) { + return `computed(() => ${computedValue})`; + } + return computedValue; + } + return expr; + } + / "{" _ expr:attributeValue _ "}" { + // Handle single brace expressions like {item.name} or {@text} if (expr.trim().match(/^@?[a-zA-Z_][a-zA-Z0-9_.]*$/)) { let foundSignal = false; const computedValue = expr.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => { @@ -396,14 +437,50 @@ dynamicAttribute "dynamic attribute" const needsQuotes = /[^a-zA-Z0-9_$]/.test(attributeName); const formattedName = needsQuotes ? `'${attributeName}'` : attributeName; - // If it's a complex object literal starting with curly braces, preserve it as is - if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) { - return `${formattedName}: ${attributeValue}`; - } - // If it's a template string, preserve it as is + // If it's a complex object with strings, preserve it as is + if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}') && + (attributeValue.includes('"') || attributeValue.includes("'"))) { + return `${formattedName}: ${attributeValue}`; + } + + // If it's a template string, transform expressions inside ${} if (attributeValue.trim().startsWith('`') && attributeValue.trim().endsWith('`')) { - return `${formattedName}: ${attributeValue}`; + // Transform expressions inside ${} in template strings + let transformedTemplate = attributeValue; + + // Find and replace ${expression} patterns + let startIndex = 0; + while (true) { + const dollarIndex = transformedTemplate.indexOf('${', startIndex); + if (dollarIndex === -1) break; + + const braceIndex = transformedTemplate.indexOf('}', dollarIndex); + if (braceIndex === -1) break; + + const expr = transformedTemplate.substring(dollarIndex + 2, braceIndex); + const trimmedExpr = expr.trim(); + + let replacement; + if (trimmedExpr.startsWith('@')) { + // Remove @ prefix for literals + replacement = '${' + trimmedExpr.substring(1) + '}'; + } else if (trimmedExpr.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) { + // Transform identifiers to signals + replacement = '${' + trimmedExpr + '()}'; + } else { + // Keep as is for complex expressions + replacement = '${' + expr + '}'; + } + + transformedTemplate = transformedTemplate.substring(0, dollarIndex) + + replacement + + transformedTemplate.substring(braceIndex + 1); + + startIndex = dollarIndex + replacement.length; + } + + return formattedName + ': ' + transformedTemplate; } // Handle other types of values @@ -412,17 +489,68 @@ dynamicAttribute "dynamic attribute" } else if (attributeValue.trim().match(/^[a-zA-Z_]\w*$/)) { return `${formattedName}: ${attributeValue}`; } else { + // Check if this is an object or array literal + const isObjectLiteral = attributeValue.trim().startsWith('{ ') && attributeValue.trim().endsWith(' }'); + const isArrayLiteral = attributeValue.trim().startsWith('[') && attributeValue.trim().endsWith(']'); + let foundSignal = false; - const computedValue = attributeValue.replace(/@?[a-zA-Z_][a-zA-Z0-9_]*(?!:)/g, (match) => { - if (match.startsWith('@')) { - return match.substring(1); + let hasLiterals = false; + let computedValue = attributeValue; + + // For simple object and array literals (like {x: x, y: 20} or [x, 20]), + // don't use computed() at all and don't transform identifiers + if ((isObjectLiteral || isArrayLiteral) && !attributeValue.includes('()')) { + // Don't transform anything, return as is + foundSignal = false; + computedValue = attributeValue; + } else { + // Apply signal transformation for other values + computedValue = attributeValue.replace(/@?([a-zA-Z_][a-zA-Z0-9_]*)\b(?!\s*:)/g, (match, p1, offset) => { + // Don't transform keywords, numbers, or if we're inside quotes + if (['true', 'false', 'null'].includes(p1) || /^\d+(\.\d+)?$/.test(p1)) { + return match; + } + + // Check if we're inside a string literal + const beforeMatch = attributeValue.substring(0, offset); + const singleQuotesBefore = (beforeMatch.match(/'/g) || []).length; + const doubleQuotesBefore = (beforeMatch.match(/"/g) || []).length; + + // If we're inside quotes, don't transform + if (singleQuotesBefore % 2 === 1 || doubleQuotesBefore % 2 === 1) { + return match; + } + + if (match.startsWith('@')) { + hasLiterals = true; + return p1; // Remove @ prefix + } + foundSignal = true; + return `${p1}()`; + }); + + // Check if any values already contain signals (ending with ()) + if (attributeValue.includes('()')) { + foundSignal = true; } - foundSignal = true; - return `${match}()`; - }); + } + if (foundSignal) { + // For objects, wrap in parentheses + if (attributeValue.trim().startsWith('{') && attributeValue.trim().endsWith('}')) { + // Remove spaces for objects in parentheses + const cleanedObject = computedValue.replace(/{ /g, '{').replace(/ }/g, '}'); + return `${formattedName}: computed(() => (${cleanedObject}))`; + } return `${formattedName}: computed(() => ${computedValue})`; } + + // If only literals (all @), don't use computed + if (hasLiterals && !foundSignal) { + return `${formattedName}: ${computedValue}`; + } + + // For static objects and arrays, return as is without parentheses return `${formattedName}: ${computedValue}`; } } @@ -436,11 +564,7 @@ attributeValue "attribute value" / functionWithElement / objectLiteral / $([^{}]* ("{" [^{}]* "}" [^{}]*)*) { - const t = text().trim() - if (t.startsWith("{") && t.endsWith("}")) { - return `(${t})`; - } - return t + return text().trim() } objectLiteral "object literal" @@ -467,6 +591,7 @@ propertyValue / element / functionWithElement / stringLiteral + / number / identifier nestedObject diff --git a/packages/compiler/index.ts b/packages/compiler/index.ts index c5cef58e..72469516 100644 --- a/packages/compiler/index.ts +++ b/packages/compiler/index.ts @@ -146,8 +146,88 @@ export default function canvasengine() { throw new Error(`Error parsing template in file ${id}:\n${errorMsg}`); } - // trick to avoid typescript remove imports in scriptContent - scriptContent += FLAG_COMMENT + parsedTemplate + // Extract ALL variables declared with defineProps to avoid TypeScript removing them + // This handles both simple and destructured declarations + const definePropsRegex = /(?:const|let|var)\s+([^=]+?)\s*=\s*defineProps\s*\(/g; + const definePropsVars: string[] = []; + let match; + + while ((match = definePropsRegex.exec(scriptContent)) !== null) { + const declaration = match[1].trim(); + + if (declaration.startsWith('{') && declaration.endsWith('}')) { + // Destructured variables like: const { text, value } = defineProps() + const destructuredContent = declaration.slice(1, -1); + const destructuredVars = destructuredContent.split(',').map(v => { + // Handle both "prop" and "prop: alias" cases + const cleanVar = v.trim().split(':')[0].trim(); + return cleanVar; + }); + definePropsVars.push(...destructuredVars); + } else { + // Simple variable like: const props = defineProps() + definePropsVars.push(declaration); + } + } + + // Extract ALL imports to avoid TypeScript removing them when they're used in template but not in script + // This regex captures all import patterns including mixed imports + const importRegex = /import\s+(?:type\s+)?([^;]+?)\s+from\s+['"]([^'"]+)['"];?/g; + const importedVars: string[] = []; + let importMatch; + + while ((importMatch = importRegex.exec(scriptContent)) !== null) { + const importClause = importMatch[1].trim(); + + // Skip type-only imports as they don't need to be preserved for runtime + if (importMatch[0].includes('import type')) { + continue; + } + + // Handle different import patterns + if (importClause.includes('*')) { + // Namespace import: import * as module from 'module' + const namespaceMatch = importClause.match(/\*\s+as\s+(\w+)/); + if (namespaceMatch) { + importedVars.push(namespaceMatch[1]); + } + } else if (importClause.includes('{')) { + // Named imports (possibly with default): import Default, { Named1, Named2 } from 'module' + const parts = importClause.split('{'); + + // Check for default import before the brace + const beforeBrace = parts[0].trim(); + if (beforeBrace) { + const defaultImport = beforeBrace.replace(',', '').trim(); + if (defaultImport) { + importedVars.push(defaultImport); + } + } + + // Extract named imports + const namedPart = parts[1].replace('}', ''); + const namedImports = namedPart.split(',').map(v => { + // Handle both "import" and "import as alias" cases + const cleanVar = v.trim().split(' as ')[0].trim(); + return cleanVar; + }).filter(v => v); + importedVars.push(...namedImports); + } else { + // Default import only: import Component from 'module' + importedVars.push(importClause); + } + } + + // trick to avoid typescript remove imports and defineProps variables in scriptContent + // We reference all defineProps variables and imported variables so TypeScript doesn't remove them + let varRefs = ''; + if (definePropsVars.length > 0) { + varRefs += `;${definePropsVars.join(';')};`; + } + if (importedVars.length > 0) { + varRefs += `;${importedVars.join(';')};`; + } + scriptContent += FLAG_COMMENT + parsedTemplate + varRefs let transpiledCode = ts.transpileModule(scriptContent, { compilerOptions: { diff --git a/packages/compiler/tests/compiler.spec.ts b/packages/compiler/tests/compiler.spec.ts index aa187570..269c12ba 100644 --- a/packages/compiler/tests/compiler.spec.ts +++ b/packages/compiler/tests/compiler.spec.ts @@ -6,7 +6,7 @@ const { generate } = pkg; let parser: any; beforeAll(() => { - const grammar = fs.readFileSync("packages/compiler/grammar.pegjs", "utf8"); + const grammar = fs.readFileSync("grammar.pegjs", "utf8"); parser = generate(grammar); }); diff --git a/packages/compiler/vitest.config.ts b/packages/compiler/vitest.config.ts new file mode 100644 index 00000000..2703523c --- /dev/null +++ b/packages/compiler/vitest.config.ts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + pool: 'forks', + } +}) \ No newline at end of file