@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
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// LWC templates: each `.html` in a component bundle becomes virtual TypeScript that uses the component
|
|
2
|
+
// the way the template does, so TypeScript checks the template's bindings against the component's class.
|
|
3
|
+
//
|
|
4
|
+
// <template lwc:if={hasOrders}> import type __Component from "./orders.js";
|
|
5
|
+
// <template for:each={rows} for:item="row"> declare const $cmp: InstanceType<typeof __Component>;
|
|
6
|
+
// <p key={row.id}>{row.name}</p> -> if ($cmp.hasOrders) {
|
|
7
|
+
// </template> for (const row of $cmp.rows ?? []) {
|
|
8
|
+
// </template> row.id; row.name;
|
|
9
|
+
// }
|
|
10
|
+
// }
|
|
11
|
+
//
|
|
12
|
+
// Names map back to the template verbatim, so diagnostics, hover and go-to-definition work there, and a
|
|
13
|
+
// loop variable's go-to-definition lands on its `for:item`. The output is a module (it imports the
|
|
14
|
+
// component), so its helpers don't clash with other templates'. Its default export is the compiled
|
|
15
|
+
// template, so a component can import a template and return it from `render()`.
|
|
16
|
+
import { LIGHTNING_COMPONENTS } from "@lwc-bolts/lightning-base-components";
|
|
17
|
+
import { isIdentifier } from "../../identifiers.js";
|
|
18
|
+
import { MapFeature } from "../../text-builder.js";
|
|
19
|
+
import { hasComplexExpressions, isExpression, loadTemplateCompiler, } from "./ast.js";
|
|
20
|
+
import { importNameParts } from "../../names.js";
|
|
21
|
+
import { ComponentTypes, TemplateRefs } from "./declarations.js";
|
|
22
|
+
import { componentNames, isSimple } from "./expressions.js";
|
|
23
|
+
import { helperDeclarations } from "./helpers.js";
|
|
24
|
+
import { slotDeclarations, slottedContent, templateSlots } from "./slots.js";
|
|
25
|
+
import { TemplateSource } from "./source.js";
|
|
26
|
+
import { elementType, tagKind } from "./tags.js";
|
|
27
|
+
import { Writer } from "./writer.js";
|
|
28
|
+
/** What a handler must be: any function, whatever the event it's given. */
|
|
29
|
+
const HANDLER_TYPE = "(...args: any[]) => unknown";
|
|
30
|
+
/**
|
|
31
|
+
* Converts one of a bundle's templates (`convert` with a `LightningComponentBundle`'s `.html`). What the
|
|
32
|
+
* template needs to know about its bundle is on the component: how it imports the bundle's component
|
|
33
|
+
* (`componentModule`), the bundle's name, its API version, and whether it has other templates. Without
|
|
34
|
+
* them, the template is taken to be in the bundle's folder, named for it. Other components' slots come
|
|
35
|
+
* from their own templates' declarations. The compiler's errors are `TemplateError` diagnostics.
|
|
36
|
+
*/
|
|
37
|
+
export async function convertBundleTemplate(component, context) {
|
|
38
|
+
const bundleName = component.bundleName ?? component.path.split(/[\\/]/).at(-2);
|
|
39
|
+
if (!bundleName) {
|
|
40
|
+
throw new Error(`no bundle name, and no bundle folder in the file path ${component.path}`);
|
|
41
|
+
}
|
|
42
|
+
const { namespace, name } = importNameParts(bundleName, context.projectNamespace);
|
|
43
|
+
const { apiVersion, alternateTemplates, componentModule = `./${bundleName}.js` } = component;
|
|
44
|
+
const { text, mapData, diagnostics } = await convertTemplate(component.content, componentModule, {
|
|
45
|
+
fileName: component.path,
|
|
46
|
+
module: `${namespace ?? "c"}/${name}`,
|
|
47
|
+
...(alternateTemplates !== undefined && { alternateTemplates }),
|
|
48
|
+
...(apiVersion !== undefined && { apiVersion }),
|
|
49
|
+
...(context.projectNamespace !== undefined && { projectNamespace: context.projectNamespace }),
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
text,
|
|
53
|
+
mapData,
|
|
54
|
+
...(diagnostics.length > 0 && { diagnostics: diagnostics.map((diagnostic) => ({ kind: "TemplateError", ...diagnostic })) }),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The name of a template's default export, which editors show and offer in auto-imports: `<file>Template`,
|
|
59
|
+
* so `default.html`'s is `defaultTemplate` rather than a reserved word. A file name can have characters
|
|
60
|
+
* a name can't.
|
|
61
|
+
*/
|
|
62
|
+
export function templateExportName(fileName) {
|
|
63
|
+
const name = fileName
|
|
64
|
+
.split(/[\\/]/)
|
|
65
|
+
.pop()
|
|
66
|
+
.replace(/\.html$/i, "")
|
|
67
|
+
.replace(/[^\w$]/g, "_");
|
|
68
|
+
return `${/^\d/.test(name) ? "_" : ""}${name}Template`;
|
|
69
|
+
}
|
|
70
|
+
class TemplateConverter {
|
|
71
|
+
out = new Writer();
|
|
72
|
+
/** What only LWC's codegen rejects, which parsing the template doesn't report. */
|
|
73
|
+
diagnostics = [];
|
|
74
|
+
#context;
|
|
75
|
+
#source;
|
|
76
|
+
/** How many conditional blocks (`lwc:if`, `if:true`, scoped slots) enclose the current node. */
|
|
77
|
+
#conditional = 0;
|
|
78
|
+
#refs = new TemplateRefs();
|
|
79
|
+
#components;
|
|
80
|
+
#compiler;
|
|
81
|
+
/** The base components, by tag (see TemplateContext.baseComponents). */
|
|
82
|
+
#baseComponents;
|
|
83
|
+
/** Loop variables in scope; any other name is a member of the component. */
|
|
84
|
+
#scope = [];
|
|
85
|
+
/** How many loops have needed an item variable of their own (see #loop). */
|
|
86
|
+
#loopItems = 0;
|
|
87
|
+
/** How many loops have needed a variable for their list (see #loop). */
|
|
88
|
+
#loopLists = 0;
|
|
89
|
+
constructor(source, context, compiler) {
|
|
90
|
+
this.#source = new TemplateSource(source, { complexExpressions: hasComplexExpressions(context.apiVersion) });
|
|
91
|
+
this.#context = context;
|
|
92
|
+
this.#compiler = compiler;
|
|
93
|
+
this.#baseComponents = context.baseComponents ?? LIGHTNING_COMPONENTS;
|
|
94
|
+
this.#components = new ComponentTypes(this.#baseComponents);
|
|
95
|
+
}
|
|
96
|
+
#tag(node) {
|
|
97
|
+
return tagKind(node, this.#compiler.tagModule, this.#context.projectNamespace, this.#baseComponents);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Checks the content passed to a child component, `module`, against the slots its templates declare (see
|
|
101
|
+
* slotDeclarations): a `slot="name"` must be one of them, and content without one needs an unnamed slot.
|
|
102
|
+
*/
|
|
103
|
+
#checkSlots(node, module) {
|
|
104
|
+
const allowed = `__lwcSlots<${JSON.stringify(module)}>`;
|
|
105
|
+
for (const { slot, position, length } of slottedContent(node, this.#source)) {
|
|
106
|
+
// `satisfies` rather than a helper call, whose argument would get a parameter name inlay hint, and in
|
|
107
|
+
// a tuple so TypeScript reports a mismatch on the slot's name rather than on the type
|
|
108
|
+
this.out.line(() => {
|
|
109
|
+
this.out.write("[");
|
|
110
|
+
this.out.writeAt(JSON.stringify(slot), position, length);
|
|
111
|
+
this.out.write(`] satisfies [${allowed}];`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Checks a component's properties by writing them as an object literal that must satisfy its type:
|
|
117
|
+
* TypeScript reports a property the component doesn't have, or a value of the wrong type, on the
|
|
118
|
+
* property, which maps to the attribute's name. A child component's type is its public members
|
|
119
|
+
* (`__lwcProperties<InstanceType<...>>`, a `Partial` that leaves out private ones). Base components coerce what they're
|
|
120
|
+
* given, so for them (`lenient`) a literal value only checks the attribute's name.
|
|
121
|
+
*/
|
|
122
|
+
#checkProperties(properties, type, lenient) {
|
|
123
|
+
// one literal per property: TypeScript reports only the first problem in each
|
|
124
|
+
for (const property of properties) {
|
|
125
|
+
const start = property.location.start;
|
|
126
|
+
const { text } = this.#source;
|
|
127
|
+
const named = text.slice(start, start + property.attributeName.length).toLowerCase() === property.attributeName.toLowerCase();
|
|
128
|
+
this.out.line(() => {
|
|
129
|
+
this.out.write("({ ");
|
|
130
|
+
const key = isIdentifier(property.name) ? property.name : JSON.stringify(property.name);
|
|
131
|
+
this.out.writeAt(key, named ? start : undefined, property.attributeName.length);
|
|
132
|
+
this.out.write(": ");
|
|
133
|
+
const { value } = property;
|
|
134
|
+
const origin = this.#source.attribute(property.location);
|
|
135
|
+
if (isExpression(value) && origin !== undefined) {
|
|
136
|
+
this.#expression(value, origin);
|
|
137
|
+
}
|
|
138
|
+
else if (value.type === "Literal") {
|
|
139
|
+
// LWC normalizes tabindex; a literal is otherwise the string (or, bare, `true`) it's set to
|
|
140
|
+
this.out.write(lenient || property.name === "tabIndex" ? "undefined" : JSON.stringify(value.value ?? null));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.out.write("undefined as any");
|
|
144
|
+
}
|
|
145
|
+
this.out.write(` }) satisfies ${type};`);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** The type of an `lwc:ref` on an element or component. */
|
|
150
|
+
#refType(node, tag) {
|
|
151
|
+
switch (tag.kind) {
|
|
152
|
+
case "child":
|
|
153
|
+
return `HTMLElement & InstanceType<${this.#components.component(tag.module, node)}>`;
|
|
154
|
+
case "package":
|
|
155
|
+
// its source is hidden, so its members beyond the cached stub's are unknown
|
|
156
|
+
return `HTMLElement & InstanceType<${this.#components.component(tag.module, node)}> & Record<string, any>`;
|
|
157
|
+
case "element":
|
|
158
|
+
return elementType(node);
|
|
159
|
+
case "base": {
|
|
160
|
+
const alias = this.#components.base(tag.tag);
|
|
161
|
+
return `HTMLElement & ${alias}${Object.keys(this.#baseComponents[tag.tag].methods).length > 0 ? ` & ${alias}Methods` : ""}`;
|
|
162
|
+
}
|
|
163
|
+
default:
|
|
164
|
+
return "HTMLElement & Record<string, any>"; // a component nothing describes
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Records an `lwc:ref`, typed as the element (or child component) it names. */
|
|
168
|
+
#ref(node, tag, name, location) {
|
|
169
|
+
if (isIdentifier(name)) {
|
|
170
|
+
const optional = this.#conditional > 0 || this.#context.alternateTemplates === true;
|
|
171
|
+
this.#refs.add(name, this.#refType(node, tag), optional, this.#source.name(name, location, "="));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
#inConditional(body) {
|
|
175
|
+
this.#conditional++;
|
|
176
|
+
body();
|
|
177
|
+
this.#conditional--;
|
|
178
|
+
}
|
|
179
|
+
/** Declares what the template's code used: child components' types, and its `lwc:ref`s for `this.refs`. */
|
|
180
|
+
finish(componentModule) {
|
|
181
|
+
this.#components.write(this.out);
|
|
182
|
+
this.#refs.write(this.out, componentModule);
|
|
183
|
+
}
|
|
184
|
+
#inScope(name) {
|
|
185
|
+
return this.#scope.some((names) => names.has(name));
|
|
186
|
+
}
|
|
187
|
+
#withScope(names, body) {
|
|
188
|
+
this.#scope.push(new Set(names));
|
|
189
|
+
body();
|
|
190
|
+
this.#scope.pop();
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Writes an expression as it is in the template, so anything that's valid in one is in the other and
|
|
194
|
+
* every piece maps back, with `$cmp.` before the component's members. `$cmp.` maps to the `{`, so a
|
|
195
|
+
* diagnostic about a whole expression (such as a handler that isn't a function) starts there rather
|
|
196
|
+
* than at the start of the file.
|
|
197
|
+
*/
|
|
198
|
+
#expression(expression, origin) {
|
|
199
|
+
const { brace, at } = origin;
|
|
200
|
+
const names = this.#componentNames(expression, origin);
|
|
201
|
+
if (names === undefined) {
|
|
202
|
+
this.out.write("(undefined as any)");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const copy = (start, end) => {
|
|
206
|
+
if (start < end) {
|
|
207
|
+
this.out.write(this.#source.text.slice(at(start), at(end)), at(start), at(end));
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const simple = isSimple(expression);
|
|
211
|
+
if (!simple) {
|
|
212
|
+
this.out.write("(", brace, brace + 1);
|
|
213
|
+
}
|
|
214
|
+
let copied = expression.start;
|
|
215
|
+
for (const { start, end, shorthand } of names) {
|
|
216
|
+
copy(copied, shorthand ? end : start);
|
|
217
|
+
if (shorthand) {
|
|
218
|
+
this.out.write(": ");
|
|
219
|
+
}
|
|
220
|
+
this.out.write("$cmp.", brace, brace + 1);
|
|
221
|
+
copied = start;
|
|
222
|
+
}
|
|
223
|
+
copy(copied, expression.end);
|
|
224
|
+
if (!simple) {
|
|
225
|
+
this.out.write(")", brace, brace + 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** An expression's members of the component, or undefined when LWC rejects it, which is reported on it. */
|
|
229
|
+
#componentNames(expression, origin) {
|
|
230
|
+
try {
|
|
231
|
+
const isLocal = (name) => this.#inScope(name);
|
|
232
|
+
return componentNames(expression, isLocal, hasComplexExpressions(this.#context.apiVersion), this.#compiler.bind);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const start = origin.at(expression.start);
|
|
236
|
+
this.diagnostics.push({ message: error.message, start, length: origin.at(expression.end) - start });
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Writes a value as a statement, when it's an expression. `wrap` can check it there. */
|
|
241
|
+
#statement(value, location, wrap = (write) => write()) {
|
|
242
|
+
const origin = this.#source.attribute(location);
|
|
243
|
+
if (!isExpression(value) || origin === undefined) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.out.line(() => {
|
|
247
|
+
wrap(() => this.#expression(value, origin));
|
|
248
|
+
this.out.write(";");
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** Writes a value as a statement that must satisfy a type, reported on the value, not on the type. */
|
|
252
|
+
#checked(value, location, type) {
|
|
253
|
+
this.#statement(value, location, (write) => {
|
|
254
|
+
this.out.write("[");
|
|
255
|
+
write();
|
|
256
|
+
this.out.write(`] satisfies [${type}]`);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** Declares a loop variable, mapped to where the template names it, and marks it used. */
|
|
260
|
+
#declare(name, initializer, position) {
|
|
261
|
+
this.out.line(() => {
|
|
262
|
+
this.out.write("const ");
|
|
263
|
+
this.out.writeAt(name, position, name.length);
|
|
264
|
+
this.out.write(` = ${initializer}; void ${name};`);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
nodes(nodes) {
|
|
268
|
+
for (const node of nodes) {
|
|
269
|
+
this.node(node);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
node(node) {
|
|
273
|
+
switch (node.type) {
|
|
274
|
+
case "Text":
|
|
275
|
+
return this.#text(node);
|
|
276
|
+
case "Element":
|
|
277
|
+
case "Component":
|
|
278
|
+
case "ExternalComponent":
|
|
279
|
+
case "Slot":
|
|
280
|
+
case "Lwc":
|
|
281
|
+
return this.#element(node);
|
|
282
|
+
case "IfBlock":
|
|
283
|
+
return this.#ifBlock(node);
|
|
284
|
+
case "If":
|
|
285
|
+
return this.#if(node);
|
|
286
|
+
case "ForEach":
|
|
287
|
+
return this.#forEach(node);
|
|
288
|
+
case "ForOf":
|
|
289
|
+
return this.#forOf(node);
|
|
290
|
+
case "ScopedSlotFragment":
|
|
291
|
+
return this.#scopedSlot(node);
|
|
292
|
+
case "Comment":
|
|
293
|
+
return;
|
|
294
|
+
default:
|
|
295
|
+
// anything newer: its children may still have bindings
|
|
296
|
+
return this.nodes(node.children);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
#text(node) {
|
|
300
|
+
const expression = isExpression(node.value) ? node.value : undefined;
|
|
301
|
+
const origin = this.#source.textExpression(node.raw, expression, node.location);
|
|
302
|
+
if (expression === undefined || origin === undefined) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.out.line(() => {
|
|
306
|
+
this.#expression(expression, origin);
|
|
307
|
+
this.out.write(";");
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
#element(node) {
|
|
311
|
+
const tag = this.#tag(node);
|
|
312
|
+
for (const attribute of node.attributes) {
|
|
313
|
+
this.#statement(attribute.value, attribute.location);
|
|
314
|
+
}
|
|
315
|
+
switch (tag.kind) {
|
|
316
|
+
case "child":
|
|
317
|
+
// every child is referenced, so one that doesn't exist is reported on its tag
|
|
318
|
+
this.#checkProperties(node.properties, `__lwcProperties<InstanceType<${this.#components.component(tag.module, node)}>>`, false);
|
|
319
|
+
break;
|
|
320
|
+
case "base":
|
|
321
|
+
if (node.properties.length > 0) {
|
|
322
|
+
this.#checkProperties(node.properties, this.#components.base(tag.tag), true);
|
|
323
|
+
}
|
|
324
|
+
break;
|
|
325
|
+
case "package":
|
|
326
|
+
// An installed package's component is referenced too, so one the project doesn't have (not cached, or
|
|
327
|
+
// misspelt) is reported on its tag. Its source is hidden, so its properties, events and slots aren't
|
|
328
|
+
// known: they're left open, and only the values' expressions are checked.
|
|
329
|
+
this.#components.component(tag.module, node);
|
|
330
|
+
// falls through
|
|
331
|
+
default:
|
|
332
|
+
for (const property of node.properties) {
|
|
333
|
+
this.#statement(property.value, property.location);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (tag.kind === "child") {
|
|
337
|
+
this.#checkSlots(node, tag.module);
|
|
338
|
+
}
|
|
339
|
+
for (const directive of node.directives) {
|
|
340
|
+
switch (directive.name) {
|
|
341
|
+
case "Ref":
|
|
342
|
+
this.#ref(node, tag, directive.value.value, directive.location);
|
|
343
|
+
break;
|
|
344
|
+
case "Is":
|
|
345
|
+
// `lwc:is` renders nothing until it has a constructor
|
|
346
|
+
this.#checked(directive.value, directive.location, "(new (...args: any[]) => unknown) | null | undefined");
|
|
347
|
+
break;
|
|
348
|
+
case "On":
|
|
349
|
+
// `lwc:on` takes listeners by event name; which events isn't known, so nor is what they're given
|
|
350
|
+
this.#checked(directive.value, directive.location, "Record<string, (event: any) => unknown> | null | undefined");
|
|
351
|
+
break;
|
|
352
|
+
default:
|
|
353
|
+
this.#statement(directive.value, directive.location);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// a handler (`on<event>={...}`) must be a function; which event it's given isn't checked. It's checked
|
|
357
|
+
// with `satisfies` rather than passed to a helper, since an argument made of several mapped spans gets a
|
|
358
|
+
// parameter name inlay hint for each of them. In a tuple, so TypeScript reports a mismatch on the
|
|
359
|
+
// handler, which maps to the template, rather than on the `satisfies` type, which doesn't.
|
|
360
|
+
for (const listener of node.listeners) {
|
|
361
|
+
this.#checked(listener.handler, listener.location, HANDLER_TYPE);
|
|
362
|
+
}
|
|
363
|
+
this.nodes(node.children);
|
|
364
|
+
}
|
|
365
|
+
/** Writes a block's condition, which is all `true` is when there's nothing to check. */
|
|
366
|
+
#condition(condition, location) {
|
|
367
|
+
const origin = this.#source.attribute(location);
|
|
368
|
+
if (condition === undefined || origin === undefined) {
|
|
369
|
+
this.out.write("true");
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
this.#expression(condition, origin);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** `lwc:if`, with its `lwc:elseif` and `lwc:else` chain. */
|
|
376
|
+
#ifBlock(node, keyword = "if") {
|
|
377
|
+
this.out.block(() => {
|
|
378
|
+
this.out.write(`${keyword} (`);
|
|
379
|
+
this.#condition(node.condition, node.directiveLocation);
|
|
380
|
+
this.out.write(")");
|
|
381
|
+
}, () => this.#inConditional(() => this.nodes(node.children)));
|
|
382
|
+
const next = node.else;
|
|
383
|
+
if (next?.type === "ElseifBlock") {
|
|
384
|
+
this.#ifBlock(next, "else if");
|
|
385
|
+
}
|
|
386
|
+
else if (next?.type === "ElseBlock") {
|
|
387
|
+
this.out.block(() => this.out.write("else"), () => this.#inConditional(() => this.nodes(next.children)));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/** The legacy `if:true` and `if:false`. */
|
|
391
|
+
#if(node) {
|
|
392
|
+
this.out.block(() => {
|
|
393
|
+
this.out.write(node.modifier === "false" ? "if (!" : "if (");
|
|
394
|
+
this.#condition(node.condition, node.directiveLocation);
|
|
395
|
+
this.out.write(")");
|
|
396
|
+
}, () => this.#inConditional(() => this.nodes(node.children)));
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* A loop over the list in `node`'s directive. `for...of` checks the list is iterable (`?? []` allows
|
|
400
|
+
* `null` and `undefined`, which render nothing), so the loop needs no helper call, whose argument would
|
|
401
|
+
* get a parameter name inlay hint for each of its mapped spans.
|
|
402
|
+
*/
|
|
403
|
+
#loop(node, variable, body, iterator = false) {
|
|
404
|
+
const origin = this.#source.attribute(node.directiveLocation);
|
|
405
|
+
if (origin === undefined) {
|
|
406
|
+
return this.#withScope([variable.name], () => this.nodes(node.children));
|
|
407
|
+
}
|
|
408
|
+
const expression = node.expression;
|
|
409
|
+
// TypeScript rejects `?? []` after an expression that it can see is never nullish (`"{rows ?? []}"`), so
|
|
410
|
+
// a complex list goes through a variable, which hides that
|
|
411
|
+
const list = isSimple(expression) ? undefined : `__lwcList${this.#loopLists++}`;
|
|
412
|
+
if (list !== undefined) {
|
|
413
|
+
this.out.line(() => {
|
|
414
|
+
this.out.write(`const ${list} = `);
|
|
415
|
+
this.#expression(expression, origin);
|
|
416
|
+
this.out.write(";");
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const { name, position } = variable;
|
|
420
|
+
// An iterator's variable wraps each item, and a variable that shadows an outer loop's, which the list
|
|
421
|
+
// may use (`for:each={item.children} for:item="item"`), can't be the loop's own: in
|
|
422
|
+
// `for (const item of item.children)` the list would read it before its declaration. Either is
|
|
423
|
+
// declared in the loop's body instead, from the loop's item.
|
|
424
|
+
const direct = !iterator && !this.#inScope(name);
|
|
425
|
+
const item = direct ? name : `__lwcItem${this.#loopItems++}`;
|
|
426
|
+
this.out.block(() => {
|
|
427
|
+
this.out.write("for (const ");
|
|
428
|
+
if (direct) {
|
|
429
|
+
this.out.writeAt(name, position, name.length);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
this.out.write(item);
|
|
433
|
+
}
|
|
434
|
+
this.out.write(" of ");
|
|
435
|
+
if (list === undefined) {
|
|
436
|
+
this.#expression(expression, origin);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
this.out.write(list, origin.brace, origin.brace + 1);
|
|
440
|
+
}
|
|
441
|
+
this.out.write(" ?? [])");
|
|
442
|
+
}, () => {
|
|
443
|
+
if (iterator) {
|
|
444
|
+
this.#declare(name, `{ value: ${item}, index: 0 as number, first: false as boolean, last: false as boolean }`, position);
|
|
445
|
+
}
|
|
446
|
+
else if (!direct) {
|
|
447
|
+
this.#declare(name, item, position);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
this.out.line(() => this.out.write(`void ${name};`));
|
|
451
|
+
}
|
|
452
|
+
body();
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
/** `for:each={list} for:item="item" for:index="index"`. */
|
|
456
|
+
#forEach(node) {
|
|
457
|
+
const { item, index } = node;
|
|
458
|
+
const names = [item.name, ...(index ? [index.name] : [])];
|
|
459
|
+
this.#loop(node, { name: item.name, position: this.#source.name(item.name, item.location, "=") }, () => {
|
|
460
|
+
if (index) {
|
|
461
|
+
this.#declare(index.name, "0 as number", this.#source.name(index.name, index.location, "="));
|
|
462
|
+
}
|
|
463
|
+
this.#withScope(names, () => this.nodes(node.children));
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
/** `iterator:it={list}`, whose variable has `value`, `index`, `first` and `last`. */
|
|
467
|
+
#forOf(node) {
|
|
468
|
+
const { iterator } = node;
|
|
469
|
+
const position = this.#source.name(iterator.name, iterator.location, ":");
|
|
470
|
+
this.#loop(node, { name: iterator.name, position }, () => this.#withScope([iterator.name], () => this.nodes(node.children)), true);
|
|
471
|
+
}
|
|
472
|
+
/** `lwc:slot-data="data"` in a scoped slot: the parent can't know the data's type. */
|
|
473
|
+
#scopedSlot(node) {
|
|
474
|
+
const { name } = node.slotData.value;
|
|
475
|
+
this.out.block(() => { }, () => {
|
|
476
|
+
this.#declare(name, "undefined as any", this.#source.name(name, node.slotData.location, "="));
|
|
477
|
+
this.#withScope([name], () => this.#inConditional(() => this.nodes(node.children)));
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Converts a component's template to TypeScript that checks its bindings against the component, imported
|
|
483
|
+
* as `componentModule` (such as `./orders.js`). `context` tells it about its own bundle, and names
|
|
484
|
+
* the component's module, for the slots it declares.
|
|
485
|
+
*/
|
|
486
|
+
export async function convertTemplate(content, componentModule, context = {}) {
|
|
487
|
+
const compiler = await loadTemplateCompiler();
|
|
488
|
+
const { root, errors } = compiler.parse(content, context.apiVersion);
|
|
489
|
+
const converter = new TemplateConverter(content, context, compiler);
|
|
490
|
+
const out = converter.out;
|
|
491
|
+
out.write(`import type __Component from ${JSON.stringify(componentModule)};
|
|
492
|
+
declare const $cmp: InstanceType<typeof __Component>;
|
|
493
|
+
`);
|
|
494
|
+
if (root !== undefined) {
|
|
495
|
+
converter.nodes(root.children);
|
|
496
|
+
converter.finish(componentModule);
|
|
497
|
+
}
|
|
498
|
+
if (context.module !== undefined) {
|
|
499
|
+
// a template with errors may be missing slots, so it allows any
|
|
500
|
+
out.write(slotDeclarations(context.module, root === undefined || errors.length > 0 ? undefined : templateSlots(root)));
|
|
501
|
+
}
|
|
502
|
+
// Written even for a template that doesn't parse, so importing one being edited stays valid. The name
|
|
503
|
+
// maps to the root tag, for go-to-definition from an import. `lwc` typings without `Template` (the
|
|
504
|
+
// tests', or ones written by other tools) make it `any` rather than an error on every template.
|
|
505
|
+
const name = templateExportName(context.fileName ?? "default.html");
|
|
506
|
+
const tag = root === undefined ? undefined : root.location.start + 1;
|
|
507
|
+
out.write("// @ts-ignore\ndeclare const ");
|
|
508
|
+
out.writeAt(name, tag, "template".length, MapFeature.Definition);
|
|
509
|
+
out.write(`: import("lwc").Template;\nexport default ${name};\n`);
|
|
510
|
+
// the helpers the code uses, after it: TypeScript hoists declarations
|
|
511
|
+
out.write(helperDeclarations(out.text));
|
|
512
|
+
const parseErrors = errors.map((error) => {
|
|
513
|
+
const start = Math.min(Math.max(error.location?.start ?? 0, 0), content.length);
|
|
514
|
+
const end = Math.min(start + Math.max(error.location?.length ?? 0, 0), content.length);
|
|
515
|
+
return { message: error.message, start, length: end - start };
|
|
516
|
+
});
|
|
517
|
+
return { text: out.text, mapData: out.mapData, diagnostics: [...parseErrors, ...converter.diagnostics] };
|
|
518
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { TextBuilder } from "../../text-builder.ts";
|
|
2
|
+
/** Writes virtual text, recording which parts map to which template text, in indented lines and blocks. */
|
|
3
|
+
export declare class Writer extends TextBuilder {
|
|
4
|
+
indent: string;
|
|
5
|
+
/** Writes text mapped to `length` characters at `position`, or unmapped text without somewhere to map it to. */
|
|
6
|
+
writeAt(text: string, position: number | undefined, length: number, features?: number): void;
|
|
7
|
+
line(build: () => void): void;
|
|
8
|
+
block(open: () => void, body: () => void): void;
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { TextBuilder } from "../../text-builder.js";
|
|
2
|
+
/** Writes virtual text, recording which parts map to which template text, in indented lines and blocks. */
|
|
3
|
+
export class Writer extends TextBuilder {
|
|
4
|
+
indent = "";
|
|
5
|
+
/** Writes text mapped to `length` characters at `position`, or unmapped text without somewhere to map it to. */
|
|
6
|
+
writeAt(text, position, length, features) {
|
|
7
|
+
if (position === undefined || length === 0) {
|
|
8
|
+
this.write(text);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
this.write(text, position, position + length, features);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
line(build) {
|
|
15
|
+
this.write(this.indent);
|
|
16
|
+
build();
|
|
17
|
+
this.write("\n");
|
|
18
|
+
}
|
|
19
|
+
block(open, body) {
|
|
20
|
+
this.line(() => {
|
|
21
|
+
const start = this.text.length;
|
|
22
|
+
open();
|
|
23
|
+
// a block that only scopes variables has nothing before its brace
|
|
24
|
+
this.write(this.text.length === start ? "{" : " {");
|
|
25
|
+
});
|
|
26
|
+
const outer = this.indent;
|
|
27
|
+
this.indent += " ";
|
|
28
|
+
body();
|
|
29
|
+
this.indent = outer;
|
|
30
|
+
this.line(() => this.write("}"));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SourceRange, TextBuilder } from "./text-builder.ts";
|
|
2
|
+
export interface DefaultExportModule {
|
|
3
|
+
/** The module's name, as imports name it (`@salesforce/label/c.Greeting`). */
|
|
4
|
+
readonly name: string;
|
|
5
|
+
/** The default export's name, which gets a `_` prefix when it's a reserved word (see `variableName`). */
|
|
6
|
+
readonly variable: string;
|
|
7
|
+
/** The default export's type. */
|
|
8
|
+
readonly type: string;
|
|
9
|
+
/** The default export's documentation, as JSDoc lines (see `jsDoc`). */
|
|
10
|
+
readonly doc?: readonly (string | undefined)[];
|
|
11
|
+
/** Where the module's name and its variable map to in the source; unmapped without one. */
|
|
12
|
+
readonly range?: SourceRange | null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Declares a module whose default export is a `const` of `type`:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* declare module "@salesforce/label/c.Greeting" {
|
|
19
|
+
* /** Hello *\/
|
|
20
|
+
* const lblGreeting: string;
|
|
21
|
+
* export default lblGreeting;
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeDefaultExportModule(builder: TextBuilder, { name, variable, type, doc, range }: DefaultExportModule): void;
|
|
26
|
+
/** The value of a schema import: `{ objectApiName: "Account" }`, with a field's `fieldApiName` first. */
|
|
27
|
+
export declare function schemaValueType(objectApiName: string, fieldApiName?: string): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// The `declare module` block most metadata becomes: a module whose default export is a `const`, such as
|
|
2
|
+
// `@salesforce/label/c.Greeting` or `@salesforce/schema/Account.Name`.
|
|
3
|
+
import { variableName } from "./identifiers.js";
|
|
4
|
+
import { jsDoc } from "./jsdoc.js";
|
|
5
|
+
/**
|
|
6
|
+
* Declares a module whose default export is a `const` of `type`:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* declare module "@salesforce/label/c.Greeting" {
|
|
10
|
+
* /** Hello *\/
|
|
11
|
+
* const lblGreeting: string;
|
|
12
|
+
* export default lblGreeting;
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function writeDefaultExportModule(builder, { name, variable, type, doc = [], range }) {
|
|
17
|
+
const constName = variableName(variable);
|
|
18
|
+
builder.write("declare module ");
|
|
19
|
+
builder.writeMapped(JSON.stringify(name), range);
|
|
20
|
+
builder.write(` {\n${jsDoc(" ", [...doc])} const `);
|
|
21
|
+
builder.writeMapped(constName, range);
|
|
22
|
+
builder.write(`: ${type};\n export default ${constName};\n}\n`);
|
|
23
|
+
}
|
|
24
|
+
/** The value of a schema import: `{ objectApiName: "Account" }`, with a field's `fieldApiName` first. */
|
|
25
|
+
export function schemaValueType(objectApiName, fieldApiName) {
|
|
26
|
+
const field = fieldApiName === undefined ? "" : ` fieldApiName: ${JSON.stringify(fieldApiName)};\n`;
|
|
27
|
+
return `{\n${field} objectApiName: ${JSON.stringify(objectApiName)};\n }`;
|
|
28
|
+
}
|
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Component } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* A custom name with the project's namespace (`acme__Invoice__c` for `Invoice__c`), as records and the
|
|
4
|
+
* values of schema imports have it inside the namespace. Standard names (`Account`, `Name`), names that
|
|
5
|
+
* already have a namespace (`other__Invoice__c`), and every name without a project namespace are left as
|
|
6
|
+
* they are.
|
|
7
|
+
*/
|
|
8
|
+
export declare function recordName(name: string, namespace: string | undefined): string;
|
|
9
|
+
/**
|
|
10
|
+
* An API name's namespace and its name without it. Salesforce names can't contain `__` themselves, so a
|
|
11
|
+
* name ending in a custom suffix has a namespace when it has three parts (`dlrs__Log__c`), and any other
|
|
12
|
+
* name when it has two (`dlrs__RollupController`, a label's `dlrs__fflib_error`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function splitNamespace(apiName: string): {
|
|
15
|
+
namespace: string | undefined;
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* How a component in the project names an item in an import: inside a namespace, LWC rejects the
|
|
20
|
+
* project's own namespace and requires any other. So `namespace` is the item's namespace when it's
|
|
21
|
+
* another's (an installed package's, such as `dlrs`), and undefined when it's the project's or it has
|
|
22
|
+
* none; `name` is the item's name without its namespace either way.
|
|
23
|
+
*/
|
|
24
|
+
export declare function importNameParts(apiName: string, projectNamespace: string | undefined): {
|
|
25
|
+
namespace: string | undefined;
|
|
26
|
+
name: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* A name without the project's namespace (`Invoice__c` for `acme__Invoice__c`, `logo` for `acme__logo`),
|
|
30
|
+
* as LWC inside the namespace must import it. Other namespaces' names are left as they are.
|
|
31
|
+
*/
|
|
32
|
+
export declare function importName(name: string, namespace: string | undefined): string;
|
|
33
|
+
/**
|
|
34
|
+
* The name of a metadata file: its file name up to the first `.`, such as `Account` for
|
|
35
|
+
* `objects/Account/Account.object-meta.xml`. Paths can use either separator.
|
|
36
|
+
*/
|
|
37
|
+
export declare function metadataName(path: string): string;
|
|
38
|
+
/** The import form of a metadata file's name (see `metadataName`), such as `logo` for `acme__logo.resource-meta.xml` in project `acme`. */
|
|
39
|
+
export declare function fileImportName(component: Component, projectNamespace: string | undefined): string;
|