@lwc-bolts/sf2ts 0.0.1

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/README.MD ADDED
@@ -0,0 +1,5 @@
1
+ The purpose of this library is to take Salesforce Metadata content and transpile it to TypeScript Declaration (d.ts format) content so it can be written to a file or proxied on file reads for editors and language servers.
2
+
3
+ This library does not engage with file systems and is not responsible for discovering or identifying available metadata.
4
+
5
+ Apex should include raw AuraEnabled, AuraEnabled + cacheable, classes with fields (fields not AuraEnabled should show as deprecated with a note), and Apex Continuations, should also carry forward ApexDoc text comments so it can be exposed on definitions.
@@ -0,0 +1,7 @@
1
+ export { ApexClass } from "./apex-class.mjs";
2
+ export { ContentAsset } from "./content-asset.mjs";
3
+ export { CustomPermission } from "./custom-permission.mjs";
4
+ export { CustomField } from "./custom-field.mjs";
5
+ export { CustomLabels } from "./custom-labels.mjs";
6
+ export { CustomObject } from "./custom-object.mjs";
7
+ export { StaticResource } from "./static-resource.mjs";
@@ -0,0 +1,590 @@
1
+ /** @import Parser from "web-tree-sitter" */
2
+ /** @import { Component, ConvertedComponent } from "../index.mjs" */
3
+ import { getApexParser } from "web-tree-sitter-sfapex";
4
+
5
+ // TOOD: Apex Continuation
6
+
7
+ /** @type { Parser } */
8
+ let prsr;
9
+
10
+ const getParserPromise = getApexParser().then((newParser) => {
11
+ prsr = newParser;
12
+ });
13
+
14
+ /**
15
+ *
16
+ * @param {Component} comp
17
+ * @returns {ConvertedComponent}
18
+ */
19
+ export function ApexClass(comp) {
20
+ // generate d.ts
21
+ if (prsr === undefined) {
22
+ /** @type { any } */
23
+ const err = new Error("Apex parser not ready yet");
24
+ err.waitFor = getParserPromise; // smuggle a "ready" signal out to the invoker
25
+ throw err;
26
+ }
27
+
28
+ const [className, declarationContent, mappingContent] =
29
+ buildDeclarationForApexFile(prsr, comp.path, comp.content, false);
30
+ if (declarationContent === undefined) {
31
+ throw new Error("Problem building declaration content");
32
+ }
33
+
34
+ return { ...comp, declarationContent: declarationContent, mapData: [] };
35
+ }
36
+
37
+ /**
38
+ * @typedef DeltaPoint
39
+ * @property {number} rowDelta
40
+ * @property {number} column
41
+ */
42
+
43
+ /**
44
+ * @typedef MappingPos
45
+ * @property {DeltaPoint} startPosition
46
+ * @property {DeltaPoint} endPosition
47
+ */
48
+
49
+ /**
50
+ * @typedef Mapping
51
+ * @property { MappingPos } dPos
52
+ * @property { {startPosition: Parser.Point, endPosition: Parser.Point }?} sPos
53
+ */
54
+
55
+ /** @type { Record<string, string> } */
56
+ const typeOverrides = {
57
+ string: "string",
58
+ datetime: "Date",
59
+ decimal: "number",
60
+ integer: "number",
61
+ boolean: "boolean",
62
+ id: "string",
63
+ };
64
+
65
+ /** @type { string[] } */
66
+ let localNestedClasses = [];
67
+ /** @type { string? } */
68
+ let outerClassName;
69
+
70
+ /**
71
+ *
72
+ * @param {Parser} parser
73
+ * @param {string} relativeFilePath
74
+ * @param {string} apexText
75
+ * @param {boolean} isSobject
76
+ * @returns { string[] }
77
+ */
78
+ export function buildDeclarationForApexFile(
79
+ parser,
80
+ relativeFilePath,
81
+ apexText,
82
+ isSobject = false
83
+ ) {
84
+ try {
85
+ const tree = parser.parse(apexText);
86
+ // step into top-level class, if any
87
+ // if outer-most thing isn't a class the leave
88
+ const classes = tree.rootNode.descendantsOfType("class_declaration");
89
+
90
+ if (classes.length === 0 || !classes[0]) {
91
+ return []; // this isn't a class
92
+ }
93
+
94
+ // TODO: build mapping file
95
+ const [className, declarationText, mappings] = processClass(
96
+ classes[0],
97
+ isSobject
98
+ );
99
+ const mappingText = getMapFile(className, relativeFilePath, mappings);
100
+ localNestedClasses = [];
101
+ outerClassName = null;
102
+ return [className, declarationText, mappingText];
103
+ } catch (error) {
104
+ console.error(error);
105
+
106
+ localNestedClasses = [];
107
+ outerClassName = null;
108
+ return [];
109
+ }
110
+ }
111
+
112
+ /**
113
+ *
114
+ * @param {Parser.SyntaxNode} classNode
115
+ * @param {boolean} isSobject
116
+ * @returns { [string, string, Mapping[]] }
117
+ */
118
+ function processClass(classNode, isSobject) {
119
+ let className = classNode.childForFieldName("name")?.text ?? "";
120
+ if (!outerClassName) {
121
+ outerClassName = className;
122
+ }
123
+ let classBody = classNode.childForFieldName("body");
124
+ /** @type {Mapping[]} */
125
+ const mappings = [];
126
+ /** @type {string[]} */
127
+ let outputText = [];
128
+ if (!classBody) {
129
+ return [className, outputText.join(""), mappings];
130
+ }
131
+
132
+ const methodsToProcess = [];
133
+ const fieldsToProcess = [];
134
+ const classesToProcess = [];
135
+
136
+ for (const cNode of classBody.children) {
137
+ switch (cNode.type) {
138
+ case "method_declaration":
139
+ // gather methods because AuraEnabled methods create their own exports
140
+ methodsToProcess.push(cNode);
141
+ break;
142
+ case "field_declaration":
143
+ fieldsToProcess.push(cNode);
144
+ break;
145
+ case "class_declaration":
146
+ classesToProcess.push(cNode);
147
+ const innerNameNode = cNode.childForFieldName("name");
148
+ if (innerNameNode) {
149
+ // queue up nested class names so type resolution can use them
150
+ localNestedClasses.push(innerNameNode.text);
151
+ }
152
+ break;
153
+ }
154
+ }
155
+ if (fieldsToProcess.length > 0) {
156
+ outputText.push(`declare interface ${className} {\n`);
157
+ mappings.push({
158
+ dPos: {
159
+ startPosition: {
160
+ rowDelta: 0,
161
+ column: 0,
162
+ },
163
+ endPosition: {
164
+ rowDelta: 0,
165
+ column: 0,
166
+ },
167
+ },
168
+ sPos: null,
169
+ });
170
+ for (let fNode of fieldsToProcess) {
171
+ const [newOutputText, newMappings] = transformFieldDeclaration(
172
+ fNode,
173
+ isSobject
174
+ );
175
+ outputText.push(newOutputText);
176
+ mappings.push(...newMappings);
177
+ }
178
+ outputText.push("}\n");
179
+ mappings.push({
180
+ dPos: {
181
+ startPosition: {
182
+ rowDelta: 1,
183
+ column: 0,
184
+ },
185
+ endPosition: {
186
+ rowDelta: 0,
187
+ column: 0,
188
+ },
189
+ },
190
+ sPos: null,
191
+ });
192
+ }
193
+
194
+ if (classesToProcess.length > 0) {
195
+ outputText.push(`declare namespace ${className} {\n`);
196
+ mappings.push({
197
+ dPos: {
198
+ startPosition: {
199
+ rowDelta: 2,
200
+ column: 0,
201
+ },
202
+ endPosition: {
203
+ rowDelta: 0,
204
+ column: 0,
205
+ },
206
+ },
207
+ sPos: null,
208
+ });
209
+ for (const cNode of classesToProcess) {
210
+ const [className, newOutputText, newMappings] = processClass(
211
+ cNode,
212
+ isSobject
213
+ );
214
+ if (newOutputText.length > 0) {
215
+ outputText.push(" " + newOutputText.replaceAll(/\n(.)/g, "\n $1"));
216
+ mappings.push(...newMappings);
217
+ }
218
+ // TODO: somehow queue up local classes for type resolution
219
+ }
220
+
221
+ outputText.push(`}\n`);
222
+ mappings.push({
223
+ dPos: {
224
+ startPosition: {
225
+ rowDelta: 2,
226
+ column: 0,
227
+ },
228
+ endPosition: {
229
+ rowDelta: 0,
230
+ column: 0,
231
+ },
232
+ },
233
+ sPos: null,
234
+ });
235
+ }
236
+
237
+ for (const mNode of methodsToProcess) {
238
+ const [newOutputText, newMappings] = transformMethodDeclaration(
239
+ className,
240
+ mNode
241
+ );
242
+ outputText.push(newOutputText);
243
+ mappings.push(...newMappings);
244
+ }
245
+ return [className, outputText.join(""), mappings];
246
+ }
247
+
248
+ /**
249
+ *
250
+ * @param {string} className
251
+ * @param {Parser.SyntaxNode} methodNode
252
+ * @returns {[string, Mapping[]]}
253
+ */
254
+ function transformMethodDeclaration(className, methodNode) {
255
+ /** @type {Mapping[]} */
256
+ const mappings = [];
257
+ const methodNameNode = methodNode.childForFieldName("name");
258
+ if (!methodNameNode) {
259
+ return ["", mappings];
260
+ }
261
+ const methodName = methodNameNode.text;
262
+ const returnType = methodNode.childForFieldName("type");
263
+ if (!returnType) {
264
+ return ["", mappings];
265
+ }
266
+ const parameters = getMethodParameters(methodNode);
267
+
268
+ const auraEnabledNode = getAuraEnabledAnnotation(methodNode);
269
+ if (!auraEnabledNode) {
270
+ // method doesn't hold the AuraEnabled Annotation
271
+ return ["", mappings];
272
+ }
273
+ const isCacheableEnabled = getAuraEnabledIsCacheable(auraEnabledNode);
274
+
275
+ const [newOutputText, methodNameMapping] = declarationString(
276
+ className,
277
+ methodName,
278
+ getType(returnType),
279
+ parameters,
280
+ isCacheableEnabled
281
+ );
282
+ const declarationParts = newOutputText.split("\n");
283
+ methodNameMapping.rowDelta;
284
+
285
+ mappings.push({
286
+ dPos: {
287
+ startPosition: methodNameMapping,
288
+ endPosition: {
289
+ rowDelta: methodNameMapping.rowDelta,
290
+ column: methodNameMapping.column + methodName.length,
291
+ },
292
+ },
293
+ sPos: {
294
+ startPosition: methodNameNode.startPosition,
295
+ endPosition: methodNameNode.endPosition,
296
+ },
297
+ });
298
+ // add consumption of remaining lines
299
+ mappings.push({
300
+ dPos: {
301
+ startPosition: {
302
+ rowDelta: declarationParts.length - methodNameMapping.rowDelta,
303
+ column: 0,
304
+ },
305
+ endPosition: {
306
+ rowDelta: declarationParts.length - methodNameMapping.rowDelta,
307
+ column: 0,
308
+ },
309
+ },
310
+ sPos: null,
311
+ });
312
+ return [newOutputText, mappings];
313
+ }
314
+
315
+ /**
316
+ *
317
+ * @param {Parser.SyntaxNode} node
318
+ * @param {boolean} isSobject
319
+ * @returns {[string, Mapping[]]}
320
+ */
321
+ function transformFieldDeclaration(node, isSobject) {
322
+ /** @type {string[]} */
323
+ let deprecatedReasons = [];
324
+ let output = "";
325
+ const modifiers = node.descendantsOfType("modifiers");
326
+ /** @type {Mapping} */
327
+ const mapping = {
328
+ dPos: {
329
+ startPosition: {
330
+ rowDelta: 1,
331
+ column: 2,
332
+ },
333
+ endPosition: {
334
+ // TODO
335
+ rowDelta: 0,
336
+ column: 0,
337
+ },
338
+ },
339
+ sPos: {
340
+ startPosition: node.startPosition,
341
+ endPosition: node.endPosition,
342
+ },
343
+ };
344
+
345
+ if (modifiers.length === 0 || !modifiers[0]) {
346
+ deprecatedReasons.push("public or global");
347
+ } else {
348
+ // initialize so the generated SObjects don't flag missing AuraEnabled
349
+ let isAuraEnabled = isSobject;
350
+ for (const aNode of modifiers[0].descendantsOfType("annotation")) {
351
+ const aNameNode = aNode.childForFieldName("name");
352
+ if (aNameNode && aNameNode.text.toLowerCase() === "auraenabled") {
353
+ isAuraEnabled = true;
354
+ }
355
+ }
356
+ if (!isAuraEnabled) {
357
+ deprecatedReasons.push("@AuraEnabled");
358
+ }
359
+
360
+ let isAtLeastPublic = false;
361
+ for (const mNode of modifiers[0].descendantsOfType("modifier")) {
362
+ switch (mNode.text.toLowerCase()) {
363
+ case "public":
364
+ case "global":
365
+ isAtLeastPublic = true;
366
+ break;
367
+ case "static":
368
+ deprecatedReasons.push("non-static");
369
+ break;
370
+ }
371
+ }
372
+ if (!isAtLeastPublic) {
373
+ deprecatedReasons.push("public or global");
374
+ }
375
+ }
376
+ /** TODO:
377
+ * determine if AuraEnabled & public or global & not static
378
+ * otherwise mark as deprecated with note (dynamic based on missing access component)
379
+ * {property}: {getType({type})}?;
380
+ */
381
+ if (deprecatedReasons.length > 0) {
382
+ mapping.dPos.startPosition.rowDelta++;
383
+ output += ` /** @deprecated not exposed; property must be ${deprecatedReasons.join(
384
+ ", "
385
+ )} */\n`;
386
+ }
387
+ output += ` ${
388
+ node.childForFieldName("declarator")?.childForFieldName("name")?.text ?? ""
389
+ }?: ${getType(node.childForFieldName("type"))};\n`;
390
+ return [output, [mapping]];
391
+ }
392
+
393
+ /**
394
+ *
395
+ * @param {Parser.SyntaxNode} node
396
+ * @returns {boolean}
397
+ */
398
+ function getAuraEnabledIsCacheable(node) {
399
+ const args = node.childForFieldName("arguments");
400
+ if (args) {
401
+ for (const a of args.descendantsOfType("annotation_key_value")) {
402
+ const k = a.childForFieldName("key");
403
+ const v = a.childForFieldName("value");
404
+ if (
405
+ k &&
406
+ v &&
407
+ k.text.toLowerCase() === "cacheable" &&
408
+ v.text.toLowerCase() === "true"
409
+ ) {
410
+ return true;
411
+ }
412
+ }
413
+ }
414
+ return false;
415
+ }
416
+
417
+ /**
418
+ *
419
+ * @param {Parser.SyntaxNode} node
420
+ * @returns {Parser.SyntaxNode | null}
421
+ */
422
+ function getAuraEnabledAnnotation(node) {
423
+ for (const m of node.descendantsOfType("modifiers")) {
424
+ for (const a of m.descendantsOfType("annotation")) {
425
+ if (a.childForFieldName("name")?.text.toLowerCase() === "auraenabled") {
426
+ return a;
427
+ }
428
+ }
429
+ }
430
+ return null;
431
+ }
432
+
433
+ /**
434
+ *
435
+ * @param {Parser.SyntaxNode} node
436
+ * @returns {Map<string, string>}
437
+ */
438
+ function getMethodParameters(node) {
439
+ const params = new Map();
440
+ const parameters = node.childForFieldName("parameters");
441
+ if (!parameters) {
442
+ return params;
443
+ }
444
+ for (const p of parameters.namedChildren) {
445
+ params.set(
446
+ p.childForFieldName("name")?.text,
447
+ getType(p.childForFieldName("type"))
448
+ );
449
+ }
450
+ return params;
451
+ }
452
+
453
+ /**
454
+ *
455
+ * @param {string} className
456
+ * @param {string} methodName
457
+ * @param {string} returnType
458
+ * @param {Map<string, string>} params
459
+ * @param {boolean} isCacheable
460
+ * @returns {[string, DeltaPoint]}
461
+ */
462
+ function declarationString(
463
+ className,
464
+ methodName,
465
+ returnType,
466
+ params,
467
+ isCacheable
468
+ ) {
469
+ let text = `declare module "@salesforce/apex/${className}.${methodName}" {
470
+ const ${methodName}: {
471
+ (${
472
+ params.size === 0
473
+ ? ""
474
+ : `param: { ${Array.from(params.entries())
475
+ .map(([name, type]) => `${name}: ${type}`)
476
+ .join("; ")} }`
477
+ }): Promise<${returnType}>;${
478
+ // Apex can only be wired if it is cacheable
479
+ // the adapter is the WireAdapterConstructor that the @wire decorator consumes
480
+ !isCacheable
481
+ ? ""
482
+ : `
483
+ adapter: import("lwc").WireAdapterConstructor<
484
+ ${
485
+ params.size === 0
486
+ ? "never"
487
+ : `{ ${Array.from(params.entries())
488
+ .map(([name, type]) => `${name}: ${type}`)
489
+ .join("; ")} }`
490
+ },
491
+ { error?: any; data?: ${returnType}; }
492
+ >;`
493
+ }
494
+ };
495
+ export default `;
496
+ // analyze how much text we're adding before the method name
497
+ const lines = text.split("\n");
498
+ /** @type {DeltaPoint} */
499
+ const m = {
500
+ rowDelta: lines.length - 1,
501
+ column: lines[lines.length - 1]?.length ?? 0,
502
+ };
503
+ // finish building
504
+ text += `${methodName};
505
+ }
506
+ `;
507
+ return [text, m];
508
+ }
509
+
510
+ /**
511
+ *
512
+ * @param {string} className
513
+ * @param {string} relativeClassPath
514
+ * @param {Mapping[]} mappings
515
+ * @returns
516
+ */
517
+ function getMapFile(className, relativeClassPath, mappings) {
518
+ let mappingsString = "";
519
+ let lastSourceRow = 0;
520
+ let lastMethodColumnNum = 0;
521
+ for (const m of mappings) {
522
+ mappingsString += ";".repeat(m.dPos.startPosition.rowDelta);
523
+ if (!m.sPos) {
524
+ continue;
525
+ }
526
+ /**
527
+ * Generated column
528
+ * Original file this appeared in
529
+ * Original line number relative to last given value in this field
530
+ * Original column relative to last given value in this field
531
+ */
532
+ // const incrementalColumnCount =
533
+ // m.sPos.startPosition.column - lastMethodColumnNum;
534
+
535
+ // mappingsString += `${encode(m.dPos.startPosition.column)}${encode(
536
+ // 0
537
+ // )}${encode(m.sPos.startPosition.row - lastSourceRow)}${encode(
538
+ // incrementalColumnCount
539
+ // )}`;
540
+
541
+ lastMethodColumnNum = m.sPos.startPosition.column;
542
+ lastSourceRow = m.sPos.startPosition.row;
543
+ }
544
+ return JSON.stringify({
545
+ version: 3,
546
+ file: `${className}.d.ts`,
547
+ sourceRoot: "",
548
+ // TODO: not so badly hardcoded
549
+ sources: [`../../../../../${relativeClassPath}`],
550
+ names: [],
551
+ mappings: mappingsString,
552
+ });
553
+ }
554
+
555
+ /**
556
+ *
557
+ * @param {Parser.SyntaxNode | null} node
558
+ * @returns {string}
559
+ */
560
+ function getType(node) {
561
+ if (!node) {
562
+ return "";
563
+ }
564
+ // if generic type, see if it is a list or map
565
+ switch (node.type) {
566
+ case "generic_type":
567
+ const genType = getType(node.namedChildren[0]);
568
+ let typeNode = node.namedChildren[1];
569
+ if (!typeNode) {
570
+ return "";
571
+ }
572
+ switch (genType.toLowerCase()) {
573
+ case "list":
574
+ return getType(typeNode.namedChildren[0]) + "[]";
575
+ case "map":
576
+ return `Record<${getType(typeNode.namedChildren[0])}, ${getType(
577
+ typeNode.namedChildren[1]
578
+ )}>`;
579
+ case "set":
580
+ return `Set<${getType(typeNode.namedChildren[0])}>`;
581
+
582
+ default:
583
+ console.error("Unexpected generic type", genType);
584
+ }
585
+ }
586
+ if (localNestedClasses.includes(node.text)) {
587
+ return `${outerClassName}.${node.text}`;
588
+ }
589
+ return typeOverrides[node.text.toLowerCase()] ?? node.text;
590
+ }
@@ -0,0 +1,24 @@
1
+ /** @import { Component, ConvertedComponent } from "../index.mjs" */
2
+ import { parseString } from "xml2js";
3
+
4
+ /**
5
+ *
6
+ * @param {Component} component
7
+ * @returns {ConvertedComponent}
8
+ */
9
+ export function ContentAsset(component) {
10
+ // generate d.ts
11
+ /** @type {any} */
12
+ let asset;
13
+ parseString(component.content, (error, result) => {
14
+ if (error) {
15
+ throw error;
16
+ }
17
+ asset = result;
18
+ });
19
+ const dec = `declare module "@salesforce/contentAssetUrl/${asset.ContentAsset.masterLabel}" {
20
+ var ${asset.ContentAsset.masterLabel}: string;
21
+ export default ${asset.ContentAsset.masterLabel};
22
+ }`;
23
+ return { declarationContent: dec, mapData: [], ...component };
24
+ }
@@ -0,0 +1,35 @@
1
+ /** @import { Component, ConvertedComponent } from "../index.mjs" */
2
+ import { parseString } from "xml2js";
3
+
4
+ /**
5
+ *
6
+ * @param {Component} component
7
+ * @returns {ConvertedComponent}
8
+ */
9
+ export function CustomField(component) {
10
+ // generate d.ts
11
+ /** @type {any} */
12
+ let field;
13
+ parseString(component.content, (error, result) => {
14
+ if (error) {
15
+ throw error;
16
+ }
17
+ field = result;
18
+ });
19
+ const pathParts = component.path.split("/");
20
+ const fieldName = pathParts.pop()?.split(".")[0];
21
+ pathParts?.pop(); // discard `fields`
22
+ const objectName = pathParts.pop();
23
+ const dec = `declare module '@salesforce/schema/${objectName}.${fieldName}' {
24
+ /**
25
+ * @description ${field.CustomField.inlineHelpText}
26
+ * @description ${field.CustomField.description}
27
+ */
28
+ const ${fieldName}: {
29
+ fieldApiName: '${fieldName}';
30
+ objectApiName: '${objectName}';
31
+ }
32
+ export default ${fieldName};
33
+ }`;
34
+ return { declarationContent: dec, mapData: [], ...component };
35
+ }
@@ -0,0 +1,34 @@
1
+ /** @import { Component, ConvertedComponent } from "../index.mjs" */
2
+ import { parseString } from "xml2js";
3
+
4
+ /**
5
+ *
6
+ * @param {Component} component
7
+ * @returns {ConvertedComponent}
8
+ */
9
+ export function CustomLabels(component) {
10
+ // generate d.ts
11
+ /** @type {any} */
12
+ let labels;
13
+ parseString(component.content, (error, result) => {
14
+ if (error) {
15
+ throw error;
16
+ }
17
+ labels = result;
18
+ });
19
+ if (!Array.isArray(labels.CustomLabels.labels)) {
20
+ labels.CustomLabels.labels = [labels.CustomLabels.labels];
21
+ }
22
+ let dec = "";
23
+ for (let label of labels.CustomLabels.labels) {
24
+ dec += `declare module "@salesforce/label/c.${label.fullName}" {
25
+ /**
26
+ * @description ${label.shortDescription}
27
+ */
28
+ const lbl${label.fullName} = '${label.value}';
29
+ export default lbl${label.fullName};
30
+ }
31
+ `;
32
+ }
33
+ return { declarationContent: dec, mapData: [], ...component };
34
+ }