@lwc-bolts/sf2ts 0.0.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +285 -0
  3. package/dist/apex/apexdoc.d.ts +19 -0
  4. package/dist/apex/apexdoc.js +79 -0
  5. package/dist/apex/file.d.ts +37 -0
  6. package/dist/apex/file.js +623 -0
  7. package/dist/apex/library.config.d.ts +23 -0
  8. package/dist/apex/library.config.js +17 -0
  9. package/dist/apex/library.d.ts +54 -0
  10. package/dist/apex/library.generated.d.ts +2 -0
  11. package/dist/apex/library.generated.js +215 -0
  12. package/dist/apex/library.js +47 -0
  13. package/dist/apex/namespace.d.ts +32 -0
  14. package/dist/apex/namespace.js +41 -0
  15. package/dist/apex/resolution.d.ts +77 -0
  16. package/dist/apex/resolution.js +306 -0
  17. package/dist/apex/sobjects.d.ts +45 -0
  18. package/dist/apex/sobjects.js +141 -0
  19. package/dist/context.d.ts +16 -0
  20. package/dist/context.js +28 -0
  21. package/dist/converters/apex-class.d.ts +7 -0
  22. package/dist/converters/apex-class.js +23 -0
  23. package/dist/converters/content-asset.d.ts +3 -0
  24. package/dist/converters/content-asset.js +20 -0
  25. package/dist/converters/custom-field.d.ts +3 -0
  26. package/dist/converters/custom-field.js +107 -0
  27. package/dist/converters/custom-label.d.ts +10 -0
  28. package/dist/converters/custom-label.js +36 -0
  29. package/dist/converters/custom-labels.d.ts +3 -0
  30. package/dist/converters/custom-labels.js +10 -0
  31. package/dist/converters/custom-object.d.ts +3 -0
  32. package/dist/converters/custom-object.js +37 -0
  33. package/dist/converters/custom-permission.d.ts +3 -0
  34. package/dist/converters/custom-permission.js +21 -0
  35. package/dist/converters/lightning-component-bundle.d.ts +7 -0
  36. package/dist/converters/lightning-component-bundle.js +31 -0
  37. package/dist/converters/lightning-message-channel.d.ts +10 -0
  38. package/dist/converters/lightning-message-channel.js +39 -0
  39. package/dist/converters/registry.d.ts +22 -0
  40. package/dist/converters/registry.js +18 -0
  41. package/dist/converters/static-resource.d.ts +3 -0
  42. package/dist/converters/static-resource.js +20 -0
  43. package/dist/diagnostics.d.ts +5 -0
  44. package/dist/diagnostics.js +18 -0
  45. package/dist/identifiers.d.ts +12 -0
  46. package/dist/identifiers.js +19 -0
  47. package/dist/index.d.ts +34 -0
  48. package/dist/index.js +47 -0
  49. package/dist/jsdoc.d.ts +14 -0
  50. package/dist/jsdoc.js +28 -0
  51. package/dist/lightning-component-bundle/component-source.d.ts +6 -0
  52. package/dist/lightning-component-bundle/component-source.js +21 -0
  53. package/dist/lightning-component-bundle/metadata.d.ts +36 -0
  54. package/dist/lightning-component-bundle/metadata.js +93 -0
  55. package/dist/lightning-component-bundle/templates/ast.d.ts +47 -0
  56. package/dist/lightning-component-bundle/templates/ast.js +46 -0
  57. package/dist/lightning-component-bundle/templates/base-components.d.ts +6 -0
  58. package/dist/lightning-component-bundle/templates/base-components.js +17 -0
  59. package/dist/lightning-component-bundle/templates/declarations.d.ts +30 -0
  60. package/dist/lightning-component-bundle/templates/declarations.js +82 -0
  61. package/dist/lightning-component-bundle/templates/expressions.d.ts +16 -0
  62. package/dist/lightning-component-bundle/templates/expressions.js +33 -0
  63. package/dist/lightning-component-bundle/templates/helpers.d.ts +10 -0
  64. package/dist/lightning-component-bundle/templates/helpers.js +47 -0
  65. package/dist/lightning-component-bundle/templates/slots.d.ts +24 -0
  66. package/dist/lightning-component-bundle/templates/slots.js +78 -0
  67. package/dist/lightning-component-bundle/templates/source.d.ts +36 -0
  68. package/dist/lightning-component-bundle/templates/source.js +81 -0
  69. package/dist/lightning-component-bundle/templates/tags.d.ts +41 -0
  70. package/dist/lightning-component-bundle/templates/tags.js +36 -0
  71. package/dist/lightning-component-bundle/templates/template.d.ts +53 -0
  72. package/dist/lightning-component-bundle/templates/template.js +518 -0
  73. package/dist/lightning-component-bundle/templates/writer.d.ts +9 -0
  74. package/dist/lightning-component-bundle/templates/writer.js +32 -0
  75. package/dist/module-declaration.d.ts +27 -0
  76. package/dist/module-declaration.js +28 -0
  77. package/dist/names.d.ts +39 -0
  78. package/dist/names.js +67 -0
  79. package/dist/text-builder.d.ts +34 -0
  80. package/dist/text-builder.js +46 -0
  81. package/dist/types.d.ts +128 -0
  82. package/dist/types.js +3 -0
  83. package/dist/xml.d.ts +32 -0
  84. package/dist/xml.js +80 -0
  85. package/package.json +37 -26
  86. package/README.MD +0 -5
  87. package/lib/converts/_index.mjs +0 -8
  88. package/lib/converts/apex-class.mjs +0 -469
  89. package/lib/converts/content-asset.mjs +0 -48
  90. package/lib/converts/custom-field.mjs +0 -63
  91. package/lib/converts/custom-label.mjs +0 -61
  92. package/lib/converts/custom-labels.mjs +0 -34
  93. package/lib/converts/custom-object.mjs +0 -51
  94. package/lib/converts/custom-permission.mjs +0 -51
  95. package/lib/converts/static-resource.mjs +0 -56
  96. package/lib/index.mjs +0 -51
  97. package/lib/util.mjs +0 -68
  98. package/lib-cjs/bundle.cjs +0 -962
@@ -0,0 +1,623 @@
1
+ import { docParagraphs, jsDoc } from "../jsdoc.js";
2
+ import { importName } from "../names.js";
3
+ import { schemaValueType, writeDefaultExportModule } from "../module-declaration.js";
4
+ import { TextBuilder } from "../text-builder.js";
5
+ import { getApexDoc, inlineDoc } from "./apexdoc.js";
6
+ import { apexLibrary } from "./library.js";
7
+ import { CLASSES_REGISTRY, registration, SOBJECTS_REGISTRY } from "./namespace.js";
8
+ import { PRIMITIVE_TYPES, TypeResolver } from "./resolution.js";
9
+ /**
10
+ * Converts an Apex file's top-level type to declarations: its types in the Apex namespace, then, for a
11
+ * class, its `@salesforce/apex` modules (or, for an SObject definition, its `@salesforce/schema`
12
+ * modules). Returns undefined for a file that declares no type, and throws for one with syntax errors
13
+ * and no type to convert. A type with syntax errors is converted as far as it parses, so a class being
14
+ * edited keeps its other declarations. The parse tree lives in WebAssembly memory, which isn't garbage
15
+ * collected, so it's deleted once the declarations are built.
16
+ */
17
+ export function convertApexFile(parser, apexText, options) {
18
+ const tree = parser.parse(apexText);
19
+ try {
20
+ // The file's type is one of the root's children; in a tree with errors it can be under an error node,
21
+ // so it's the first in document order, which is the outermost. Only that search reads the whole tree.
22
+ const { rootNode } = tree;
23
+ const typeNode = rootNode.hasError
24
+ ? rootNode.descendantsOfType(TYPE_DECLARATIONS)[0]
25
+ : rootNode.namedChildren.find((child) => TYPE_DECLARATIONS.includes(child.type));
26
+ if (typeNode === undefined) {
27
+ if (rootNode.hasError) {
28
+ throw new Error("the file has syntax errors, and no class, interface or enum declaration was found");
29
+ }
30
+ return undefined; // no type
31
+ }
32
+ return new ApexFile(typeNode, options).convert();
33
+ }
34
+ finally {
35
+ tree?.delete();
36
+ }
37
+ }
38
+ const TYPE_DECLARATIONS = ["class_declaration", "interface_declaration", "enum_declaration"];
39
+ /**
40
+ * One Apex file's conversion, with what its declarations depend on.
41
+ *
42
+ * A class of an installed package (a cached copy of a managed class, named `dlrs__RollupController.cls`)
43
+ * is declared in a namespace of the package's in the Apex namespace (`Apex.dlrs.RollupController`), where
44
+ * the project's references through the package (`dlrs.RollupController`) find it. Only its `global`
45
+ * `@AuraEnabled` methods are declared, as `@salesforce/apex/dlrs.RollupController.<method>`, since other
46
+ * namespaces can't import the others. In it, custom objects are the package's, a name is the package's
47
+ * class when it has one, then a library type or an SObject, and a dotted name, or one through the
48
+ * package's namespace, is the package's type (see `TypeResolver`).
49
+ */
50
+ class ApexFile {
51
+ #builder = new TextBuilder();
52
+ /** The file's top-level class, interface or enum. */
53
+ #classNode;
54
+ #classNameNode;
55
+ #isSobject;
56
+ #polymorphicTargets;
57
+ #inaccessibleProperties;
58
+ /**
59
+ * Nested classes, interfaces and enums of the class being converted, keyed by lower-cased name (Apex
60
+ * names are case-insensitive) to their declared spelling.
61
+ */
62
+ #nestedTypes = new Map();
63
+ #projectNamespace;
64
+ /** The installed package the class belongs to, when it isn't the project's. */
65
+ #packageNamespace;
66
+ #resolver;
67
+ constructor(classNode, options) {
68
+ const classNameNode = classNode.childForFieldName("name");
69
+ if (!classNameNode) {
70
+ throw new Error(`the ${classNode.type.replace("_declaration", "")} has no name`);
71
+ }
72
+ this.#classNode = classNode;
73
+ this.#classNameNode = classNameNode;
74
+ this.#isSobject = options.isSobject;
75
+ this.#polymorphicTargets = options.polymorphicTargets;
76
+ this.#inaccessibleProperties = options.inaccessibleProperties;
77
+ this.#projectNamespace = options.projectNamespace;
78
+ this.#packageNamespace = options.packageNamespace;
79
+ this.#resolver = new TypeResolver({
80
+ namespace: options.apexNamespace,
81
+ projectNamespace: options.projectNamespace,
82
+ packageNamespace: options.packageNamespace,
83
+ className: classNameNode.text,
84
+ isSobject: options.isSobject,
85
+ nestedTypes: this.#nestedTypes,
86
+ });
87
+ }
88
+ convert() {
89
+ const builder = this.#builder;
90
+ let methods;
91
+ const typesNamespace = this.#resolver.typesNamespace;
92
+ if (typesNamespace === null) {
93
+ methods = this.#processType(this.#classNode, "");
94
+ }
95
+ else {
96
+ builder.write(`declare namespace ${typesNamespace} {\n`);
97
+ methods = this.#processType(this.#classNode, " ");
98
+ if (this.#packageNamespace !== undefined) {
99
+ // the package's classes resolve each other through its registry (see `packageLookup`), which has
100
+ // their nested types too, by their dotted names
101
+ const outer = this.#classNameNode.text;
102
+ builder.write(registration(" ", CLASSES_REGISTRY, [outer, ...[...this.#nestedTypes.values()].map((nested) => `${outer}.${nested}`)]));
103
+ }
104
+ builder.write("}\n");
105
+ }
106
+ if (this.#classNode.type !== "class_declaration") {
107
+ // an interface or an enum has no modules
108
+ }
109
+ else if (this.#isSobject) {
110
+ // SObject definitions only describe records; their methods (if any) aren't callable from LWC
111
+ this.#addSchemaModules();
112
+ }
113
+ else {
114
+ // Salesforce rejects overloaded @AuraEnabled methods, so only the first of a name is declared (Apex
115
+ // names are case-insensitive); declaring each would give the module several default exports.
116
+ const declared = new Set();
117
+ for (const mNode of methods) {
118
+ const nameNode = mNode.childForFieldName("name");
119
+ const name = nameNode?.text.toLowerCase();
120
+ // instance @AuraEnabled methods are Aura getters; LWC can only import static methods
121
+ const { auraEnabled, modifiers } = declarationModifiers(mNode);
122
+ if (!nameNode || name === undefined || auraEnabled === undefined || !modifiers.has("static")) {
123
+ continue;
124
+ }
125
+ if (this.#packageNamespace !== undefined && !modifiers.has("global")) {
126
+ // other namespaces can only import a package's global methods
127
+ continue;
128
+ }
129
+ if (declared.has(name)) {
130
+ continue;
131
+ }
132
+ declared.add(name);
133
+ this.#transformMethodDeclaration(mNode, nameNode, auraEnabled, methods);
134
+ }
135
+ }
136
+ this.#addLibraryDeclarations();
137
+ return builder.output();
138
+ }
139
+ /** Declares a class, interface or enum, returning a class's methods. */
140
+ #processType(node, indent) {
141
+ switch (node.type) {
142
+ case "class_declaration":
143
+ return this.#processClass(node, indent);
144
+ case "interface_declaration":
145
+ this.#processInterface(node, indent);
146
+ return [];
147
+ default:
148
+ this.#processEnum(node, indent);
149
+ return [];
150
+ }
151
+ }
152
+ /**
153
+ * Declares an interface as a type any object fits, with any properties: its value is an instance of
154
+ * whichever class implements it, so nothing is known about its properties.
155
+ */
156
+ #processInterface(node, indent) {
157
+ const nameNode = node.childForFieldName("name");
158
+ if (!nameNode) {
159
+ throw new Error("an interface has no name");
160
+ }
161
+ this.#builder.write(jsDoc(indent, getApexDoc(node)?.lines ?? []));
162
+ this.#builder.write(`${indent}interface `);
163
+ this.#builder.write(nameNode.text, nameNode.startIndex, nameNode.endIndex);
164
+ this.#builder.write(" extends Record<string, unknown> {}\n");
165
+ }
166
+ /** Declares an enum as the union of its values' names, which is how Apex serializes them. */
167
+ #processEnum(node, indent) {
168
+ const builder = this.#builder;
169
+ const nameNode = node.childForFieldName("name");
170
+ if (!nameNode) {
171
+ throw new Error("an enum has no name");
172
+ }
173
+ const constants = (node.childForFieldName("body")?.namedChildren ?? [])
174
+ .filter((child) => child.type === "enum_constant")
175
+ .map((constant) => constant.childForFieldName("name") ?? constant);
176
+ builder.write(jsDoc(indent, getApexDoc(node)?.lines ?? []));
177
+ builder.write(`${indent}type `);
178
+ builder.write(nameNode.text, nameNode.startIndex, nameNode.endIndex);
179
+ builder.write(" = ");
180
+ if (constants.length === 0) {
181
+ builder.write("never");
182
+ }
183
+ constants.forEach((constant, i) => {
184
+ builder.write(i === 0 ? "" : " | ");
185
+ builder.write(JSON.stringify(constant.text), constant.startIndex, constant.endIndex);
186
+ });
187
+ builder.write(";\n");
188
+ }
189
+ /**
190
+ * Declares the class's type (always, so other classes and methods can reference it) and its nested
191
+ * classes inside the Apex namespace (or globally, without one). Returns the class's methods; only
192
+ * top-level classes can declare the static `@AuraEnabled` methods LWC imports.
193
+ */
194
+ #processClass(classNode, indent) {
195
+ const builder = this.#builder;
196
+ const classNameNode = classNode.childForFieldName("name");
197
+ if (!classNameNode) {
198
+ throw new Error("a nested class has no name");
199
+ }
200
+ const className = classNameNode.text;
201
+ const classBody = classNode.childForFieldName("body");
202
+ const methodsToProcess = [];
203
+ const fieldsToProcess = [];
204
+ const typesToProcess = [];
205
+ for (const cNode of classBody?.children ?? []) {
206
+ switch (cNode.type) {
207
+ case "method_declaration":
208
+ // gather methods because AuraEnabled methods create their own exports
209
+ methodsToProcess.push(cNode);
210
+ break;
211
+ case "field_declaration": {
212
+ // a field LWC can't read is declared deprecated, declared plainly, or left out
213
+ const unexposed = this.#inaccessibleProperties === "include" ? [] : this.#unexposedReasons(cNode);
214
+ if (this.#isSobject || this.#inaccessibleProperties !== "exclude" || unexposed.length === 0) {
215
+ fieldsToProcess.push({ node: cNode, unexposed });
216
+ }
217
+ break;
218
+ }
219
+ case "class_declaration":
220
+ case "interface_declaration":
221
+ case "enum_declaration": {
222
+ typesToProcess.push(cNode);
223
+ const innerNameNode = cNode.childForFieldName("name");
224
+ if (innerNameNode) {
225
+ // queue up nested type names so type resolution can use them
226
+ this.#nestedTypes.set(innerNameNode.text.toLowerCase(), innerNameNode.text);
227
+ }
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ // `extends` names a type, so a base resolved through the registries gets a name of its own
233
+ const superclass = classNode.childForFieldName("superclass")?.namedChildren[0];
234
+ // an interface can't extend `unknown`, so a base a package's copy doesn't have is `{}`
235
+ const base = superclass === undefined ? undefined : this.#getType(superclass, { base: true });
236
+ builder.write(jsDoc(indent, getApexDoc(classNode, this.#isSobject)?.lines ?? []));
237
+ builder.write(`${indent}interface `);
238
+ builder.write(className, classNameNode.startIndex, classNameNode.endIndex);
239
+ if (superclass !== undefined && base !== undefined) {
240
+ builder.write(" extends ");
241
+ builder.write(base, superclass.startIndex, superclass.endIndex);
242
+ }
243
+ if (fieldsToProcess.length > 0) {
244
+ builder.write(" {\n");
245
+ for (const { node, unexposed } of fieldsToProcess) {
246
+ this.#transformFieldDeclaration(node, unexposed, indent + " ");
247
+ }
248
+ builder.write(`${indent}}\n`);
249
+ }
250
+ else {
251
+ builder.write(" {}\n");
252
+ }
253
+ if (typesToProcess.length > 0) {
254
+ // only a top-level namespace takes `declare`; nested ones must not repeat it (TS1038)
255
+ builder.write(`${indent}${indent === "" ? "declare " : ""}namespace `);
256
+ builder.write(className, classNameNode.startIndex, classNameNode.endIndex);
257
+ builder.write(" {\n");
258
+ for (const cNode of typesToProcess) {
259
+ this.#processType(cNode, indent + " ");
260
+ }
261
+ builder.write(`${indent}}\n`);
262
+ }
263
+ return methodsToProcess;
264
+ }
265
+ /**
266
+ * Declares a static `@AuraEnabled` method's module.
267
+ *
268
+ * @param methodNameNode the method's name
269
+ * @param auraEnabled the method's `@AuraEnabled` annotation
270
+ * @param classMethods the class's methods, where a continuation's callback is found
271
+ */
272
+ #transformMethodDeclaration(methodNode, methodNameNode, auraEnabled, classMethods) {
273
+ const returnType = methodNode.childForFieldName("type");
274
+ if (!returnType) {
275
+ return;
276
+ }
277
+ const continuation = annotationFlag(auraEnabled, "continuation");
278
+ // A continuation method returns the Continuation, but LWC receives what its callback returns.
279
+ const callback = continuation ? getContinuationCallback(methodNode, classMethods) : undefined;
280
+ this.#writeMethodModule({
281
+ nameNode: methodNameNode,
282
+ returnType: this.#getType(callback?.childForFieldName("type") ?? returnType),
283
+ params: this.#getMethodParameters(methodNode),
284
+ cacheable: annotationFlag(auraEnabled, "cacheable"),
285
+ continuation,
286
+ doc: getApexDoc(methodNode),
287
+ signature: apexSignature(methodNode),
288
+ });
289
+ }
290
+ /**
291
+ * Why LWC can't read a field, as what it must be (`@AuraEnabled`, `public or global`, `non-static`), or
292
+ * nothing when it can. An SObject definition's fields are exposed without `@AuraEnabled`.
293
+ */
294
+ #unexposedReasons(node) {
295
+ const { auraEnabled, modifiers } = declarationModifiers(node);
296
+ const reasons = [];
297
+ if (!this.#isSobject && auraEnabled === undefined) {
298
+ reasons.push("@AuraEnabled");
299
+ }
300
+ if (modifiers.has("static")) {
301
+ reasons.push("non-static");
302
+ }
303
+ if (!modifiers.has("public") && !modifiers.has("global")) {
304
+ reasons.push("public or global");
305
+ }
306
+ return reasons;
307
+ }
308
+ /** Declares a field's properties; `deprecatedReasons` are why LWC can't read it (see `#unexposedReasons`). */
309
+ #transformFieldDeclaration(node, deprecatedReasons, indent) {
310
+ const builder = this.#builder;
311
+ const typeNode = node.childForFieldName("type");
312
+ if (!typeNode) {
313
+ throw new Error("Type node not found");
314
+ }
315
+ // `public String a, b;` declares several fields of the same type
316
+ const nameNodes = node.childrenForFieldName("declarator").map((declarator) => declarator.childForFieldName("name"));
317
+ if (nameNodes.length === 0) {
318
+ throw new Error("Field doesn't have a name");
319
+ }
320
+ // A field without a type, such as `global RecordId;` (which the SObject generator writes for some
321
+ // fields), parses as a type with a missing name. There's nothing to declare, so skip it.
322
+ if (nameNodes.every((nameNode) => nameNode?.text === "")) {
323
+ return;
324
+ }
325
+ const declaredType = this.#getType(typeNode);
326
+ const doc = getApexDoc(node, this.#isSobject);
327
+ for (const nameNode of nameNodes) {
328
+ if (!nameNode) {
329
+ throw new Error("Field doesn't have a name");
330
+ }
331
+ const targets = typeNode.text.toLowerCase() === "sobject" ? this.#polymorphicTargets.get(nameNode.text.toLowerCase()) : undefined;
332
+ const typeText = targets && targets.length > 0 ? this.#polymorphicType(targets, declaredType) : declaredType;
333
+ /**
334
+ * determine if AuraEnabled & public or global & not static
335
+ * otherwise mark as deprecated with note (dynamic based on missing access component)
336
+ * {property}: {this.#getType({type})}?;
337
+ */
338
+ const deprecated = deprecatedReasons.length > 0 ? `@deprecated not exposed; property must be ${deprecatedReasons.join(", ")}` : undefined;
339
+ if (doc !== undefined) {
340
+ builder.write(jsDoc(indent, [...doc.lines, deprecated === undefined ? undefined : "", deprecated]));
341
+ }
342
+ else if (deprecated !== undefined) {
343
+ builder.write(`${indent}/** ${deprecated} */\n`);
344
+ }
345
+ builder.write(indent);
346
+ builder.write(nameNode.text, nameNode.startIndex, nameNode.endIndex);
347
+ builder.write("?: ");
348
+ builder.write(typeText, typeNode.startIndex, typeNode.endIndex);
349
+ builder.write(";\n");
350
+ }
351
+ }
352
+ /**
353
+ * Declares the `@salesforce/schema/<Object>` and `@salesforce/schema/<Object>.<Field>` modules of an
354
+ * SObject definition, for the objects and fields (such as `Account.Name`) the project has no metadata for.
355
+ *
356
+ * Each module name is a pattern with a leading `*`, which in practice only matches the exact import, and
357
+ * TypeScript prefers an exact `declare module` over a pattern. So when the project's own object or field
358
+ * metadata declares the same module, that declaration wins rather than conflicting with this one.
359
+ *
360
+ * Fields LWC can import are declared: scalar fields, lookups' Id fields and compound fields (such as
361
+ * `BillingAddress`). Child relationships (`List<Contact> Contacts`) and the related records of lookups
362
+ * (`Owner`, next to `OwnerId`, or `Account__r`) aren't fields.
363
+ */
364
+ #addSchemaModules() {
365
+ const builder = this.#builder;
366
+ const classNameNode = this.#classNameNode;
367
+ const objectName = classNameNode.text;
368
+ const fields = [];
369
+ for (const fieldNode of this.#classNode.childForFieldName("body")?.children ?? []) {
370
+ if (fieldNode.type !== "field_declaration") {
371
+ continue;
372
+ }
373
+ const typeNode = fieldNode.childForFieldName("type");
374
+ if (!typeNode) {
375
+ continue;
376
+ }
377
+ for (const declarator of fieldNode.childrenForFieldName("declarator")) {
378
+ const nameNode = declarator.childForFieldName("name");
379
+ if (nameNode && nameNode.text !== "") {
380
+ fields.push({ nameNode, typeNode, doc: getApexDoc(fieldNode, true) });
381
+ }
382
+ }
383
+ }
384
+ const names = new Set(fields.map(({ nameNode }) => nameNode.text.toLowerCase()));
385
+ // LWC imports the project's own objects and fields without its namespace, which the definitions have
386
+ const local = (name) => importName(name, this.#projectNamespace);
387
+ writeDefaultExportModule(builder, {
388
+ name: `*@salesforce/schema/${local(objectName)}`,
389
+ variable: objectName,
390
+ type: schemaValueType(objectName),
391
+ range: { start: classNameNode.startIndex, end: classNameNode.endIndex },
392
+ });
393
+ for (const { nameNode, typeNode, doc } of fields) {
394
+ const name = nameNode.text;
395
+ if (typeNode.type === "generic_type" || typeNode.type === "array_type") {
396
+ continue; // a child relationship
397
+ }
398
+ const isScalar = PRIMITIVE_TYPES[typeNode.text.toLowerCase()] !== undefined && typeNode.text.toLowerCase() !== "sobject";
399
+ if (!isScalar && (names.has(`${name.toLowerCase()}id`) || /__r$/i.test(name))) {
400
+ continue; // a lookup's related record
401
+ }
402
+ writeDefaultExportModule(builder, {
403
+ name: `*@salesforce/schema/${local(objectName)}.${local(name)}`,
404
+ variable: name,
405
+ type: schemaValueType(objectName, name),
406
+ doc: doc?.lines,
407
+ range: { start: nameNode.startIndex, end: nameNode.endIndex },
408
+ });
409
+ }
410
+ }
411
+ /**
412
+ * The type of a polymorphic relationship, such as Task's `What`: one of its targets, once every one of
413
+ * them is declared, and otherwise `sobject` (its declared type), so it never names an SObject that isn't.
414
+ * A query that doesn't use `TYPEOF` returns the fields of the `Name` object for it (`Name`, `Type`,
415
+ * `Email`, ...), whichever the target is, so when the `Name` object is declared the union is intersected
416
+ * with it. That keeps `What.Name` valid even though some targets (such as Case) have no `Name` field.
417
+ *
418
+ * Which SObjects are declared is TypeScript's to find out, through the SObjects' registry, so the
419
+ * relationship's type follows the definitions as they come and go:
420
+ *
421
+ * ```ts
422
+ * R extends { "account": infer T0 } ? R extends { "case": infer T1 }
423
+ * ? R extends { "name": infer N } ? N & (T0 | T1) : T0 | T1
424
+ * : sobject : sobject
425
+ * ```
426
+ */
427
+ #polymorphicType(targets, sobject) {
428
+ const registry = this.#resolver.sobjectsRegistry;
429
+ const union = targets.map((_, i) => `T${i}`).join(" | ");
430
+ let type = `${registry} extends { "name": infer N } ? N & (${union}) : ${union}`;
431
+ for (let i = targets.length - 1; i >= 0; i--) {
432
+ type = `${registry} extends { ${JSON.stringify(targets[i].toLowerCase())}: infer T${i} } ? ${type} : ${sobject}`;
433
+ }
434
+ return type;
435
+ }
436
+ #getMethodParameters(node) {
437
+ const params = new Map();
438
+ const parameters = node.childForFieldName("parameters");
439
+ if (!parameters) {
440
+ return params;
441
+ }
442
+ for (const p of parameters.namedChildren) {
443
+ const name = p.childForFieldName("name")?.text;
444
+ if (name !== undefined) {
445
+ params.set(name, this.#getType(p.childForFieldName("type")));
446
+ }
447
+ }
448
+ return params;
449
+ }
450
+ /**
451
+ * Writes a static `@AuraEnabled` method's module: `@salesforce/apex/<Class>.<method>` (or
452
+ * `@salesforce/apexContinuation/...` for a continuation), whose default export LWC calls imperatively
453
+ * and, when it's cacheable, wires (its `adapter`).
454
+ */
455
+ #writeMethodModule({ nameNode, returnType, params, cacheable, continuation, doc, signature }) {
456
+ const builder = this.#builder;
457
+ const name = nameNode.text;
458
+ // `@param` docs are repeated on the parameter object's properties, which is where editors show them
459
+ const paramType = `{ ${Array.from(params.entries())
460
+ .map(([param, type]) => {
461
+ const paramDoc = doc?.params.get(param.toLowerCase());
462
+ return `${paramDoc ? `/** ${inlineDoc(paramDoc)} */ ` : ""}${param}: ${type}`;
463
+ })
464
+ .join("; ")} }`;
465
+ const packagePrefix = this.#packageNamespace === undefined ? "" : `${this.#packageNamespace}.`;
466
+ const module = `@salesforce/apex${continuation ? "Continuation" : ""}/${packagePrefix}${this.#classNameNode.text}.${name}`;
467
+ const docLines = docParagraphs(signature === undefined ? undefined : ["---", "**Apex**", "", "```apex", signature, "```"], doc?.lines ?? []);
468
+ builder.write("declare module ");
469
+ builder.write(JSON.stringify(module), nameNode.startIndex, nameNode.endIndex);
470
+ builder.write(` {\n${jsDoc(" ", docLines)} const `);
471
+ builder.write(name, nameNode.startIndex, nameNode.endIndex);
472
+ builder.write(`: {\n (${params.size === 0 ? "" : `param: ${paramType}`}): Promise<${returnType}>;`);
473
+ if (cacheable) {
474
+ // only cacheable Apex can be wired; the adapter is the WireAdapterConstructor `@wire` takes
475
+ builder.write(`
476
+ adapter: import("lwc").WireAdapterConstructor<
477
+ ${params.size === 0 ? "never" : paramType},
478
+ { error?: unknown; data?: ${returnType}; }
479
+ >;`);
480
+ }
481
+ builder.write(`\n };\n export default ${name};\n}\n`);
482
+ }
483
+ /**
484
+ * The TypeScript type of an Apex type reference: `base` when it's a class's base class (see
485
+ * `TypeResolver.resolve`). A reference that's missing, as in a class being edited, is `unknown` (`{}`
486
+ * for a base class, since an interface can't extend `unknown`).
487
+ */
488
+ #getType(node, { base = false } = {}) {
489
+ const unknown = base ? "{}" : "unknown";
490
+ if (!node) {
491
+ return unknown;
492
+ }
493
+ switch (node.type) {
494
+ case "generic_type": {
495
+ // if generic type, see if it is a list or map
496
+ const genType = node.namedChildren[0]?.text ?? "";
497
+ const typeNode = node.namedChildren[1];
498
+ if (!typeNode) {
499
+ return unknown;
500
+ }
501
+ switch (genType.toLowerCase()) {
502
+ case "list":
503
+ case "set": // sets serialize to JSON arrays
504
+ return this.#getType(typeNode.namedChildren[0]) + "[]";
505
+ case "map":
506
+ // JSON object keys are strings, whatever the map's key type
507
+ return `Record<string, ${this.#getType(typeNode.namedChildren[1])}>`;
508
+ default:
509
+ // another generic type (such as `Iterable<String>` or `Database.Batchable<SObject>`) is an
510
+ // instance of whichever class implements it, so nothing is known about its value
511
+ return unknown;
512
+ }
513
+ }
514
+ case "array_type": {
515
+ // `String[]` is the same as `List<String>`
516
+ const elementType = this.#getType(node.namedChildren[0]);
517
+ const dimensions = node.namedChildren[1]?.text.replace(/\s/g, "") ?? "[]";
518
+ return elementType + dimensions;
519
+ }
520
+ case "scoped_type_identifier":
521
+ // `a.b.c` nests one scoped identifier in another, so its parts come from its text
522
+ return this.#resolver.resolve(node.text.split(".").map((part) => part.trim()), { base });
523
+ }
524
+ const override = PRIMITIVE_TYPES[node.text.toLowerCase()];
525
+ if (override) {
526
+ return override;
527
+ }
528
+ return this.#resolver.resolve([node.text], { base });
529
+ }
530
+ /**
531
+ * Declares what the file's references need (see `TypeResolver.declarations`), and the file's own type
532
+ * in a registry: an SObject definition's in the SObjects', and a project class's in the project's when
533
+ * a library type has its name, which it shadows. A package's classes aren't the project's, so they
534
+ * don't shadow library types in its code.
535
+ */
536
+ #addLibraryDeclarations() {
537
+ const ownName = this.#classNameNode.text;
538
+ const ownRegistry = this.#isSobject
539
+ ? SOBJECTS_REGISTRY
540
+ : this.#packageNamespace === undefined && apexLibrary.hasTypeNamed(ownName)
541
+ ? CLASSES_REGISTRY
542
+ : undefined;
543
+ const declarations = this.#resolver.declarations(ownName, ownRegistry);
544
+ if (declarations !== "") {
545
+ this.#builder.write(declarations);
546
+ }
547
+ }
548
+ }
549
+ /**
550
+ * Finds the callback of a continuation method: the method of this class named by its
551
+ * `<continuation>.continuationMethod = '<name>'` assignment. Returns undefined when the method doesn't
552
+ * name exactly one callback with a string literal, or the class has no method of that name.
553
+ */
554
+ function getContinuationCallback(methodNode, classMethods) {
555
+ const names = new Set();
556
+ for (const assignment of methodNode.childForFieldName("body")?.descendantsOfType("assignment_expression") ?? []) {
557
+ const left = assignment.childForFieldName("left");
558
+ const right = assignment.childForFieldName("right");
559
+ if (left?.type !== "field_access" || left.childForFieldName("field")?.text.toLowerCase() !== "continuationmethod") {
560
+ continue;
561
+ }
562
+ if (right?.type !== "string_literal") {
563
+ return undefined; // a callback chosen at runtime
564
+ }
565
+ // Apex method names are case-insensitive
566
+ names.add(right.text.slice(1, -1).toLowerCase());
567
+ }
568
+ if (names.size !== 1) {
569
+ return undefined;
570
+ }
571
+ const [name] = names;
572
+ return classMethods.find((m) => m !== methodNode && m.childForFieldName("name")?.text.toLowerCase() === name);
573
+ }
574
+ /**
575
+ * A declaration's own modifiers (such as `static` or `global`, lower-cased, as Apex's are
576
+ * case-insensitive) and its `@AuraEnabled` annotation, if it has one; not those of what it contains (such
577
+ * as a method's body). Read in one pass, since each node read crosses into the parser's WebAssembly.
578
+ */
579
+ function declarationModifiers(node) {
580
+ const modifiers = new Set();
581
+ let auraEnabled;
582
+ for (const child of node.namedChildren) {
583
+ if (child.type !== "modifiers") {
584
+ continue;
585
+ }
586
+ for (const modifier of child.namedChildren) {
587
+ if (modifier.type === "modifier") {
588
+ modifiers.add(modifier.text.toLowerCase());
589
+ }
590
+ else if (auraEnabled === undefined &&
591
+ modifier.type === "annotation" &&
592
+ modifier.childForFieldName("name")?.text.toLowerCase() === "auraenabled") {
593
+ auraEnabled = modifier;
594
+ }
595
+ }
596
+ }
597
+ return { modifiers, auraEnabled };
598
+ }
599
+ /**
600
+ * A method's Apex signature, shown in its module's documentation: its return type, name and parameters
601
+ * (`List<SalesOrderDto> getRecentOrders(Integer maxResults)`), without its annotations and modifiers.
602
+ */
603
+ function apexSignature(methodNode) {
604
+ const type = methodNode.childForFieldName("type");
605
+ const name = methodNode.childForFieldName("name");
606
+ if (!type || !name) {
607
+ return undefined;
608
+ }
609
+ const parameters = (methodNode.childForFieldName("parameters")?.namedChildren ?? []).map((p) => [p.childForFieldName("type"), p.childForFieldName("name")].map((node) => typeText(node?.text ?? "")).join(" "));
610
+ return `${typeText(type.text)} ${name.text}(${parameters.join(", ")})`;
611
+ }
612
+ /** A type's text on one line, spaced like `Map<String, List<Account>>`. */
613
+ function typeText(text) {
614
+ return text
615
+ .replace(/\s+/g, " ")
616
+ .replace(/\s*([<>[\]])\s*/g, "$1")
617
+ .replace(/\s*,\s*/g, ", ")
618
+ .trim();
619
+ }
620
+ /** Whether an annotation sets `key` to true, as in `@AuraEnabled(cacheable=true)` (case-insensitive). */
621
+ function annotationFlag(annotation, key) {
622
+ return (annotation.childForFieldName("arguments")?.descendantsOfType("annotation_key_value") ?? []).some((pair) => pair.childForFieldName("key")?.text.toLowerCase() === key && pair.childForFieldName("value")?.text.toLowerCase() === "true");
623
+ }
@@ -0,0 +1,23 @@
1
+ import type { LibraryType } from "./library.ts";
2
+ export interface ApexNamespaceConfig {
3
+ /** The namespace's name, as Apex and the StandardApexLibrary folder spell it. */
4
+ readonly name: string;
5
+ /**
6
+ * Whether the project's SObjects belong to this namespace, as they do to Schema. Where the namespace
7
+ * comes in an unqualified name's resolution, an SObject of that name is found there.
8
+ */
9
+ readonly sobjects?: boolean;
10
+ }
11
+ export interface ApexLibraryConfig {
12
+ /**
13
+ * The namespaces, in the order an unqualified name (such as `Exception`) is looked for in them. The
14
+ * project's own types come before all of them, so a project class shadows a library type of the same
15
+ * name, which then needs its namespace (`System.Exception`).
16
+ */
17
+ readonly namespaces: readonly ApexNamespaceConfig[];
18
+ /** Names (case-insensitive) looked for in a different order of namespaces than `namespaces` gives. */
19
+ readonly resolutionOrder: Readonly<Record<string, readonly string[]>>;
20
+ /** Types the StandardApexLibrary leaves out, by namespace and name. They're added to the generated ones. */
21
+ readonly additionalTypes: Readonly<Record<string, Readonly<Record<string, LibraryType>>>>;
22
+ }
23
+ export declare const APEX_LIBRARY_CONFIG: ApexLibraryConfig;
@@ -0,0 +1,17 @@
1
+ export const APEX_LIBRARY_CONFIG = {
2
+ namespaces: [{ name: "Schema", sobjects: true }, { name: "System" }],
3
+ resolutionOrder: {
4
+ // Unqualified, these are the compound field types, even in an org with the Address and Location
5
+ // objects, which need `Schema.Address` and `Schema.Location` (as System.Address's and
6
+ // System.Location's documentation says). The SObject definitions use them for compound fields.
7
+ address: ["System", "Schema"],
8
+ location: ["System", "Schema"],
9
+ },
10
+ additionalTypes: {
11
+ System: {
12
+ // the base of every exception, built into the language
13
+ Exception: { kind: "class" },
14
+ AuraHandledException: { kind: "class", extends: "System.Exception" },
15
+ },
16
+ },
17
+ };