lwc-convert 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +719 -0
  3. package/dist/cli/commands/aura.d.ts +6 -0
  4. package/dist/cli/commands/aura.d.ts.map +1 -0
  5. package/dist/cli/commands/aura.js +225 -0
  6. package/dist/cli/commands/aura.js.map +1 -0
  7. package/dist/cli/commands/vf.d.ts +6 -0
  8. package/dist/cli/commands/vf.d.ts.map +1 -0
  9. package/dist/cli/commands/vf.js +218 -0
  10. package/dist/cli/commands/vf.js.map +1 -0
  11. package/dist/cli/interactive.d.ts +20 -0
  12. package/dist/cli/interactive.d.ts.map +1 -0
  13. package/dist/cli/interactive.js +577 -0
  14. package/dist/cli/interactive.js.map +1 -0
  15. package/dist/cli/options.d.ts +21 -0
  16. package/dist/cli/options.d.ts.map +1 -0
  17. package/dist/cli/options.js +24 -0
  18. package/dist/cli/options.js.map +1 -0
  19. package/dist/generators/full-conversion.d.ts +41 -0
  20. package/dist/generators/full-conversion.d.ts.map +1 -0
  21. package/dist/generators/full-conversion.js +538 -0
  22. package/dist/generators/full-conversion.js.map +1 -0
  23. package/dist/generators/scaffolding.d.ts +40 -0
  24. package/dist/generators/scaffolding.d.ts.map +1 -0
  25. package/dist/generators/scaffolding.js +716 -0
  26. package/dist/generators/scaffolding.js.map +1 -0
  27. package/dist/generators/test-comparison.d.ts +47 -0
  28. package/dist/generators/test-comparison.d.ts.map +1 -0
  29. package/dist/generators/test-comparison.js +855 -0
  30. package/dist/generators/test-comparison.js.map +1 -0
  31. package/dist/generators/test-generator.d.ts +27 -0
  32. package/dist/generators/test-generator.d.ts.map +1 -0
  33. package/dist/generators/test-generator.js +385 -0
  34. package/dist/generators/test-generator.js.map +1 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +226 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/mappings/aura-to-lwc.json +321 -0
  40. package/dist/mappings/vf-to-lwc.json +354 -0
  41. package/dist/parsers/aura/controller-parser.d.ts +36 -0
  42. package/dist/parsers/aura/controller-parser.d.ts.map +1 -0
  43. package/dist/parsers/aura/controller-parser.js +269 -0
  44. package/dist/parsers/aura/controller-parser.js.map +1 -0
  45. package/dist/parsers/aura/helper-parser.d.ts +21 -0
  46. package/dist/parsers/aura/helper-parser.d.ts.map +1 -0
  47. package/dist/parsers/aura/helper-parser.js +173 -0
  48. package/dist/parsers/aura/helper-parser.js.map +1 -0
  49. package/dist/parsers/aura/markup-parser.d.ts +59 -0
  50. package/dist/parsers/aura/markup-parser.d.ts.map +1 -0
  51. package/dist/parsers/aura/markup-parser.js +279 -0
  52. package/dist/parsers/aura/markup-parser.js.map +1 -0
  53. package/dist/parsers/aura/style-parser.d.ts +37 -0
  54. package/dist/parsers/aura/style-parser.d.ts.map +1 -0
  55. package/dist/parsers/aura/style-parser.js +151 -0
  56. package/dist/parsers/aura/style-parser.js.map +1 -0
  57. package/dist/parsers/vf/apex-parser.d.ts +51 -0
  58. package/dist/parsers/vf/apex-parser.d.ts.map +1 -0
  59. package/dist/parsers/vf/apex-parser.js +251 -0
  60. package/dist/parsers/vf/apex-parser.js.map +1 -0
  61. package/dist/parsers/vf/page-parser.d.ts +61 -0
  62. package/dist/parsers/vf/page-parser.d.ts.map +1 -0
  63. package/dist/parsers/vf/page-parser.js +403 -0
  64. package/dist/parsers/vf/page-parser.js.map +1 -0
  65. package/dist/transformers/aura-to-lwc/controller.d.ts +36 -0
  66. package/dist/transformers/aura-to-lwc/controller.d.ts.map +1 -0
  67. package/dist/transformers/aura-to-lwc/controller.js +372 -0
  68. package/dist/transformers/aura-to-lwc/controller.js.map +1 -0
  69. package/dist/transformers/aura-to-lwc/events.d.ts +47 -0
  70. package/dist/transformers/aura-to-lwc/events.d.ts.map +1 -0
  71. package/dist/transformers/aura-to-lwc/events.js +262 -0
  72. package/dist/transformers/aura-to-lwc/events.js.map +1 -0
  73. package/dist/transformers/aura-to-lwc/markup.d.ts +51 -0
  74. package/dist/transformers/aura-to-lwc/markup.d.ts.map +1 -0
  75. package/dist/transformers/aura-to-lwc/markup.js +465 -0
  76. package/dist/transformers/aura-to-lwc/markup.js.map +1 -0
  77. package/dist/transformers/vf-to-lwc/components.d.ts +40 -0
  78. package/dist/transformers/vf-to-lwc/components.d.ts.map +1 -0
  79. package/dist/transformers/vf-to-lwc/components.js +374 -0
  80. package/dist/transformers/vf-to-lwc/components.js.map +1 -0
  81. package/dist/transformers/vf-to-lwc/data-binding.d.ts +53 -0
  82. package/dist/transformers/vf-to-lwc/data-binding.d.ts.map +1 -0
  83. package/dist/transformers/vf-to-lwc/data-binding.js +660 -0
  84. package/dist/transformers/vf-to-lwc/data-binding.js.map +1 -0
  85. package/dist/transformers/vf-to-lwc/markup.d.ts +44 -0
  86. package/dist/transformers/vf-to-lwc/markup.d.ts.map +1 -0
  87. package/dist/transformers/vf-to-lwc/markup.js +816 -0
  88. package/dist/transformers/vf-to-lwc/markup.js.map +1 -0
  89. package/dist/utils/confidence-scorer.d.ts +100 -0
  90. package/dist/utils/confidence-scorer.d.ts.map +1 -0
  91. package/dist/utils/confidence-scorer.js +358 -0
  92. package/dist/utils/confidence-scorer.js.map +1 -0
  93. package/dist/utils/file-io.d.ts +62 -0
  94. package/dist/utils/file-io.d.ts.map +1 -0
  95. package/dist/utils/file-io.js +248 -0
  96. package/dist/utils/file-io.js.map +1 -0
  97. package/dist/utils/logger.d.ts +34 -0
  98. package/dist/utils/logger.d.ts.map +1 -0
  99. package/dist/utils/logger.js +130 -0
  100. package/dist/utils/logger.js.map +1 -0
  101. package/dist/utils/open-folder.d.ts +9 -0
  102. package/dist/utils/open-folder.d.ts.map +1 -0
  103. package/dist/utils/open-folder.js +76 -0
  104. package/dist/utils/open-folder.js.map +1 -0
  105. package/dist/utils/path-resolver.d.ts +29 -0
  106. package/dist/utils/path-resolver.d.ts.map +1 -0
  107. package/dist/utils/path-resolver.js +240 -0
  108. package/dist/utils/path-resolver.js.map +1 -0
  109. package/dist/utils/session-store.d.ts +158 -0
  110. package/dist/utils/session-store.d.ts.map +1 -0
  111. package/dist/utils/session-store.js +518 -0
  112. package/dist/utils/session-store.js.map +1 -0
  113. package/dist/utils/vf-controller-resolver.d.ts +36 -0
  114. package/dist/utils/vf-controller-resolver.d.ts.map +1 -0
  115. package/dist/utils/vf-controller-resolver.js +162 -0
  116. package/dist/utils/vf-controller-resolver.js.map +1 -0
  117. package/package.json +81 -0
@@ -0,0 +1,816 @@
1
+ "use strict";
2
+ /**
3
+ * Transform Visualforce page markup to LWC HTML template
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.transformVfMarkup = transformVfMarkup;
40
+ const logger_1 = require("../../utils/logger");
41
+ const vfMapping = __importStar(require("../../mappings/vf-to-lwc.json"));
42
+ // Create a normalized mapping with lowercase keys for case-insensitive lookup
43
+ const rawMappings = vfMapping.components;
44
+ const componentMappings = {};
45
+ for (const [key, value] of Object.entries(rawMappings)) {
46
+ componentMappings[key.toLowerCase()] = value;
47
+ }
48
+ /**
49
+ * Convert VF formula to a JavaScript getter name
50
+ * e.g., "NOT(ISBLANK(contactRecord.Id))" -> "hasContactRecord"
51
+ */
52
+ function convertFormulaToGetterName(formula) {
53
+ // Common patterns
54
+ const normalized = formula.trim();
55
+ // NOT(ISBLANK(x.y)) or NOT(ISNULL(x.y)) -> hasX
56
+ const notIsBlankMatch = normalized.match(/NOT\s*\(\s*(?:ISBLANK|ISNULL)\s*\(\s*(\w+)(?:\.(\w+))?\s*\)\s*\)/i);
57
+ if (notIsBlankMatch) {
58
+ const objName = notIsBlankMatch[1];
59
+ return `has${objName.charAt(0).toUpperCase() + objName.slice(1)}`;
60
+ }
61
+ // ISBLANK(x.y) or ISNULL(x.y) -> isXEmpty
62
+ const isBlankMatch = normalized.match(/(?:ISBLANK|ISNULL)\s*\(\s*(\w+)(?:\.(\w+))?\s*\)/i);
63
+ if (isBlankMatch) {
64
+ const objName = isBlankMatch[1];
65
+ return `is${objName.charAt(0).toUpperCase() + objName.slice(1)}Empty`;
66
+ }
67
+ // NOT(x) -> isNotX
68
+ const notMatch = normalized.match(/NOT\s*\(\s*(\w+)\s*\)/i);
69
+ if (notMatch) {
70
+ const propName = notMatch[1];
71
+ return `isNot${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
72
+ }
73
+ // AND(...) -> combinedCondition
74
+ if (/^AND\s*\(/i.test(normalized)) {
75
+ return 'combinedCondition';
76
+ }
77
+ // OR(...) -> anyCondition
78
+ if (/^OR\s*\(/i.test(normalized)) {
79
+ return 'anyCondition';
80
+ }
81
+ // IF(...) -> conditionalValue
82
+ if (/^IF\s*\(/i.test(normalized)) {
83
+ return 'conditionalValue';
84
+ }
85
+ // LEN(x) > 0 or similar -> hasX
86
+ const lenMatch = normalized.match(/LEN\s*\(\s*(\w+)\s*\)/i);
87
+ if (lenMatch) {
88
+ const propName = lenMatch[1];
89
+ return `has${propName.charAt(0).toUpperCase() + propName.slice(1)}`;
90
+ }
91
+ // Default: sanitize the formula to create a getter name
92
+ const sanitized = normalized
93
+ .replace(/[^a-zA-Z0-9]/g, '_')
94
+ .replace(/_+/g, '_')
95
+ .replace(/^_|_$/g, '')
96
+ .toLowerCase();
97
+ return `computed_${sanitized.substring(0, 30)}`;
98
+ }
99
+ /**
100
+ * Parse formula arguments, handling nested parentheses
101
+ */
102
+ function parseFormulaArgs(formula, funcName) {
103
+ // Find the opening paren after function name
104
+ const startIdx = formula.toUpperCase().indexOf(funcName.toUpperCase() + '(') + funcName.length + 1;
105
+ if (startIdx < funcName.length + 1)
106
+ return [];
107
+ let depth = 1;
108
+ let current = '';
109
+ const args = [];
110
+ for (let i = startIdx; i < formula.length && depth > 0; i++) {
111
+ const char = formula[i];
112
+ if (char === '(') {
113
+ depth++;
114
+ current += char;
115
+ }
116
+ else if (char === ')') {
117
+ depth--;
118
+ if (depth === 0) {
119
+ // End of function - push final argument if any
120
+ if (current.trim())
121
+ args.push(current.trim());
122
+ break;
123
+ }
124
+ current += char;
125
+ }
126
+ else if (char === ',' && depth === 1) {
127
+ // Separator at top level
128
+ if (current.trim())
129
+ args.push(current.trim());
130
+ current = '';
131
+ }
132
+ else {
133
+ current += char;
134
+ }
135
+ }
136
+ return args;
137
+ }
138
+ /**
139
+ * Convert VF formula to JavaScript expression
140
+ */
141
+ function convertFormulaToJsExpression(formula, depth = 0) {
142
+ const normalized = formula.trim();
143
+ // Prevent infinite recursion
144
+ if (depth > 10)
145
+ return `/* ${formula} */`;
146
+ // NOT(x) -> !(x)
147
+ if (/^NOT\s*\(/i.test(normalized)) {
148
+ const args = parseFormulaArgs(normalized, 'NOT');
149
+ if (args.length === 1) {
150
+ const innerJs = convertFormulaToJsExpression(args[0], depth + 1);
151
+ return `!(${innerJs})`;
152
+ }
153
+ }
154
+ // ISBLANK(x) or ISNULL(x) -> x == null || x === ''
155
+ if (/^(?:ISBLANK|ISNULL)\s*\(/i.test(normalized)) {
156
+ const funcName = normalized.match(/^(ISBLANK|ISNULL)/i)?.[1] || 'ISBLANK';
157
+ const args = parseFormulaArgs(normalized, funcName);
158
+ if (args.length === 1) {
159
+ const propRef = convertPropertyReference(args[0]);
160
+ return `(${propRef} == null || ${propRef} === '')`;
161
+ }
162
+ }
163
+ // AND(a, b, c) -> (a && b && c)
164
+ if (/^AND\s*\(/i.test(normalized)) {
165
+ const args = parseFormulaArgs(normalized, 'AND');
166
+ if (args.length > 0) {
167
+ const converted = args.map(arg => convertFormulaToJsExpression(arg, depth + 1));
168
+ return `(${converted.join(' && ')})`;
169
+ }
170
+ }
171
+ // OR(a, b, c) -> (a || b || c)
172
+ if (/^OR\s*\(/i.test(normalized)) {
173
+ const args = parseFormulaArgs(normalized, 'OR');
174
+ if (args.length > 0) {
175
+ const converted = args.map(arg => convertFormulaToJsExpression(arg, depth + 1));
176
+ return `(${converted.join(' || ')})`;
177
+ }
178
+ }
179
+ // IF(condition, trueVal, falseVal) -> condition ? trueVal : falseVal
180
+ if (/^IF\s*\(/i.test(normalized)) {
181
+ const args = parseFormulaArgs(normalized, 'IF');
182
+ if (args.length === 3) {
183
+ const cond = convertFormulaToJsExpression(args[0], depth + 1);
184
+ const trueVal = convertFormulaToJsExpression(args[1], depth + 1);
185
+ const falseVal = convertFormulaToJsExpression(args[2], depth + 1);
186
+ return `(${cond} ? ${trueVal} : ${falseVal})`;
187
+ }
188
+ }
189
+ // LEN(x) -> x?.length || 0
190
+ if (/^LEN\s*\(/i.test(normalized)) {
191
+ const args = parseFormulaArgs(normalized, 'LEN');
192
+ if (args.length === 1) {
193
+ const propRef = convertPropertyReference(args[0]);
194
+ return `(${propRef}?.length || 0)`;
195
+ }
196
+ }
197
+ // CONTAINS(text, substring) -> text?.includes(substring)
198
+ if (/^CONTAINS\s*\(/i.test(normalized)) {
199
+ const args = parseFormulaArgs(normalized, 'CONTAINS');
200
+ if (args.length === 2) {
201
+ const text = convertPropertyReference(args[0]);
202
+ const substring = args[1].trim();
203
+ return `${text}?.includes(${substring})`;
204
+ }
205
+ }
206
+ // BEGINS(text, prefix) -> text?.startsWith(prefix)
207
+ if (/^BEGINS\s*\(/i.test(normalized)) {
208
+ const args = parseFormulaArgs(normalized, 'BEGINS');
209
+ if (args.length === 2) {
210
+ const text = convertPropertyReference(args[0]);
211
+ const prefix = args[1].trim();
212
+ return `${text}?.startsWith(${prefix})`;
213
+ }
214
+ }
215
+ // Handle simple property references or literals
216
+ return convertPropertyReference(normalized);
217
+ }
218
+ /**
219
+ * Convert VF property reference to JS this.property reference
220
+ */
221
+ function convertPropertyReference(ref) {
222
+ const trimmed = ref.trim();
223
+ // String literal
224
+ if (/^['"].*['"]$/.test(trimmed))
225
+ return trimmed;
226
+ // Number literal
227
+ if (/^\d+(\.\d+)?$/.test(trimmed))
228
+ return trimmed;
229
+ // Boolean literals
230
+ if (/^(true|false)$/i.test(trimmed))
231
+ return trimmed.toLowerCase();
232
+ // Property reference like objectName.fieldName or simple property
233
+ if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(trimmed)) {
234
+ const parts = trimmed.split('.');
235
+ if (parts.length === 1) {
236
+ return `this.${parts[0]}`;
237
+ }
238
+ // Use optional chaining for nested properties
239
+ return `this.${parts[0]}?.${parts.slice(1).join('?.')}`;
240
+ }
241
+ return trimmed;
242
+ }
243
+ /**
244
+ * Convert Visualforce expression to LWC expression
245
+ */
246
+ function convertExpression(expr) {
247
+ const warnings = [];
248
+ let converted = expr;
249
+ let detectedFormula;
250
+ let controllerProperty;
251
+ // Handle URLFOR function - common in VF for static resources
252
+ // {!URLFOR($Resource.name, 'path')} -> needs static resource import
253
+ if (converted.includes('URLFOR')) {
254
+ warnings.push('URLFOR found - use @salesforce/resourceUrl import with string concatenation');
255
+ // Extract resource name from URLFOR($Resource.name, 'path')
256
+ converted = converted.replace(/\{!URLFOR\(\$Resource\.(\w+),\s*['"]([^'"]+)['"]\)\}/gi, '{$1Resource_$2}');
257
+ // Simple URLFOR($Resource.name)
258
+ converted = converted.replace(/\{!URLFOR\(\$Resource\.(\w+)\)\}/gi, '{$1Resource}');
259
+ }
260
+ // Handle $MessageChannel - Lightning Message Service
261
+ if (converted.includes('$MessageChannel')) {
262
+ warnings.push('$MessageChannel found - import from @salesforce/messageChannel and use lightning/messageService');
263
+ converted = converted.replace(/\{!\$MessageChannel\.([^}]+)\}/g, '{messageChannel_$1}');
264
+ }
265
+ // {!$CurrentPage.parameters.x} -> needs currentPageReference wire
266
+ if (converted.includes('$CurrentPage')) {
267
+ warnings.push('$CurrentPage found - use @wire(CurrentPageReference) to access URL parameters');
268
+ converted = converted.replace(/\{!\$CurrentPage\.parameters\.(\w+)\}/g, '{pageRef.state.$1}');
269
+ converted = converted.replace(/\{!\$CurrentPage\.Name\}/gi, '{pageName}');
270
+ converted = converted.replace(/\{!\$CurrentPage\.([^}]+)\}/g, '{pageRef.$1}');
271
+ }
272
+ // {!$User.x} -> needs user info import
273
+ if (converted.includes('$User')) {
274
+ warnings.push('$User found - import from @salesforce/user');
275
+ converted = converted.replace(/\{!\$User\.(\w+)\}/g, '{user$1}');
276
+ }
277
+ // {!$Label.namespace.label} -> needs label import
278
+ if (converted.includes('$Label')) {
279
+ warnings.push('$Label found - import labels from @salesforce/label');
280
+ converted = converted.replace(/\{!\$Label\.(\w+)\.(\w+)\}/g, '{label_$1_$2}');
281
+ converted = converted.replace(/\{!\$Label\.(\w+)\}/g, '{label_$1}');
282
+ }
283
+ // {!$Resource.name} -> needs static resource import
284
+ if (converted.includes('$Resource')) {
285
+ warnings.push('$Resource found - import from @salesforce/resourceUrl');
286
+ converted = converted.replace(/\{!\$Resource\.(\w+)\}/g, '{$1Resource}');
287
+ }
288
+ // {!$ObjectType.Account.fields.Name.label} -> needs schema import
289
+ if (converted.includes('$ObjectType')) {
290
+ warnings.push('$ObjectType found - import from @salesforce/schema');
291
+ converted = converted.replace(/\{!\$ObjectType\.(\w+)\.fields\.(\w+)\.(\w+)\}/g, '{schema_$1_$2_$3}');
292
+ }
293
+ // {!$Api.Session_ID} and other $Api globals
294
+ if (converted.includes('$Api')) {
295
+ warnings.push('$Api found - these globals may not be available in LWC, check alternatives');
296
+ converted = converted.replace(/\{!\$Api\.(\w+)\}/g, '{api$1}');
297
+ }
298
+ // Handle VF formula functions (NOT, ISBLANK, AND, OR, IF, etc.)
299
+ // These need to be converted to JavaScript getter references
300
+ const formulaFunctionPattern = /\{!(NOT|ISBLANK|ISNULL|AND|OR|IF|LEN|CONTAINS|BEGINS|INCLUDES)\s*\(/i;
301
+ if (formulaFunctionPattern.test(converted)) {
302
+ // Extract the formula and convert to a getter name
303
+ const formulaMatch = converted.match(/\{!([^}]+)\}/);
304
+ if (formulaMatch) {
305
+ const formula = formulaMatch[1];
306
+ // Generate a getter name based on the formula content
307
+ const getterName = convertFormulaToGetterName(formula);
308
+ // Convert formula to JavaScript expression for the getter body
309
+ const jsExpression = convertFormulaToJsExpression(formula);
310
+ const suggestedLogic = `return ${jsExpression};`;
311
+ detectedFormula = {
312
+ original: formula,
313
+ getterName,
314
+ suggestedLogic,
315
+ };
316
+ warnings.push(`VF formula detected - implement getter: get ${getterName}()`);
317
+ converted = converted.replace(/\{![^}]+\}/g, `{${getterName}}`);
318
+ }
319
+ }
320
+ // Detect controller property bindings like {!controllerProp.field}
321
+ // This pattern matches property.field (not starting with $)
322
+ const controllerPropMatch = converted.match(/\{!(\w+)\.(\w+)\}/);
323
+ if (controllerPropMatch && !controllerPropMatch[1].startsWith('$')) {
324
+ const propName = controllerPropMatch[1];
325
+ const fieldName = controllerPropMatch[2];
326
+ // Check if this looks like a controller property (not a formula function)
327
+ if (!/^(NOT|ISBLANK|ISNULL|AND|OR|IF|LEN|CONTAINS|BEGINS|INCLUDES)$/i.test(propName)) {
328
+ controllerProperty = {
329
+ name: propName,
330
+ fields: [fieldName],
331
+ };
332
+ }
333
+ }
334
+ // Handle remaining simple VF expressions: {!property} -> {property}
335
+ // This must come AFTER the $-prefixed handlers to avoid double-processing
336
+ converted = converted.replace(/\{!([^}]+)\}/g, (match, inner) => {
337
+ // Skip if already processed (contains our replacement markers)
338
+ if (inner.startsWith('$') || inner.includes('URLFOR')) {
339
+ // These should have been handled above, log a warning if not
340
+ warnings.push(`Unhandled VF expression: ${match}`);
341
+ return match;
342
+ }
343
+ return `{${inner}}`;
344
+ });
345
+ return { converted, warnings, detectedFormula, controllerProperty };
346
+ }
347
+ /**
348
+ * Get the LWC tag name for a VF component
349
+ */
350
+ function getVfToLwcTag(vfTag) {
351
+ const warnings = [];
352
+ const lowerTag = vfTag.toLowerCase();
353
+ const mapping = componentMappings[lowerTag];
354
+ if (mapping) {
355
+ if (mapping.lwc === null) {
356
+ warnings.push(`${vfTag} has no direct LWC equivalent - ${mapping.notes || 'manual conversion required'}`);
357
+ return { lwcTag: 'div', mapping, warnings };
358
+ }
359
+ if (mapping.notes) {
360
+ warnings.push(`${vfTag}: ${mapping.notes}`);
361
+ }
362
+ return { lwcTag: mapping.lwc, mapping, warnings };
363
+ }
364
+ // Handle apex: prefix generically
365
+ if (lowerTag.startsWith('apex:')) {
366
+ warnings.push(`No specific mapping for ${vfTag} - using generic conversion`);
367
+ return { lwcTag: `div`, warnings };
368
+ }
369
+ // Pass through HTML tags
370
+ return { lwcTag: vfTag, warnings };
371
+ }
372
+ /**
373
+ * Transform VF component attributes to LWC attributes
374
+ */
375
+ function transformAttributes(vfAttrs, mapping) {
376
+ const attrs = {};
377
+ const warnings = [];
378
+ for (const [key, value] of Object.entries(vfAttrs)) {
379
+ // Skip VF-specific attributes that don't translate
380
+ if (['id', 'rendered'].includes(key.toLowerCase())) {
381
+ if (key.toLowerCase() === 'id') {
382
+ attrs['data-id'] = value;
383
+ }
384
+ continue;
385
+ }
386
+ // Check for attribute mapping
387
+ let lwcAttr = key;
388
+ if (mapping?.attributes && key in mapping.attributes) {
389
+ const mappedAttr = mapping.attributes[key];
390
+ if (mappedAttr === null) {
391
+ warnings.push(`Attribute "${key}" has no LWC equivalent`);
392
+ continue;
393
+ }
394
+ lwcAttr = mappedAttr;
395
+ }
396
+ // Convert attribute name from camelCase to kebab-case
397
+ lwcAttr = lwcAttr.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
398
+ // Convert expression in value
399
+ const { converted, warnings: exprWarnings } = convertExpression(value);
400
+ warnings.push(...exprWarnings);
401
+ attrs[lwcAttr] = converted;
402
+ }
403
+ return { attrs, warnings };
404
+ }
405
+ /**
406
+ * Transform a VF component to LWC HTML
407
+ */
408
+ function transformVfComponent(comp, indent, context) {
409
+ const lowerName = comp.name.toLowerCase();
410
+ const { lwcTag, mapping, warnings: tagWarnings } = getVfToLwcTag(comp.name);
411
+ context.warnings.push(...tagWarnings);
412
+ // Special handling for specific VF components
413
+ if (lowerName === 'apex:form') {
414
+ return transformForm(comp, indent, context);
415
+ }
416
+ if (lowerName === 'apex:pageblocktable' || lowerName === 'apex:datatable') {
417
+ return transformDataTable(comp, indent, context);
418
+ }
419
+ if (lowerName === 'apex:repeat') {
420
+ return transformRepeat(comp, indent, context);
421
+ }
422
+ if (lowerName === 'apex:outputpanel' || lowerName === 'apex:panelgroup') {
423
+ return transformPanel(comp, indent, context);
424
+ }
425
+ if (lowerName === 'apex:pagemessages' || lowerName === 'apex:messages') {
426
+ context.warnings.push('Page messages should use ShowToastEvent for notifications');
427
+ return `${indent}<!-- TODO: Replace with ShowToastEvent or custom error display -->`;
428
+ }
429
+ if (lowerName === 'apex:actionfunction') {
430
+ context.warnings.push(`apex:actionFunction "${comp.attributes.name}" - convert to imperative Apex`);
431
+ return `${indent}<!-- actionFunction "${comp.attributes.name}" converted to imperative Apex call -->`;
432
+ }
433
+ if (lowerName === 'apex:actionstatus') {
434
+ context.usedComponents.push('lightning-spinner');
435
+ return `${indent}<template if:true={isLoading}>\n${indent} <lightning-spinner alternative-text="Loading"></lightning-spinner>\n${indent}</template>`;
436
+ }
437
+ // apex:stylesheet - handled via loadStyle in JS, not in HTML
438
+ if (lowerName === 'apex:stylesheet') {
439
+ const resourceValue = comp.attributes.value || '';
440
+ context.warnings.push(`apex:stylesheet detected - use loadStyle() in renderedCallback. Resource: ${resourceValue}`);
441
+ // Add required import tracking
442
+ if (!context.requiredImports.has('lightning/platformResourceLoader')) {
443
+ context.requiredImports.set('lightning/platformResourceLoader', new Set());
444
+ }
445
+ context.requiredImports.get('lightning/platformResourceLoader').add('loadStyle');
446
+ // Return empty - stylesheet loading happens in JS
447
+ return '';
448
+ }
449
+ // apex:includeScript - handled via loadScript in JS
450
+ if (lowerName === 'apex:includescript') {
451
+ const resourceValue = comp.attributes.value || '';
452
+ context.warnings.push(`apex:includeScript detected - use loadScript() in connectedCallback. Resource: ${resourceValue}`);
453
+ if (!context.requiredImports.has('lightning/platformResourceLoader')) {
454
+ context.requiredImports.set('lightning/platformResourceLoader', new Set());
455
+ }
456
+ context.requiredImports.get('lightning/platformResourceLoader').add('loadScript');
457
+ return '';
458
+ }
459
+ // apex:outputText - convert to direct text interpolation
460
+ if (lowerName === 'apex:outputtext') {
461
+ const valueAttr = comp.attributes.value || '';
462
+ if (valueAttr) {
463
+ const { converted, warnings: exprWarnings } = convertExpression(valueAttr);
464
+ context.warnings.push(...exprWarnings);
465
+ // If the converted expression has curly braces, it's a data binding
466
+ // Otherwise it's static text
467
+ if (converted.startsWith('{') && converted.endsWith('}')) {
468
+ return `${indent}${converted}`;
469
+ }
470
+ // Handle escape attribute (HTML escaping) - LWC does this by default
471
+ if (comp.attributes.escape === 'false') {
472
+ context.warnings.push('apex:outputText with escape="false" detected - use lwc:dom="manual" with innerHTML for raw HTML');
473
+ return `${indent}<span lwc:dom="manual" data-output-text>${converted}</span>`;
474
+ }
475
+ return `${indent}${converted}`;
476
+ }
477
+ // No value attribute, process children as content
478
+ if (comp.textContent) {
479
+ const { converted } = convertExpression(comp.textContent);
480
+ return `${indent}${converted}`;
481
+ }
482
+ return '';
483
+ }
484
+ // apex:slds - SLDS is automatically available in LWC
485
+ if (lowerName === 'apex:slds') {
486
+ context.warnings.push('apex:slds removed - SLDS is automatically available in LWC');
487
+ return '';
488
+ }
489
+ // apex:remoteObjects / apex:remoteObjectModel - convert to wire adapter pattern
490
+ if (lowerName === 'apex:remoteobjects') {
491
+ context.warnings.push('apex:remoteObjects detected - convert to @wire adapter or imperative Apex calls');
492
+ let comment = `${indent}<!-- TODO: Replace Remote Objects with @wire adapter or imperative Apex -->\n`;
493
+ comment += `${indent}<!-- Remote Objects detected: -->`;
494
+ // Process children to extract object models
495
+ for (const child of comp.children) {
496
+ if (child.name.toLowerCase() === 'apex:remoteobjectmodel') {
497
+ const objName = child.attributes.name || 'Unknown';
498
+ const fields = child.attributes.fields || '';
499
+ comment += `\n${indent}<!-- Object: ${objName}, Fields: ${fields} -->`;
500
+ }
501
+ }
502
+ return comment;
503
+ }
504
+ if (lowerName === 'apex:remoteobjectmodel') {
505
+ // Handled by parent apex:remoteObjects, but in case it appears standalone
506
+ const objName = comp.attributes.name || 'Unknown';
507
+ const fields = comp.attributes.fields || '';
508
+ context.warnings.push(`apex:remoteObjectModel "${objName}" - convert to @wire adapter`);
509
+ return `${indent}<!-- TODO: Remote Object "${objName}" (fields: ${fields}) - use @wire adapter -->`;
510
+ }
511
+ // Handle c:componentName - custom VF components to LWC format
512
+ if (lowerName.startsWith('c:')) {
513
+ const componentName = lowerName.substring(2);
514
+ // Convert camelCase to kebab-case for LWC
515
+ const lwcName = 'c-' + componentName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
516
+ context.warnings.push(`Custom VF component ${comp.name} - verify LWC equivalent exists as <${lwcName}>`);
517
+ // Transform attributes for the custom component
518
+ const { attrs, warnings: attrWarnings } = transformAttributes(comp.attributes, undefined);
519
+ context.warnings.push(...attrWarnings);
520
+ // Build attribute string
521
+ const attrParts = [];
522
+ for (const [key, value] of Object.entries(attrs)) {
523
+ if (value.startsWith('{') && value.endsWith('}')) {
524
+ attrParts.push(`${key}=${value}`);
525
+ }
526
+ else {
527
+ attrParts.push(`${key}="${value}"`);
528
+ }
529
+ }
530
+ const attrString = attrParts.length > 0 ? ' ' + attrParts.join(' ') : '';
531
+ // Process children
532
+ if (comp.children.length === 0 && !comp.textContent) {
533
+ return `${indent}<${lwcName}${attrString}></${lwcName}>`;
534
+ }
535
+ let childContent = '';
536
+ for (const child of comp.children) {
537
+ childContent += transformVfComponent(child, indent + ' ', context) + '\n';
538
+ }
539
+ if (comp.textContent) {
540
+ const { converted } = convertExpression(comp.textContent);
541
+ childContent += indent + ' ' + converted + '\n';
542
+ }
543
+ return `${indent}<${lwcName}${attrString}>\n${childContent}${indent}</${lwcName}>`;
544
+ }
545
+ // Transform attributes
546
+ const { attrs, warnings: attrWarnings } = transformAttributes(comp.attributes, mapping);
547
+ context.warnings.push(...attrWarnings);
548
+ // Track used components
549
+ if (lwcTag.startsWith('lightning-')) {
550
+ context.usedComponents.push(lwcTag);
551
+ }
552
+ // Track form fields
553
+ if (lowerName.includes('input') || lowerName.includes('select')) {
554
+ context.formFields.push({
555
+ name: comp.attributes.value || comp.attributes.id || 'unknown',
556
+ type: lowerName,
557
+ label: comp.attributes.label,
558
+ vfComponent: comp.name,
559
+ });
560
+ }
561
+ // Build attribute string
562
+ const attrParts = [];
563
+ for (const [key, value] of Object.entries(attrs)) {
564
+ if (value.startsWith('{') && value.endsWith('}')) {
565
+ attrParts.push(`${key}=${value}`);
566
+ }
567
+ else {
568
+ attrParts.push(`${key}="${value}"`);
569
+ }
570
+ }
571
+ // Add type override if needed (e.g., checkbox input)
572
+ if (mapping?.typeOverride) {
573
+ attrParts.push(`type="${mapping.typeOverride}"`);
574
+ }
575
+ const attrString = attrParts.length > 0 ? ' ' + attrParts.join(' ') : '';
576
+ // Detect list elements with data-* attributes that suggest dynamic rendering
577
+ const isListElement = ['ul', 'ol', 'tbody'].includes(lwcTag.toLowerCase());
578
+ const hasDataAttribute = Object.keys(attrs).some(k => k.startsWith('data-'));
579
+ let listComment = '';
580
+ if (isListElement && hasDataAttribute && comp.children.length === 0) {
581
+ context.warnings.push(`Empty ${lwcTag} with data attribute detected - likely needs template iteration (for:each)`);
582
+ listComment = `${indent}<!-- TODO: Add template iteration for dynamic content -->\n`;
583
+ listComment += `${indent}<!-- Example:\n`;
584
+ listComment += `${indent}<template for:each={items} for:item="item">\n`;
585
+ listComment += `${indent} <li key={item.Id} onclick={handleItemClick}>{item.Name}</li>\n`;
586
+ listComment += `${indent}</template>\n`;
587
+ listComment += `${indent}-->\n`;
588
+ }
589
+ // Process children
590
+ if (comp.children.length === 0 && !comp.textContent) {
591
+ if (listComment) {
592
+ return `${listComment}${indent}<${lwcTag}${attrString}>\n${indent}</${lwcTag}>`;
593
+ }
594
+ return `${indent}<${lwcTag}${attrString}></${lwcTag}>`;
595
+ }
596
+ let childContent = '';
597
+ for (const child of comp.children) {
598
+ childContent += transformVfComponent(child, indent + ' ', context) + '\n';
599
+ }
600
+ if (comp.textContent) {
601
+ const { converted } = convertExpression(comp.textContent);
602
+ childContent += indent + ' ' + converted + '\n';
603
+ }
604
+ return `${indent}<${lwcTag}${attrString}>\n${childContent}${indent}</${lwcTag}>`;
605
+ }
606
+ /**
607
+ * Check if a component tree contains input fields (apex:inputField, apex:inputText, etc.)
608
+ */
609
+ function hasInputComponents(comp) {
610
+ const inputTypes = [
611
+ 'apex:inputfield',
612
+ 'apex:inputtext',
613
+ 'apex:inputtextarea',
614
+ 'apex:inputcheckbox',
615
+ 'apex:inputsecret',
616
+ 'apex:inputhidden',
617
+ 'apex:selectlist',
618
+ 'apex:selectcheckboxes',
619
+ 'apex:selectradio',
620
+ ];
621
+ const lowerName = comp.name.toLowerCase();
622
+ if (inputTypes.includes(lowerName)) {
623
+ return true;
624
+ }
625
+ // Check children recursively
626
+ for (const child of comp.children) {
627
+ if (hasInputComponents(child)) {
628
+ return true;
629
+ }
630
+ }
631
+ return false;
632
+ }
633
+ /**
634
+ * Transform apex:form to lightning-record-edit-form (if inputs exist) or div wrapper
635
+ */
636
+ function transformForm(comp, indent, context) {
637
+ // Check if this form contains any input fields
638
+ const containsInputs = hasInputComponents(comp);
639
+ // Update context to track if form has inputs
640
+ if (context.hasInputFields === undefined) {
641
+ context.hasInputFields = containsInputs;
642
+ }
643
+ else {
644
+ context.hasInputFields = context.hasInputFields || containsInputs;
645
+ }
646
+ if (containsInputs) {
647
+ // Use lightning-record-edit-form for forms with input fields
648
+ context.warnings.push('apex:form with inputs converted to lightning-record-edit-form - add record-id and object-api-name attributes');
649
+ context.usedComponents.push('lightning-record-edit-form');
650
+ let html = `${indent}<!-- Form contains input fields - using lightning-record-edit-form -->\n`;
651
+ html += `${indent}<lightning-record-edit-form record-id={recordId} object-api-name={objectApiName}>\n`;
652
+ // Process children
653
+ for (const child of comp.children) {
654
+ html += transformVfComponent(child, indent + ' ', context) + '\n';
655
+ }
656
+ html += `${indent}</lightning-record-edit-form>`;
657
+ return html;
658
+ }
659
+ else {
660
+ // Use simple div wrapper for forms without input fields (display-only forms)
661
+ context.warnings.push('apex:form without input fields converted to div wrapper - no lightning-record-edit-form needed');
662
+ let html = `${indent}<!-- Form without inputs - using div wrapper -->\n`;
663
+ html += `${indent}<div class="slds-form">\n`;
664
+ // Process children
665
+ for (const child of comp.children) {
666
+ html += transformVfComponent(child, indent + ' ', context) + '\n';
667
+ }
668
+ html += `${indent}</div>`;
669
+ return html;
670
+ }
671
+ }
672
+ /**
673
+ * Transform apex:pageBlockTable to lightning-datatable
674
+ */
675
+ function transformDataTable(comp, indent, context) {
676
+ context.usedComponents.push('lightning-datatable');
677
+ const { converted: dataExpr } = convertExpression(comp.attributes.value || '');
678
+ const varName = comp.attributes.var || 'item';
679
+ // Extract columns from apex:column children
680
+ const columns = [];
681
+ for (const child of comp.children) {
682
+ if (child.name.toLowerCase() === 'apex:column') {
683
+ const col = {
684
+ label: child.attributes.headerlabel || child.attributes.value || '',
685
+ fieldName: child.attributes.value?.replace(/\{!|\}/g, '').replace(`${varName}.`, '') || '',
686
+ };
687
+ columns.push(col);
688
+ context.dataTableColumns.push(col);
689
+ }
690
+ }
691
+ context.warnings.push('apex:pageBlockTable converted to lightning-datatable - define columns in JavaScript');
692
+ let html = `${indent}<!-- Columns definition needed in JS:\n`;
693
+ html += `${indent} columns = [\n`;
694
+ for (const col of columns) {
695
+ html += `${indent} { label: '${col.label}', fieldName: '${col.fieldName}' },\n`;
696
+ }
697
+ html += `${indent} ];\n`;
698
+ html += `${indent}-->\n`;
699
+ html += `${indent}<lightning-datatable\n`;
700
+ html += `${indent} data=${dataExpr}\n`;
701
+ html += `${indent} columns={columns}\n`;
702
+ html += `${indent} key-field="Id">\n`;
703
+ html += `${indent}</lightning-datatable>`;
704
+ return html;
705
+ }
706
+ /**
707
+ * Transform apex:repeat to template for:each
708
+ */
709
+ function transformRepeat(comp, indent, context) {
710
+ const { converted: itemsExpr } = convertExpression(comp.attributes.value || '');
711
+ const varName = comp.attributes.var || 'item';
712
+ context.warnings.push('apex:repeat converted - add key attribute to first child element');
713
+ let html = `${indent}<template for:each=${itemsExpr} for:item="${varName}">\n`;
714
+ // Process children
715
+ for (const child of comp.children) {
716
+ html += transformVfComponent(child, indent + ' ', context) + '\n';
717
+ }
718
+ html += `${indent}</template>`;
719
+ return html;
720
+ }
721
+ /**
722
+ * Transform apex:outputPanel to template with conditional
723
+ */
724
+ function transformPanel(comp, indent, context) {
725
+ // Check if it has rendered attribute
726
+ if (comp.attributes.rendered) {
727
+ const { converted: renderExpr } = convertExpression(comp.attributes.rendered);
728
+ let html = `${indent}<template if:true=${renderExpr}>\n`;
729
+ for (const child of comp.children) {
730
+ html += transformVfComponent(child, indent + ' ', context) + '\n';
731
+ }
732
+ html += `${indent}</template>`;
733
+ return html;
734
+ }
735
+ // Simple div wrapper
736
+ let html = `${indent}<div>\n`;
737
+ for (const child of comp.children) {
738
+ html += transformVfComponent(child, indent + ' ', context) + '\n';
739
+ }
740
+ html += `${indent}</div>`;
741
+ return html;
742
+ }
743
+ /**
744
+ * Transform parsed VF page markup to LWC HTML
745
+ */
746
+ function transformVfMarkup(parsed) {
747
+ const context = {
748
+ warnings: [],
749
+ usedComponents: [],
750
+ formFields: [],
751
+ dataTableColumns: [],
752
+ requiredImports: new Map(),
753
+ detectedFormulas: [],
754
+ controllerProperties: [],
755
+ hasInputFields: false,
756
+ };
757
+ // Pre-scan for formulas and controller properties in expressions
758
+ for (const expr of parsed.expressions) {
759
+ const result = convertExpression(expr.original);
760
+ if (result.detectedFormula) {
761
+ // Avoid duplicates by checking getter name
762
+ const exists = context.detectedFormulas.some(f => f.getterName === result.detectedFormula.getterName);
763
+ if (!exists) {
764
+ context.detectedFormulas.push(result.detectedFormula);
765
+ }
766
+ }
767
+ if (result.controllerProperty) {
768
+ // Merge fields for same property
769
+ const existing = context.controllerProperties.find(p => p.name === result.controllerProperty.name);
770
+ if (existing) {
771
+ for (const field of result.controllerProperty.fields) {
772
+ if (!existing.fields.includes(field)) {
773
+ existing.fields.push(field);
774
+ }
775
+ }
776
+ }
777
+ else {
778
+ context.controllerProperties.push(result.controllerProperty);
779
+ }
780
+ }
781
+ }
782
+ let bodyContent = '';
783
+ for (const comp of parsed.components) {
784
+ // Skip the apex:page wrapper - process its contents
785
+ if (comp.name.toLowerCase() === 'apex:page') {
786
+ for (const child of comp.children) {
787
+ bodyContent += transformVfComponent(child, ' ', context) + '\n';
788
+ }
789
+ }
790
+ else {
791
+ bodyContent += transformVfComponent(comp, ' ', context) + '\n';
792
+ }
793
+ }
794
+ const html = `<template>\n${bodyContent}</template>`;
795
+ // Build required imports
796
+ const requiredImports = [];
797
+ context.requiredImports.forEach((items, module) => {
798
+ requiredImports.push({ module, items: Array.from(items) });
799
+ });
800
+ // Deduplicate
801
+ context.usedComponents = [...new Set(context.usedComponents)];
802
+ logger_1.logger.debug(`Transformed VF markup with ${context.warnings.length} warnings`);
803
+ logger_1.logger.debug(`Used components: ${context.usedComponents.join(', ')}`);
804
+ return {
805
+ html,
806
+ warnings: context.warnings,
807
+ usedComponents: context.usedComponents,
808
+ requiredImports,
809
+ formFields: context.formFields,
810
+ dataTableColumns: context.dataTableColumns,
811
+ detectedFormulas: context.detectedFormulas,
812
+ controllerProperties: context.controllerProperties,
813
+ hasInputFields: context.hasInputFields,
814
+ };
815
+ }
816
+ //# sourceMappingURL=markup.js.map