Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions COMPILER_FIX_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -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
<Canvas>
<Text text />
<MyComponent />
</Canvas>

<script>
import { MyComponent } from 'components'; // Cet import était supprimé
const props = defineProps(); // Cette ligne était supprimée
</script>
```

## 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
```
163 changes: 144 additions & 19 deletions docs/public/grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand All @@ -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}`;
}
}
Expand All @@ -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"
Expand All @@ -467,6 +591,7 @@ propertyValue
/ element
/ functionWithElement
/ stringLiteral
/ number
/ identifier

nestedObject
Expand Down
Loading
Loading