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