@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.
- package/LICENSE.md +7 -0
- package/README.md +285 -0
- package/dist/apex/apexdoc.d.ts +19 -0
- package/dist/apex/apexdoc.js +79 -0
- package/dist/apex/file.d.ts +37 -0
- package/dist/apex/file.js +623 -0
- package/dist/apex/library.config.d.ts +23 -0
- package/dist/apex/library.config.js +17 -0
- package/dist/apex/library.d.ts +54 -0
- package/dist/apex/library.generated.d.ts +2 -0
- package/dist/apex/library.generated.js +215 -0
- package/dist/apex/library.js +47 -0
- package/dist/apex/namespace.d.ts +32 -0
- package/dist/apex/namespace.js +41 -0
- package/dist/apex/resolution.d.ts +77 -0
- package/dist/apex/resolution.js +306 -0
- package/dist/apex/sobjects.d.ts +45 -0
- package/dist/apex/sobjects.js +141 -0
- package/dist/context.d.ts +16 -0
- package/dist/context.js +28 -0
- package/dist/converters/apex-class.d.ts +7 -0
- package/dist/converters/apex-class.js +23 -0
- package/dist/converters/content-asset.d.ts +3 -0
- package/dist/converters/content-asset.js +20 -0
- package/dist/converters/custom-field.d.ts +3 -0
- package/dist/converters/custom-field.js +107 -0
- package/dist/converters/custom-label.d.ts +10 -0
- package/dist/converters/custom-label.js +36 -0
- package/dist/converters/custom-labels.d.ts +3 -0
- package/dist/converters/custom-labels.js +10 -0
- package/dist/converters/custom-object.d.ts +3 -0
- package/dist/converters/custom-object.js +37 -0
- package/dist/converters/custom-permission.d.ts +3 -0
- package/dist/converters/custom-permission.js +21 -0
- package/dist/converters/lightning-component-bundle.d.ts +7 -0
- package/dist/converters/lightning-component-bundle.js +31 -0
- package/dist/converters/lightning-message-channel.d.ts +10 -0
- package/dist/converters/lightning-message-channel.js +39 -0
- package/dist/converters/registry.d.ts +22 -0
- package/dist/converters/registry.js +18 -0
- package/dist/converters/static-resource.d.ts +3 -0
- package/dist/converters/static-resource.js +20 -0
- package/dist/diagnostics.d.ts +5 -0
- package/dist/diagnostics.js +18 -0
- package/dist/identifiers.d.ts +12 -0
- package/dist/identifiers.js +19 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +47 -0
- package/dist/jsdoc.d.ts +14 -0
- package/dist/jsdoc.js +28 -0
- package/dist/lightning-component-bundle/component-source.d.ts +6 -0
- package/dist/lightning-component-bundle/component-source.js +21 -0
- package/dist/lightning-component-bundle/metadata.d.ts +36 -0
- package/dist/lightning-component-bundle/metadata.js +93 -0
- package/dist/lightning-component-bundle/templates/ast.d.ts +47 -0
- package/dist/lightning-component-bundle/templates/ast.js +46 -0
- package/dist/lightning-component-bundle/templates/base-components.d.ts +6 -0
- package/dist/lightning-component-bundle/templates/base-components.js +17 -0
- package/dist/lightning-component-bundle/templates/declarations.d.ts +30 -0
- package/dist/lightning-component-bundle/templates/declarations.js +82 -0
- package/dist/lightning-component-bundle/templates/expressions.d.ts +16 -0
- package/dist/lightning-component-bundle/templates/expressions.js +33 -0
- package/dist/lightning-component-bundle/templates/helpers.d.ts +10 -0
- package/dist/lightning-component-bundle/templates/helpers.js +47 -0
- package/dist/lightning-component-bundle/templates/slots.d.ts +24 -0
- package/dist/lightning-component-bundle/templates/slots.js +78 -0
- package/dist/lightning-component-bundle/templates/source.d.ts +36 -0
- package/dist/lightning-component-bundle/templates/source.js +81 -0
- package/dist/lightning-component-bundle/templates/tags.d.ts +41 -0
- package/dist/lightning-component-bundle/templates/tags.js +36 -0
- package/dist/lightning-component-bundle/templates/template.d.ts +53 -0
- package/dist/lightning-component-bundle/templates/template.js +518 -0
- package/dist/lightning-component-bundle/templates/writer.d.ts +9 -0
- package/dist/lightning-component-bundle/templates/writer.js +32 -0
- package/dist/module-declaration.d.ts +27 -0
- package/dist/module-declaration.js +28 -0
- package/dist/names.d.ts +39 -0
- package/dist/names.js +67 -0
- package/dist/text-builder.d.ts +34 -0
- package/dist/text-builder.js +46 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.js +3 -0
- package/dist/xml.d.ts +32 -0
- package/dist/xml.js +80 -0
- package/package.json +37 -26
- package/README.MD +0 -5
- package/lib/converts/_index.mjs +0 -8
- package/lib/converts/apex-class.mjs +0 -469
- package/lib/converts/content-asset.mjs +0 -48
- package/lib/converts/custom-field.mjs +0 -63
- package/lib/converts/custom-label.mjs +0 -61
- package/lib/converts/custom-labels.mjs +0 -34
- package/lib/converts/custom-object.mjs +0 -51
- package/lib/converts/custom-permission.mjs +0 -51
- package/lib/converts/static-resource.mjs +0 -56
- package/lib/index.mjs +0 -51
- package/lib/util.mjs +0 -68
- package/lib-cjs/bundle.cjs +0 -962
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Anthony Heber
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# @lwc-bolts/sf2ts
|
|
2
|
+
|
|
3
|
+
`sf2ts` transforms Salesforce Metadata content into TypeScript compatible content so it can be written to a file or proxied on file reads for editors and language servers.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
import { convert, MetadataType } from "@lwc-bolts/sf2ts";
|
|
7
|
+
|
|
8
|
+
const [{ text, mapData, diagnostics }] = await convert([{ path, type: MetadataType.ApexClass, content }]);
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Each converted file has its TypeScript `text` & `mapData` pairing pieces of it with the content they came from (UTF-16 offsets), and any `diagnostics`. If `convert` is unable to successfully process a file then that file gets empty `text` and a `ConversionFailed` diagnostic covering it. A file that declares nothing of use to LWCs, such as an Apex interface, has empty `text` and no diagnostics.
|
|
12
|
+
|
|
13
|
+
`convert` is async and each metadata type's converter lazy loads on its first conversion. This means you only ever load what you used, nothing extra.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Metadata Conversion Details
|
|
17
|
+
|
|
18
|
+
## Lightning Web Component
|
|
19
|
+
|
|
20
|
+
### LWC Metadata
|
|
21
|
+
|
|
22
|
+
A bundle's `<name>.js-meta.xml` declares the `c/<name>` module components import each other through. It re-exports the component's module by type. This makes it possible to keep TypeScript aware of the aliased import name without needing to write them disk in the `jsconfig.json`'s `paths`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
declare module "c/myComponent" {
|
|
26
|
+
const myComponent: typeof import("./myComponent.js");
|
|
27
|
+
export = myComponent;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### LWC Template
|
|
32
|
+
|
|
33
|
+
A `LightningComponentBundle`'s templates (its `.html` files) convert to TypeScript that checks against the component's JavaScript and other components it references in the markup (`<c-component-name>`).
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { bundleApiVersion, convert, defaultExportName, MetadataType } from "@lwc-bolts/sf2ts";
|
|
37
|
+
|
|
38
|
+
const [{ text, mapData, diagnostics }] = await convert([
|
|
39
|
+
{
|
|
40
|
+
path: "/project/force-app/lwc/orders/views/compact.html",
|
|
41
|
+
type: MetadataType.LightningComponentBundle,
|
|
42
|
+
content,
|
|
43
|
+
// how it imports the bundle's component, from a subfolder here (by default, ./<bundleName>.js)
|
|
44
|
+
componentModule: "../orders.js",
|
|
45
|
+
// the bundle's name, which its slots are declared under (by default, the template's folder's name)
|
|
46
|
+
bundleName: "orders",
|
|
47
|
+
// the API version it's parsed at (by default, the template compiler's latest)
|
|
48
|
+
apiVersion: await bundleApiVersion(ordersJsMetaXml),
|
|
49
|
+
// whether the bundle has other templates, which make every lwc:ref optional
|
|
50
|
+
alternateTemplates: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
path: "/project/force-app/lwc/orders/orders.js-meta.xml",
|
|
54
|
+
type: MetadataType.LightningComponentBundle,
|
|
55
|
+
content: ordersJsMetaXml,
|
|
56
|
+
// names the c/orders module's export (by default, the bundle's name)
|
|
57
|
+
defaultExportName: await defaultExportName(ordersJs),
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`text` contains TypeScript module text that imports the bundle's component (`componentModule`, `../orders.js` here), unlike metadata's global declarations. Its default export is the compiled template, named after the file (`compactTemplate` here), so the component can import it for `render()`. `diagnostics` include the LWC template compiler's errors, as `TemplateError`s. `bundleApiVersion` and `defaultExportName` are provided by the caller (we expose utility methods to help capture them from related files). Each template's output declares its own slots so they can be checked by containing components. Templates are parsed with `@lwc/template-compiler`, base component definitions come from [`@lwc-bolts/lightning-base-components`](../lightning-base-components).
|
|
63
|
+
|
|
64
|
+
## Custom Label
|
|
65
|
+
|
|
66
|
+
Importing a Custom Label named `MyLabel`:
|
|
67
|
+
```javascript
|
|
68
|
+
import lblMyLabel from "@salesforce/label/c.MyLabel";
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A label's value is the label in the user's language. We show the default language as the first line of its documentation when hovering.
|
|
72
|
+
|
|
73
|
+
Custom Labels are supported in both the combined `CustomLabels` and the decomposed `CustomLabel` file formats.
|
|
74
|
+
|
|
75
|
+
## Custom Permission
|
|
76
|
+
|
|
77
|
+
Importing a Custom Permission named `TestPerm`
|
|
78
|
+
```javascript
|
|
79
|
+
import hasTestPerm from "@salesforce/customPermission/TestPerm";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A custom permission's value is `true` when the user has the permission and `undefined` when they don't.
|
|
83
|
+
|
|
84
|
+
## Static Resource
|
|
85
|
+
|
|
86
|
+
Importing a Static Resource named `bolt`:
|
|
87
|
+
```javascript
|
|
88
|
+
import bolt from "@salesforce/resourceUrl/bolt";
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Static Resources import as URL strings.
|
|
92
|
+
|
|
93
|
+
## Content Asset
|
|
94
|
+
|
|
95
|
+
Importing a Content Asset named `bolt`:
|
|
96
|
+
```javascript
|
|
97
|
+
import bolt from "@salesforce/contentAssetUrl/bolt";
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Content Assets import as URL strings.
|
|
101
|
+
|
|
102
|
+
## Lightning Message Channels
|
|
103
|
+
|
|
104
|
+
Importing a Lightning Message Channel named `RecordSelected__c`:
|
|
105
|
+
```javascript
|
|
106
|
+
import RecordSelected__c from "@salesforce/messageChannel/RecordSelected__c";
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A `*.messageChannel-meta.xml` file declares `@salesforce/messageChannel/<Name>__c` or `@salesforce/messageChannel/c__<Name>__c`, which is the same channel. The imported value is the channel's ID, an opaque string for `lightning/messageService`. It's typed `string & { readonly __messageChannel: "<Name>__c" }`, is a branded type so you can discriminate should you have a need. The channel's label, description and message fields are listed in its documentation.
|
|
110
|
+
|
|
111
|
+
## Apex
|
|
112
|
+
|
|
113
|
+
### Class
|
|
114
|
+
|
|
115
|
+
Every Apex class is declared as an interface in the global `Apex` namespace, even a class without fields, and a class that extends another extends its interface, so it has the base class's fields. Nested classes, interfaces and enums are declared in a namespace named after their outer class. Namespaces merge across files, so a method in one class can return a class declared in another file. Because of the namespace, a class named `Response` or `File` doesn't merge with the DOM type of the same name.
|
|
116
|
+
|
|
117
|
+
An enum is declared as the union of its values' names, which is how Apex serializes them: `public enum Color { RED, GREEN }` becomes `type Color = "RED" | "GREEN"`. An interface is declared as an object type with any properties (`interface Shape extends Record<string, unknown> {}`), since its value is an instance of whichever class implements it.
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
### JSDoc Generation
|
|
121
|
+
|
|
122
|
+
ApexDoc comments (`/** ... */`) on classes, fields and `@AuraEnabled` methods become JSDoc on the generated interfaces, properties and module exports, so editors show them when hovering an import. A comment can go before a declaration or between its annotations and the rest of it. `@description` text joins the description and `@return` becomes `@returns`. Other tags, such as `@param`, `@example` and `@see`, are kept as written. Each `@param` description is also attached to its property in the method's parameter object, so it shows while filling in the call.
|
|
123
|
+
|
|
124
|
+
An `@AuraEnabled` method's module is documented with its Apex signature, before its ApexDoc, so hovering an import shows the method as Apex declares it as well as the type LWC calls it with. The signature is the return type, name and parameters, on one line, without annotations or modifiers (`List<SalesOrderDto> getRecentOrders(Integer maxResults)`).
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
declare namespace Apex {
|
|
128
|
+
interface Outer {
|
|
129
|
+
name?: string;
|
|
130
|
+
}
|
|
131
|
+
namespace Outer {
|
|
132
|
+
interface Inner {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
declare module "@salesforce/apex/Outer.get" {
|
|
136
|
+
/**
|
|
137
|
+
* ---
|
|
138
|
+
* **Apex**
|
|
139
|
+
*
|
|
140
|
+
* ```apex
|
|
141
|
+
* Inner get()
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
const get: {
|
|
145
|
+
(): Promise<Apex.Outer.Inner>;
|
|
146
|
+
};
|
|
147
|
+
export default get;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
To use a different namespace prefix for the generated content, pass `apexNamespace` to `convert`. With `{ apexNamespace: null }`, classes are declared globally. That can create conflicts within the project. For instance, a class named `Response` merges with the DOM's `Response` type.
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
### @AuraEnabled Methods
|
|
155
|
+
Static `@AuraEnabled` methods are exported as `@salesforce/apex/...` modules for imperative calls, if that method is also `cacheable=true` then it is additionall configured to be comaptible with the `@wire` decorator.
|
|
156
|
+
|
|
157
|
+
### Properties
|
|
158
|
+
Properties declared on a class can be exposed for the LWC to interact with, such as when an Apex method returns a class. An LWC only receives the properties that are `@AuraEnabled`, `public` or `global`, and not `static`. By default the others are declared `@deprecated` but the LWC is still made aware so that the user can recognize and correct the problem. To leave them out entirely, pass `inaccessibleApexProperties: "exclude"` (for example; `await convert(components, { inaccessibleApexProperties: "exclude" })`). Trying to access one in the LWC is then an error. To declare them like any other field, without the `@deprecated`, pass `inaccessibleApexProperties: "include"`. The default is `"include as deprecated"`. (SObject fields, from files or Apex stubs, are always included and never deprecated)
|
|
159
|
+
|
|
160
|
+
### Continuation
|
|
161
|
+
A continuation method (`@AuraEnabled(continuation=true)`) returns a `Continuation` object, but the promise an LWC gets resolves to what its callback eventually returns. When the method names its callback with a string, as in `con.continuationMethod = 'processResponse'`, and the class has a method of that name, the `@salesforce/apexContinuation/...` module uses that method's return type. Otherwise, such as when the name is chosen at runtime or set to more than one value, it keeps the continuation method's declared return type.
|
|
162
|
+
|
|
163
|
+
### Apex -> Typescript Mappings
|
|
164
|
+
|
|
165
|
+
Types describe the JSON values that LWC receives:
|
|
166
|
+
|
|
167
|
+
| Apex | TypeScript |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| `String`, `Id` | `string` |
|
|
170
|
+
| `Date`, `Datetime`, `Time` | `string` (ISO-8601) |
|
|
171
|
+
| `Blob` | `string` (base64) |
|
|
172
|
+
| `Integer`, `Long`, `Decimal`, `Double` | `number` |
|
|
173
|
+
| `Boolean` | `boolean` |
|
|
174
|
+
| `List<T>`, `Set<T>`, `T[]` | `T[]` |
|
|
175
|
+
| `Map<K, V>` | `Record<string, V>` (JSON keys are strings) |
|
|
176
|
+
| `SObject` | `Record<string, unknown>` |
|
|
177
|
+
| `Object`, `Json` | `unknown` |
|
|
178
|
+
| A standard library type, such as `Exception` or `Schema.DisplayType` | `Apex.System.Exception`, `Apex.Schema.__lwcEnums["DisplayType"]` (see [Standard library types](#standard-library-types)) |
|
|
179
|
+
| Other class references | `Apex.<Name>` |
|
|
180
|
+
|
|
181
|
+
sf2ts converts one file at a time, so it can't resolve types declared elsewhere, such as SObjects. They are still referenced as `Apex.<Name>`. Object and field metadata declare SObjects (see [SObject record types](#sobject-record-types)), and so do the SObject definitions the Salesforce VS Code extensions generate (see below). For anything else, declare the types you need in the `Apex` namespace, for example `declare namespace Apex { interface Account { Name?: string } }`. Generic types other than `List`, `Set` and `Map` (such as `Iterable<String>`) are `unknown`, since their value is whichever class implements them, and so is a type reference that's incomplete in a class being edited.
|
|
182
|
+
|
|
183
|
+
# Namespaced Project
|
|
184
|
+
|
|
185
|
+
Namespaces are tricky for LWCs, for the most part Salesforce doesn't want you to reference your own namespace, on foreign packages. The generated types attempt to respect that, recognizing the difference between the project's namespace and the component's namespace.
|
|
186
|
+
|
|
187
|
+
In a project with a namespace (sfdx-project.json's `namespace`), pass it as `projectNamespace`: `await convert(components, { projectNamespace: "acme" })`. Inside a namespace, LWC must import its own metadata without the namespace (it rejects `@salesforce/schema/acme__Invoice__c` and `@salesforce/apex/acme.Invoices.recent`), but the org adds it when it compiles a component, so the values of schema imports and the records LWC get namespaced custom objects and fields with it:
|
|
188
|
+
|
|
189
|
+
- Modules keep the local LWC imports: `@salesforce/schema/Invoice__c.Amount__c`, `@salesforce/label/c.Name`, `@salesforce/apex/Invoices.recent`.
|
|
190
|
+
- Schema imports' values have the namespace: `{ fieldApiName: "acme__Amount__c"; objectApiName: "acme__Invoice__c" }`. Standard objects and fields (`Account`, `Name`), and another namespace's (`other__Score__c`), are left as they are.
|
|
191
|
+
- Record types are named with it, and so are their custom fields and relationships: `Apex.acme__Invoice__c` with `acme__Amount__c` and `acme__Account__r`. They merge with the SObject definitions of a namespaced org, which name them the same way, and whose schema modules take the names LWC imports.
|
|
192
|
+
- In Apex code, a reference through the namespace (`acme.Invoices`) is the project's type, and a custom object is its record type with or without the namespace (`Invoice__c` and `acme__Invoice__c` are both `Apex.acme__Invoice__c`).
|
|
193
|
+
|
|
194
|
+
## Installed packages
|
|
195
|
+
|
|
196
|
+
If Metadata of an installed managed package (not the namespace under development) is available locally it can be converted too. The resulting TypeScript text is named the way the Metadata API names foreign managed metadata: with the package's namespace (`dlrs__LookupRollupSummaryLog__c`, `dlrs__RollupController.cls`, a label `dlrs__fflib_error`):
|
|
197
|
+
|
|
198
|
+
| Metadata | Declared as |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| Objects and fields | `@salesforce/schema/dlrs__Log__c.dlrs__ErrorMessage__c`, and the project's own field on the package's object as `dlrs__Log__c.Notes__c` |
|
|
201
|
+
| Labels | `@salesforce/label/dlrs.fflib_error`, where the project's own are `c.<name>` |
|
|
202
|
+
| Static resources, custom permissions, message channels, content assets | `dlrs__Name` (`@salesforce/messageChannel/dlrs__Name__c`, without a `c__` form) |
|
|
203
|
+
| Apex classes | the class's types in the package's namespace, `Apex.dlrs.RollupController`, which the project's references `dlrs.RollupController` find; only its `global` `@AuraEnabled` methods, as `@salesforce/apex/dlrs.RollupController.<method>`, since other namespaces can't import the others |
|
|
204
|
+
| Components | `dlrs/manageRollups`, only when the bundle is exposed (`<isExposed>true</...>`), since other namespaces can't import it otherwise |
|
|
205
|
+
|
|
206
|
+
In a package's Apex, names resolve as they do in the package. A custom object is the package's (`LookupRollupSummaryLog__c` is `dlrs__LookupRollupSummaryLog__c`). A name is the package's class of that name when the copy has one (`RollupSummary` is `Apex.dlrs.RollupSummary`), and otherwise a library type or an SObject; a package can't see the project's classes. A dotted name (`RollupController.Result`) and one through the package's namespace (`dlrs.RollupSummary`, `dlrs.RollupController.Result`) are the package's types. Each of a package's classes registers itself and its nested types in its package's type-only registry (`Apex.dlrs.__lwcClasses`, keyed by lower-cased dotted name), which TypeScript checks. A type nothing in the copy declares, including another package's Apex, is `unknown` rather than an error, since a package's copied code isn't yours to fix. Managed Apex and components are compiled and hidden, so a copy only has what it's given, such as stubs of their signatures. A tag of another namespace's component in a template (`<dlrs-manage-rollups>`) isn't checked against it.
|
|
207
|
+
|
|
208
|
+
## Apex Standard library types
|
|
209
|
+
|
|
210
|
+
Apex code can use the types of the standard library's namespaces without naming the namespace: `Exception` is `System.Exception`. sf2ts knows the types of System and Schema. They're declared in namespaces of those names in the `Apex` namespace, in each file that uses them; declarations of the same type merge. A class becomes an empty interface, or one with any properties when its instances have some, since their JSON names aren't known exactly (`Address`). An interface has any properties, and an enum is the union of its values (`Apex.Schema.__lwcEnums["DisplayType"]`).
|
|
211
|
+
|
|
212
|
+
As in Apex, an unqualified name such as `Exception` is the project's own type of that name if it has one, which shadows the library's. Otherwise it's looked up in Schema (its describe types, then the project's SObjects), then System. `Address` and `Location` are looked up in System first, so they're the compound field types even in an org with the Address and Location objects, which Apex code calls `Schema.Address` and `Schema.Location`. A qualified name is always the library's (`System.Exception`), and a Schema name that isn't one of its types is an SObject (`Schema.Account` is `Apex.Account`).
|
|
213
|
+
|
|
214
|
+
Which of these a name is depends on the whole project, and sf2ts converts one file at a time. So a reference to a name the library has checks registries, interfaces each file adds its types to: `__lwcClasses` has the project's Apex types whose names the library has, and `__lwcSObjects` has every SObject. TypeScript then picks the project's type if the registries have one. Such a reference is a conditional type, written where it's used: `Apex.__lwcClasses extends { "exception": infer T } ? T : …`. Hovers and errors show the type it resolves to. A class's base class is the exception, since an interface can only extend a type by name: its reference is an alias in a namespace of the class's own, such as `Apex.__lwcRefs.MyException.exception` (`Apex.dlrs.__lwcRefs.<Class>` in an installed package). The resolution order is in `src/apex/resolution.ts`.
|
|
215
|
+
|
|
216
|
+
The namespaces, the order they're looked up in, the names looked up in a different order, and types the stubs leave out (such as `Exception` itself) are in `src/apex/library.config.ts`. The types are read from the Salesforce extensions' StandardApexLibrary stubs (`.sfdx/tools/<release>/StandardApexLibrary`) into `src/apex/library.generated.ts`. To add a namespace, list it in the config, then regenerate:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
npm run generate:apex-library # the example project's newest release
|
|
220
|
+
npm run generate:apex-library -- path/to/StandardApexLibrary
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## SObject
|
|
224
|
+
|
|
225
|
+
Besides its `@salesforce/schema/...` module, an object's `*.object-meta.xml` declares the object's record type in the Apex namespace, and each `*.field-meta.xml` adds its field to it. Interfaces merge across files, so together they describe the object as the project defines it:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
declare namespace Apex {
|
|
229
|
+
interface Sales_Order__c {
|
|
230
|
+
Id?: string;
|
|
231
|
+
Name?: string;
|
|
232
|
+
// ...the other standard fields
|
|
233
|
+
}
|
|
234
|
+
interface User {}
|
|
235
|
+
}
|
|
236
|
+
declare namespace Apex {
|
|
237
|
+
interface Sales_Order__c {
|
|
238
|
+
/**
|
|
239
|
+
* @description Total order amount
|
|
240
|
+
*/
|
|
241
|
+
Amount__c?: number;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- **Objects** declare their standard fields: `Id`, `Name`, `IsDeleted`, the created and last-modified fields, `SystemModstamp`, and `OwnerId` unless records are owned by their master-detail parent (`ControlledByParent` sharing). The owner itself (`Owner`, a user or a queue) is left to the SObject definitions the Salesforce extensions generate, which type it by its targets when the project has the object's describe, since a property declared in more than one file must have the same type. Custom metadata types (`__mdt`) and platform events (`__e`) declare their own standard fields. Standard objects only declare `Id`, because their fields vary. `CreatedBy` and `LastModifiedBy` are `Apex.User`, so object files also declare an empty `interface User {}` to make that reference resolve.
|
|
247
|
+
- **Fields** are typed as LWC receives them. `Checkbox` is `boolean`. `Currency`, `Number` and `Percent` are `number`. Dates, times, picklists, text and every other string-valued type are `string`. Formula fields use their result type.
|
|
248
|
+
- **Lookups and master-detail fields** declare the related record's Id (`Account__c?: string`) and the record (`Account__r?: Apex.Account`). They also add the child relationship to the parent (`interface Account { Sales_Orders__r?: Apex.Sales_Order__c[] }`), which also declares the parent, so the reference resolves even when nothing else declares `Account`. Standard lookups, such as `AccountId`, only declare the Id, because their relationships aren't named `__r`.
|
|
249
|
+
- **Geolocation fields** declare their latitude and longitude (`Location__Latitude__s`, `Location__Longitude__s`).
|
|
250
|
+
- **Fields whose type can't be known from their file are left out.** These include roll-up summaries other than `count` and `sum`, whose type depends on the summarized field, unsupported types such as `ExternalLookup`, and standard fields' metadata, which has no `<type>`.
|
|
251
|
+
|
|
252
|
+
The types match the ones the generated SObject definitions use, so an object declared both ways merges into one type. TypeScript requires a property declared in more than one place to have the same type (TS2717). The record types use the same [`apexNamespace`](#apex-types) as Apex classes.
|
|
253
|
+
|
|
254
|
+
### SObject Definitions from Org
|
|
255
|
+
|
|
256
|
+
**SFDX: Refresh SObject Definitions** in the Salesforce VS Code extensions writes each of the org's SObjects as an Apex class, in `.sfdx/tools/sobjects/standardObjects` and `.sfdx/tools/sobjects/customObjects`:
|
|
257
|
+
|
|
258
|
+
```apex
|
|
259
|
+
global class Account {
|
|
260
|
+
global String Name;
|
|
261
|
+
/* The account's annual revenue
|
|
262
|
+
*/
|
|
263
|
+
global Decimal AnnualRevenue;
|
|
264
|
+
global List<Contact> Contacts;
|
|
265
|
+
|
|
266
|
+
global Account ()
|
|
267
|
+
{
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Convert one as an `ApexClass` with `isSobject: true` on the component (an `ApexClassComponent`; options a type takes are only on its own component type), and it declares `Apex.Account` for other classes and components to use:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
await convert([{ path, type: MetadataType.ApexClass, content, isSobject: true }]);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The `isSobject` flag causes the following changes:
|
|
279
|
+
|
|
280
|
+
- Fields are exposed without `@AuraEnabled`, so none are marked `@deprecated`.
|
|
281
|
+
- Block comments (`/* ... */`), which the generator writes from field descriptions, document fields as ApexDoc would.
|
|
282
|
+
- No `@salesforce/apex/...` modules are declared. Instead, the object and the fields LWC can import get `@salesforce/schema/...` modules: scalar fields, lookups' Id fields and compound fields, but not child relationships or lookups' related records. They're declared as patterns with a leading `*` (`"*@salesforce/schema/Account.Name"`), which only match the exact import. TypeScript prefers an exact `declare module` over a pattern, so where an object or field file declares the same module, as for a standard field the project customizes, that one is used and the two don't conflict.
|
|
283
|
+
- A field without a type is skipped. The generator writes a few of these, such as `global RecordId;` on `UriEvent`. This also applies to ordinary classes.
|
|
284
|
+
|
|
285
|
+
Fields are optional, as for any class, because a query only returns the fields it selects. Child relationships (`List<Contact> Contacts`) become arrays and lookups become the related SObject's type. Polymorphic lookups, such as Task's `What` and `Owner`, are declared as `SObject` in the definitions, which becomes `Record<string, unknown>`. Pass their targets as `polymorphicTargets`, by relationship name, to type them as one of their targets: with `{ What: ["Account", "Case"] }`, `What` is an `Apex.Account | Apex.Case`. A query that doesn't use `TYPEOF` returns the fields of the `Name` object for a polymorphic relationship, whatever its target, so when the `Name` object is declared too the type is `Apex.Name & (Apex.Account | Apex.Case)`. sf2ts doesn't need to know which objects are declared: every SObject adds itself to the SObjects' registry (`Apex.__lwcSObjects`), and the relationship's type is a conditional type that looks each target, and the `Name` object, up there. Until every target is declared, it stays `SObject`, so it never names an object that isn't. That keeps `What.Name` valid even though Case has no `Name` field, while a field only some targets have, such as `Industry`, is an error. Only fields declared as `SObject` in an SObject definition are typed this way. Compound fields such as `BillingAddress` are `Address` in the definitions, which is `System.Address` (see [Standard library types](#standard-library-types)), so any of their properties (`city`, `street`) can be read.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Node } from "web-tree-sitter-sfapex";
|
|
2
|
+
export interface ApexDoc {
|
|
3
|
+
/** JSDoc lines: the description, then tags */
|
|
4
|
+
lines: string[];
|
|
5
|
+
/** `@param` descriptions keyed by lower-cased parameter name */
|
|
6
|
+
params: Map<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reads the ApexDoc comment of a class, field or method: the `/** ... *\/` comment right before it, or
|
|
10
|
+
* one placed between its annotations and the rest of its declaration.
|
|
11
|
+
*
|
|
12
|
+
* @param isSobject also accept `/* ... *\/` comments, which the generated SObject definitions use for
|
|
13
|
+
* field descriptions
|
|
14
|
+
*/
|
|
15
|
+
export declare function getApexDoc(node: Node, isSobject?: boolean): ApexDoc | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Flattens documentation onto one line for an inline `/** ... *\/` comment.
|
|
18
|
+
*/
|
|
19
|
+
export declare function inlineDoc(text: string): string;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the ApexDoc comment of a class, field or method: the `/** ... *\/` comment right before it, or
|
|
3
|
+
* one placed between its annotations and the rest of its declaration.
|
|
4
|
+
*
|
|
5
|
+
* @param isSobject also accept `/* ... *\/` comments, which the generated SObject definitions use for
|
|
6
|
+
* field descriptions
|
|
7
|
+
*/
|
|
8
|
+
export function getApexDoc(node, isSobject = false) {
|
|
9
|
+
const isDoc = (n) => n?.type === "block_comment" && n.text.startsWith(isSobject ? "/*" : "/**") && !/^\/\*\*?\/$/.test(n.text);
|
|
10
|
+
const previous = node.previousNamedSibling;
|
|
11
|
+
if (isDoc(previous)) {
|
|
12
|
+
return parseApexDoc(previous.text);
|
|
13
|
+
}
|
|
14
|
+
const modifiers = node.namedChildren.find((child) => child.type === "modifiers");
|
|
15
|
+
const inner = modifiers?.namedChildren.find(isDoc);
|
|
16
|
+
return inner ? parseApexDoc(inner.text) : undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Converts an ApexDoc comment to JSDoc lines. `@description` text joins the description, `@return`
|
|
20
|
+
* becomes `@returns`, and other tags (`@param`, `@example`, `@see`, ...) are kept as written.
|
|
21
|
+
*/
|
|
22
|
+
function parseApexDoc(comment) {
|
|
23
|
+
const raw = comment
|
|
24
|
+
.replace(/^\/\*\*?/, "")
|
|
25
|
+
.replace(/\*\/$/, "")
|
|
26
|
+
.split(/\r\n|\r|\n/)
|
|
27
|
+
// keep indentation after the `* ` gutter, which matters in `@example` code
|
|
28
|
+
.map((line) => (/^\s*\*/.test(line) ? line.replace(/^\s*\* ?/, "") : line.trimStart()).trimEnd());
|
|
29
|
+
const description = [];
|
|
30
|
+
const tags = [];
|
|
31
|
+
for (const line of raw) {
|
|
32
|
+
const tag = /^\s*@(\w+)\s*(.*)$/.exec(line);
|
|
33
|
+
if (tag) {
|
|
34
|
+
const name = tag[1] ?? "";
|
|
35
|
+
const rest = tag[2] ?? "";
|
|
36
|
+
if (name.toLowerCase() === "description") {
|
|
37
|
+
description.push(rest);
|
|
38
|
+
tags.push(description); // continuation lines join the description
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
tags.push([`@${name.toLowerCase() === "return" ? "returns" : name} ${rest}`.trimEnd()]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (tags.length > 0) {
|
|
45
|
+
tags.at(-1).push(line);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
description.push(line);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const params = new Map();
|
|
52
|
+
for (const tag of tags) {
|
|
53
|
+
const param = /^@param\s+(\w+)\s*([\s\S]*)$/.exec(tag.join("\n"));
|
|
54
|
+
if (param?.[1] && param[2]) {
|
|
55
|
+
params.set(param[1].toLowerCase(), param[2].trim());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const trim = (lines) => {
|
|
59
|
+
let start = 0;
|
|
60
|
+
let end = lines.length;
|
|
61
|
+
while (start < end && lines[start]?.trim() === "")
|
|
62
|
+
start++;
|
|
63
|
+
while (end > start && lines[end - 1]?.trim() === "")
|
|
64
|
+
end--;
|
|
65
|
+
return lines.slice(start, end);
|
|
66
|
+
};
|
|
67
|
+
const tagLines = tags.filter((tag) => tag !== description).map(trim);
|
|
68
|
+
const descriptionLines = trim(description);
|
|
69
|
+
return {
|
|
70
|
+
lines: [...descriptionLines, ...(descriptionLines.length > 0 && tagLines.length > 0 ? [""] : []), ...tagLines.flat()],
|
|
71
|
+
params,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Flattens documentation onto one line for an inline `/** ... *\/` comment.
|
|
76
|
+
*/
|
|
77
|
+
export function inlineDoc(text) {
|
|
78
|
+
return text.replace(/\s+/g, " ").replace(/\*\//g, "*\\/");
|
|
79
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Parser } from "web-tree-sitter-sfapex";
|
|
2
|
+
import type { ConversionOutput, InaccessibleApexProperties } from "../types.ts";
|
|
3
|
+
/** How an Apex file is converted. */
|
|
4
|
+
export interface ApexFileOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Whether the class is an SObject definition generated by the Salesforce extensions
|
|
7
|
+
* (`.sfdx/tools/sobjects`): its fields are exposed without `@AuraEnabled`, its block comments document
|
|
8
|
+
* them, and it declares no `@salesforce/apex` modules.
|
|
9
|
+
*/
|
|
10
|
+
readonly isSobject: boolean;
|
|
11
|
+
/** The (possibly dotted) namespace Apex types are declared in, or `null` to declare them globally. */
|
|
12
|
+
readonly apexNamespace: string | null;
|
|
13
|
+
/** The project's namespace. */
|
|
14
|
+
readonly projectNamespace: string | undefined;
|
|
15
|
+
/** The installed package the class belongs to, when it isn't the project's (see `ApexFile`). */
|
|
16
|
+
readonly packageNamespace: string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* For an SObject definition, the targets of its polymorphic relationships keyed by lower-cased
|
|
19
|
+
* relationship name (such as `what` on Task), which type them (see `#polymorphicType`).
|
|
20
|
+
*/
|
|
21
|
+
readonly polymorphicTargets: ReadonlyMap<string, readonly string[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a class's fields that LWC can't read are declared, marked `@deprecated` with why
|
|
24
|
+
* (`"include as deprecated"`), declared like the others (`"include"`), or left out (`"exclude"`). An
|
|
25
|
+
* SObject definition's fields are always declared.
|
|
26
|
+
*/
|
|
27
|
+
readonly inaccessibleProperties: InaccessibleApexProperties;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Converts an Apex file's top-level type to declarations: its types in the Apex namespace, then, for a
|
|
31
|
+
* class, its `@salesforce/apex` modules (or, for an SObject definition, its `@salesforce/schema`
|
|
32
|
+
* modules). Returns undefined for a file that declares no type, and throws for one with syntax errors
|
|
33
|
+
* and no type to convert. A type with syntax errors is converted as far as it parses, so a class being
|
|
34
|
+
* edited keeps its other declarations. The parse tree lives in WebAssembly memory, which isn't garbage
|
|
35
|
+
* collected, so it's deleted once the declarations are built.
|
|
36
|
+
*/
|
|
37
|
+
export declare function convertApexFile(parser: Parser, apexText: string, options: ApexFileOptions): ConversionOutput | undefined;
|