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.
- package/LICENSE +21 -0
- package/README.md +719 -0
- package/dist/cli/commands/aura.d.ts +6 -0
- package/dist/cli/commands/aura.d.ts.map +1 -0
- package/dist/cli/commands/aura.js +225 -0
- package/dist/cli/commands/aura.js.map +1 -0
- package/dist/cli/commands/vf.d.ts +6 -0
- package/dist/cli/commands/vf.d.ts.map +1 -0
- package/dist/cli/commands/vf.js +218 -0
- package/dist/cli/commands/vf.js.map +1 -0
- package/dist/cli/interactive.d.ts +20 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +577 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/options.d.ts +21 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +24 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/generators/full-conversion.d.ts +41 -0
- package/dist/generators/full-conversion.d.ts.map +1 -0
- package/dist/generators/full-conversion.js +538 -0
- package/dist/generators/full-conversion.js.map +1 -0
- package/dist/generators/scaffolding.d.ts +40 -0
- package/dist/generators/scaffolding.d.ts.map +1 -0
- package/dist/generators/scaffolding.js +716 -0
- package/dist/generators/scaffolding.js.map +1 -0
- package/dist/generators/test-comparison.d.ts +47 -0
- package/dist/generators/test-comparison.d.ts.map +1 -0
- package/dist/generators/test-comparison.js +855 -0
- package/dist/generators/test-comparison.js.map +1 -0
- package/dist/generators/test-generator.d.ts +27 -0
- package/dist/generators/test-generator.d.ts.map +1 -0
- package/dist/generators/test-generator.js +385 -0
- package/dist/generators/test-generator.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +226 -0
- package/dist/index.js.map +1 -0
- package/dist/mappings/aura-to-lwc.json +321 -0
- package/dist/mappings/vf-to-lwc.json +354 -0
- package/dist/parsers/aura/controller-parser.d.ts +36 -0
- package/dist/parsers/aura/controller-parser.d.ts.map +1 -0
- package/dist/parsers/aura/controller-parser.js +269 -0
- package/dist/parsers/aura/controller-parser.js.map +1 -0
- package/dist/parsers/aura/helper-parser.d.ts +21 -0
- package/dist/parsers/aura/helper-parser.d.ts.map +1 -0
- package/dist/parsers/aura/helper-parser.js +173 -0
- package/dist/parsers/aura/helper-parser.js.map +1 -0
- package/dist/parsers/aura/markup-parser.d.ts +59 -0
- package/dist/parsers/aura/markup-parser.d.ts.map +1 -0
- package/dist/parsers/aura/markup-parser.js +279 -0
- package/dist/parsers/aura/markup-parser.js.map +1 -0
- package/dist/parsers/aura/style-parser.d.ts +37 -0
- package/dist/parsers/aura/style-parser.d.ts.map +1 -0
- package/dist/parsers/aura/style-parser.js +151 -0
- package/dist/parsers/aura/style-parser.js.map +1 -0
- package/dist/parsers/vf/apex-parser.d.ts +51 -0
- package/dist/parsers/vf/apex-parser.d.ts.map +1 -0
- package/dist/parsers/vf/apex-parser.js +251 -0
- package/dist/parsers/vf/apex-parser.js.map +1 -0
- package/dist/parsers/vf/page-parser.d.ts +61 -0
- package/dist/parsers/vf/page-parser.d.ts.map +1 -0
- package/dist/parsers/vf/page-parser.js +403 -0
- package/dist/parsers/vf/page-parser.js.map +1 -0
- package/dist/transformers/aura-to-lwc/controller.d.ts +36 -0
- package/dist/transformers/aura-to-lwc/controller.d.ts.map +1 -0
- package/dist/transformers/aura-to-lwc/controller.js +372 -0
- package/dist/transformers/aura-to-lwc/controller.js.map +1 -0
- package/dist/transformers/aura-to-lwc/events.d.ts +47 -0
- package/dist/transformers/aura-to-lwc/events.d.ts.map +1 -0
- package/dist/transformers/aura-to-lwc/events.js +262 -0
- package/dist/transformers/aura-to-lwc/events.js.map +1 -0
- package/dist/transformers/aura-to-lwc/markup.d.ts +51 -0
- package/dist/transformers/aura-to-lwc/markup.d.ts.map +1 -0
- package/dist/transformers/aura-to-lwc/markup.js +465 -0
- package/dist/transformers/aura-to-lwc/markup.js.map +1 -0
- package/dist/transformers/vf-to-lwc/components.d.ts +40 -0
- package/dist/transformers/vf-to-lwc/components.d.ts.map +1 -0
- package/dist/transformers/vf-to-lwc/components.js +374 -0
- package/dist/transformers/vf-to-lwc/components.js.map +1 -0
- package/dist/transformers/vf-to-lwc/data-binding.d.ts +53 -0
- package/dist/transformers/vf-to-lwc/data-binding.d.ts.map +1 -0
- package/dist/transformers/vf-to-lwc/data-binding.js +660 -0
- package/dist/transformers/vf-to-lwc/data-binding.js.map +1 -0
- package/dist/transformers/vf-to-lwc/markup.d.ts +44 -0
- package/dist/transformers/vf-to-lwc/markup.d.ts.map +1 -0
- package/dist/transformers/vf-to-lwc/markup.js +816 -0
- package/dist/transformers/vf-to-lwc/markup.js.map +1 -0
- package/dist/utils/confidence-scorer.d.ts +100 -0
- package/dist/utils/confidence-scorer.d.ts.map +1 -0
- package/dist/utils/confidence-scorer.js +358 -0
- package/dist/utils/confidence-scorer.js.map +1 -0
- package/dist/utils/file-io.d.ts +62 -0
- package/dist/utils/file-io.d.ts.map +1 -0
- package/dist/utils/file-io.js +248 -0
- package/dist/utils/file-io.js.map +1 -0
- package/dist/utils/logger.d.ts +34 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +130 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/open-folder.d.ts +9 -0
- package/dist/utils/open-folder.d.ts.map +1 -0
- package/dist/utils/open-folder.js +76 -0
- package/dist/utils/open-folder.js.map +1 -0
- package/dist/utils/path-resolver.d.ts +29 -0
- package/dist/utils/path-resolver.d.ts.map +1 -0
- package/dist/utils/path-resolver.js +240 -0
- package/dist/utils/path-resolver.js.map +1 -0
- package/dist/utils/session-store.d.ts +158 -0
- package/dist/utils/session-store.d.ts.map +1 -0
- package/dist/utils/session-store.js +518 -0
- package/dist/utils/session-store.js.map +1 -0
- package/dist/utils/vf-controller-resolver.d.ts +36 -0
- package/dist/utils/vf-controller-resolver.d.ts.map +1 -0
- package/dist/utils/vf-controller-resolver.js +162 -0
- package/dist/utils/vf-controller-resolver.js.map +1 -0
- 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
|