@lwc-bolts/sf2ts 0.0.4 → 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 -62
- 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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { fileImportName } from "../names.js";
|
|
2
|
+
import { writeDefaultExportModule } from "../module-declaration.js";
|
|
3
|
+
import { TextBuilder } from "../text-builder.js";
|
|
4
|
+
import { parseMetadataXml } from "../xml.js";
|
|
5
|
+
export function ContentAsset(component, context) {
|
|
6
|
+
const asset = parseMetadataXml(component.content, "ContentAsset");
|
|
7
|
+
// LWC imports content assets by developer name (the file name), not by the display masterLabel,
|
|
8
|
+
// which may contain spaces and other characters that aren't valid in an identifier
|
|
9
|
+
// the project's own namespace is left out of imports; another's (an installed package's) is kept
|
|
10
|
+
const assetName = fileImportName(component, context.projectNamespace);
|
|
11
|
+
const builder = new TextBuilder();
|
|
12
|
+
writeDefaultExportModule(builder, {
|
|
13
|
+
name: `@salesforce/contentAssetUrl/${assetName}`,
|
|
14
|
+
variable: assetName,
|
|
15
|
+
type: "string",
|
|
16
|
+
// go-to-definition lands on the label, or on the root element without one
|
|
17
|
+
range: asset.child("masterLabel")?.contentRange ?? asset.nameRange,
|
|
18
|
+
});
|
|
19
|
+
return builder.output();
|
|
20
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { importName, metadataName, recordName, splitNamespace } from "../names.js";
|
|
2
|
+
import { qualifyApex } from "../apex/namespace.js";
|
|
3
|
+
import { addSObjectInterfaces, fieldDoc, fieldTsType } from "../apex/sobjects.js";
|
|
4
|
+
import { schemaValueType, writeDefaultExportModule } from "../module-declaration.js";
|
|
5
|
+
import { TextBuilder } from "../text-builder.js";
|
|
6
|
+
import { parseMetadataXml } from "../xml.js";
|
|
7
|
+
export function CustomField(component, context) {
|
|
8
|
+
const field = parseMetadataXml(component.content, "CustomField");
|
|
9
|
+
const namespace = context.projectNamespace;
|
|
10
|
+
// a field's file is in its object's `fields` folder: `objects/<Object>/fields/<Field>.field-meta.xml`
|
|
11
|
+
const fileField = metadataName(component.path);
|
|
12
|
+
const fileObject = component.path.split(/[\\/]/).at(-3);
|
|
13
|
+
if (!fileObject) {
|
|
14
|
+
throw new Error(`no object name in the file path ${component.path}`);
|
|
15
|
+
}
|
|
16
|
+
// LWC imports the field by its names without the project's namespace; records and the import's value
|
|
17
|
+
// name the object and field with the namespace
|
|
18
|
+
const objectName = importName(fileObject, namespace);
|
|
19
|
+
const fieldName = importName(fileField, namespace);
|
|
20
|
+
const objectApiName = recordName(objectName, namespace);
|
|
21
|
+
const fieldApiName = recordName(fieldName, namespace);
|
|
22
|
+
// go-to-definition lands on the field's name, or on the root element without one
|
|
23
|
+
const nameRange = field.child("fullName")?.contentRange ?? field.nameRange;
|
|
24
|
+
const doc = fieldDoc(field.childText("description"), field.childText("inlineHelpText"));
|
|
25
|
+
const builder = new TextBuilder();
|
|
26
|
+
writeDefaultExportModule(builder, {
|
|
27
|
+
name: `@salesforce/schema/${objectName}.${fieldName}`,
|
|
28
|
+
variable: fieldName,
|
|
29
|
+
type: schemaValueType(objectApiName, fieldApiName),
|
|
30
|
+
doc,
|
|
31
|
+
range: nameRange,
|
|
32
|
+
});
|
|
33
|
+
const interfaces = sobjectInterfaces(field, {
|
|
34
|
+
objectName: objectApiName,
|
|
35
|
+
fieldName: fieldApiName,
|
|
36
|
+
nameRange,
|
|
37
|
+
doc,
|
|
38
|
+
namespace,
|
|
39
|
+
apexNamespace: context.apexNamespace,
|
|
40
|
+
});
|
|
41
|
+
if (interfaces.length > 0) {
|
|
42
|
+
addSObjectInterfaces(builder, context.apexNamespace, interfaces);
|
|
43
|
+
}
|
|
44
|
+
return builder.output();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The field's part of its object's record type: the field itself, and for a lookup its related record
|
|
48
|
+
* and the parent's child relationship. Compound geolocation fields declare their latitude and longitude.
|
|
49
|
+
* `objectName` and `fieldName` are API names, with the project's namespace (`namespace`) for custom ones;
|
|
50
|
+
* the records it refers to are types in `apexNamespace`.
|
|
51
|
+
*/
|
|
52
|
+
function sobjectInterfaces(field, { objectName, fieldName, nameRange, doc, namespace, apexNamespace, }) {
|
|
53
|
+
const fieldType = field.childText("type");
|
|
54
|
+
const typeRange = field.child("type")?.contentRange;
|
|
55
|
+
if (fieldType?.toLowerCase() === "location") {
|
|
56
|
+
const base = fieldName.replace(/__c$/, "");
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
name: objectName,
|
|
60
|
+
properties: ["Latitude", "Longitude"].map((part) => ({
|
|
61
|
+
name: `${base}__${part}__s`,
|
|
62
|
+
type: "number",
|
|
63
|
+
nameRange,
|
|
64
|
+
doc,
|
|
65
|
+
})),
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
const type = fieldTsType(fieldType, field.childText("summaryOperation"));
|
|
70
|
+
if (type === undefined) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const properties = [{ name: fieldName, type, nameRange, typeRange, doc }];
|
|
74
|
+
const interfaces = [{ name: objectName, properties }];
|
|
75
|
+
const writtenReferenceTo = field.childText("referenceTo");
|
|
76
|
+
const referenceTo = writtenReferenceTo && recordName(importName(writtenReferenceTo, namespace), namespace);
|
|
77
|
+
const isRelationship = ["lookup", "masterdetail", "hierarchy"].includes(fieldType?.toLowerCase() ?? "");
|
|
78
|
+
// Standard lookups (AccountId -> Account) name their relationships differently; only custom ones follow `__r`.
|
|
79
|
+
if (isRelationship && referenceTo && fieldName.endsWith("__c")) {
|
|
80
|
+
const referenceRange = field.child("referenceTo")?.contentRange;
|
|
81
|
+
properties.push({
|
|
82
|
+
name: fieldName.replace(/__c$/, "__r"),
|
|
83
|
+
type: qualifyApex(apexNamespace, [referenceTo]),
|
|
84
|
+
nameRange,
|
|
85
|
+
typeRange: referenceRange,
|
|
86
|
+
doc,
|
|
87
|
+
});
|
|
88
|
+
const relationshipName = field.childText("relationshipName");
|
|
89
|
+
// A child relationship is in its field's namespace, as its fields are: the project's own, or an
|
|
90
|
+
// installed package's (`dlrs__Log__c.dlrs__Parent__c` gives `dlrs__Children__r`), whatever the parent's.
|
|
91
|
+
const relationshipNamespace = splitNamespace(fieldName).namespace;
|
|
92
|
+
// the parent's records list their children; declaring the parent also makes the related record's type resolve
|
|
93
|
+
interfaces.push({
|
|
94
|
+
name: referenceTo,
|
|
95
|
+
properties: relationshipName
|
|
96
|
+
? [
|
|
97
|
+
{
|
|
98
|
+
name: relationshipNamespace ? `${relationshipNamespace}__${relationshipName}__r` : `${relationshipName}__r`,
|
|
99
|
+
type: `${qualifyApex(apexNamespace, [objectName])}[]`,
|
|
100
|
+
nameRange: field.child("relationshipName")?.contentRange,
|
|
101
|
+
},
|
|
102
|
+
]
|
|
103
|
+
: [],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return interfaces;
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConversionContext } from "../context.ts";
|
|
2
|
+
import type { Component, ConversionOutput } from "../types.ts";
|
|
3
|
+
import { TextBuilder } from "../text-builder.ts";
|
|
4
|
+
import { type XmlElement } from "../xml.ts";
|
|
5
|
+
export declare function CustomLabel(component: Component, context: ConversionContext): ConversionOutput;
|
|
6
|
+
/**
|
|
7
|
+
* Declares a label's module as `c.Name`, mapped to its `<fullName>`. Its value is a string in the user's language.
|
|
8
|
+
* A label of another namespace (an installed package's, named `dlrs__Name`) is imported as `dlrs.Name`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function writeLabel(builder: TextBuilder, label: XmlElement, projectNamespace: string | undefined): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { importNameParts } from "../names.js";
|
|
2
|
+
import { docParagraphs, docTag } from "../jsdoc.js";
|
|
3
|
+
import { writeDefaultExportModule } from "../module-declaration.js";
|
|
4
|
+
import { TextBuilder } from "../text-builder.js";
|
|
5
|
+
import { parseMetadataXml } from "../xml.js";
|
|
6
|
+
export function CustomLabel(component, context) {
|
|
7
|
+
const label = parseMetadataXml(component.content, "CustomLabel");
|
|
8
|
+
if (!label.childText("fullName")) {
|
|
9
|
+
throw new Error("the label has no <fullName>");
|
|
10
|
+
}
|
|
11
|
+
const builder = new TextBuilder();
|
|
12
|
+
writeLabel(builder, label, context.projectNamespace);
|
|
13
|
+
return builder.output();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Declares a label's module as `c.Name`, mapped to its `<fullName>`. Its value is a string in the user's language.
|
|
17
|
+
* A label of another namespace (an installed package's, named `dlrs__Name`) is imported as `dlrs.Name`.
|
|
18
|
+
*/
|
|
19
|
+
export function writeLabel(builder, label, projectNamespace) {
|
|
20
|
+
const fullName = label.child("fullName");
|
|
21
|
+
if (fullName === undefined || fullName.text === "") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const imported = importNameParts(fullName.text, projectNamespace);
|
|
25
|
+
writeDefaultExportModule(builder, {
|
|
26
|
+
name: `@salesforce/label/${imported.namespace ?? "c"}.${imported.name}`,
|
|
27
|
+
variable: `lbl${imported.name}`,
|
|
28
|
+
type: "string",
|
|
29
|
+
doc: docParagraphs(quoted(label.childText("value") || undefined), docTag("description", label.childText("shortDescription"))),
|
|
30
|
+
range: fullName.contentRange,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** A label's value in double quotes, so the documentation shows where it starts and ends. */
|
|
34
|
+
function quoted(value) {
|
|
35
|
+
return value === undefined ? undefined : `"${value}"`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TextBuilder } from "../text-builder.js";
|
|
2
|
+
import { writeLabel } from "./custom-label.js";
|
|
3
|
+
import { parseMetadataXml } from "../xml.js";
|
|
4
|
+
export function CustomLabels(component, context) {
|
|
5
|
+
const builder = new TextBuilder();
|
|
6
|
+
for (const label of parseMetadataXml(component.content, "CustomLabels").childrenNamed("labels")) {
|
|
7
|
+
writeLabel(builder, label, context.projectNamespace);
|
|
8
|
+
}
|
|
9
|
+
return builder.output();
|
|
10
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { fileImportName, recordName } from "../names.js";
|
|
2
|
+
import { qualifyApex } from "../apex/namespace.js";
|
|
3
|
+
import { addSObjectInterfaces, standardFields } from "../apex/sobjects.js";
|
|
4
|
+
import { docTag } from "../jsdoc.js";
|
|
5
|
+
import { schemaValueType, writeDefaultExportModule } from "../module-declaration.js";
|
|
6
|
+
import { TextBuilder } from "../text-builder.js";
|
|
7
|
+
import { parseMetadataXml } from "../xml.js";
|
|
8
|
+
export function CustomObject(component, context) {
|
|
9
|
+
const obj = parseMetadataXml(component.content, "CustomObject");
|
|
10
|
+
// LWC imports the object by its name without the project's namespace; records and the import's value
|
|
11
|
+
// name it with the namespace
|
|
12
|
+
const objectName = fileImportName(component, context.projectNamespace);
|
|
13
|
+
const apiName = recordName(objectName, context.projectNamespace);
|
|
14
|
+
// go-to-definition lands on the label, or on the root element without one
|
|
15
|
+
const labelRange = obj.child("label")?.contentRange ?? obj.nameRange;
|
|
16
|
+
const doc = [docTag("description", obj.childText("description"))];
|
|
17
|
+
const builder = new TextBuilder();
|
|
18
|
+
writeDefaultExportModule(builder, {
|
|
19
|
+
name: `@salesforce/schema/${objectName}`,
|
|
20
|
+
variable: objectName,
|
|
21
|
+
type: schemaValueType(apiName),
|
|
22
|
+
doc,
|
|
23
|
+
range: labelRange,
|
|
24
|
+
});
|
|
25
|
+
// The object's record type, with its standard fields; each field file adds its own field.
|
|
26
|
+
const properties = standardFields(objectName, {
|
|
27
|
+
owned: obj.childText("sharingModel") !== "ControlledByParent",
|
|
28
|
+
apexNamespace: context.apexNamespace,
|
|
29
|
+
});
|
|
30
|
+
const user = qualifyApex(context.apexNamespace, ["User"]);
|
|
31
|
+
addSObjectInterfaces(builder, context.apexNamespace, [
|
|
32
|
+
{ name: apiName, nameRange: labelRange, doc, properties },
|
|
33
|
+
// referenced by CreatedBy and LastModifiedBy; declared here so the reference always resolves
|
|
34
|
+
...(properties.some((p) => p.type === user) ? [{ name: "User", properties: [] }] : []),
|
|
35
|
+
]);
|
|
36
|
+
return builder.output();
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileImportName } from "../names.js";
|
|
2
|
+
import { docParagraphs, docTag } from "../jsdoc.js";
|
|
3
|
+
import { writeDefaultExportModule } from "../module-declaration.js";
|
|
4
|
+
import { TextBuilder } from "../text-builder.js";
|
|
5
|
+
import { parseMetadataXml } from "../xml.js";
|
|
6
|
+
export function CustomPermission(component, context) {
|
|
7
|
+
const perm = parseMetadataXml(component.content, "CustomPermission");
|
|
8
|
+
// the project's own namespace is left out of imports; another's (an installed package's) is kept
|
|
9
|
+
const permName = fileImportName(component, context.projectNamespace);
|
|
10
|
+
const builder = new TextBuilder();
|
|
11
|
+
writeDefaultExportModule(builder, {
|
|
12
|
+
name: `@salesforce/customPermission/${permName}`,
|
|
13
|
+
variable: `has${permName}`,
|
|
14
|
+
// `true` when the user has the permission, and `undefined` when they don't
|
|
15
|
+
type: "true | undefined",
|
|
16
|
+
doc: docParagraphs(perm.childText("label"), docTag("description", perm.childText("description"))),
|
|
17
|
+
// go-to-definition lands on the label, or on the root element without one
|
|
18
|
+
range: perm.child("label")?.contentRange ?? perm.nameRange,
|
|
19
|
+
});
|
|
20
|
+
return builder.output();
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConversionContext } from "../context.ts";
|
|
2
|
+
import type { ConversionOutput, LightningComponentBundleComponent } from "../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Converts a file of a Lightning web component's bundle with the converter for its kind (see
|
|
5
|
+
* `BUNDLE_FILES`). Any other file of the bundle, such as its JavaScript or CSS, gets a diagnostic.
|
|
6
|
+
*/
|
|
7
|
+
export declare function LightningComponentBundle(component: LightningComponentBundleComponent, context: ConversionContext): Promise<ConversionOutput>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files of a Lightning web component's bundle that sf2ts converts, each by a converter of its own,
|
|
3
|
+
* loaded on its first file.
|
|
4
|
+
*/
|
|
5
|
+
const BUNDLE_FILES = [
|
|
6
|
+
{
|
|
7
|
+
// create the `c/<name>` module which re-exports the component exports
|
|
8
|
+
description: "<name>.js-meta.xml",
|
|
9
|
+
pattern: /\.js-meta\.xml$/i,
|
|
10
|
+
load: async () => (await import("../lightning-component-bundle/metadata.js")).convertBundleMetadata,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
// create modules from the template files so imports and references can go both directions
|
|
14
|
+
description: ".html templates",
|
|
15
|
+
pattern: /\.html$/i,
|
|
16
|
+
load: async () => (await import("../lightning-component-bundle/templates/template.js")).convertBundleTemplate,
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Converts a file of a Lightning web component's bundle with the converter for its kind (see
|
|
21
|
+
* `BUNDLE_FILES`). Any other file of the bundle, such as its JavaScript or CSS, gets a diagnostic.
|
|
22
|
+
*/
|
|
23
|
+
export async function LightningComponentBundle(component, context) {
|
|
24
|
+
const file = BUNDLE_FILES.find(({ pattern }) => pattern.test(component.path));
|
|
25
|
+
if (file === undefined) {
|
|
26
|
+
const fileName = component.path.split(/[\\/]/).pop();
|
|
27
|
+
throw new Error(`${fileName} isn't a bundle file sf2ts converts; it converts a bundle's ${BUNDLE_FILES.map(({ description }) => description).join(" and ")}`);
|
|
28
|
+
}
|
|
29
|
+
const convert = await file.load();
|
|
30
|
+
return convert(component, context);
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConversionContext } from "../context.ts";
|
|
2
|
+
import type { Component, ConversionOutput } from "../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Declares the `@salesforce/messageChannel/<Name>__c` module LWC imports a Lightning message channel
|
|
5
|
+
* from. The imported value is an opaque string passed to `lightning/messageService`,
|
|
6
|
+
* we also add a branded type naming the channel (the property doesn't exist at runtime) should you want to discriminate
|
|
7
|
+
* one channel from another. A channel that isn't another namespace's can also be imported through the default namespace,
|
|
8
|
+
* as `@salesforce/messageChannel/c__<Name>__c`, which is the same channel, with the same brand.
|
|
9
|
+
*/
|
|
10
|
+
export declare function LightningMessageChannel(component: Component, context: ConversionContext): ConversionOutput;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { fileImportName, splitNamespace } from "../names.js";
|
|
2
|
+
import { docParagraphs, docTag } from "../jsdoc.js";
|
|
3
|
+
import { writeDefaultExportModule } from "../module-declaration.js";
|
|
4
|
+
import { TextBuilder } from "../text-builder.js";
|
|
5
|
+
import { parseMetadataXml } from "../xml.js";
|
|
6
|
+
/**
|
|
7
|
+
* Declares the `@salesforce/messageChannel/<Name>__c` module LWC imports a Lightning message channel
|
|
8
|
+
* from. The imported value is an opaque string passed to `lightning/messageService`,
|
|
9
|
+
* we also add a branded type naming the channel (the property doesn't exist at runtime) should you want to discriminate
|
|
10
|
+
* one channel from another. A channel that isn't another namespace's can also be imported through the default namespace,
|
|
11
|
+
* as `@salesforce/messageChannel/c__<Name>__c`, which is the same channel, with the same brand.
|
|
12
|
+
*/
|
|
13
|
+
export function LightningMessageChannel(component, context) {
|
|
14
|
+
const channel = parseMetadataXml(component.content, "LightningMessageChannel");
|
|
15
|
+
// the project's own namespace is left out of imports; another's (an installed package's) is kept
|
|
16
|
+
const channelName = `${fileImportName(component, context.projectNamespace)}__c`;
|
|
17
|
+
const fieldLines = channel.childrenNamed("lightningMessageFields").flatMap((field) => {
|
|
18
|
+
const name = field.childText("fieldName");
|
|
19
|
+
if (name === undefined) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const fieldDescription = field.childText("description");
|
|
23
|
+
return [`- \`${name}\`${fieldDescription === undefined ? "" : `: ${fieldDescription}`}`];
|
|
24
|
+
});
|
|
25
|
+
const doc = docParagraphs(channel.childText("masterLabel"), docTag("description", channel.childText("description")), fieldLines.length === 0 ? undefined : ["Message fields:", ...fieldLines]);
|
|
26
|
+
const moduleNames = splitNamespace(channelName).namespace === undefined ? [channelName, `c__${channelName}`] : [channelName];
|
|
27
|
+
const builder = new TextBuilder();
|
|
28
|
+
for (const moduleName of moduleNames) {
|
|
29
|
+
writeDefaultExportModule(builder, {
|
|
30
|
+
name: `@salesforce/messageChannel/${moduleName}`,
|
|
31
|
+
variable: channelName,
|
|
32
|
+
type: `string & { readonly __messageChannel: ${JSON.stringify(channelName)} }`,
|
|
33
|
+
doc,
|
|
34
|
+
// go-to-definition lands on the label, or on the root element without one
|
|
35
|
+
range: channel.child("masterLabel")?.contentRange ?? channel.nameRange,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return builder.output();
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ConversionContext } from "../context.ts";
|
|
2
|
+
import type { Component, ConversionOutput } from "../types.ts";
|
|
3
|
+
/** Converts a component of one type, or of any (`Component`). */
|
|
4
|
+
export type Converter<C extends Component = Component> = (component: C, context: ConversionContext) => ConversionOutput | Promise<ConversionOutput>;
|
|
5
|
+
/**
|
|
6
|
+
* Each metadata type's converter, loaded on its first conversion, so importing sf2ts doesn't load what
|
|
7
|
+
* converting other types needs (the Apex parser, the template converter and its compiler, an XML
|
|
8
|
+
* parser). Node caches each module after its first import. It has a converter for each `MetadataType`,
|
|
9
|
+
* which `MetadataType`'s values are made from.
|
|
10
|
+
*/
|
|
11
|
+
export declare const converters: {
|
|
12
|
+
ApexClass: () => Promise<typeof import("./apex-class.ts").ApexClass>;
|
|
13
|
+
ContentAsset: () => Promise<typeof import("./content-asset.ts").ContentAsset>;
|
|
14
|
+
CustomField: () => Promise<typeof import("./custom-field.ts").CustomField>;
|
|
15
|
+
CustomLabel: () => Promise<typeof import("./custom-label.ts").CustomLabel>;
|
|
16
|
+
CustomLabels: () => Promise<typeof import("./custom-labels.ts").CustomLabels>;
|
|
17
|
+
CustomObject: () => Promise<typeof import("./custom-object.ts").CustomObject>;
|
|
18
|
+
CustomPermission: () => Promise<typeof import("./custom-permission.ts").CustomPermission>;
|
|
19
|
+
LightningComponentBundle: () => Promise<typeof import("./lightning-component-bundle.ts").LightningComponentBundle>;
|
|
20
|
+
LightningMessageChannel: () => Promise<typeof import("./lightning-message-channel.ts").LightningMessageChannel>;
|
|
21
|
+
StaticResource: () => Promise<typeof import("./static-resource.ts").StaticResource>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each metadata type's converter, loaded on its first conversion, so importing sf2ts doesn't load what
|
|
3
|
+
* converting other types needs (the Apex parser, the template converter and its compiler, an XML
|
|
4
|
+
* parser). Node caches each module after its first import. It has a converter for each `MetadataType`,
|
|
5
|
+
* which `MetadataType`'s values are made from.
|
|
6
|
+
*/
|
|
7
|
+
export const converters = {
|
|
8
|
+
ApexClass: async () => (await import("./apex-class.js")).ApexClass,
|
|
9
|
+
ContentAsset: async () => (await import("./content-asset.js")).ContentAsset,
|
|
10
|
+
CustomField: async () => (await import("./custom-field.js")).CustomField,
|
|
11
|
+
CustomLabel: async () => (await import("./custom-label.js")).CustomLabel,
|
|
12
|
+
CustomLabels: async () => (await import("./custom-labels.js")).CustomLabels,
|
|
13
|
+
CustomObject: async () => (await import("./custom-object.js")).CustomObject,
|
|
14
|
+
CustomPermission: async () => (await import("./custom-permission.js")).CustomPermission,
|
|
15
|
+
LightningComponentBundle: async () => (await import("./lightning-component-bundle.js")).LightningComponentBundle,
|
|
16
|
+
LightningMessageChannel: async () => (await import("./lightning-message-channel.js")).LightningMessageChannel,
|
|
17
|
+
StaticResource: async () => (await import("./static-resource.js")).StaticResource,
|
|
18
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { fileImportName } from "../names.js";
|
|
2
|
+
import { docTag } from "../jsdoc.js";
|
|
3
|
+
import { writeDefaultExportModule } from "../module-declaration.js";
|
|
4
|
+
import { TextBuilder } from "../text-builder.js";
|
|
5
|
+
import { parseMetadataXml } from "../xml.js";
|
|
6
|
+
export function StaticResource(component, context) {
|
|
7
|
+
const resource = parseMetadataXml(component.content, "StaticResource");
|
|
8
|
+
// the project's own namespace is left out of imports; another's (an installed package's) is kept
|
|
9
|
+
const resourceName = fileImportName(component, context.projectNamespace);
|
|
10
|
+
const builder = new TextBuilder();
|
|
11
|
+
writeDefaultExportModule(builder, {
|
|
12
|
+
name: `@salesforce/resourceUrl/${resourceName}`,
|
|
13
|
+
variable: resourceName,
|
|
14
|
+
type: "string",
|
|
15
|
+
doc: [docTag("description", resource.childText("description")), docTag("cacheControl", resource.childText("cacheControl"))],
|
|
16
|
+
// go-to-definition lands on the description, or on the root element without one
|
|
17
|
+
range: resource.child("description")?.contentRange ?? resource.nameRange,
|
|
18
|
+
});
|
|
19
|
+
return builder.output();
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Component, ConvertedComponent } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A file that couldn't be converted: empty output, and a `ConversionFailed` diagnostic covering the file.
|
|
4
|
+
*/
|
|
5
|
+
export declare function conversionFailed(component: Component, error: unknown): ConvertedComponent;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file that couldn't be converted: empty output, and a `ConversionFailed` diagnostic covering the file.
|
|
3
|
+
*/
|
|
4
|
+
export function conversionFailed(component, error) {
|
|
5
|
+
return {
|
|
6
|
+
...component,
|
|
7
|
+
text: "",
|
|
8
|
+
mapData: [],
|
|
9
|
+
diagnostics: [
|
|
10
|
+
{
|
|
11
|
+
kind: "ConversionFailed",
|
|
12
|
+
message: `Unable to convert ${component.type} to TypeScript: ${error instanceof Error ? error.message : String(error)}`,
|
|
13
|
+
start: 0,
|
|
14
|
+
length: component.content.length,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Words that can't name a variable, such as a `const` declared after a component or a field. */
|
|
2
|
+
export declare const RESERVED_WORDS: ReadonlySet<string>;
|
|
3
|
+
/**
|
|
4
|
+
* Words that can't name a namespace, such as a part of the `apexNamespace` Apex types are declared in: the
|
|
5
|
+
* reserved words but `await`, which TypeScript accepts as a namespace's name (`declare namespace
|
|
6
|
+
* Acme.await`), though not as a variable's that's exported (`export default await;` is an expression).
|
|
7
|
+
*/
|
|
8
|
+
export declare const NAMESPACE_RESERVED_WORDS: ReadonlySet<string>;
|
|
9
|
+
/** Whether a name is an identifier, so it can be written without quotes. */
|
|
10
|
+
export declare function isIdentifier(name: string): boolean;
|
|
11
|
+
/** A name for a `const`: the name itself, or with a `_` prefix when it's a reserved word (`_delete`). */
|
|
12
|
+
export declare function variableName(name: string): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// JavaScript identifiers in generated declarations, such as the `const` a module exports.
|
|
2
|
+
/** Words that can't name a variable, such as a `const` declared after a component or a field. */
|
|
3
|
+
export const RESERVED_WORDS = new Set(("break case catch class const continue debugger default delete do else enum export extends false finally for " +
|
|
4
|
+
"function if import in instanceof new null return super switch this throw true try typeof var void while with " +
|
|
5
|
+
"implements interface let package private protected public static yield await").split(" "));
|
|
6
|
+
/**
|
|
7
|
+
* Words that can't name a namespace, such as a part of the `apexNamespace` Apex types are declared in: the
|
|
8
|
+
* reserved words but `await`, which TypeScript accepts as a namespace's name (`declare namespace
|
|
9
|
+
* Acme.await`), though not as a variable's that's exported (`export default await;` is an expression).
|
|
10
|
+
*/
|
|
11
|
+
export const NAMESPACE_RESERVED_WORDS = new Set([...RESERVED_WORDS].filter((word) => word !== "await"));
|
|
12
|
+
/** Whether a name is an identifier, so it can be written without quotes. */
|
|
13
|
+
export function isIdentifier(name) {
|
|
14
|
+
return /^[A-Za-z_$][\w$]*$/.test(name);
|
|
15
|
+
}
|
|
16
|
+
/** A name for a `const`: the name itself, or with a `_` prefix when it's a reserved word (`_delete`). */
|
|
17
|
+
export function variableName(name) {
|
|
18
|
+
return RESERVED_WORDS.has(name) ? `_${name}` : name;
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Component, ConvertedComponent, ConvertOptions, MetadataType as MetadataTypeName } from "./types.ts";
|
|
2
|
+
export type { ApexClassComponent, Component, ConvertedComponent, ConvertOptions, Diagnostic, InaccessibleApexProperties, LightningComponentBundleComponent, MapPair, MetadataComponent, } from "./types.ts";
|
|
3
|
+
export type MetadataType = MetadataTypeName;
|
|
4
|
+
export declare const MetadataType: Readonly<{
|
|
5
|
+
ApexClass: "ApexClass";
|
|
6
|
+
ContentAsset: "ContentAsset";
|
|
7
|
+
CustomField: "CustomField";
|
|
8
|
+
CustomLabel: "CustomLabel";
|
|
9
|
+
CustomLabels: "CustomLabels";
|
|
10
|
+
CustomObject: "CustomObject";
|
|
11
|
+
CustomPermission: "CustomPermission";
|
|
12
|
+
LightningComponentBundle: "LightningComponentBundle";
|
|
13
|
+
LightningMessageChannel: "LightningMessageChannel";
|
|
14
|
+
StaticResource: "StaticResource";
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Converts components to TypeScript: metadata to declarations, and templates to TypeScript that checks
|
|
18
|
+
* them. A file that can't be converted gets a `ConversionFailed` diagnostic and empty output rather than
|
|
19
|
+
* failing the others; invalid options (a namespace that can't be declared) reject. Each type's converter
|
|
20
|
+
* are lazy loaded.
|
|
21
|
+
*/
|
|
22
|
+
export declare function convert(components: Component[], options?: ConvertOptions): Promise<ConvertedComponent[]>;
|
|
23
|
+
/**
|
|
24
|
+
* A Lightning web component bundle's API version, from the content of its `.js-meta.xml`, or undefined when
|
|
25
|
+
* it has none that can be read (such as one being written): a template's `apiVersion`. It loads the XML
|
|
26
|
+
* parser on its first call.
|
|
27
|
+
*/
|
|
28
|
+
export declare function bundleApiVersion(metadata: string): Promise<number | undefined>;
|
|
29
|
+
/**
|
|
30
|
+
* The name of a component module's default export, from its JavaScript or TypeScript source, or undefined
|
|
31
|
+
* when it has no named one (or doesn't parse): a `.js-meta.xml`'s `defaultExportName`. It loads the
|
|
32
|
+
* JavaScript parser on its first call.
|
|
33
|
+
*/
|
|
34
|
+
export declare function defaultExportName(source: string): Promise<string | undefined>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { conversionContext } from "./context.js";
|
|
2
|
+
import { converters } from "./converters/registry.js";
|
|
3
|
+
import { conversionFailed } from "./diagnostics.js";
|
|
4
|
+
export const MetadataType = Object.freeze(Object.fromEntries(Object.keys(converters).map((type) => [type, type])));
|
|
5
|
+
/**
|
|
6
|
+
* Converts components to TypeScript: metadata to declarations, and templates to TypeScript that checks
|
|
7
|
+
* them. A file that can't be converted gets a `ConversionFailed` diagnostic and empty output rather than
|
|
8
|
+
* failing the others; invalid options (a namespace that can't be declared) reject. Each type's converter
|
|
9
|
+
* are lazy loaded.
|
|
10
|
+
*/
|
|
11
|
+
export async function convert(components, options) {
|
|
12
|
+
// an async function, so invalid options reject rather than throw
|
|
13
|
+
const context = conversionContext(options);
|
|
14
|
+
return Promise.all(components.map((component) => convertOne(component, context)));
|
|
15
|
+
}
|
|
16
|
+
async function convertOne(component, context) {
|
|
17
|
+
try {
|
|
18
|
+
const loadConverter = Object.hasOwn(converters, component.type) ? converters[component.type] : undefined;
|
|
19
|
+
if (loadConverter === undefined) {
|
|
20
|
+
throw new Error(`unsupported metadata type ${component.type}; supported types are ${Object.keys(converters).join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
// each type's converter takes that type's component, which `component.type` says this is
|
|
23
|
+
const converter = (await loadConverter());
|
|
24
|
+
// use destructing to strip text, mapData, and diagnostics if present, leaving everything else as "input"
|
|
25
|
+
const { text: _text, mapData: _mapData, diagnostics: _diagnostics, ...input } = component;
|
|
26
|
+
return { ...input, ...(await converter(component, context)) };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return conversionFailed(component, error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A Lightning web component bundle's API version, from the content of its `.js-meta.xml`, or undefined when
|
|
34
|
+
* it has none that can be read (such as one being written): a template's `apiVersion`. It loads the XML
|
|
35
|
+
* parser on its first call.
|
|
36
|
+
*/
|
|
37
|
+
export async function bundleApiVersion(metadata) {
|
|
38
|
+
return (await import("./lightning-component-bundle/metadata.js")).bundleApiVersion(metadata);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The name of a component module's default export, from its JavaScript or TypeScript source, or undefined
|
|
42
|
+
* when it has no named one (or doesn't parse): a `.js-meta.xml`'s `defaultExportName`. It loads the
|
|
43
|
+
* JavaScript parser on its first call.
|
|
44
|
+
*/
|
|
45
|
+
export async function defaultExportName(source) {
|
|
46
|
+
return (await import("./lightning-component-bundle/metadata.js")).findDefaultExportName(source);
|
|
47
|
+
}
|
package/dist/jsdoc.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a JSDoc block at `indent`, or an empty string when there are no lines. `undefined` lines
|
|
3
|
+
* are dropped, empty strings become blank comment lines, and `*\/` in the text can't end the comment.
|
|
4
|
+
*/
|
|
5
|
+
export declare function jsDoc(indent: string, lines: (string | undefined)[]): string;
|
|
6
|
+
/**
|
|
7
|
+
* Prefixes a JSDoc tag to a value, or returns undefined when the value is missing.
|
|
8
|
+
*/
|
|
9
|
+
export declare function docTag(tag: string, value: string | undefined): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Documentation lines from paragraphs, each a line or several, with a blank line between them. Missing
|
|
12
|
+
* paragraphs, and missing lines in them, are left out.
|
|
13
|
+
*/
|
|
14
|
+
export declare function docParagraphs(...paragraphs: (string | undefined | readonly (string | undefined)[])[]): string[];
|
package/dist/jsdoc.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Documentation comments on generated declarations, from metadata's descriptions and Apex's ApexDoc.
|
|
2
|
+
/**
|
|
3
|
+
* Renders a JSDoc block at `indent`, or an empty string when there are no lines. `undefined` lines
|
|
4
|
+
* are dropped, empty strings become blank comment lines, and `*\/` in the text can't end the comment.
|
|
5
|
+
*/
|
|
6
|
+
export function jsDoc(indent, lines) {
|
|
7
|
+
const body = lines.filter((line) => line !== undefined).flatMap((line) => line.replace(/\*\//g, "*\\/").split(/\r\n|\r|\n/));
|
|
8
|
+
if (body.length === 0) {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
return [`${indent}/**`, ...body.map((line) => (line === "" ? `${indent} *` : `${indent} * ${line}`)), `${indent} */`, ""].join("\n");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Prefixes a JSDoc tag to a value, or returns undefined when the value is missing.
|
|
15
|
+
*/
|
|
16
|
+
export function docTag(tag, value) {
|
|
17
|
+
return value === undefined ? undefined : `@${tag} ${value}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Documentation lines from paragraphs, each a line or several, with a blank line between them. Missing
|
|
21
|
+
* paragraphs, and missing lines in them, are left out.
|
|
22
|
+
*/
|
|
23
|
+
export function docParagraphs(...paragraphs) {
|
|
24
|
+
const present = paragraphs
|
|
25
|
+
.map((paragraph) => (typeof paragraph === "string" || paragraph === undefined ? [paragraph] : paragraph).filter((line) => line !== undefined))
|
|
26
|
+
.filter((lines) => lines.length > 0);
|
|
27
|
+
return present.flatMap((lines, i) => (i === 0 ? lines : ["", ...lines]));
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Program } from "acorn";
|
|
2
|
+
/**
|
|
3
|
+
* Parses a component's module, or returns undefined when it doesn't parse (such as a file being edited),
|
|
4
|
+
* so callers find nothing in it rather than guessing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseComponentSource(source: string): Program | undefined;
|