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,660 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Transform VF data binding patterns to LWC patterns
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.convertGlobalVariable = convertGlobalVariable;
|
|
7
|
+
exports.convertControllerProperty = convertControllerProperty;
|
|
8
|
+
exports.convertControllerMethod = convertControllerMethod;
|
|
9
|
+
exports.generateDataAccessLayer = generateDataAccessLayer;
|
|
10
|
+
/**
|
|
11
|
+
* Analyze VF page JavaScript for sforce.one.subscribe patterns that call actionFunctions
|
|
12
|
+
* Pattern 1: sforce.one.subscribe(channel, function(message) { actionFunctionName(message.property); })
|
|
13
|
+
* Pattern 2: sforce.one.subscribe(channel, (message) => actionFunc(message.prop))
|
|
14
|
+
* Pattern 3: Config-based: { actionFunction: refreshContactsFunction, lmsSubscribe: sforce.one.subscribe }
|
|
15
|
+
*/
|
|
16
|
+
function analyzeLmsToActionFunctionBindings(customJavaScript, actionFunctions) {
|
|
17
|
+
const bindings = [];
|
|
18
|
+
const actionFunctionNames = new Set(actionFunctions.map(af => af.name));
|
|
19
|
+
for (const js of customJavaScript) {
|
|
20
|
+
// Pattern 1 & 2: Direct sforce.one.subscribe(channel, function(message) { actionFunc(message.prop); })
|
|
21
|
+
const subscribePattern = /sforce\.one\.subscribe\s*\(\s*([^,]+),\s*(?:function\s*\(\s*(\w+)\s*\)|(?:\(\s*(\w+)\s*\)\s*=>))/gi;
|
|
22
|
+
let match;
|
|
23
|
+
while ((match = subscribePattern.exec(js)) !== null) {
|
|
24
|
+
const channelRef = match[1].trim();
|
|
25
|
+
const messageParam = match[2] || match[3];
|
|
26
|
+
// Find the function body after this match
|
|
27
|
+
const afterMatch = js.substring(match.index + match[0].length);
|
|
28
|
+
// Look for actionFunction calls in the body
|
|
29
|
+
for (const afName of actionFunctionNames) {
|
|
30
|
+
// Pattern: actionFunctionName(message.propertyName) or actionFunctionName(message['propertyName'])
|
|
31
|
+
const afCallPattern = new RegExp(`${afName}\\s*\\(\\s*${messageParam}\\.(\\w+)|${afName}\\s*\\(\\s*${messageParam}\\[['"]([\\w]+)['"]\\]`, 'i');
|
|
32
|
+
const afMatch = afterMatch.match(afCallPattern);
|
|
33
|
+
if (afMatch) {
|
|
34
|
+
const messageProperty = afMatch[1] || afMatch[2];
|
|
35
|
+
bindings.push({
|
|
36
|
+
channelName: channelRef,
|
|
37
|
+
actionFunctionName: afName,
|
|
38
|
+
messageProperty: messageProperty || 'recordId', // Default to recordId
|
|
39
|
+
});
|
|
40
|
+
break; // Found a binding for this subscribe, move to next
|
|
41
|
+
}
|
|
42
|
+
// Also check for simpler pattern: just calling the actionFunction with message param
|
|
43
|
+
const simplePattern = new RegExp(`${afName}\\s*\\(\\s*${messageParam}\\s*\\)`, 'i');
|
|
44
|
+
if (simplePattern.test(afterMatch)) {
|
|
45
|
+
bindings.push({
|
|
46
|
+
channelName: channelRef,
|
|
47
|
+
actionFunctionName: afName,
|
|
48
|
+
messageProperty: 'recordId', // Default
|
|
49
|
+
});
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Pattern 3: Config-based pattern where actionFunction and lmsSubscribe are passed to a config object
|
|
55
|
+
// Example: setPageConfigs({ messageChannel: '...', actionFunction: refreshContactsFunction, lmsSubscribe: sforce.one.subscribe })
|
|
56
|
+
// This indicates the external JS will wire them together, typically with message.recordId
|
|
57
|
+
if (js.includes('lmsSubscribe') && js.includes('sforce.one.subscribe')) {
|
|
58
|
+
// Look for actionFunction: functionName pattern in config objects
|
|
59
|
+
const configActionFunctionPattern = /actionFunction\s*:\s*(\w+)/gi;
|
|
60
|
+
let configMatch;
|
|
61
|
+
while ((configMatch = configActionFunctionPattern.exec(js)) !== null) {
|
|
62
|
+
const afName = configMatch[1];
|
|
63
|
+
if (actionFunctionNames.has(afName)) {
|
|
64
|
+
// Check if there's already a binding for this actionFunction
|
|
65
|
+
const existingBinding = bindings.find(b => b.actionFunctionName === afName);
|
|
66
|
+
if (!existingBinding) {
|
|
67
|
+
// This is a config-based pattern - the external JS typically uses message.recordId
|
|
68
|
+
bindings.push({
|
|
69
|
+
channelName: 'config-based',
|
|
70
|
+
actionFunctionName: afName,
|
|
71
|
+
messageProperty: 'recordId', // Standard convention
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return bindings;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Determine if a property should use @wire
|
|
82
|
+
*/
|
|
83
|
+
function shouldUseWire(property) {
|
|
84
|
+
// Getters that just return data are good candidates for wire
|
|
85
|
+
// Setters or properties with side effects should use imperative
|
|
86
|
+
return property.hasGetter && !property.hasSetter && property.isPublic;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Determine if a method should use @wire vs imperative
|
|
90
|
+
*/
|
|
91
|
+
function shouldMethodUseWire(method) {
|
|
92
|
+
// Wire is best for:
|
|
93
|
+
// - Methods that take no params or only recordId
|
|
94
|
+
// - Read-only operations
|
|
95
|
+
// - Cacheable methods
|
|
96
|
+
if (method.isCacheable)
|
|
97
|
+
return true;
|
|
98
|
+
// Methods with many parameters are better suited for imperative
|
|
99
|
+
if (method.parameters.length > 2)
|
|
100
|
+
return false;
|
|
101
|
+
// Methods that likely modify data (based on name) should be imperative
|
|
102
|
+
const modifyingPrefixes = ['save', 'update', 'delete', 'insert', 'create', 'remove', 'upsert'];
|
|
103
|
+
const nameLower = method.name.toLowerCase();
|
|
104
|
+
if (modifyingPrefixes.some((p) => nameLower.startsWith(p)))
|
|
105
|
+
return false;
|
|
106
|
+
// Default to wire for simple getters
|
|
107
|
+
return method.parameters.length <= 1 && method.returnType !== 'void';
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Convert VF global variable expression to LWC
|
|
111
|
+
*/
|
|
112
|
+
function convertGlobalVariable(expression, lmsBinding) {
|
|
113
|
+
const imports = [];
|
|
114
|
+
const warnings = [];
|
|
115
|
+
let code = '';
|
|
116
|
+
let pattern = 'property';
|
|
117
|
+
const expr = expression.reference;
|
|
118
|
+
// $CurrentPage.parameters.x or $CurrentPage.Name
|
|
119
|
+
if (expr.includes('$CurrentPage')) {
|
|
120
|
+
pattern = 'wire';
|
|
121
|
+
imports.push("import { CurrentPageReference } from 'lightning/navigation';");
|
|
122
|
+
const paramMatch = expr.match(/\$CurrentPage\.parameters\.(\w+)/);
|
|
123
|
+
const nameMatch = expr.match(/\$CurrentPage\.Name/i);
|
|
124
|
+
if (paramMatch) {
|
|
125
|
+
const paramName = paramMatch[1];
|
|
126
|
+
code = `// Access URL parameter: ${paramName}
|
|
127
|
+
@wire(CurrentPageReference)
|
|
128
|
+
pageRef;
|
|
129
|
+
|
|
130
|
+
get ${paramName}() {
|
|
131
|
+
return this.pageRef?.state?.${paramName};
|
|
132
|
+
}`;
|
|
133
|
+
}
|
|
134
|
+
else if (nameMatch) {
|
|
135
|
+
code = `// Access current page name
|
|
136
|
+
@wire(CurrentPageReference)
|
|
137
|
+
pageRef;
|
|
138
|
+
|
|
139
|
+
get pageName() {
|
|
140
|
+
return this.pageRef?.attributes?.name || this.pageRef?.state?.c__pageName || '';
|
|
141
|
+
}`;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
code = `@wire(CurrentPageReference)
|
|
145
|
+
pageRef;`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// $MessageChannel.Channel_Name__c - Lightning Message Service
|
|
149
|
+
else if (expr.includes('$MessageChannel')) {
|
|
150
|
+
pattern = 'wire';
|
|
151
|
+
const channelMatch = expr.match(/\$MessageChannel\.([^}'"]+)/);
|
|
152
|
+
const channelName = channelMatch ? channelMatch[1] : 'Unknown_Channel__c';
|
|
153
|
+
const channelVar = channelName.replace(/__c$/i, '').toUpperCase() + '_CHANNEL';
|
|
154
|
+
imports.push("import { publish, subscribe, unsubscribe, MessageContext } from 'lightning/messageService';");
|
|
155
|
+
imports.push(`import ${channelVar} from '@salesforce/messageChannel/${channelName}';`);
|
|
156
|
+
// Generate handleMessage body based on LMS binding analysis
|
|
157
|
+
let handleMessageBody;
|
|
158
|
+
if (lmsBinding) {
|
|
159
|
+
handleMessageBody = `// Auto-detected from VF: calls ${lmsBinding.actionFunctionName} with message.${lmsBinding.messageProperty}
|
|
160
|
+
if (message.${lmsBinding.messageProperty}) {
|
|
161
|
+
this.${lmsBinding.actionFunctionName}(message.${lmsBinding.messageProperty})
|
|
162
|
+
.then(result => {
|
|
163
|
+
// Update component state with result
|
|
164
|
+
// Example: this.contactRecord = result;
|
|
165
|
+
})
|
|
166
|
+
.catch(error => {
|
|
167
|
+
this.handleError(error);
|
|
168
|
+
});
|
|
169
|
+
}`;
|
|
170
|
+
warnings.push(`LMS handleMessage auto-wired to ${lmsBinding.actionFunctionName}(message.${lmsBinding.messageProperty})`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
handleMessageBody = `// TODO: Handle message payload
|
|
174
|
+
// Example: this.recordId = message.recordId;`;
|
|
175
|
+
}
|
|
176
|
+
code = `// Lightning Message Service: ${channelName}
|
|
177
|
+
@wire(MessageContext)
|
|
178
|
+
messageContext;
|
|
179
|
+
|
|
180
|
+
// Subscription reference for cleanup
|
|
181
|
+
_subscription = null;
|
|
182
|
+
|
|
183
|
+
// Lifecycle: Subscribe to message channel
|
|
184
|
+
connectedCallback() {
|
|
185
|
+
this.subscribeToMessageChannel();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Lifecycle: Cleanup subscription
|
|
189
|
+
disconnectedCallback() {
|
|
190
|
+
this.unsubscribeFromMessageChannel();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Subscribe to messages
|
|
194
|
+
subscribeToMessageChannel() {
|
|
195
|
+
if (!this._subscription) {
|
|
196
|
+
this._subscription = subscribe(
|
|
197
|
+
this.messageContext,
|
|
198
|
+
${channelVar},
|
|
199
|
+
(message) => this.handleMessage(message)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Unsubscribe from messages
|
|
205
|
+
unsubscribeFromMessageChannel() {
|
|
206
|
+
unsubscribe(this._subscription);
|
|
207
|
+
this._subscription = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle incoming messages
|
|
211
|
+
handleMessage(message) {
|
|
212
|
+
${handleMessageBody}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Publish a message
|
|
216
|
+
publishMessage(payload) {
|
|
217
|
+
publish(this.messageContext, ${channelVar}, payload);
|
|
218
|
+
}`;
|
|
219
|
+
warnings.push(`Lightning Message Service detected: ${channelName} - connectedCallback/disconnectedCallback auto-generated`);
|
|
220
|
+
}
|
|
221
|
+
// $User.x
|
|
222
|
+
else if (expr.includes('$User')) {
|
|
223
|
+
pattern = 'import';
|
|
224
|
+
const fieldMatch = expr.match(/\$User\.(\w+)/);
|
|
225
|
+
if (fieldMatch) {
|
|
226
|
+
const field = fieldMatch[1];
|
|
227
|
+
if (field === 'Id') {
|
|
228
|
+
imports.push("import userId from '@salesforce/user/Id';");
|
|
229
|
+
code = `// Current user ID
|
|
230
|
+
userId = userId;`;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
imports.push("import { getRecord } from 'lightning/uiRecordApi';");
|
|
234
|
+
imports.push("import userId from '@salesforce/user/Id';");
|
|
235
|
+
imports.push(`import USER_${field.toUpperCase()}_FIELD from '@salesforce/schema/User.${field}';`);
|
|
236
|
+
code = `// Current user ${field}
|
|
237
|
+
@wire(getRecord, { recordId: userId, fields: [USER_${field.toUpperCase()}_FIELD] })
|
|
238
|
+
user;
|
|
239
|
+
|
|
240
|
+
get user${field}() {
|
|
241
|
+
return this.user?.data?.fields?.${field}?.value;
|
|
242
|
+
}`;
|
|
243
|
+
pattern = 'wire';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// $Label.namespace.labelName
|
|
248
|
+
else if (expr.includes('$Label')) {
|
|
249
|
+
pattern = 'import';
|
|
250
|
+
const labelMatch = expr.match(/\$Label\.(\w+)\.(\w+)/);
|
|
251
|
+
if (labelMatch) {
|
|
252
|
+
const [, namespace, labelName] = labelMatch;
|
|
253
|
+
imports.push(`import ${labelName} from '@salesforce/label/${namespace}.${labelName}';`);
|
|
254
|
+
code = `// Label: ${namespace}.${labelName}
|
|
255
|
+
label = {
|
|
256
|
+
${labelName}
|
|
257
|
+
};`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// $Resource.resourceName
|
|
261
|
+
else if (expr.includes('$Resource')) {
|
|
262
|
+
pattern = 'import';
|
|
263
|
+
const resourceMatch = expr.match(/\$Resource\.(\w+)/);
|
|
264
|
+
if (resourceMatch) {
|
|
265
|
+
const resourceName = resourceMatch[1];
|
|
266
|
+
imports.push(`import ${resourceName} from '@salesforce/resourceUrl/${resourceName}';`);
|
|
267
|
+
code = `// Static resource: ${resourceName}
|
|
268
|
+
${resourceName}Url = ${resourceName};`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// $Action
|
|
272
|
+
else if (expr.includes('$Action')) {
|
|
273
|
+
pattern = 'imperative';
|
|
274
|
+
warnings.push('$Action expressions need NavigationMixin for navigation');
|
|
275
|
+
imports.push("import { NavigationMixin } from 'lightning/navigation';");
|
|
276
|
+
code = `// Navigation action - use NavigationMixin
|
|
277
|
+
// Example: this[NavigationMixin.Navigate]({
|
|
278
|
+
// type: 'standard__recordPage',
|
|
279
|
+
// attributes: { recordId: id, actionName: 'view' }
|
|
280
|
+
// });`;
|
|
281
|
+
}
|
|
282
|
+
// $ObjectType
|
|
283
|
+
else if (expr.includes('$ObjectType')) {
|
|
284
|
+
pattern = 'import';
|
|
285
|
+
const objectMatch = expr.match(/\$ObjectType\.(\w+)\.fields\.(\w+)/);
|
|
286
|
+
if (objectMatch) {
|
|
287
|
+
const [, objectName, fieldName] = objectMatch;
|
|
288
|
+
imports.push(`import ${fieldName.toUpperCase()}_FIELD from '@salesforce/schema/${objectName}.${fieldName}';`);
|
|
289
|
+
code = `// Field reference: ${objectName}.${fieldName}
|
|
290
|
+
// Use in wire adapter or getFieldValue`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
warnings.push(`Unknown global variable pattern: ${expr}`);
|
|
295
|
+
code = `// TODO: Convert ${expr}`;
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
vfExpression: expression.original,
|
|
299
|
+
lwcPattern: pattern,
|
|
300
|
+
code,
|
|
301
|
+
imports,
|
|
302
|
+
warnings,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Convert Apex controller property to LWC
|
|
307
|
+
*/
|
|
308
|
+
function convertControllerProperty(property, controllerName) {
|
|
309
|
+
const warnings = [];
|
|
310
|
+
const isWireCandidate = shouldUseWire(property);
|
|
311
|
+
let code = '';
|
|
312
|
+
if (isWireCandidate) {
|
|
313
|
+
// Use wire adapter
|
|
314
|
+
code = `// Property: ${property.name} (from ${controllerName})
|
|
315
|
+
// Option 1: Wire to Apex getter
|
|
316
|
+
@wire(get${capitalize(property.name)})
|
|
317
|
+
${property.name};
|
|
318
|
+
|
|
319
|
+
// Option 2: Simple reactive property
|
|
320
|
+
${property.name};
|
|
321
|
+
|
|
322
|
+
// If using wire, add this to imports:
|
|
323
|
+
// import get${capitalize(property.name)} from '@salesforce/apex/${controllerName}.get${capitalize(property.name)}';`;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
// Use imperative or reactive property
|
|
327
|
+
code = `// Property: ${property.name}
|
|
328
|
+
${property.name}${property.initialValue ? ` = ${property.initialValue}` : ''};`;
|
|
329
|
+
if (property.hasSetter) {
|
|
330
|
+
code += `
|
|
331
|
+
|
|
332
|
+
// This property had a setter in VF - update via method call
|
|
333
|
+
async update${capitalize(property.name)}(value) {
|
|
334
|
+
this.${property.name} = value;
|
|
335
|
+
// TODO: If this needs to persist, call Apex method
|
|
336
|
+
}`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
property,
|
|
341
|
+
lwcCode: code,
|
|
342
|
+
isWireCandidate,
|
|
343
|
+
warnings,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Convert Apex controller method to LWC
|
|
348
|
+
*/
|
|
349
|
+
function convertControllerMethod(method, controllerName) {
|
|
350
|
+
const warnings = [];
|
|
351
|
+
const isWireCandidate = shouldMethodUseWire(method);
|
|
352
|
+
let importStatement = '';
|
|
353
|
+
let code = '';
|
|
354
|
+
// Check if method already has @AuraEnabled
|
|
355
|
+
if (!method.isAuraEnabled && !method.isRemoteAction) {
|
|
356
|
+
warnings.push(`Method "${method.name}" needs @AuraEnabled annotation to be called from LWC`);
|
|
357
|
+
}
|
|
358
|
+
importStatement = `import ${method.name} from '@salesforce/apex/${controllerName}.${method.name}';`;
|
|
359
|
+
if (isWireCandidate) {
|
|
360
|
+
// Generate wire-based code
|
|
361
|
+
if (method.parameters.length === 0) {
|
|
362
|
+
code = `// Wired Apex method: ${method.name}
|
|
363
|
+
@wire(${method.name})
|
|
364
|
+
${method.name}Result;
|
|
365
|
+
|
|
366
|
+
get ${method.name}Data() {
|
|
367
|
+
return this.${method.name}Result?.data;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
get ${method.name}Error() {
|
|
371
|
+
return this.${method.name}Result?.error;
|
|
372
|
+
}`;
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
// Wire with parameters
|
|
376
|
+
const params = method.parameters
|
|
377
|
+
.map((p) => `${p.name}: this.${p.name}`)
|
|
378
|
+
.join(', ');
|
|
379
|
+
code = `// Wired Apex method: ${method.name}
|
|
380
|
+
@wire(${method.name}, { ${params} })
|
|
381
|
+
${method.name}Result;`;
|
|
382
|
+
}
|
|
383
|
+
if (method.isCacheable) {
|
|
384
|
+
code += `
|
|
385
|
+
|
|
386
|
+
// Note: This method is cacheable. Use refreshApex() to refresh:
|
|
387
|
+
// import { refreshApex } from '@salesforce/apex';
|
|
388
|
+
// await refreshApex(this.${method.name}Result);`;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
// Generate imperative call code
|
|
393
|
+
const params = method.parameters.map((p) => p.name).join(', ');
|
|
394
|
+
const paramObj = method.parameters.length > 0
|
|
395
|
+
? `{ ${method.parameters.map((p) => p.name).join(', ')} }`
|
|
396
|
+
: '';
|
|
397
|
+
code = `// Imperative Apex call: ${method.name}
|
|
398
|
+
async call${capitalize(method.name)}(${params}) {
|
|
399
|
+
try {
|
|
400
|
+
const result = await ${method.name}(${paramObj});
|
|
401
|
+
return result;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.error('Error calling ${method.name}:', error);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
}`;
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
method,
|
|
410
|
+
lwcCode: code,
|
|
411
|
+
importStatement,
|
|
412
|
+
isWireCandidate,
|
|
413
|
+
warnings,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Generate complete data access layer for VF page
|
|
418
|
+
*/
|
|
419
|
+
function generateDataAccessLayer(vfPage, apexController) {
|
|
420
|
+
const imports = [];
|
|
421
|
+
const properties = [];
|
|
422
|
+
const methods = [];
|
|
423
|
+
const wireDeclarations = [];
|
|
424
|
+
const warnings = [];
|
|
425
|
+
// Analyze VF page JavaScript for LMS-to-actionFunction bindings
|
|
426
|
+
const lmsBindings = analyzeLmsToActionFunctionBindings(vfPage.customJavaScript, vfPage.actionFunctions);
|
|
427
|
+
// Process global variable expressions
|
|
428
|
+
const processedExpressions = new Set();
|
|
429
|
+
for (const expr of vfPage.expressions) {
|
|
430
|
+
// Avoid processing the same expression multiple times
|
|
431
|
+
if (processedExpressions.has(expr.reference))
|
|
432
|
+
continue;
|
|
433
|
+
processedExpressions.add(expr.reference);
|
|
434
|
+
if (expr.type === 'global' || expr.reference.includes('$MessageChannel')) {
|
|
435
|
+
// Find matching LMS binding for this MessageChannel
|
|
436
|
+
let matchingBinding;
|
|
437
|
+
if (expr.reference.includes('$MessageChannel')) {
|
|
438
|
+
matchingBinding = lmsBindings.find(_b => {
|
|
439
|
+
// The binding channelName might be a variable reference or the actual channel name
|
|
440
|
+
return lmsBindings.length > 0; // Use first binding if any exist
|
|
441
|
+
});
|
|
442
|
+
if (lmsBindings.length > 0 && !matchingBinding) {
|
|
443
|
+
matchingBinding = lmsBindings[0]; // Default to first binding
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const conversion = convertGlobalVariable(expr, matchingBinding);
|
|
447
|
+
imports.push(...conversion.imports);
|
|
448
|
+
if (conversion.lwcPattern === 'wire') {
|
|
449
|
+
wireDeclarations.push(conversion.code);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
properties.push(conversion.code);
|
|
453
|
+
}
|
|
454
|
+
warnings.push(...conversion.warnings);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Process Remote Objects from VF components
|
|
458
|
+
const remoteObjects = findRemoteObjects(vfPage.components);
|
|
459
|
+
if (remoteObjects.length > 0) {
|
|
460
|
+
for (const ro of remoteObjects) {
|
|
461
|
+
const objectNameLower = ro.objectName.toLowerCase();
|
|
462
|
+
// Generate wire adapter scaffolding
|
|
463
|
+
wireDeclarations.push(`// Wire adapter for ${ro.objectName} (converted from apex:remoteObjectModel)
|
|
464
|
+
// TODO: Create Apex controller method to replace Remote Objects
|
|
465
|
+
//
|
|
466
|
+
// Example Apex controller:
|
|
467
|
+
// @AuraEnabled(cacheable=true)
|
|
468
|
+
// public static List<${ro.objectName}> get${ro.objectName}s() {
|
|
469
|
+
// return [SELECT ${ro.fields} FROM ${ro.objectName} LIMIT 10];
|
|
470
|
+
// }
|
|
471
|
+
//
|
|
472
|
+
// @wire(get${ro.objectName}s)
|
|
473
|
+
// ${objectNameLower}s;
|
|
474
|
+
//
|
|
475
|
+
// Or use lightning/uiRecordApi for standard objects:
|
|
476
|
+
// import { getRecords } from 'lightning/uiRecordApi';
|
|
477
|
+
`);
|
|
478
|
+
imports.push(`// import get${ro.objectName}s from '@salesforce/apex/YourController.get${ro.objectName}s';`);
|
|
479
|
+
warnings.push(`Remote Object "${ro.objectName}" detected - create Apex controller method to replace`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Process Apex controller if available
|
|
483
|
+
if (apexController) {
|
|
484
|
+
// Process properties
|
|
485
|
+
for (const prop of apexController.properties) {
|
|
486
|
+
const conversion = convertControllerProperty(prop, apexController.className);
|
|
487
|
+
if (conversion.isWireCandidate) {
|
|
488
|
+
wireDeclarations.push(conversion.lwcCode);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
properties.push(conversion.lwcCode);
|
|
492
|
+
}
|
|
493
|
+
warnings.push(...conversion.warnings);
|
|
494
|
+
}
|
|
495
|
+
// Process methods
|
|
496
|
+
for (const method of apexController.methods) {
|
|
497
|
+
if (method.isPublic || method.isAuraEnabled || method.isRemoteAction) {
|
|
498
|
+
const conversion = convertControllerMethod(method, apexController.className);
|
|
499
|
+
imports.push(conversion.importStatement);
|
|
500
|
+
if (conversion.isWireCandidate) {
|
|
501
|
+
wireDeclarations.push(conversion.lwcCode);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
methods.push(conversion.lwcCode);
|
|
505
|
+
}
|
|
506
|
+
warnings.push(...conversion.warnings);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Note about SOQL queries
|
|
510
|
+
if (apexController.soqlQueries.length > 0) {
|
|
511
|
+
warnings.push(`Controller has ${apexController.soqlQueries.length} SOQL queries - ensure FLS/CRUD checks are in place`);
|
|
512
|
+
}
|
|
513
|
+
// Note about DML operations
|
|
514
|
+
if (apexController.dmlOperations.length > 0) {
|
|
515
|
+
warnings.push(`Controller has DML operations (${apexController.dmlOperations.join(', ')}) - these should use imperative calls`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Process remote actions from VF page
|
|
519
|
+
for (const ra of vfPage.remoteActions) {
|
|
520
|
+
warnings.push(`RemoteAction "${ra.controller}.${ra.method}" - add @AuraEnabled annotation and import`);
|
|
521
|
+
imports.push(`// import ${ra.method} from '@salesforce/apex/${ra.controller}.${ra.method}';`);
|
|
522
|
+
}
|
|
523
|
+
// Process action functions
|
|
524
|
+
// Action functions with parameters should use imperative Apex pattern instead of wire
|
|
525
|
+
for (const af of vfPage.actionFunctions) {
|
|
526
|
+
// Check if this action function has parameters (apex:param children)
|
|
527
|
+
const hasParams = findActionFunctionParams(vfPage.components, af.name);
|
|
528
|
+
if (hasParams.length > 0) {
|
|
529
|
+
// Use imperative pattern for actionFunction with params
|
|
530
|
+
const paramNames = hasParams.map(p => p.name);
|
|
531
|
+
const paramDeclaration = paramNames.join(', ');
|
|
532
|
+
const paramObject = paramNames.length > 0
|
|
533
|
+
? `{ ${paramNames.join(', ')} }`
|
|
534
|
+
: '';
|
|
535
|
+
// Extract the method name from the action (e.g., "{!doSomething}" -> "doSomething")
|
|
536
|
+
const methodName = extractMethodFromAction(af.action);
|
|
537
|
+
methods.push(`// Converted from apex:actionFunction "${af.name}" (imperative - has parameters)
|
|
538
|
+
// Parameters: ${paramNames.join(', ')}
|
|
539
|
+
async ${af.name}(${paramDeclaration}) {
|
|
540
|
+
this.isLoading = true;
|
|
541
|
+
try {
|
|
542
|
+
const result = await ${methodName || af.name}(${paramObject});
|
|
543
|
+
${af.rerender ? `// Originally rerendered: ${af.rerender} - update reactive properties to trigger re-render` : ''}
|
|
544
|
+
${af.oncomplete ? `// Original oncomplete callback: ${af.oncomplete}` : ''}
|
|
545
|
+
return result;
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error('Error calling ${af.name}:', error);
|
|
548
|
+
throw error;
|
|
549
|
+
} finally {
|
|
550
|
+
this.isLoading = false;
|
|
551
|
+
}
|
|
552
|
+
}`);
|
|
553
|
+
// Add import for the Apex method if we can determine the controller
|
|
554
|
+
if (methodName && apexController) {
|
|
555
|
+
imports.push(`import ${methodName} from '@salesforce/apex/${apexController.className}.${methodName}';`);
|
|
556
|
+
}
|
|
557
|
+
else if (methodName) {
|
|
558
|
+
imports.push(`// import ${methodName} from '@salesforce/apex/YourController.${methodName}';`);
|
|
559
|
+
}
|
|
560
|
+
warnings.push(`actionFunction "${af.name}" has ${hasParams.length} parameters - using imperative Apex pattern`);
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
// Simple action function without params can potentially use wire (but still default to imperative for consistency)
|
|
564
|
+
methods.push(`// Converted from apex:actionFunction "${af.name}"
|
|
565
|
+
async ${af.name}() {
|
|
566
|
+
this.isLoading = true;
|
|
567
|
+
try {
|
|
568
|
+
// TODO: Implement - original action: ${af.action}
|
|
569
|
+
${af.rerender ? `// Originally rerendered: ${af.rerender}` : ''}
|
|
570
|
+
${af.oncomplete ? `// Had oncomplete: ${af.oncomplete}` : ''}
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error('Error calling ${af.name}:', error);
|
|
573
|
+
throw error;
|
|
574
|
+
} finally {
|
|
575
|
+
this.isLoading = false;
|
|
576
|
+
}
|
|
577
|
+
}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Deduplicate imports
|
|
581
|
+
const uniqueImports = [...new Set(imports)];
|
|
582
|
+
return {
|
|
583
|
+
imports: uniqueImports,
|
|
584
|
+
properties,
|
|
585
|
+
methods,
|
|
586
|
+
wireDeclarations,
|
|
587
|
+
warnings,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Helper: capitalize first letter
|
|
592
|
+
*/
|
|
593
|
+
function capitalize(str) {
|
|
594
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
595
|
+
}
|
|
596
|
+
function findRemoteObjects(components) {
|
|
597
|
+
const remoteObjects = [];
|
|
598
|
+
function traverse(comp) {
|
|
599
|
+
if (comp.name.toLowerCase() === 'apex:remoteobjectmodel') {
|
|
600
|
+
const objectName = comp.attributes.name || 'Unknown';
|
|
601
|
+
const fields = comp.attributes.fields || 'Id';
|
|
602
|
+
remoteObjects.push({ objectName, fields });
|
|
603
|
+
}
|
|
604
|
+
// Check children
|
|
605
|
+
for (const child of comp.children) {
|
|
606
|
+
traverse(child);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
for (const comp of components) {
|
|
610
|
+
traverse(comp);
|
|
611
|
+
}
|
|
612
|
+
return remoteObjects;
|
|
613
|
+
}
|
|
614
|
+
function findActionFunctionParams(components, actionFunctionName) {
|
|
615
|
+
const params = [];
|
|
616
|
+
function traverse(comp) {
|
|
617
|
+
const lowerName = comp.name.toLowerCase();
|
|
618
|
+
// Found the action function
|
|
619
|
+
if (lowerName === 'apex:actionfunction' && comp.attributes.name === actionFunctionName) {
|
|
620
|
+
// Look for apex:param children
|
|
621
|
+
for (const child of comp.children) {
|
|
622
|
+
if (child.name.toLowerCase() === 'apex:param') {
|
|
623
|
+
const param = {
|
|
624
|
+
name: child.attributes.name || child.attributes.assignto?.replace(/\{!|\}/g, '') || 'param',
|
|
625
|
+
value: child.attributes.value,
|
|
626
|
+
assignTo: child.attributes.assignto,
|
|
627
|
+
};
|
|
628
|
+
params.push(param);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
// Keep searching in children
|
|
634
|
+
for (const child of comp.children) {
|
|
635
|
+
traverse(child);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
for (const comp of components) {
|
|
639
|
+
traverse(comp);
|
|
640
|
+
}
|
|
641
|
+
return params;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Extract method name from VF action expression
|
|
645
|
+
* e.g., "{!doSomething}" -> "doSomething"
|
|
646
|
+
* "{!controller.myMethod}" -> "myMethod"
|
|
647
|
+
*/
|
|
648
|
+
function extractMethodFromAction(action) {
|
|
649
|
+
if (!action)
|
|
650
|
+
return null;
|
|
651
|
+
// Remove {! and } delimiters
|
|
652
|
+
const cleaned = action.replace(/\{!|\}/g, '').trim();
|
|
653
|
+
// Handle controller.method pattern
|
|
654
|
+
if (cleaned.includes('.')) {
|
|
655
|
+
const parts = cleaned.split('.');
|
|
656
|
+
return parts[parts.length - 1];
|
|
657
|
+
}
|
|
658
|
+
return cleaned;
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=data-binding.js.map
|