@lwc/ssr-compiler 9.0.2 → 9.0.3
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/dist/index.cjs +2720 -0
- package/dist/index.js +1 -1
- package/package.json +9 -8
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 Salesforce, Inc.
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
7
|
+
|
|
8
|
+
var shared = require('@lwc/shared');
|
|
9
|
+
var astring = require('astring');
|
|
10
|
+
var estreeToolkit = require('estree-toolkit');
|
|
11
|
+
var meriyah = require('meriyah');
|
|
12
|
+
var errors = require('@lwc/errors');
|
|
13
|
+
var immer = require('immer');
|
|
14
|
+
var acorn = require('acorn');
|
|
15
|
+
var node_path = require('node:path');
|
|
16
|
+
var templateCompiler = require('@lwc/template-compiler');
|
|
17
|
+
var builders = require('estree-toolkit/dist/builders');
|
|
18
|
+
var types = require('@babel/types');
|
|
19
|
+
var node_util = require('node:util');
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
23
|
+
* All rights reserved.
|
|
24
|
+
* SPDX-License-Identifier: MIT
|
|
25
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
26
|
+
*/
|
|
27
|
+
/** Placeholder value to use to opt out of validation. */
|
|
28
|
+
const NO_VALIDATION = false;
|
|
29
|
+
/**
|
|
30
|
+
* `esTemplate` generates JS code with "holes" to be filled later. In order to have a valid AST,
|
|
31
|
+
* it uses identifiers with this prefix at the location of the holes.
|
|
32
|
+
*/
|
|
33
|
+
const PLACEHOLDER_PREFIX = '__lwc_ESTEMPLATE_PLACEHOLDER__';
|
|
34
|
+
const getReplacementNode = (state, placeholderId) => {
|
|
35
|
+
const key = Number(placeholderId.slice(PLACEHOLDER_PREFIX.length));
|
|
36
|
+
const nodeCount = state.replacementNodes.length;
|
|
37
|
+
if (key >= nodeCount) {
|
|
38
|
+
throw new Error(`Cannot use index ${key} when only ${nodeCount} values have been provided.`);
|
|
39
|
+
}
|
|
40
|
+
const validateReplacement = state.placeholderToValidator.get(key);
|
|
41
|
+
const replacementNode = state.replacementNodes[key];
|
|
42
|
+
if (validateReplacement &&
|
|
43
|
+
!(Array.isArray(replacementNode)
|
|
44
|
+
? replacementNode.every(validateReplacement)
|
|
45
|
+
: validateReplacement(replacementNode))) {
|
|
46
|
+
const expectedType = validateReplacement.__debugName ||
|
|
47
|
+
validateReplacement.name ||
|
|
48
|
+
'(could not determine)';
|
|
49
|
+
const actualType = Array.isArray(replacementNode)
|
|
50
|
+
? `[${replacementNode.map((n) => n && n.type).join(', ')}]`
|
|
51
|
+
: replacementNode?.type;
|
|
52
|
+
throw new Error(`Validation failed for templated node. Expected type ${expectedType}, but received ${actualType}.`);
|
|
53
|
+
}
|
|
54
|
+
return replacementNode;
|
|
55
|
+
};
|
|
56
|
+
const visitors$2 = {
|
|
57
|
+
Identifier(path, state) {
|
|
58
|
+
if (path.node?.name.startsWith(PLACEHOLDER_PREFIX)) {
|
|
59
|
+
const replacementNode = getReplacementNode(state, path.node.name);
|
|
60
|
+
if (replacementNode === null) {
|
|
61
|
+
path.remove();
|
|
62
|
+
}
|
|
63
|
+
else if (Array.isArray(replacementNode)) {
|
|
64
|
+
if (replacementNode.length === 0) {
|
|
65
|
+
path.remove();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
if (path.parentPath?.node?.type === 'ExpressionStatement') {
|
|
69
|
+
path.parentPath.replaceWithMultiple(replacementNode);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
path.replaceWithMultiple(replacementNode);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
path.replaceWith(replacementNode);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
Literal(path, state) {
|
|
82
|
+
if (typeof path.node?.value === 'string' &&
|
|
83
|
+
path.node.value.startsWith(PLACEHOLDER_PREFIX)) {
|
|
84
|
+
// A literal can only be replaced with a single node
|
|
85
|
+
const replacementNode = getReplacementNode(state, path.node.value);
|
|
86
|
+
path.replaceWith(replacementNode);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
function esTemplateImpl(javascriptSegments, validators, wrap, unwrap) {
|
|
91
|
+
let placeholderCount = 0;
|
|
92
|
+
let parsableCode = javascriptSegments[0];
|
|
93
|
+
const placeholderToValidator = new Map();
|
|
94
|
+
for (let i = 1; i < javascriptSegments.length; i += 1) {
|
|
95
|
+
const segment = javascriptSegments[i];
|
|
96
|
+
const validator = validators[i - 1]; // always one less value than strings in template literals
|
|
97
|
+
if (typeof validator === 'function' || validator === NO_VALIDATION) {
|
|
98
|
+
// Template slot will be filled by a *new* argument passed to the generated function
|
|
99
|
+
if (validator !== NO_VALIDATION) {
|
|
100
|
+
placeholderToValidator.set(placeholderCount, validator);
|
|
101
|
+
}
|
|
102
|
+
parsableCode += `${PLACEHOLDER_PREFIX}${placeholderCount}`;
|
|
103
|
+
placeholderCount += 1;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Template slot uses a *previously defined* argument passed to the generated function
|
|
107
|
+
if (validator >= placeholderCount) {
|
|
108
|
+
throw new Error(`Reference to argument ${validator} at index ${i} cannot be used. Only ${placeholderCount - 1} arguments have been defined.`);
|
|
109
|
+
}
|
|
110
|
+
parsableCode += `${PLACEHOLDER_PREFIX}${validator}`;
|
|
111
|
+
}
|
|
112
|
+
parsableCode += segment;
|
|
113
|
+
}
|
|
114
|
+
if (wrap) {
|
|
115
|
+
parsableCode = wrap(parsableCode);
|
|
116
|
+
}
|
|
117
|
+
const originalAstProgram = acorn.parse(parsableCode, {
|
|
118
|
+
ecmaVersion: 2022,
|
|
119
|
+
allowAwaitOutsideFunction: true,
|
|
120
|
+
allowReturnOutsideFunction: true,
|
|
121
|
+
allowSuperOutsideMethod: true,
|
|
122
|
+
allowImportExportEverywhere: true,
|
|
123
|
+
locations: false,
|
|
124
|
+
});
|
|
125
|
+
let originalAst;
|
|
126
|
+
const finalCharacter = javascriptSegments.at(-1)?.trimEnd()?.at(-1);
|
|
127
|
+
if (originalAstProgram.body.length === 1) {
|
|
128
|
+
originalAst =
|
|
129
|
+
finalCharacter === ';' && originalAstProgram.body[0].type === 'ExpressionStatement'
|
|
130
|
+
? (originalAst = originalAstProgram.body[0].expression)
|
|
131
|
+
: (originalAst = originalAstProgram.body[0]);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
originalAst = originalAstProgram.body;
|
|
135
|
+
}
|
|
136
|
+
// Turns Acorn AST objects into POJOs, for use with Immer.
|
|
137
|
+
originalAst = JSON.parse(JSON.stringify(originalAst));
|
|
138
|
+
return function templatedAst(...replacementNodes) {
|
|
139
|
+
const result = immer.produce(originalAst, (astDraft) => estreeToolkit.traverse(astDraft, visitors$2, {
|
|
140
|
+
placeholderToValidator,
|
|
141
|
+
replacementNodes,
|
|
142
|
+
}));
|
|
143
|
+
return (unwrap ? unwrap(result) : result);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Template literal tag that generates a builder function. Like estree's `builders`, but for more
|
|
148
|
+
* complex structures. The template values should be estree `is` validators or a back reference to
|
|
149
|
+
* a previous slot (to re-use the referenced value).
|
|
150
|
+
*
|
|
151
|
+
* To have the generated function return a particular node type, the generic comes _after_ the
|
|
152
|
+
* template literal. Kinda weird, but it's necessary to infer the types of the template values.
|
|
153
|
+
* (If it were at the start, we'd need to explicitly provide _all_ type params. Tedious!)
|
|
154
|
+
* @example
|
|
155
|
+
* const bSum = esTemplate`(${is.identifier}, ${is.identifier}) => ${0} + ${1}`<EsArrowFunctionExpression>
|
|
156
|
+
* const sumFuncNode = bSum(b.identifier('a'), b.identifier('b'))
|
|
157
|
+
* // `sumFuncNode` is an AST node representing `(a, b) => a + b`
|
|
158
|
+
*/
|
|
159
|
+
function esTemplate(javascriptSegments, ...Validators) {
|
|
160
|
+
return esTemplateImpl(javascriptSegments, Validators);
|
|
161
|
+
}
|
|
162
|
+
/** Similar to {@linkcode esTemplate}, but supports `yield` expressions. */
|
|
163
|
+
function esTemplateWithYield(javascriptSegments, ...validators) {
|
|
164
|
+
const wrap = (code) => `function* placeholder() {${code}}`;
|
|
165
|
+
const unwrap = (node) => node.body.body.length === 1 ? node.body.body[0] : node.body.body;
|
|
166
|
+
return esTemplateImpl(javascriptSegments, validators, wrap, unwrap);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
171
|
+
* All rights reserved.
|
|
172
|
+
* SPDX-License-Identifier: MIT
|
|
173
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
174
|
+
*/
|
|
175
|
+
/** Function names that may be transmogrified. All should start with `__lwc`. */
|
|
176
|
+
// Rollup may rename variables to prevent shadowing. When it does, it uses the format `foo$0`, `foo$1`, etc.
|
|
177
|
+
const TRANSMOGRIFY_TARGET = /^__lwc(Generate|Tmpl).*$/;
|
|
178
|
+
const isNonArrowFunction = (node) => {
|
|
179
|
+
return estreeToolkit.is.functionDeclaration(node) || estreeToolkit.is.functionExpression(node);
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Determines whether a node is a function we want to transmogrify or within one, at any level.
|
|
183
|
+
*/
|
|
184
|
+
const isWithinTargetFunc = (nodePath) => {
|
|
185
|
+
let path = isNonArrowFunction(nodePath)
|
|
186
|
+
? nodePath
|
|
187
|
+
: nodePath.findParent(isNonArrowFunction);
|
|
188
|
+
while (path?.node) {
|
|
189
|
+
const { id } = path.node;
|
|
190
|
+
if (id && TRANSMOGRIFY_TARGET.test(id.name)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
path = path.findParent(isNonArrowFunction);
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Determines whether the nearest function encapsulating this node is a function we transmogrify.
|
|
199
|
+
*/
|
|
200
|
+
const isImmediateWithinTargetFunc = (nodePath) => {
|
|
201
|
+
const parentFunc = nodePath.findParent(estreeToolkit.is.function);
|
|
202
|
+
return Boolean(parentFunc &&
|
|
203
|
+
isNonArrowFunction(parentFunc) &&
|
|
204
|
+
parentFunc.node?.id &&
|
|
205
|
+
TRANSMOGRIFY_TARGET.test(parentFunc.node.id.name));
|
|
206
|
+
};
|
|
207
|
+
const bDeclareYieldVar = (esTemplate `let __lwcYield = '';`);
|
|
208
|
+
const bAppendToYieldVar = (esTemplate `__lwcYield += ${estreeToolkit.is.expression};`);
|
|
209
|
+
const bReturnYieldVar = (esTemplate `return __lwcYield;`);
|
|
210
|
+
const visitors$1 = {
|
|
211
|
+
// @ts-expect-error types for `traverse` do not support sharing a visitor between node types:
|
|
212
|
+
// https://github.com/sarsamurmu/estree-toolkit/issues/20
|
|
213
|
+
'FunctionDeclaration|FunctionExpression'(path, state) {
|
|
214
|
+
const { node } = path;
|
|
215
|
+
if (!node?.async || !node?.generator) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Component authors might conceivably use async generator functions in their own code. Therefore,
|
|
219
|
+
// when traversing & transforming written+generated code, we need to disambiguate generated async
|
|
220
|
+
// generator functions from those that were written by the component author.
|
|
221
|
+
if (!isWithinTargetFunc(path)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
node.generator = false;
|
|
225
|
+
node.async = state.mode === 'async';
|
|
226
|
+
node.body.body = [bDeclareYieldVar(), ...node.body.body, bReturnYieldVar()];
|
|
227
|
+
},
|
|
228
|
+
YieldExpression(path, state) {
|
|
229
|
+
const { node } = path;
|
|
230
|
+
if (!node) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Component authors might conceivably use generator functions within their own code. Therefore,
|
|
234
|
+
// when traversing & transforming written+generated code, we need to disambiguate generated yield
|
|
235
|
+
// expressions from those that were written by the component author.
|
|
236
|
+
if (!isWithinTargetFunc(path)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const arg = node.argument;
|
|
240
|
+
if (!arg) {
|
|
241
|
+
const type = node.delegate ? 'yield*' : 'yield';
|
|
242
|
+
throw new Error(`Cannot transmogrify ${type} statement without an argument.`);
|
|
243
|
+
}
|
|
244
|
+
path.replaceWith(bAppendToYieldVar(state.mode === 'sync' ? arg : estreeToolkit.builders.awaitExpression(arg)));
|
|
245
|
+
},
|
|
246
|
+
ImportSpecifier(path, _state) {
|
|
247
|
+
// @lwc/ssr-runtime has a couple of helper functions that need to conform to either the generator or
|
|
248
|
+
// no-generator compilation mode/paradigm. Since these are simple helper functions, we can maintain
|
|
249
|
+
// two implementations of each helper method:
|
|
250
|
+
//
|
|
251
|
+
// - renderAttrs vs renderAttrsNoYield
|
|
252
|
+
// - fallbackTmpl vs fallbackTmplNoYield
|
|
253
|
+
//
|
|
254
|
+
// If this becomes too burdensome to maintain, we can officially deprecate the generator-based approach
|
|
255
|
+
// and switch the @lwc/ssr-runtime implementation wholesale over to the no-generator paradigm.
|
|
256
|
+
const { node } = path;
|
|
257
|
+
if (!node || node.imported.type !== 'Identifier') {
|
|
258
|
+
throw new Error('Implementation error: unexpected missing identifier in import specifier');
|
|
259
|
+
}
|
|
260
|
+
if (path.parent?.type !== 'ImportDeclaration' ||
|
|
261
|
+
path.parent.source.value !== '@lwc/ssr-runtime') {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (node.imported.name === 'fallbackTmpl') {
|
|
265
|
+
node.imported.name = 'fallbackTmplNoYield';
|
|
266
|
+
}
|
|
267
|
+
else if (node.imported.name === 'renderAttrs') {
|
|
268
|
+
node.imported.name = 'renderAttrsNoYield';
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
ReturnStatement(path) {
|
|
272
|
+
if (!isImmediateWithinTargetFunc(path)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// The transmogrify result returns __lwcYield, so we skip it
|
|
276
|
+
const arg = path.node?.argument;
|
|
277
|
+
if (estreeToolkit.is.identifier(arg) && arg.name === '__lwcYield') {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
throw new Error('Cannot transmogrify function with return statement.');
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Transforms async-generator code into either the async or synchronous alternatives that are
|
|
285
|
+
* ~semantically equivalent. For example, this template:
|
|
286
|
+
*
|
|
287
|
+
* <template>
|
|
288
|
+
* <div>foobar</div>
|
|
289
|
+
* <x-child></x-child>
|
|
290
|
+
* </template>
|
|
291
|
+
*
|
|
292
|
+
* Is compiled into the following JavaScript, intended for execution during SSR & stripped down
|
|
293
|
+
* for the purposes of this example:
|
|
294
|
+
*
|
|
295
|
+
* async function* __lwcTmpl(props, attrs, slottedContent, Cmp, instance) {
|
|
296
|
+
* yield '<div>foobar</div>';
|
|
297
|
+
* const childProps = {};
|
|
298
|
+
* const childAttrs = {};
|
|
299
|
+
* yield* generateChildMarkup("x-child", childProps, childAttrs, childSlottedContentGenerator);
|
|
300
|
+
* }
|
|
301
|
+
*
|
|
302
|
+
* When transmogrified in async-mode, the above generated template function becomes the following:
|
|
303
|
+
*
|
|
304
|
+
* async function __lwcTmpl($$emit, props, attrs, slottedContent, Cmp, instance) {
|
|
305
|
+
* $$emit('<div>foobar</div>');
|
|
306
|
+
* const childProps = {};
|
|
307
|
+
* const childAttrs = {};
|
|
308
|
+
* await generateChildMarkup($$emit, "x-child", childProps, childAttrs, childSlottedContentGenerator);
|
|
309
|
+
* }
|
|
310
|
+
*
|
|
311
|
+
* When transmogrified in sync-mode, the template function becomes the following:
|
|
312
|
+
*
|
|
313
|
+
* function __lwcTmpl($$emit, props, attrs, slottedContent, Cmp, instance) {
|
|
314
|
+
* $$emit('<div>foobar</div>');
|
|
315
|
+
* const childProps = {};
|
|
316
|
+
* const childAttrs = {};
|
|
317
|
+
* generateChildMarkup($$emit, "x-child", childProps, childAttrs, childSlottedContentGenerator);
|
|
318
|
+
* }
|
|
319
|
+
*
|
|
320
|
+
* There are tradeoffs for each of these modes. Notably, the async-yield variety is the easiest to transform
|
|
321
|
+
* into either of the other varieties and, for that reason, is the variety that is "authored" by the SSR compiler.
|
|
322
|
+
*/
|
|
323
|
+
function transmogrify(compiledComponentAst, mode = 'sync') {
|
|
324
|
+
const state = {
|
|
325
|
+
mode,
|
|
326
|
+
};
|
|
327
|
+
return immer.produce(compiledComponentAst, (astDraft) => estreeToolkit.traverse(astDraft, visitors$1, state));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/******************************************************************************
|
|
331
|
+
Copyright (c) Microsoft Corporation.
|
|
332
|
+
|
|
333
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
334
|
+
purpose with or without fee is hereby granted.
|
|
335
|
+
|
|
336
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
337
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
338
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
339
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
340
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
341
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
342
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
343
|
+
***************************************************************************** */
|
|
344
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
function __classPrivateFieldGet(receiver, state, kind, f) {
|
|
348
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
349
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
350
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
354
|
+
var e = new Error(message);
|
|
355
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
/*
|
|
359
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
360
|
+
* All rights reserved.
|
|
361
|
+
* SPDX-License-Identifier: MIT
|
|
362
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
363
|
+
*/
|
|
364
|
+
/**
|
|
365
|
+
* Creates an import statement, e.g. `import { foo, bar as $bar$ } from "pkg"`
|
|
366
|
+
* @param imports names to be imported; values can be a string (plain import) or object (aliased)
|
|
367
|
+
* @param source source location to import from; defaults to @lwc/ssr-runtime
|
|
368
|
+
*/
|
|
369
|
+
const bImportDeclaration = (imports, source = '@lwc/ssr-runtime') => {
|
|
370
|
+
let parsed;
|
|
371
|
+
if (typeof imports === 'string') {
|
|
372
|
+
parsed = [[imports, undefined]];
|
|
373
|
+
}
|
|
374
|
+
else if (Array.isArray(imports)) {
|
|
375
|
+
parsed = imports.map((imp) => [imp, undefined]);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
parsed = Object.entries(imports);
|
|
379
|
+
}
|
|
380
|
+
const specifiers = parsed.map(([imported, local]) => {
|
|
381
|
+
if (imported === 'default') {
|
|
382
|
+
return estreeToolkit.builders.importDefaultSpecifier(estreeToolkit.builders.identifier(local));
|
|
383
|
+
}
|
|
384
|
+
else if (imported === '*') {
|
|
385
|
+
return estreeToolkit.builders.importNamespaceSpecifier(estreeToolkit.builders.identifier(local));
|
|
386
|
+
}
|
|
387
|
+
else if (local) {
|
|
388
|
+
return estreeToolkit.builders.importSpecifier(estreeToolkit.builders.identifier(imported), estreeToolkit.builders.identifier(local));
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
return estreeToolkit.builders.importSpecifier(estreeToolkit.builders.identifier(imported));
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
return estreeToolkit.builders.importDeclaration(specifiers, estreeToolkit.builders.literal(source));
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/*
|
|
398
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
399
|
+
* All rights reserved.
|
|
400
|
+
* SPDX-License-Identifier: MIT
|
|
401
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
402
|
+
*/
|
|
403
|
+
var _ImportManager_map;
|
|
404
|
+
class ImportManager {
|
|
405
|
+
constructor() {
|
|
406
|
+
_ImportManager_map.set(this, new Map());
|
|
407
|
+
}
|
|
408
|
+
/** Add an import to a collection of imports, probably for adding to the AST later. */
|
|
409
|
+
add(imports, source = '@lwc/ssr-runtime') {
|
|
410
|
+
let specifiers;
|
|
411
|
+
if (typeof imports === 'string') {
|
|
412
|
+
specifiers = [[imports, undefined]];
|
|
413
|
+
}
|
|
414
|
+
else if (Array.isArray(imports)) {
|
|
415
|
+
specifiers = imports.map((name) => [name, undefined]);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
specifiers = Object.entries(imports);
|
|
419
|
+
}
|
|
420
|
+
let specifierMap = __classPrivateFieldGet(this, _ImportManager_map, "f").get(source);
|
|
421
|
+
if (specifierMap) {
|
|
422
|
+
for (const [imported, local] of specifiers) {
|
|
423
|
+
specifierMap.set(imported, local);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
specifierMap = new Map(specifiers);
|
|
428
|
+
__classPrivateFieldGet(this, _ImportManager_map, "f").set(source, specifierMap);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Get the collection of imports for adding to the AST, probably soon! */
|
|
432
|
+
getImportDeclarations() {
|
|
433
|
+
return Array.from(__classPrivateFieldGet(this, _ImportManager_map, "f"), ([source, specifierMap]) => {
|
|
434
|
+
return bImportDeclaration(Object.fromEntries(specifierMap), source);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
_ImportManager_map = new WeakMap();
|
|
439
|
+
|
|
440
|
+
/*
|
|
441
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
442
|
+
* All rights reserved.
|
|
443
|
+
* SPDX-License-Identifier: MIT
|
|
444
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
445
|
+
*/
|
|
446
|
+
/**
|
|
447
|
+
* This accomplishes two things:
|
|
448
|
+
*
|
|
449
|
+
* 1. it replaces "lwc" with "@lwc/ssr-runtime" in an import specifier
|
|
450
|
+
* 2. it makes note of the local var name associated with the `LightningElement` import
|
|
451
|
+
*/
|
|
452
|
+
function replaceLwcImport(path, state) {
|
|
453
|
+
if (!path.node || !isLwcSource(path)) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
for (const specifier of path.node.specifiers) {
|
|
457
|
+
if (specifier.type === 'ImportSpecifier' &&
|
|
458
|
+
specifier.imported.type === 'Identifier' &&
|
|
459
|
+
specifier.imported.name === 'LightningElement') {
|
|
460
|
+
state.lightningElementIdentifier = specifier.local.name;
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
path.replaceWith(estreeToolkit.builders.importDeclaration(structuredClone(path.node.specifiers), estreeToolkit.builders.literal('@lwc/ssr-runtime')));
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* This handles lwc barrel exports by replacing "lwc" with "@lwc/ssr-runtime"
|
|
468
|
+
*/
|
|
469
|
+
function replaceNamedLwcExport(path) {
|
|
470
|
+
if (!path.node || !isLwcSource(path)) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
path.replaceWith(estreeToolkit.builders.exportNamedDeclaration(structuredClone(path.node.declaration), structuredClone(path.node.specifiers), estreeToolkit.builders.literal('@lwc/ssr-runtime')));
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* This handles all lwc barrel exports by replacing "lwc" with "@lwc/ssr-runtime"
|
|
477
|
+
*/
|
|
478
|
+
function replaceAllLwcExport(path) {
|
|
479
|
+
if (!path.node || !isLwcSource(path)) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
path.replaceWith(estreeToolkit.builders.exportAllDeclaration(estreeToolkit.builders.literal('@lwc/ssr-runtime'), structuredClone(path.node.exported)));
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Utility to determine if a node source is 'lwc'
|
|
486
|
+
*/
|
|
487
|
+
function isLwcSource(path) {
|
|
488
|
+
return path.node?.source?.value === 'lwc';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/*
|
|
492
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
493
|
+
* All rights reserved.
|
|
494
|
+
* SPDX-License-Identifier: MIT
|
|
495
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
496
|
+
*/
|
|
497
|
+
function catalogAndReplaceStyleImports(path, state) {
|
|
498
|
+
const specifier = path.node.specifiers[0];
|
|
499
|
+
if (typeof path.node.source.value !== 'string' ||
|
|
500
|
+
!path.node.source.value.endsWith('.css') ||
|
|
501
|
+
path.node.specifiers.length !== 1 ||
|
|
502
|
+
specifier.type !== 'ImportDefaultSpecifier') {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Any file ending in `*.scoped.css` which is directly imported into a Component `*.js` file (and assumed
|
|
506
|
+
// to be used for `static stylesheets`) is assumed to be scoped, so needs to be marked as such with a query param.
|
|
507
|
+
// Outside of SSR, this is done by `@lwc/babel-plugin-component`, so we need to emulate its behavior. The goal here
|
|
508
|
+
// is for `@lwc/template-compiler` to know to add `stylesheet.$scoped$ = true` to its compiled output, which it
|
|
509
|
+
// detects using the query param.
|
|
510
|
+
if (path.node?.source.value.endsWith('.scoped.css')) {
|
|
511
|
+
path.replaceWith(estreeToolkit.builders.importDeclaration(path.node.specifiers, estreeToolkit.builders.literal(path.node.source.value + '?scoped=true')));
|
|
512
|
+
}
|
|
513
|
+
state.cssExplicitImports = state.cssExplicitImports ?? new Map();
|
|
514
|
+
state.cssExplicitImports.set(specifier.local.name, path.node.source.value);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* This adds implicit style imports to the compiled component artifact.
|
|
518
|
+
*/
|
|
519
|
+
function getStylesheetImports(filepath) {
|
|
520
|
+
const moduleName = /(?<moduleName>[^/]+)\.html$/.exec(filepath)?.groups?.moduleName;
|
|
521
|
+
if (!moduleName) {
|
|
522
|
+
throw new Error(`Could not determine module name from file path: ${filepath}`);
|
|
523
|
+
}
|
|
524
|
+
return [
|
|
525
|
+
[{ default: 'defaultStylesheets' }, `./${moduleName}.css`],
|
|
526
|
+
[{ default: 'defaultScopedStylesheets' }, `./${moduleName}.scoped.css?scoped=true`],
|
|
527
|
+
];
|
|
528
|
+
}
|
|
529
|
+
function catalogStaticStylesheets(ids, state) {
|
|
530
|
+
state.staticStylesheetIds = state.staticStylesheetIds ?? new Set();
|
|
531
|
+
for (const id of ids) {
|
|
532
|
+
state.staticStylesheetIds.add(id);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/*
|
|
537
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
538
|
+
* All rights reserved.
|
|
539
|
+
* SPDX-License-Identifier: MIT
|
|
540
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
541
|
+
*/
|
|
542
|
+
function generateError(node, error, ...messageArgs) {
|
|
543
|
+
return errors.generateCompilerError(error, {
|
|
544
|
+
messageArgs,
|
|
545
|
+
origin: node.loc
|
|
546
|
+
? {
|
|
547
|
+
filename: node.loc.source || undefined,
|
|
548
|
+
location: {
|
|
549
|
+
line: node.loc.start.line,
|
|
550
|
+
column: node.loc.start.column,
|
|
551
|
+
...(node.range
|
|
552
|
+
? { start: node.range[0], length: node.range[1] - node.range[0] }
|
|
553
|
+
: {}),
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
: undefined,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/*
|
|
561
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
562
|
+
* All rights reserved.
|
|
563
|
+
* SPDX-License-Identifier: MIT
|
|
564
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
565
|
+
*/
|
|
566
|
+
function bMemberExpressionChain(props) {
|
|
567
|
+
// Technically an incorrect assertion, but it works fine...
|
|
568
|
+
let expr = estreeToolkit.builders.identifier('instance');
|
|
569
|
+
for (const prop of props) {
|
|
570
|
+
expr = estreeToolkit.builders.memberExpression(expr, estreeToolkit.builders.literal(prop), true);
|
|
571
|
+
}
|
|
572
|
+
return expr;
|
|
573
|
+
}
|
|
574
|
+
function getWireParams(node) {
|
|
575
|
+
const { decorators } = node;
|
|
576
|
+
if (decorators.length > 1) {
|
|
577
|
+
throw generateError(node, errors.DecoratorErrors.ONE_WIRE_DECORATOR_ALLOWED);
|
|
578
|
+
}
|
|
579
|
+
// Before calling this function, we validate that it has exactly one decorator, @wire
|
|
580
|
+
const wireDecorator = decorators[0].expression;
|
|
581
|
+
if (!estreeToolkit.is.callExpression(wireDecorator)) {
|
|
582
|
+
throw generateError(node, errors.DecoratorErrors.FUNCTION_IDENTIFIER_SHOULD_BE_FIRST_PARAMETER);
|
|
583
|
+
}
|
|
584
|
+
const args = wireDecorator.arguments;
|
|
585
|
+
if (args.length === 0) {
|
|
586
|
+
throw generateError(node, errors.DecoratorErrors.ADAPTER_SHOULD_BE_FIRST_PARAMETER);
|
|
587
|
+
}
|
|
588
|
+
return args;
|
|
589
|
+
}
|
|
590
|
+
function validateWireId(id, path) {
|
|
591
|
+
// name of identifier or object used in member expression (e.g. "foo" for `foo.bar`)
|
|
592
|
+
let wireAdapterVar;
|
|
593
|
+
if (estreeToolkit.is.memberExpression(id)) {
|
|
594
|
+
if (id.computed) {
|
|
595
|
+
throw generateError(path.node, errors.DecoratorErrors.FUNCTION_IDENTIFIER_CANNOT_HAVE_COMPUTED_PROPS);
|
|
596
|
+
}
|
|
597
|
+
if (!estreeToolkit.is.identifier(id.object)) {
|
|
598
|
+
throw generateError(path.node, errors.DecoratorErrors.FUNCTION_IDENTIFIER_CANNOT_HAVE_NESTED_MEMBER_EXRESSIONS);
|
|
599
|
+
}
|
|
600
|
+
wireAdapterVar = id.object.name;
|
|
601
|
+
}
|
|
602
|
+
else if (!estreeToolkit.is.identifier(id)) {
|
|
603
|
+
throw generateError(path.node, errors.DecoratorErrors.FUNCTION_IDENTIFIER_SHOULD_BE_FIRST_PARAMETER);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
wireAdapterVar = id.name;
|
|
607
|
+
}
|
|
608
|
+
// This is not the exact same validation done in @lwc/babel-plugin-component but it accomplishes the same thing
|
|
609
|
+
if (path.scope?.getBinding(wireAdapterVar)?.kind !== 'module') {
|
|
610
|
+
throw generateError(path.node, errors.DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function validateWireConfig(config, path) {
|
|
614
|
+
if (!estreeToolkit.is.objectExpression(config)) {
|
|
615
|
+
throw generateError(path.node, errors.DecoratorErrors.CONFIG_OBJECT_SHOULD_BE_SECOND_PARAMETER);
|
|
616
|
+
}
|
|
617
|
+
for (const property of config.properties) {
|
|
618
|
+
// Only validate computed object properties because static props are all valid
|
|
619
|
+
// and we ignore {...spreads} and {methods(){}}
|
|
620
|
+
if (!estreeToolkit.is.property(property) || !property.computed)
|
|
621
|
+
continue;
|
|
622
|
+
const key = property.key;
|
|
623
|
+
if (estreeToolkit.is.identifier(key)) {
|
|
624
|
+
const binding = path.scope?.getBinding(key.name);
|
|
625
|
+
// TODO [#3956]: Investigate allowing imported constants
|
|
626
|
+
if (binding?.kind === 'const')
|
|
627
|
+
continue;
|
|
628
|
+
// By default, the identifier `undefined` has no binding (when it's actually undefined),
|
|
629
|
+
// but has a binding if it's used as a variable (e.g. `let undefined = "don't do this"`)
|
|
630
|
+
if (key.name === 'undefined' && !binding)
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
else if (estreeToolkit.is.literal(key)) {
|
|
634
|
+
if (estreeToolkit.is.templateLiteral(key)) {
|
|
635
|
+
// A template literal is not guaranteed to always result in the same value
|
|
636
|
+
// (e.g. `${Math.random()}`), so we disallow them entirely.
|
|
637
|
+
throw generateError(path.node, errors.DecoratorErrors.COMPUTED_PROPERTY_CANNOT_BE_TEMPLATE_LITERAL);
|
|
638
|
+
}
|
|
639
|
+
else if (!('regex' in key)) {
|
|
640
|
+
// A literal can be a regexp, template literal, or primitive; only allow primitives
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
else if (estreeToolkit.is.templateLiteral(key)) {
|
|
645
|
+
throw generateError(path.node, errors.DecoratorErrors.COMPUTED_PROPERTY_CANNOT_BE_TEMPLATE_LITERAL);
|
|
646
|
+
}
|
|
647
|
+
throw generateError(path.node, errors.DecoratorErrors.COMPUTED_PROPERTY_MUST_BE_CONSTANT_OR_LITERAL);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function catalogWireAdapters(path, state) {
|
|
651
|
+
const node = path.node;
|
|
652
|
+
const [id, config] = getWireParams(node);
|
|
653
|
+
validateWireId(id, path);
|
|
654
|
+
let reactiveConfig;
|
|
655
|
+
if (config) {
|
|
656
|
+
validateWireConfig(config, path);
|
|
657
|
+
reactiveConfig = immer.produce(config, (draft) => {
|
|
658
|
+
// replace '$foo' values with `instance.foo`; preserve everything else
|
|
659
|
+
for (const prop of draft.properties) {
|
|
660
|
+
const { value } = prop;
|
|
661
|
+
if (estreeToolkit.is.literal(value) &&
|
|
662
|
+
typeof value.value === 'string' &&
|
|
663
|
+
value.value.startsWith('$')) {
|
|
664
|
+
prop.value = bMemberExpressionChain(value.value.slice(1).split('.'));
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
reactiveConfig = estreeToolkit.builders.objectExpression([]); // empty object
|
|
671
|
+
}
|
|
672
|
+
state.wireAdapters = [
|
|
673
|
+
...state.wireAdapters,
|
|
674
|
+
{ adapterId: id, config: reactiveConfig, field: node },
|
|
675
|
+
];
|
|
676
|
+
}
|
|
677
|
+
const bSetWiredProp = (esTemplate `
|
|
678
|
+
instance.${ /*wire-decorated property*/estreeToolkit.is.identifier} = newValue
|
|
679
|
+
`);
|
|
680
|
+
const bCallWiredMethod = (esTemplate `
|
|
681
|
+
instance.${ /*wire-decorated method*/estreeToolkit.is.identifier}(newValue)
|
|
682
|
+
`);
|
|
683
|
+
const bWireAdapterPlumbing = (esTemplate `{
|
|
684
|
+
// Callable adapters are expressed as a function having an 'adapter' property, which
|
|
685
|
+
// is the actual wire constructor.
|
|
686
|
+
const AdapterCtor = ${ /*wire adapter constructor*/estreeToolkit.is.expression}?.adapter ?? ${ /*wire adapter constructor*/0};
|
|
687
|
+
const wireInstance = new AdapterCtor((newValue) => {
|
|
688
|
+
${ /*update the decorated property or call the decorated method*/estreeToolkit.is.expressionStatement};
|
|
689
|
+
});
|
|
690
|
+
wireInstance.connect?.();
|
|
691
|
+
if (wireInstance.update) {
|
|
692
|
+
const getLiveConfig = () => {
|
|
693
|
+
return ${ /* reactive wire config */estreeToolkit.is.objectExpression};
|
|
694
|
+
};
|
|
695
|
+
// This may look a bit weird, in that the 'update' function is called twice: once with
|
|
696
|
+
// an 'undefined' value and possibly again with a context-provided value. While weird,
|
|
697
|
+
// this preserves the behavior of the browser-side wire implementation as well as the
|
|
698
|
+
// original SSR implementation.
|
|
699
|
+
wireInstance.update(getLiveConfig(), undefined);
|
|
700
|
+
__connectContext(AdapterCtor, instance, (newContextValue) => {
|
|
701
|
+
wireInstance.update(getLiveConfig(), newContextValue);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}`);
|
|
705
|
+
function bWireAdaptersPlumbing(adapters) {
|
|
706
|
+
return adapters.map(({ adapterId, config, field }) => {
|
|
707
|
+
const actionUponNewValue = estreeToolkit.is.methodDefinition(field) && field.kind === 'method'
|
|
708
|
+
? // Validation in compile-js/index.ts `visitors` ensures `key` is an identifier
|
|
709
|
+
bCallWiredMethod(field.key)
|
|
710
|
+
: bSetWiredProp(field.key);
|
|
711
|
+
return bWireAdapterPlumbing(adapterId, actionUponNewValue, config);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function isWireDecorator(decorator) {
|
|
715
|
+
return (estreeToolkit.is.callExpression(decorator?.expression) &&
|
|
716
|
+
estreeToolkit.is.identifier(decorator.expression.callee) &&
|
|
717
|
+
decorator.expression.callee.name === 'wire');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/*
|
|
721
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
722
|
+
* All rights reserved.
|
|
723
|
+
* SPDX-License-Identifier: MIT
|
|
724
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
725
|
+
*/
|
|
726
|
+
const bGenerateMarkup = (esTemplate `
|
|
727
|
+
// These variables may mix with component-authored variables, so should be reasonably unique
|
|
728
|
+
const __lwcSuperPublicProperties__ = Array.from(Object.getPrototypeOf(${ /* Component class */estreeToolkit.is.identifier})?.__lwcPublicProperties__?.values?.() ?? []);
|
|
729
|
+
const __lwcPublicProperties__ = new Set(${ /*public properties*/estreeToolkit.is.arrayExpression}.concat(__lwcSuperPublicProperties__));
|
|
730
|
+
|
|
731
|
+
Object.defineProperty(
|
|
732
|
+
${ /* component class */0},
|
|
733
|
+
__SYMBOL__GENERATE_MARKUP,
|
|
734
|
+
{
|
|
735
|
+
configurable: false,
|
|
736
|
+
enumerable: false,
|
|
737
|
+
writable: false,
|
|
738
|
+
value: async function* __lwcGenerateMarkup(
|
|
739
|
+
tagName,
|
|
740
|
+
props,
|
|
741
|
+
attrs,
|
|
742
|
+
parent,
|
|
743
|
+
scopeToken,
|
|
744
|
+
contextfulParent,
|
|
745
|
+
renderContext,
|
|
746
|
+
shadowSlottedContent,
|
|
747
|
+
lightSlottedContent,
|
|
748
|
+
scopedSlottedContent,
|
|
749
|
+
) {
|
|
750
|
+
tagName = tagName ?? ${ /*component tag name*/estreeToolkit.is.literal};
|
|
751
|
+
attrs = attrs ?? Object.create(null);
|
|
752
|
+
props = props ?? Object.create(null);
|
|
753
|
+
const instance = new ${ /* Component class */0}({
|
|
754
|
+
tagName: tagName.toUpperCase(),
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
__establishContextfulRelationship(contextfulParent, instance);
|
|
758
|
+
|
|
759
|
+
instance[__SYMBOL__SET_INTERNALS](
|
|
760
|
+
props,
|
|
761
|
+
attrs,
|
|
762
|
+
__lwcPublicProperties__
|
|
763
|
+
);
|
|
764
|
+
${ /*connect wire*/estreeToolkit.is.statement}
|
|
765
|
+
instance.isConnected = true;
|
|
766
|
+
if (instance.connectedCallback) {
|
|
767
|
+
__mutationTracker.enable(instance);
|
|
768
|
+
instance.connectedCallback();
|
|
769
|
+
__mutationTracker.disable(instance);
|
|
770
|
+
}
|
|
771
|
+
// If a render() function is defined on the class or any of its superclasses, then that takes priority.
|
|
772
|
+
// Next, if the class or any of its superclasses has an implicitly-associated template, then that takes
|
|
773
|
+
// second priority (e.g. a foo.html file alongside a foo.js file). Finally, there is a fallback empty template.
|
|
774
|
+
const tmplFn = instance.render?.() ?? ${ /*component class*/0}[__SYMBOL__DEFAULT_TEMPLATE] ?? __fallbackTmpl;
|
|
775
|
+
yield \`<\${tagName}\`;
|
|
776
|
+
|
|
777
|
+
const hostHasScopedStylesheets =
|
|
778
|
+
tmplFn.hasScopedStylesheets ||
|
|
779
|
+
hasScopedStaticStylesheets(${ /*component class*/0});
|
|
780
|
+
const hostScopeToken = hostHasScopedStylesheets ? tmplFn.stylesheetScopeToken + "-host" : undefined;
|
|
781
|
+
|
|
782
|
+
yield* __renderAttrs(instance, attrs, hostScopeToken, scopeToken);
|
|
783
|
+
yield '>';
|
|
784
|
+
yield* tmplFn(
|
|
785
|
+
shadowSlottedContent,
|
|
786
|
+
lightSlottedContent,
|
|
787
|
+
scopedSlottedContent,
|
|
788
|
+
${ /*component class*/0},
|
|
789
|
+
instance,
|
|
790
|
+
renderContext
|
|
791
|
+
);
|
|
792
|
+
yield \`</\${tagName}>\`;
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
Object.defineProperty(
|
|
796
|
+
${ /* component class */0},
|
|
797
|
+
'__lwcPublicProperties__',
|
|
798
|
+
{
|
|
799
|
+
configurable: false,
|
|
800
|
+
enumerable: false,
|
|
801
|
+
writable: false,
|
|
802
|
+
value: __lwcPublicProperties__
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
`);
|
|
806
|
+
const bExposeTemplate = (esTemplate `
|
|
807
|
+
if (${ /*template*/estreeToolkit.is.identifier}) {
|
|
808
|
+
Object.defineProperty(
|
|
809
|
+
${ /* component class */estreeToolkit.is.identifier},
|
|
810
|
+
__SYMBOL__DEFAULT_TEMPLATE,
|
|
811
|
+
{
|
|
812
|
+
configurable: false,
|
|
813
|
+
enumerable: false,
|
|
814
|
+
writable: false,
|
|
815
|
+
value: ${ /*template*/0}
|
|
816
|
+
}
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
`);
|
|
820
|
+
/**
|
|
821
|
+
* This builds a generator function `generateMarkup` and adds it to the component JS's
|
|
822
|
+
* compilation output. `generateMarkup` acts as the glue between component JS and its
|
|
823
|
+
* template(s), including:
|
|
824
|
+
*
|
|
825
|
+
* - managing reflection of attrs & props
|
|
826
|
+
* - instantiating the component instance
|
|
827
|
+
* - setting the internal state of that component instance
|
|
828
|
+
* - invoking component lifecycle methods
|
|
829
|
+
* - yielding the tag name & attributes
|
|
830
|
+
* - deferring to the template function for yielding child content
|
|
831
|
+
*/
|
|
832
|
+
function addGenerateMarkupFunction(program, state, tagName, filename) {
|
|
833
|
+
const { publicProperties } = state;
|
|
834
|
+
// The default tag name represents the component name that's passed to the transformer.
|
|
835
|
+
// This is needed to generate markup for dynamic components which are invoked through
|
|
836
|
+
// the generateMarkup function on the constructor.
|
|
837
|
+
// At the time of generation, the invoker does not have reference to its tag name to pass as an argument.
|
|
838
|
+
const defaultTagName = estreeToolkit.builders.literal(tagName);
|
|
839
|
+
const classIdentifier = estreeToolkit.builders.identifier(state.lwcClassName);
|
|
840
|
+
let exposeTemplateBlock = null;
|
|
841
|
+
const defaultTmplPath = `./${node_path.parse(filename).name}.html`;
|
|
842
|
+
const tmplVar = estreeToolkit.builders.identifier('__lwcTmpl');
|
|
843
|
+
program.body.unshift(bImportDeclaration({ default: tmplVar.name }, defaultTmplPath));
|
|
844
|
+
program.body.unshift(bImportDeclaration({ SYMBOL__DEFAULT_TEMPLATE: '__SYMBOL__DEFAULT_TEMPLATE' }));
|
|
845
|
+
exposeTemplateBlock = bExposeTemplate(tmplVar, classIdentifier);
|
|
846
|
+
// If no wire adapters are detected on the component, we don't bother injecting the wire-related code.
|
|
847
|
+
let connectWireAdapterCode = [];
|
|
848
|
+
if (state.wireAdapters.length) {
|
|
849
|
+
connectWireAdapterCode = bWireAdaptersPlumbing(state.wireAdapters);
|
|
850
|
+
program.body.unshift(bImportDeclaration({ connectContext: '__connectContext' }));
|
|
851
|
+
}
|
|
852
|
+
program.body.unshift(bImportDeclaration({
|
|
853
|
+
fallbackTmpl: '__fallbackTmpl',
|
|
854
|
+
hasScopedStaticStylesheets: undefined,
|
|
855
|
+
mutationTracker: '__mutationTracker',
|
|
856
|
+
renderAttrs: '__renderAttrs',
|
|
857
|
+
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
|
|
858
|
+
SYMBOL__SET_INTERNALS: '__SYMBOL__SET_INTERNALS',
|
|
859
|
+
establishContextfulRelationship: '__establishContextfulRelationship',
|
|
860
|
+
}));
|
|
861
|
+
program.body.push(...bGenerateMarkup(classIdentifier, estreeToolkit.builders.arrayExpression([...publicProperties.keys()].map(estreeToolkit.builders.literal)), defaultTagName, connectWireAdapterCode));
|
|
862
|
+
if (exposeTemplateBlock) {
|
|
863
|
+
program.body.push(exposeTemplateBlock);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/*
|
|
868
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
869
|
+
* All rights reserved.
|
|
870
|
+
* SPDX-License-Identifier: MIT
|
|
871
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
872
|
+
*/
|
|
873
|
+
function validateName(definition) {
|
|
874
|
+
if (definition.computed) {
|
|
875
|
+
throw generateError(definition, errors.DecoratorErrors.PROPERTY_CANNOT_BE_COMPUTED);
|
|
876
|
+
}
|
|
877
|
+
const propertyName = definition.key.name;
|
|
878
|
+
switch (true) {
|
|
879
|
+
case propertyName === 'part':
|
|
880
|
+
throw generateError(definition, errors.DecoratorErrors.PROPERTY_NAME_PART_IS_RESERVED, propertyName);
|
|
881
|
+
case propertyName.startsWith('on'):
|
|
882
|
+
throw generateError(definition, errors.DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_ON, propertyName);
|
|
883
|
+
case propertyName.startsWith('data') && propertyName.length > 4:
|
|
884
|
+
throw generateError(definition, errors.DecoratorErrors.PROPERTY_NAME_CANNOT_START_WITH_DATA, propertyName);
|
|
885
|
+
case shared.DISALLOWED_PROP_SET.has(propertyName):
|
|
886
|
+
throw generateError(definition, errors.DecoratorErrors.PROPERTY_NAME_IS_RESERVED, propertyName);
|
|
887
|
+
case shared.AMBIGUOUS_PROP_SET.has(propertyName):
|
|
888
|
+
throw generateError(definition, errors.DecoratorErrors.PROPERTY_NAME_IS_AMBIGUOUS, propertyName, shared.AMBIGUOUS_PROP_SET.get(propertyName));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function validatePropertyValue(property) {
|
|
892
|
+
if (estreeToolkit.is.literal(property.value) && property.value.value === true) {
|
|
893
|
+
throw generateError(property, errors.DecoratorErrors.INVALID_BOOLEAN_PUBLIC_PROPERTY);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function validatePropertyUnique(node, state) {
|
|
897
|
+
if (state.publicProperties.has(node.key.name)) {
|
|
898
|
+
throw generateError(node, errors.DecoratorErrors.DUPLICATE_API_PROPERTY, node.key.name);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function validateApiProperty(node, state) {
|
|
902
|
+
validatePropertyUnique(node, state);
|
|
903
|
+
validateName(node);
|
|
904
|
+
validatePropertyValue(node);
|
|
905
|
+
}
|
|
906
|
+
function validateUniqueMethod(node, state) {
|
|
907
|
+
const field = state.publicProperties.get(node.key.name);
|
|
908
|
+
if (!field) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (field.type === 'MethodDefinition' &&
|
|
912
|
+
(field.kind === 'get' || field.kind === 'set') &&
|
|
913
|
+
(node.kind === 'get' || node.kind === 'set')) {
|
|
914
|
+
throw generateError(node, errors.DecoratorErrors.SINGLE_DECORATOR_ON_SETTER_GETTER_PAIR, node.key.name);
|
|
915
|
+
}
|
|
916
|
+
throw generateError(node, errors.DecoratorErrors.DUPLICATE_API_PROPERTY, node.key.name);
|
|
917
|
+
}
|
|
918
|
+
function validateApiMethod(node, state) {
|
|
919
|
+
validateUniqueMethod(node, state);
|
|
920
|
+
validateName(node);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function isApiDecorator(decorator) {
|
|
924
|
+
return decorator?.expression.type === 'Identifier' && decorator.expression.name === 'api';
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/*
|
|
928
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
929
|
+
* All rights reserved.
|
|
930
|
+
* SPDX-License-Identifier: MIT
|
|
931
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
932
|
+
*/
|
|
933
|
+
const decorators = new Set(['api', 'wire', 'track']);
|
|
934
|
+
function removeDecoratorImport(path) {
|
|
935
|
+
if (!path.node || path.node.source.value !== '@lwc/ssr-runtime') {
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const filteredSpecifiers = path.node.specifiers.filter((specifier) => !(specifier.type === 'ImportSpecifier' &&
|
|
939
|
+
specifier.imported.type === 'Identifier' &&
|
|
940
|
+
decorators.has(specifier.imported.name)));
|
|
941
|
+
if (filteredSpecifiers.length !== path.node.specifiers.length) {
|
|
942
|
+
path.replaceWith(estreeToolkit.builders.importDeclaration(filteredSpecifiers, estreeToolkit.builders.literal('@lwc/ssr-runtime')));
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function isTrackDecorator(decorator) {
|
|
947
|
+
return decorator?.expression.type === 'Identifier' && decorator.expression.name === 'track';
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/*
|
|
951
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
952
|
+
* All rights reserved.
|
|
953
|
+
* SPDX-License-Identifier: MIT
|
|
954
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
955
|
+
*/
|
|
956
|
+
function validateUniqueDecorator(decorators) {
|
|
957
|
+
if (decorators.length < 2) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const wire = decorators.find(isWireDecorator);
|
|
961
|
+
const api = decorators.find(isApiDecorator);
|
|
962
|
+
const track = decorators.find(isTrackDecorator);
|
|
963
|
+
if (wire) {
|
|
964
|
+
if (api) {
|
|
965
|
+
throw generateError(wire, errors.DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'api');
|
|
966
|
+
}
|
|
967
|
+
if (track) {
|
|
968
|
+
throw generateError(wire, errors.DecoratorErrors.CONFLICT_WITH_ANOTHER_DECORATOR, 'track');
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
if (api && track) {
|
|
972
|
+
throw generateError(api, errors.DecoratorErrors.API_AND_TRACK_DECORATOR_CONFLICT);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/*
|
|
977
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
978
|
+
* All rights reserved.
|
|
979
|
+
* SPDX-License-Identifier: MIT
|
|
980
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
981
|
+
*/
|
|
982
|
+
const visitors = {
|
|
983
|
+
$: { scope: true },
|
|
984
|
+
ExportNamedDeclaration(path) {
|
|
985
|
+
replaceNamedLwcExport(path);
|
|
986
|
+
},
|
|
987
|
+
ExportAllDeclaration(path) {
|
|
988
|
+
replaceAllLwcExport(path);
|
|
989
|
+
},
|
|
990
|
+
ImportDeclaration(path, state) {
|
|
991
|
+
if (!path.node || !path.node.source.value || typeof path.node.source.value !== 'string') {
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
replaceLwcImport(path, state);
|
|
995
|
+
catalogAndReplaceStyleImports(path, state);
|
|
996
|
+
removeDecoratorImport(path);
|
|
997
|
+
},
|
|
998
|
+
ImportExpression(path, state) {
|
|
999
|
+
const { dynamicImports, importManager } = state;
|
|
1000
|
+
if (!dynamicImports) {
|
|
1001
|
+
// if no `dynamicImports` config, then leave dynamic `import()`s as-is
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (dynamicImports.strictSpecifier) {
|
|
1005
|
+
if (!estreeToolkit.is.literal(path.node?.source) || typeof path.node.source.value !== 'string') {
|
|
1006
|
+
throw generateError(path.node, errors.LWCClassErrors.INVALID_DYNAMIC_IMPORT_SOURCE_STRICT, estreeToolkit.is.literal(path.node?.source) ? String(path.node.source.value) : 'undefined');
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const loader = dynamicImports.loader;
|
|
1010
|
+
if (!loader) {
|
|
1011
|
+
// if no `loader` defined, then leave dynamic `import()`s as-is
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const source = path.node.source;
|
|
1015
|
+
// 1. insert `import { load as __lwcLoad } from '${loader}'` at top of program
|
|
1016
|
+
importManager.add({ load: '__lwcLoad' }, loader);
|
|
1017
|
+
// 2. replace this `import(source)` with `__lwcLoad(source)`
|
|
1018
|
+
const load = estreeToolkit.builders.identifier('__lwcLoad');
|
|
1019
|
+
state.trustedLwcIdentifiers.add(load);
|
|
1020
|
+
path.replaceWith(estreeToolkit.builders.callExpression(load, [structuredClone(source)]));
|
|
1021
|
+
},
|
|
1022
|
+
ClassDeclaration: {
|
|
1023
|
+
enter(path, state) {
|
|
1024
|
+
const { node } = path;
|
|
1025
|
+
if (node?.superClass &&
|
|
1026
|
+
// export default class extends LightningElement {}
|
|
1027
|
+
(estreeToolkit.is.exportDefaultDeclaration(path.parentPath) ||
|
|
1028
|
+
// class Cmp extends LightningElement {}; export default Cmp
|
|
1029
|
+
path.scope
|
|
1030
|
+
?.getBinding(node.id.name)
|
|
1031
|
+
?.references.some((ref) => estreeToolkit.is.exportDefaultDeclaration(ref.parent)))) {
|
|
1032
|
+
// If it's a default-exported class with a superclass, then it's an LWC component!
|
|
1033
|
+
state.isLWC = true;
|
|
1034
|
+
state.currentComponent = node;
|
|
1035
|
+
if (node.id) {
|
|
1036
|
+
state.lwcClassName = node.id.name;
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
node.id = estreeToolkit.builders.identifier('DefaultComponentName');
|
|
1040
|
+
state.lwcClassName = 'DefaultComponentName';
|
|
1041
|
+
}
|
|
1042
|
+
// There's no builder for comment nodes :\
|
|
1043
|
+
const lwcVersionComment = {
|
|
1044
|
+
type: 'Block',
|
|
1045
|
+
value: shared.LWC_VERSION_COMMENT,
|
|
1046
|
+
};
|
|
1047
|
+
// Add LWC version comment to end of class body
|
|
1048
|
+
const { body } = node;
|
|
1049
|
+
if (body.trailingComments) {
|
|
1050
|
+
body.trailingComments.push(lwcVersionComment);
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
body.trailingComments = [lwcVersionComment];
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
leave(path, state) {
|
|
1058
|
+
// Indicate that we're no longer traversing an LWC component
|
|
1059
|
+
if (state.currentComponent && path.node === state.currentComponent) {
|
|
1060
|
+
state.currentComponent = null;
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
},
|
|
1064
|
+
PropertyDefinition(path, state) {
|
|
1065
|
+
// Don't do anything unless we're in a component
|
|
1066
|
+
if (!state.currentComponent) {
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
const node = path.node;
|
|
1070
|
+
if (!node?.key) {
|
|
1071
|
+
// Seems to occur for `@wire() [symbol];` -- not sure why
|
|
1072
|
+
throw new Error('Unknown state: property definition has no key');
|
|
1073
|
+
}
|
|
1074
|
+
if (!isKeyIdentifier(node)) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const { decorators } = node;
|
|
1078
|
+
validateUniqueDecorator(decorators);
|
|
1079
|
+
if (isApiDecorator(decorators[0])) {
|
|
1080
|
+
validateApiProperty(node, state);
|
|
1081
|
+
state.publicProperties.set(node.key.name, node);
|
|
1082
|
+
}
|
|
1083
|
+
else if (isWireDecorator(decorators[0])) {
|
|
1084
|
+
catalogWireAdapters(path, state);
|
|
1085
|
+
state.privateProperties.add(node.key.name);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
state.privateProperties.add(node.key.name);
|
|
1089
|
+
}
|
|
1090
|
+
if (node.static &&
|
|
1091
|
+
node.key.name === 'stylesheets' &&
|
|
1092
|
+
estreeToolkit.is.arrayExpression(node.value) &&
|
|
1093
|
+
node.value.elements.every((el) => estreeToolkit.is.identifier(el))) {
|
|
1094
|
+
catalogStaticStylesheets(node.value.elements.map((el) => el.name), state);
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
MethodDefinition(path, state) {
|
|
1098
|
+
const node = path.node;
|
|
1099
|
+
if (!isKeyIdentifier(node)) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
// If we mutate any class-methods that are piped through this compiler, then we'll be
|
|
1103
|
+
// inadvertently mutating things like Wire adapters.
|
|
1104
|
+
if (!state.currentComponent) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const { decorators } = node;
|
|
1108
|
+
validateUniqueDecorator(decorators);
|
|
1109
|
+
if (isApiDecorator(decorators[0])) {
|
|
1110
|
+
validateApiMethod(node, state);
|
|
1111
|
+
state.publicProperties.set(node.key.name, node);
|
|
1112
|
+
}
|
|
1113
|
+
else if (isWireDecorator(decorators[0])) {
|
|
1114
|
+
if (node.computed) {
|
|
1115
|
+
// TODO [W-17758410]: implement
|
|
1116
|
+
throw new Error('@wire cannot be used on computed properties in SSR context.');
|
|
1117
|
+
}
|
|
1118
|
+
const isRealMethod = node.kind === 'method';
|
|
1119
|
+
// Getters and setters are methods in the AST, but treated as properties by @wire
|
|
1120
|
+
// Note that this means that their implementations are ignored!
|
|
1121
|
+
if (!isRealMethod) {
|
|
1122
|
+
const methodAsProp = estreeToolkit.builders.propertyDefinition(structuredClone(node.key), null, node.computed, node.static);
|
|
1123
|
+
methodAsProp.decorators = structuredClone(decorators);
|
|
1124
|
+
path.replaceWith(methodAsProp);
|
|
1125
|
+
// We do not need to call `catalogWireAdapters()` because, by replacing the current
|
|
1126
|
+
// node, `traverse()` will visit it again automatically, so we will just call
|
|
1127
|
+
// `catalogWireAdapters()` later anyway.
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
catalogWireAdapters(path, state);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
switch (node.key.name) {
|
|
1135
|
+
case 'constructor':
|
|
1136
|
+
// add our own custom arg after any pre-existing constructor args
|
|
1137
|
+
node.value.params = [
|
|
1138
|
+
...structuredClone(node.value.params),
|
|
1139
|
+
estreeToolkit.builders.identifier('propsAvailableAtConstruction'),
|
|
1140
|
+
];
|
|
1141
|
+
break;
|
|
1142
|
+
case 'connectedCallback':
|
|
1143
|
+
state.hasConnectedCallback = true;
|
|
1144
|
+
break;
|
|
1145
|
+
case 'renderedCallback':
|
|
1146
|
+
state.hadRenderedCallback = true;
|
|
1147
|
+
path.remove();
|
|
1148
|
+
break;
|
|
1149
|
+
case 'disconnectedCallback':
|
|
1150
|
+
state.hadDisconnectedCallback = true;
|
|
1151
|
+
path.remove();
|
|
1152
|
+
break;
|
|
1153
|
+
case 'errorCallback':
|
|
1154
|
+
state.hadErrorCallback = true;
|
|
1155
|
+
path.remove();
|
|
1156
|
+
break;
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
Super(path, state) {
|
|
1160
|
+
// If we mutate any super calls that are piped through this compiler, then we'll be
|
|
1161
|
+
// inadvertently mutating things like Wire adapters.
|
|
1162
|
+
if (!state.currentComponent) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const parentFn = path.getFunctionParent();
|
|
1166
|
+
if (parentFn &&
|
|
1167
|
+
parentFn.parentPath?.node?.type === 'MethodDefinition' &&
|
|
1168
|
+
parentFn.parentPath?.node?.kind === 'constructor' &&
|
|
1169
|
+
path.parentPath &&
|
|
1170
|
+
path.parentPath.node?.type === 'CallExpression') {
|
|
1171
|
+
// add our own custom arg after any pre-existing super() args
|
|
1172
|
+
path.parentPath.node.arguments = [
|
|
1173
|
+
...structuredClone(path.parentPath.node.arguments),
|
|
1174
|
+
estreeToolkit.builders.identifier('propsAvailableAtConstruction'),
|
|
1175
|
+
];
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
Program: {
|
|
1179
|
+
leave(path, state) {
|
|
1180
|
+
// After parsing the whole tree, insert needed imports
|
|
1181
|
+
const importDeclarations = state.importManager.getImportDeclarations();
|
|
1182
|
+
if (importDeclarations.length > 0) {
|
|
1183
|
+
path.node?.body.unshift(...importDeclarations);
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
Identifier(path, state) {
|
|
1188
|
+
const { node } = path;
|
|
1189
|
+
if (node?.name.startsWith('__lwc') && !state.trustedLwcIdentifiers.has(node)) {
|
|
1190
|
+
throw generateError(node, errors.SsrCompilerErrors.RESERVED_IDENTIFIER_PREFIX);
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
function compileJS(src, filename, tagName, options, compilationMode) {
|
|
1195
|
+
let ast = meriyah.parseModule(src, {
|
|
1196
|
+
module: true,
|
|
1197
|
+
next: true,
|
|
1198
|
+
loc: true,
|
|
1199
|
+
source: filename,
|
|
1200
|
+
ranges: true,
|
|
1201
|
+
});
|
|
1202
|
+
const state = {
|
|
1203
|
+
isLWC: false,
|
|
1204
|
+
currentComponent: null,
|
|
1205
|
+
hasConstructor: false,
|
|
1206
|
+
hasConnectedCallback: false,
|
|
1207
|
+
hadRenderedCallback: false,
|
|
1208
|
+
hadDisconnectedCallback: false,
|
|
1209
|
+
hadErrorCallback: false,
|
|
1210
|
+
lightningElementIdentifier: null,
|
|
1211
|
+
lwcClassName: null,
|
|
1212
|
+
cssExplicitImports: null,
|
|
1213
|
+
staticStylesheetIds: null,
|
|
1214
|
+
publicProperties: new Map(),
|
|
1215
|
+
privateProperties: new Set(),
|
|
1216
|
+
wireAdapters: [],
|
|
1217
|
+
dynamicImports: options.dynamicImports,
|
|
1218
|
+
importManager: new ImportManager(),
|
|
1219
|
+
trustedLwcIdentifiers: new WeakSet(),
|
|
1220
|
+
};
|
|
1221
|
+
estreeToolkit.traverse(ast, visitors, state);
|
|
1222
|
+
if (!state.isLWC) {
|
|
1223
|
+
// If an `extends LightningElement` is not detected in the JS, the
|
|
1224
|
+
// file in question is likely not an LWC. With this v1 implementation,
|
|
1225
|
+
// we'll just return the original source.
|
|
1226
|
+
return {
|
|
1227
|
+
code: astring.generate(ast, {}),
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
addGenerateMarkupFunction(ast, state, tagName, filename);
|
|
1231
|
+
if (compilationMode === 'async' || compilationMode === 'sync') {
|
|
1232
|
+
ast = transmogrify(ast, compilationMode);
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
code: astring.generate(ast, {
|
|
1236
|
+
// The AST generated by meriyah doesn't seem to include comments,
|
|
1237
|
+
// so this just preserves the LWC version comment we added
|
|
1238
|
+
comments: true,
|
|
1239
|
+
}),
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function isKeyIdentifier(node) {
|
|
1243
|
+
return estreeToolkit.is.identifier(node?.key);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/*
|
|
1247
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
1248
|
+
* All rights reserved.
|
|
1249
|
+
* SPDX-License-Identifier: MIT
|
|
1250
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1251
|
+
*/
|
|
1252
|
+
const bStylesheetTokenDeclaration = (esTemplate `
|
|
1253
|
+
const stylesheetScopeToken = '${estreeToolkit.is.literal}';
|
|
1254
|
+
`);
|
|
1255
|
+
const bHasScopedStylesheetsDeclaration = (esTemplate `
|
|
1256
|
+
const hasScopedStylesheets = defaultScopedStylesheets !== undefined && defaultScopedStylesheets.length > 0;
|
|
1257
|
+
`);
|
|
1258
|
+
// Scope tokens are associated with a given template. This is assigned here so that it can be used in `generateMarkup`.
|
|
1259
|
+
// We also need to keep track of whether the template has any scoped styles or not so that we can render (or not) the
|
|
1260
|
+
// scope token.
|
|
1261
|
+
const tmplAssignmentBlock = (esTemplate `
|
|
1262
|
+
${ /* template */estreeToolkit.is.identifier}.hasScopedStylesheets = hasScopedStylesheets;
|
|
1263
|
+
${ /* template */0}.stylesheetScopeToken = stylesheetScopeToken;
|
|
1264
|
+
`);
|
|
1265
|
+
function addScopeTokenDeclarations(program, filename, namespace, componentName) {
|
|
1266
|
+
const { scopeToken } = templateCompiler.generateScopeTokens(filename, namespace, componentName);
|
|
1267
|
+
program.body.unshift(bStylesheetTokenDeclaration(builders.builders.literal(scopeToken)), bHasScopedStylesheetsDeclaration());
|
|
1268
|
+
program.body.push(...tmplAssignmentBlock(builders.builders.identifier('__lwcTmpl')));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/*
|
|
1272
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1273
|
+
* All rights reserved.
|
|
1274
|
+
* SPDX-License-Identifier: MIT
|
|
1275
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1276
|
+
*/
|
|
1277
|
+
function expressionIrToEs(node, cxt) {
|
|
1278
|
+
const isComplexTemplateExpressionEnabled = cxt.templateOptions.experimentalComplexExpressions &&
|
|
1279
|
+
shared.isAPIFeatureEnabled(11 /* APIFeature.ENABLE_COMPLEX_TEMPLATE_EXPRESSIONS */, cxt.templateOptions.apiVersion);
|
|
1280
|
+
return templateCompiler.bindExpression(node, (n) => cxt.isLocalVar(n.name), 'instance', isComplexTemplateExpressionEnabled);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Given an expression in a context, return an expression that may be scoped to that context.
|
|
1284
|
+
* For example, for the expression `foo`, it will typically be `instance.foo`, but if we're
|
|
1285
|
+
* inside a `for:each` block then the `foo` variable may refer to the scoped `foo`,
|
|
1286
|
+
* e.g. `<template for:each={foos} for:item="foo">`
|
|
1287
|
+
* @param expression
|
|
1288
|
+
* @param cxt
|
|
1289
|
+
*/
|
|
1290
|
+
function getScopedExpression(expression, cxt) {
|
|
1291
|
+
let scopeReferencedId = null;
|
|
1292
|
+
if (expression.type === 'MemberExpression') {
|
|
1293
|
+
// e.g. `foo.bar` -> scopeReferencedId is `foo`
|
|
1294
|
+
scopeReferencedId = getRootIdentifier$1(expression);
|
|
1295
|
+
}
|
|
1296
|
+
else if (expression.type === 'Identifier') {
|
|
1297
|
+
// e.g. `foo` -> scopeReferencedId is `foo`
|
|
1298
|
+
scopeReferencedId = expression;
|
|
1299
|
+
}
|
|
1300
|
+
if (scopeReferencedId === null && !cxt.templateOptions.experimentalComplexExpressions) {
|
|
1301
|
+
throw new Error(`Invalid expression, must be a MemberExpression or Identifier, found type="${expression.type}": \`${JSON.stringify(expression)}\``);
|
|
1302
|
+
}
|
|
1303
|
+
return cxt.isLocalVar(scopeReferencedId?.name)
|
|
1304
|
+
? expression
|
|
1305
|
+
: expressionIrToEs(expression, cxt);
|
|
1306
|
+
}
|
|
1307
|
+
function getRootMemberExpression$1(node) {
|
|
1308
|
+
return node.object.type === 'MemberExpression' ? getRootMemberExpression$1(node.object) : node;
|
|
1309
|
+
}
|
|
1310
|
+
function getRootIdentifier$1(node) {
|
|
1311
|
+
const rootMemberExpression = getRootMemberExpression$1(node);
|
|
1312
|
+
if (rootMemberExpression.object.type === 'Identifier') {
|
|
1313
|
+
return rootMemberExpression.object;
|
|
1314
|
+
}
|
|
1315
|
+
throw new Error(`Invalid expression, must be an Identifier, found type="${rootMemberExpression.type}": \`${JSON.stringify(rootMemberExpression)}\``);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/*
|
|
1319
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1320
|
+
* All rights reserved.
|
|
1321
|
+
* SPDX-License-Identifier: MIT
|
|
1322
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1323
|
+
*/
|
|
1324
|
+
const bYieldTernary = (esTemplateWithYield `yield ${estreeToolkit.is.expression} ? ${estreeToolkit.is.expression} : ${estreeToolkit.is.expression}`);
|
|
1325
|
+
const bOptimizedYield = (esTemplateWithYield `yield ${estreeToolkit.is.expression};`);
|
|
1326
|
+
function isOptimizableYield(stmt) {
|
|
1327
|
+
return (estreeToolkit.is.expressionStatement(stmt) &&
|
|
1328
|
+
estreeToolkit.is.yieldExpression(stmt.expression) &&
|
|
1329
|
+
stmt.expression.delegate === false);
|
|
1330
|
+
}
|
|
1331
|
+
/** Returns null if the statement cannot be optimized. */
|
|
1332
|
+
function optimizeSingleStatement(stmt) {
|
|
1333
|
+
if (estreeToolkit.is.blockStatement(stmt)) {
|
|
1334
|
+
// `if (cond) { ... }` => optimize inner yields and see if we can condense
|
|
1335
|
+
const optimizedBlock = optimizeAdjacentYieldStmts(stmt.body);
|
|
1336
|
+
// More than one statement cannot be optimized into a single yield
|
|
1337
|
+
if (optimizedBlock.length !== 1)
|
|
1338
|
+
return null;
|
|
1339
|
+
const [optimized] = optimizedBlock;
|
|
1340
|
+
return isOptimizableYield(optimized) ? optimized : null;
|
|
1341
|
+
}
|
|
1342
|
+
else if (estreeToolkit.is.expressionStatement(stmt)) {
|
|
1343
|
+
// `if (cond) expression` => just check if expression is a yield
|
|
1344
|
+
return estreeToolkit.is.yieldExpression(stmt) ? stmt : null;
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
// Can only optimize expression/block statements
|
|
1348
|
+
return null;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Tries to reduce if statements that only contain yields into a single yielded ternary
|
|
1353
|
+
* Returns null if the statement cannot be optimized.
|
|
1354
|
+
*/
|
|
1355
|
+
function optimizeIfStatement(stmt) {
|
|
1356
|
+
const consequent = optimizeSingleStatement(stmt.consequent)?.expression.argument;
|
|
1357
|
+
if (!consequent) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
const alternate = stmt.alternate
|
|
1361
|
+
? optimizeSingleStatement(stmt.alternate)?.expression.argument
|
|
1362
|
+
: estreeToolkit.builders.literal('');
|
|
1363
|
+
if (!alternate) {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
return bYieldTernary(stmt.test, consequent, alternate);
|
|
1367
|
+
}
|
|
1368
|
+
function optimizeAdjacentYieldStmts(statements) {
|
|
1369
|
+
return statements.reduce((result, stmt) => {
|
|
1370
|
+
if (estreeToolkit.is.ifStatement(stmt)) {
|
|
1371
|
+
const optimized = optimizeIfStatement(stmt);
|
|
1372
|
+
if (optimized) {
|
|
1373
|
+
stmt = optimized;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
const prev = result.at(-1);
|
|
1377
|
+
if (!isOptimizableYield(stmt) || !isOptimizableYield(prev)) {
|
|
1378
|
+
// nothing to do
|
|
1379
|
+
return [...result, stmt];
|
|
1380
|
+
}
|
|
1381
|
+
const arg = stmt.expression.argument;
|
|
1382
|
+
if (!arg || (estreeToolkit.is.literal(arg) && arg.value === '')) {
|
|
1383
|
+
// bare `yield` and `yield ""` amount to nothing, so we can drop them
|
|
1384
|
+
return result;
|
|
1385
|
+
}
|
|
1386
|
+
const newArg = immer.produce(prev.expression.argument, (draft) => {
|
|
1387
|
+
if (estreeToolkit.is.literal(arg) && typeof arg.value === 'string') {
|
|
1388
|
+
let concatTail = draft;
|
|
1389
|
+
while (estreeToolkit.is.binaryExpression(concatTail) && concatTail.operator === '+') {
|
|
1390
|
+
concatTail = concatTail.right;
|
|
1391
|
+
}
|
|
1392
|
+
if (estreeToolkit.is.literal(concatTail) && typeof concatTail.value === 'string') {
|
|
1393
|
+
// conat adjacent strings now, rather than at runtime
|
|
1394
|
+
concatTail.value += arg.value;
|
|
1395
|
+
return draft;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
// concat arbitrary values at runtime
|
|
1399
|
+
return estreeToolkit.builders.binaryExpression('+', draft, arg);
|
|
1400
|
+
});
|
|
1401
|
+
// replace the last `+` chain with a new one with the new arg
|
|
1402
|
+
return [...result.slice(0, -1), bOptimizedYield(newArg)];
|
|
1403
|
+
}, []);
|
|
1404
|
+
}
|
|
1405
|
+
function bAttributeValue(node, attrName) {
|
|
1406
|
+
if (!('attributes' in node)) {
|
|
1407
|
+
throw new TypeError(`Cannot get attribute value from ${node.type}`);
|
|
1408
|
+
}
|
|
1409
|
+
const nameAttrValue = node.attributes.find((attr) => attr.name === attrName)?.value;
|
|
1410
|
+
if (!nameAttrValue) {
|
|
1411
|
+
return estreeToolkit.builders.literal(null);
|
|
1412
|
+
}
|
|
1413
|
+
else if (nameAttrValue.type === 'Literal') {
|
|
1414
|
+
const name = typeof nameAttrValue.value === 'string' ? nameAttrValue.value : '';
|
|
1415
|
+
return estreeToolkit.builders.literal(name);
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
return estreeToolkit.builders.memberExpression(estreeToolkit.builders.literal('instance'), nameAttrValue);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function normalizeClassAttributeValue(value) {
|
|
1422
|
+
// @ts-expect-error weird indirection results in wrong overload being picked up
|
|
1423
|
+
return shared.StringReplace.call(shared.StringTrim.call(value), /\s+/g, ' ');
|
|
1424
|
+
}
|
|
1425
|
+
function getChildAttrsOrProps(attrs, cxt) {
|
|
1426
|
+
const objectAttrsOrProps = attrs
|
|
1427
|
+
.map(({ name, value, type }) => {
|
|
1428
|
+
// Babel function required to align identifier validation with babel-plugin-component: https://github.com/salesforce/lwc/issues/4826
|
|
1429
|
+
const key = types.isValidES3Identifier(name) ? estreeToolkit.builders.identifier(name) : estreeToolkit.builders.literal(name);
|
|
1430
|
+
const nameLower = name.toLowerCase();
|
|
1431
|
+
if (value.type === 'Literal' && typeof value.value === 'string') {
|
|
1432
|
+
let literalValue = value.value;
|
|
1433
|
+
if (name === 'style') {
|
|
1434
|
+
literalValue = shared.normalizeStyleAttributeValue(literalValue);
|
|
1435
|
+
}
|
|
1436
|
+
else if (name === 'class') {
|
|
1437
|
+
literalValue = normalizeClassAttributeValue(literalValue);
|
|
1438
|
+
if (literalValue === '') {
|
|
1439
|
+
return; // do not render empty `class=""`
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
else if (name === 'spellcheck') {
|
|
1443
|
+
// `spellcheck` string values are specially handled to massage them into booleans:
|
|
1444
|
+
// https://github.com/salesforce/lwc/blob/574ffbd/packages/%40lwc/template-compiler/src/codegen/index.ts#L445-L448
|
|
1445
|
+
literalValue = literalValue.toLowerCase() !== 'false';
|
|
1446
|
+
}
|
|
1447
|
+
else if (nameLower === 'tabindex') {
|
|
1448
|
+
// Global HTML "tabindex" attribute is specially massaged into a stringified number
|
|
1449
|
+
// This follows the historical behavior in api.ts:
|
|
1450
|
+
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
|
|
1451
|
+
literalValue = shared.normalizeTabIndex(literalValue);
|
|
1452
|
+
}
|
|
1453
|
+
return estreeToolkit.builders.property('init', key, estreeToolkit.builders.literal(literalValue));
|
|
1454
|
+
}
|
|
1455
|
+
else if (value.type === 'Literal' && typeof value.value === 'boolean') {
|
|
1456
|
+
if (name === 'class') {
|
|
1457
|
+
return; // do not render empty `class=""`
|
|
1458
|
+
}
|
|
1459
|
+
return estreeToolkit.builders.property('init', key, estreeToolkit.builders.literal(type === 'Attribute' ? '' : value.value));
|
|
1460
|
+
}
|
|
1461
|
+
else if (value.type === 'Identifier' || value.type === 'MemberExpression') {
|
|
1462
|
+
let propValue = expressionIrToEs(value, cxt);
|
|
1463
|
+
if (name === 'class') {
|
|
1464
|
+
cxt.import('normalizeClass');
|
|
1465
|
+
propValue = estreeToolkit.builders.callExpression(estreeToolkit.builders.identifier('normalizeClass'), [propValue]);
|
|
1466
|
+
}
|
|
1467
|
+
else if (nameLower === 'tabindex') {
|
|
1468
|
+
cxt.import('normalizeTabIndex');
|
|
1469
|
+
propValue = estreeToolkit.builders.callExpression(estreeToolkit.builders.identifier('normalizeTabIndex'), [propValue]);
|
|
1470
|
+
}
|
|
1471
|
+
return estreeToolkit.builders.property('init', key, propValue);
|
|
1472
|
+
}
|
|
1473
|
+
throw new Error(`Unimplemented child attr IR node type: ${value.type}`);
|
|
1474
|
+
})
|
|
1475
|
+
.filter(Boolean);
|
|
1476
|
+
return estreeToolkit.builders.objectExpression(objectAttrsOrProps);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Determine if the provided node is of type Literal
|
|
1480
|
+
* @param node
|
|
1481
|
+
*/
|
|
1482
|
+
function isLiteral(node) {
|
|
1483
|
+
return node.type === 'Literal';
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/*
|
|
1487
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
1488
|
+
* All rights reserved.
|
|
1489
|
+
* SPDX-License-Identifier: MIT
|
|
1490
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1491
|
+
*/
|
|
1492
|
+
const bNormalizeTextContent = (esTemplate `
|
|
1493
|
+
normalizeTextContent(${ /* string value */estreeToolkit.is.expression});
|
|
1494
|
+
`);
|
|
1495
|
+
const bYieldTextContent = (esTemplateWithYield `
|
|
1496
|
+
yield renderTextContent(${ /* text concatenation, possibly as binary expression */estreeToolkit.is.expression});
|
|
1497
|
+
`);
|
|
1498
|
+
/**
|
|
1499
|
+
* True if this is one of a series of text content nodes and/or comment node that are adjacent to one another as
|
|
1500
|
+
* siblings. (Comment nodes are ignored when preserve-comments is turned off.) This allows for adjacent text
|
|
1501
|
+
* node concatenation.
|
|
1502
|
+
*/
|
|
1503
|
+
const isConcatenatedNode = (node, cxt) => {
|
|
1504
|
+
switch (node.type) {
|
|
1505
|
+
case 'Text':
|
|
1506
|
+
return true;
|
|
1507
|
+
case 'Comment':
|
|
1508
|
+
return !cxt.templateOptions.preserveComments;
|
|
1509
|
+
default:
|
|
1510
|
+
return false;
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
const isLastConcatenatedNode = (cxt) => {
|
|
1514
|
+
const siblings = cxt.siblings;
|
|
1515
|
+
const currentNodeIndex = cxt.currentNodeIndex;
|
|
1516
|
+
const nextSibling = siblings[currentNodeIndex + 1];
|
|
1517
|
+
if (!nextSibling) {
|
|
1518
|
+
// we are the last sibling
|
|
1519
|
+
return true;
|
|
1520
|
+
}
|
|
1521
|
+
return !isConcatenatedNode(nextSibling, cxt);
|
|
1522
|
+
};
|
|
1523
|
+
function generateExpressionFromTextNode(node, cxt) {
|
|
1524
|
+
return isLiteral(node.value) ? builders.builders.literal(node.value.value) : expressionIrToEs(node.value, cxt);
|
|
1525
|
+
}
|
|
1526
|
+
function generateConcatenatedTextNodesExpressions(cxt) {
|
|
1527
|
+
const siblings = cxt.siblings;
|
|
1528
|
+
const currentNodeIndex = cxt.currentNodeIndex;
|
|
1529
|
+
const textNodes = [];
|
|
1530
|
+
for (let i = currentNodeIndex; i >= 0; i--) {
|
|
1531
|
+
const sibling = siblings[i];
|
|
1532
|
+
if (isConcatenatedNode(sibling, cxt)) {
|
|
1533
|
+
if (sibling.type === 'Text') {
|
|
1534
|
+
textNodes.unshift(sibling);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
// If we reach a non-Text/Comment node, we are done. These should not be concatenated
|
|
1539
|
+
// with sibling Text nodes separated by e.g. an Element:
|
|
1540
|
+
// {a}{b}<div></div>{c}{d}
|
|
1541
|
+
// In the above, {a} and {b} are concatenated, and {c} and {d} are concatenated,
|
|
1542
|
+
// but the `<div>` separates the two groups.
|
|
1543
|
+
break;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (!textNodes.length) {
|
|
1547
|
+
// Render nothing. This can occur if we hit a comment in non-preserveComments mode with no adjacent text nodes
|
|
1548
|
+
return [];
|
|
1549
|
+
}
|
|
1550
|
+
cxt.import(['normalizeTextContent', 'renderTextContent']);
|
|
1551
|
+
// Generate a binary expression to concatenate the text together. E.g.:
|
|
1552
|
+
// renderTextContent(
|
|
1553
|
+
// normalizeTextContent(a) +
|
|
1554
|
+
// normalizeTextContent(b) +
|
|
1555
|
+
// normalizeTextContent(c)
|
|
1556
|
+
// )
|
|
1557
|
+
const concatenatedExpression = textNodes
|
|
1558
|
+
.map((node) => bNormalizeTextContent(generateExpressionFromTextNode(node, cxt)))
|
|
1559
|
+
.reduce((accumulator, expression) => builders.builders.binaryExpression('+', accumulator, expression));
|
|
1560
|
+
return [bYieldTextContent(concatenatedExpression)];
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/*
|
|
1564
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1565
|
+
* All rights reserved.
|
|
1566
|
+
* SPDX-License-Identifier: MIT
|
|
1567
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1568
|
+
*/
|
|
1569
|
+
const Comment = function Comment(node, cxt) {
|
|
1570
|
+
if (cxt.templateOptions.preserveComments) {
|
|
1571
|
+
return [estreeToolkit.builders.expressionStatement(estreeToolkit.builders.yieldExpression(estreeToolkit.builders.literal(`<!--${node.value}-->`)))];
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
const isLastInSeries = isLastConcatenatedNode(cxt);
|
|
1575
|
+
// If preserve comments is off, we check if we should flush text content
|
|
1576
|
+
// for adjacent text nodes. (If preserve comments is on, then the previous
|
|
1577
|
+
// text node already flushed.)
|
|
1578
|
+
if (isLastInSeries) {
|
|
1579
|
+
return generateConcatenatedTextNodesExpressions(cxt);
|
|
1580
|
+
}
|
|
1581
|
+
return [];
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
/*
|
|
1586
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1587
|
+
* All rights reserved.
|
|
1588
|
+
* SPDX-License-Identifier: MIT
|
|
1589
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1590
|
+
*/
|
|
1591
|
+
/** Extends a validator to return `true` if the node is `null`. */
|
|
1592
|
+
function isNullableOf(validator) {
|
|
1593
|
+
const nullableValidator = (node) => {
|
|
1594
|
+
return node === null || validator(node);
|
|
1595
|
+
};
|
|
1596
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1597
|
+
nullableValidator.__debugName = `nullable(${validator.__debugName || validator.name || 'unknown validator'})`;
|
|
1598
|
+
}
|
|
1599
|
+
return nullableValidator;
|
|
1600
|
+
}
|
|
1601
|
+
isNullableOf.__debugName = 'isNullableOf';
|
|
1602
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
1603
|
+
// Modifying another package's exports is a code smell!
|
|
1604
|
+
for (const [key, val] of shared.entries(estreeToolkit.is)) {
|
|
1605
|
+
val.__debugName = key;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/*
|
|
1610
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1611
|
+
* All rights reserved.
|
|
1612
|
+
* SPDX-License-Identifier: MIT
|
|
1613
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1614
|
+
*/
|
|
1615
|
+
// This function will be defined once and hoisted to the top of the template function. It'll be
|
|
1616
|
+
// referenced deeper in the call stack where the function is called or passed as a parameter.
|
|
1617
|
+
// It is a higher-order function that curries local variables that may be referenced by the
|
|
1618
|
+
// shadow slot content.
|
|
1619
|
+
const bGenerateShadowSlottedContent = (esTemplateWithYield `
|
|
1620
|
+
const ${ /* function name */estreeToolkit.is.identifier} = (${ /* local vars */estreeToolkit.is.identifier}) => async function* ${ /* function name */0}(contextfulParent) {
|
|
1621
|
+
// The 'contextfulParent' variable is shadowed here so that a contextful relationship
|
|
1622
|
+
// is established between components rendered in slotted content & the "parent"
|
|
1623
|
+
// component that contains the <slot>.
|
|
1624
|
+
${ /* shadow slot content */estreeToolkit.is.statement}
|
|
1625
|
+
};
|
|
1626
|
+
`);
|
|
1627
|
+
// By passing in the set of local variables (which correspond 1:1 to the variables expected by
|
|
1628
|
+
// the referenced function), `shadowSlottedContent` will be curried function that can generate
|
|
1629
|
+
// shadow-slotted content.
|
|
1630
|
+
const bGenerateShadowSlottedContentRef = (esTemplateWithYield `
|
|
1631
|
+
const shadowSlottedContent = ${ /* reference to hoisted fn */estreeToolkit.is.identifier}(${ /* local vars */estreeToolkit.is.identifier});
|
|
1632
|
+
`);
|
|
1633
|
+
const bNullishGenerateShadowSlottedContent = (esTemplateWithYield `
|
|
1634
|
+
const shadowSlottedContent = null;
|
|
1635
|
+
`);
|
|
1636
|
+
const blightSlottedContentMap = (esTemplateWithYield `
|
|
1637
|
+
const ${ /* name of the content map */estreeToolkit.is.identifier} = Object.create(null);
|
|
1638
|
+
`);
|
|
1639
|
+
const bNullishLightSlottedContentMap = (esTemplateWithYield `
|
|
1640
|
+
const ${ /* name of the content map */estreeToolkit.is.identifier} = null;
|
|
1641
|
+
`);
|
|
1642
|
+
const bGenerateSlottedContent = (esTemplateWithYield `
|
|
1643
|
+
${ /* const shadowSlottedContent = ... */estreeToolkit.is.variableDeclaration}
|
|
1644
|
+
${ /* const lightSlottedContentMap */estreeToolkit.is.variableDeclaration}
|
|
1645
|
+
${ /* const scopedSlottedContentMap */estreeToolkit.is.variableDeclaration}
|
|
1646
|
+
${ /* light DOM addLightContent statements */estreeToolkit.is.expressionStatement}
|
|
1647
|
+
${ /* scoped slot addLightContent statements */estreeToolkit.is.expressionStatement}
|
|
1648
|
+
`);
|
|
1649
|
+
// Note that this function name (`__lwcGenerateSlottedContent`) does not need to be scoped even though
|
|
1650
|
+
// it may be repeated multiple times in the same scope, because it's a function _expression_ rather
|
|
1651
|
+
// than a function _declaration_, so it isn't available to be referenced anywhere.
|
|
1652
|
+
const bAddSlottedContent = (esTemplate `
|
|
1653
|
+
addSlottedContent(
|
|
1654
|
+
${ /* slot name */estreeToolkit.is.expression} ?? "",
|
|
1655
|
+
async function* __lwcGenerateSlottedContent(
|
|
1656
|
+
contextfulParent,
|
|
1657
|
+
${ /* scoped slot data variable */isNullableOf(estreeToolkit.is.identifier)},
|
|
1658
|
+
slotAttributeValue)
|
|
1659
|
+
{
|
|
1660
|
+
${ /* slot content */estreeToolkit.is.statement}
|
|
1661
|
+
},
|
|
1662
|
+
${ /* content map */estreeToolkit.is.identifier}
|
|
1663
|
+
);
|
|
1664
|
+
`);
|
|
1665
|
+
function getShadowSlottedContent(slottableChildren, cxt) {
|
|
1666
|
+
return optimizeAdjacentYieldStmts(irChildrenToEs(slottableChildren, cxt, (child) => {
|
|
1667
|
+
const { isSlotted } = cxt;
|
|
1668
|
+
if (child.type === 'ExternalComponent' || child.type === 'Element') {
|
|
1669
|
+
cxt.isSlotted = false;
|
|
1670
|
+
}
|
|
1671
|
+
// cleanup function
|
|
1672
|
+
return () => {
|
|
1673
|
+
cxt.isSlotted = isSlotted;
|
|
1674
|
+
};
|
|
1675
|
+
}));
|
|
1676
|
+
}
|
|
1677
|
+
// Light DOM slots are a bit complex because of needing to handle slots _not_ at the top level
|
|
1678
|
+
// At the non-top level, it matters what the ancestors are. These are relevant to slots:
|
|
1679
|
+
// - If (`if:true`, `if:false`)
|
|
1680
|
+
// - IfBlock/ElseBlock/ElseifBlock (`lwc:if`, `lwc:elseif`, `lwc:else`)
|
|
1681
|
+
// Whereas anything else breaks the relationship between the slotted content and the containing
|
|
1682
|
+
// Component (e.g. another Component/ExternalComponent) or is disallowed (e.g. ForEach/ForOf).
|
|
1683
|
+
// Then there are the leaf nodes, which _may_ have a `slot` attribute on them:
|
|
1684
|
+
// - Element/Text/Component/ExternalComponent (e.g. `<div>`, `<x-foo>`)
|
|
1685
|
+
// Once you reach a leaf, you know what content should be rendered for a given slot name. But you
|
|
1686
|
+
// also need to consider all of its ancestors, which may cause the slot content to be conditionally
|
|
1687
|
+
// rendered (e.g. IfBlock/ElseBlock).
|
|
1688
|
+
// For example:
|
|
1689
|
+
// <x-foo>
|
|
1690
|
+
// <template lwc:if={darkTheme}>
|
|
1691
|
+
// <div slot="footer"></div>
|
|
1692
|
+
// </template>
|
|
1693
|
+
// <template lwc:else>
|
|
1694
|
+
// yolo
|
|
1695
|
+
// </template>
|
|
1696
|
+
// </x-foo>
|
|
1697
|
+
// In this example, we render the `<div>` into the `footer` slot, if `darkTheme` is true.
|
|
1698
|
+
// Otherwise, we will render the text node `yolo` into the default slot.
|
|
1699
|
+
// The goal here is to traverse through the tree and identify all unique `slot` attribute names
|
|
1700
|
+
// and group those into AST trees on a per-`slot` name basis, only for leafs/ancestors that are
|
|
1701
|
+
// relevant to slots (as mentioned above).
|
|
1702
|
+
function getLightSlottedContent(rootNodes, cxt) {
|
|
1703
|
+
const results = [];
|
|
1704
|
+
// For the given slot name, get the EsExpressions we should use to render it
|
|
1705
|
+
// The ancestorIndices is an array of integers referring to the chain of ancestors
|
|
1706
|
+
// and their positions in the child arrays of their own parents
|
|
1707
|
+
const addLightDomSlotContent = (slotName, ancestorIndices) => {
|
|
1708
|
+
const clone = immer.produce(rootNodes[ancestorIndices[0]], (draft) => {
|
|
1709
|
+
// Create a clone of the AST with only the ancestors and no other siblings
|
|
1710
|
+
let current = draft;
|
|
1711
|
+
for (let i = 1; i < ancestorIndices.length; i++) {
|
|
1712
|
+
const nextIndex = ancestorIndices[i];
|
|
1713
|
+
// If i >= 1 then the current must necessarily be a SlottableAncestorIrType
|
|
1714
|
+
const next = current.children[nextIndex];
|
|
1715
|
+
current.children = [next];
|
|
1716
|
+
current = next;
|
|
1717
|
+
}
|
|
1718
|
+
// The leaf must necessarily be a SlottableLeafIrType
|
|
1719
|
+
const leaf = current;
|
|
1720
|
+
// Light DOM slots do not actually render the `slot` attribute.
|
|
1721
|
+
if (leaf.type !== 'Text') {
|
|
1722
|
+
leaf.attributes = leaf.attributes.filter((attr) => attr.name !== 'slot');
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
const { isSlotted: originalIsSlotted } = cxt;
|
|
1726
|
+
cxt.isSlotted = ancestorIndices.length > 1 || clone.type === 'Slot';
|
|
1727
|
+
const slotContent = optimizeAdjacentYieldStmts(irToEs(clone, cxt));
|
|
1728
|
+
cxt.isSlotted = originalIsSlotted;
|
|
1729
|
+
results.push(estreeToolkit.builders.expressionStatement(bAddSlottedContent(slotName, null, slotContent, estreeToolkit.builders.identifier('lightSlottedContentMap'))));
|
|
1730
|
+
};
|
|
1731
|
+
const traverse = (nodes, ancestorIndices) => {
|
|
1732
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1733
|
+
// must set the siblings inside the for loop due to nested children
|
|
1734
|
+
cxt.siblings = nodes;
|
|
1735
|
+
cxt.currentNodeIndex = i;
|
|
1736
|
+
const node = nodes[i];
|
|
1737
|
+
switch (node.type) {
|
|
1738
|
+
// SlottableAncestorIrType
|
|
1739
|
+
case 'If':
|
|
1740
|
+
case 'IfBlock':
|
|
1741
|
+
case 'ElseifBlock':
|
|
1742
|
+
case 'ElseBlock': {
|
|
1743
|
+
traverse(node.children, [...ancestorIndices, i]);
|
|
1744
|
+
break;
|
|
1745
|
+
}
|
|
1746
|
+
// SlottableLeafIrType
|
|
1747
|
+
case 'Slot':
|
|
1748
|
+
case 'Element':
|
|
1749
|
+
case 'Text':
|
|
1750
|
+
case 'Component':
|
|
1751
|
+
case 'ExternalComponent': {
|
|
1752
|
+
// '' is the default slot name. Text nodes are always slotted into the default slot
|
|
1753
|
+
const slotName = node.type === 'Text' ? estreeToolkit.builders.literal('') : bAttributeValue(node, 'slot');
|
|
1754
|
+
// For concatenated adjacent text nodes, for any but the final text node, we
|
|
1755
|
+
// should skip them and let the final text node take care of rendering its siblings
|
|
1756
|
+
if (node.type === 'Text' && !isLastConcatenatedNode(cxt)) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
addLightDomSlotContent(slotName, [...ancestorIndices, i]);
|
|
1760
|
+
break;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
// reset the context
|
|
1765
|
+
cxt.siblings = undefined;
|
|
1766
|
+
cxt.currentNodeIndex = undefined;
|
|
1767
|
+
};
|
|
1768
|
+
traverse(rootNodes, []);
|
|
1769
|
+
return results;
|
|
1770
|
+
}
|
|
1771
|
+
function getSlottedContent(node, cxt) {
|
|
1772
|
+
const { isSlotted } = cxt;
|
|
1773
|
+
cxt.isSlotted = true;
|
|
1774
|
+
// Anything inside the slotted content is a normal slotted content except for `<template lwc:slot-data>` which is a scoped slot.
|
|
1775
|
+
const slottableChildren = node.children.filter((child) => child.type !== 'ScopedSlotFragment');
|
|
1776
|
+
const scopedSlottableChildren = node.children.filter((child) => child.type === 'ScopedSlotFragment');
|
|
1777
|
+
const shadowSlotContent = getShadowSlottedContent(slottableChildren, cxt);
|
|
1778
|
+
const lightSlotContent = getLightSlottedContent(slottableChildren, cxt);
|
|
1779
|
+
const scopedSlotContent = scopedSlottableChildren.map((child) => {
|
|
1780
|
+
const boundVariableName = child.slotData.value.name;
|
|
1781
|
+
const boundVariable = estreeToolkit.builders.identifier(boundVariableName);
|
|
1782
|
+
cxt.pushLocalVars([boundVariableName]);
|
|
1783
|
+
const slotName = isLiteral(child.slotName)
|
|
1784
|
+
? estreeToolkit.builders.literal(child.slotName.value)
|
|
1785
|
+
: expressionIrToEs(child.slotName, cxt);
|
|
1786
|
+
// TODO [#4768]: what if the bound variable is `generateMarkup` or some framework-specific identifier?
|
|
1787
|
+
const addLightContentExpr = estreeToolkit.builders.expressionStatement(bAddSlottedContent(slotName, boundVariable, optimizeAdjacentYieldStmts(irChildrenToEs(child.children, cxt)), estreeToolkit.builders.identifier('scopedSlottedContentMap')));
|
|
1788
|
+
cxt.popLocalVars();
|
|
1789
|
+
return addLightContentExpr;
|
|
1790
|
+
});
|
|
1791
|
+
const hasShadowSlottedContent = shadowSlotContent.length > 0;
|
|
1792
|
+
const hasLightSlottedContent = lightSlotContent.length > 0;
|
|
1793
|
+
const hasScopedSlottedContent = scopedSlotContent.length > 0;
|
|
1794
|
+
cxt.isSlotted = isSlotted;
|
|
1795
|
+
if (hasShadowSlottedContent || hasLightSlottedContent || hasScopedSlottedContent) {
|
|
1796
|
+
cxt.import('addSlottedContent');
|
|
1797
|
+
}
|
|
1798
|
+
// Elsewhere, nodes and their subtrees are cloned. This design decision means that
|
|
1799
|
+
// the node objects themselves cannot be used as unique identifiers (e.g. as keys
|
|
1800
|
+
// in a map). However, for a given template, a node's location information does
|
|
1801
|
+
// uniquely identify that node.
|
|
1802
|
+
const uniqueNodeId = `${node.name}:${node.location.start}:${node.location.end}`;
|
|
1803
|
+
const localVars = cxt.getLocalVars();
|
|
1804
|
+
const localVarIds = localVars.map(estreeToolkit.builders.identifier);
|
|
1805
|
+
if (hasShadowSlottedContent && !cxt.slots.shadow.isDuplicate(uniqueNodeId)) {
|
|
1806
|
+
// Colon characters in <lwc:component> element name will result in an invalid
|
|
1807
|
+
// JavaScript identifier if not otherwise accounted for.
|
|
1808
|
+
const kebabCmpName = shared.kebabCaseToCamelCase(node.name).replace(':', '_');
|
|
1809
|
+
const shadowSlotContentFnName = cxt.slots.shadow.register(uniqueNodeId, kebabCmpName);
|
|
1810
|
+
const shadowSlottedContentFn = bGenerateShadowSlottedContent(estreeToolkit.builders.identifier(shadowSlotContentFnName),
|
|
1811
|
+
// If the slot-fn were defined here instead of hoisted to the top of the module,
|
|
1812
|
+
// the local variables (e.g. from for:each) would be closed-over. When hoisted,
|
|
1813
|
+
// however, we need to curry these variables.
|
|
1814
|
+
localVarIds, shadowSlotContent);
|
|
1815
|
+
cxt.hoist.templateFn(shadowSlottedContentFn, node);
|
|
1816
|
+
}
|
|
1817
|
+
const shadowSlottedContentFn = hasShadowSlottedContent
|
|
1818
|
+
? bGenerateShadowSlottedContentRef(estreeToolkit.builders.identifier(cxt.slots.shadow.getFnName(uniqueNodeId)), localVarIds)
|
|
1819
|
+
: bNullishGenerateShadowSlottedContent();
|
|
1820
|
+
const lightSlottedContentMap = hasLightSlottedContent
|
|
1821
|
+
? blightSlottedContentMap(estreeToolkit.builders.identifier('lightSlottedContentMap'))
|
|
1822
|
+
: bNullishLightSlottedContentMap(estreeToolkit.builders.identifier('lightSlottedContentMap'));
|
|
1823
|
+
const scopedSlottedContentMap = hasScopedSlottedContent
|
|
1824
|
+
? blightSlottedContentMap(estreeToolkit.builders.identifier('scopedSlottedContentMap'))
|
|
1825
|
+
: bNullishLightSlottedContentMap(estreeToolkit.builders.identifier('scopedSlottedContentMap'));
|
|
1826
|
+
return bGenerateSlottedContent(shadowSlottedContentFn, lightSlottedContentMap, scopedSlottedContentMap, lightSlotContent, scopedSlotContent);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/*
|
|
1830
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1831
|
+
* All rights reserved.
|
|
1832
|
+
* SPDX-License-Identifier: MIT
|
|
1833
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1834
|
+
*/
|
|
1835
|
+
const bYieldFromChildGenerator = (esTemplateWithYield `
|
|
1836
|
+
{
|
|
1837
|
+
const childProps = ${ /* child props */estreeToolkit.is.objectExpression};
|
|
1838
|
+
const childAttrs = ${ /* child attrs */estreeToolkit.is.objectExpression};
|
|
1839
|
+
/*
|
|
1840
|
+
If 'slotAttributeValue' is set, it references a slot that does not exist, and the 'slot' attribute should be set in the DOM. This behavior aligns with engine-server and engine-dom.
|
|
1841
|
+
See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example case.
|
|
1842
|
+
*/
|
|
1843
|
+
if (slotAttributeValue) {
|
|
1844
|
+
childAttrs.slot = slotAttributeValue;
|
|
1845
|
+
}
|
|
1846
|
+
${
|
|
1847
|
+
/*
|
|
1848
|
+
Slotted content is inserted here.
|
|
1849
|
+
Note that the slotted content will be stored in variables named
|
|
1850
|
+
`shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below
|
|
1851
|
+
when the child's generateMarkup function is invoked.
|
|
1852
|
+
*/
|
|
1853
|
+
estreeToolkit.is.statement}
|
|
1854
|
+
|
|
1855
|
+
const scopeToken = hasScopedStylesheets ? stylesheetScopeToken : undefined;
|
|
1856
|
+
const generateMarkup = ${ /* Component */estreeToolkit.is.identifier}[__SYMBOL__GENERATE_MARKUP];
|
|
1857
|
+
const tagName = ${ /* tag name */estreeToolkit.is.literal};
|
|
1858
|
+
|
|
1859
|
+
if (generateMarkup) {
|
|
1860
|
+
yield* generateMarkup(
|
|
1861
|
+
tagName,
|
|
1862
|
+
childProps,
|
|
1863
|
+
childAttrs,
|
|
1864
|
+
instance,
|
|
1865
|
+
scopeToken,
|
|
1866
|
+
contextfulParent,
|
|
1867
|
+
renderContext,
|
|
1868
|
+
shadowSlottedContent,
|
|
1869
|
+
lightSlottedContentMap,
|
|
1870
|
+
scopedSlottedContentMap
|
|
1871
|
+
);
|
|
1872
|
+
} else {
|
|
1873
|
+
yield \`<\${tagName}>\`;
|
|
1874
|
+
yield* __fallbackTmpl(shadowSlottedContent, lightSlottedContentMap, scopedSlottedContentMap, ${ /* Component */3}, instance, renderContext)
|
|
1875
|
+
yield \`</\${tagName}>\`;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
`);
|
|
1879
|
+
const Component = function Component(node, cxt) {
|
|
1880
|
+
// Import the custom component's generateMarkup export.
|
|
1881
|
+
const childComponentLocalName = `ChildComponentCtor_${templateCompiler.toPropertyName(node.name)}`;
|
|
1882
|
+
const importPath = templateCompiler.kebabcaseToCamelcase(node.name);
|
|
1883
|
+
cxt.import({ default: childComponentLocalName }, importPath);
|
|
1884
|
+
cxt.import({
|
|
1885
|
+
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
|
|
1886
|
+
fallbackTmpl: '__fallbackTmpl',
|
|
1887
|
+
});
|
|
1888
|
+
const childTagName = node.name;
|
|
1889
|
+
return [
|
|
1890
|
+
bYieldFromChildGenerator(getChildAttrsOrProps(node.properties, cxt), getChildAttrsOrProps(node.attributes, cxt), getSlottedContent(node, cxt), estreeToolkit.builders.identifier(childComponentLocalName), estreeToolkit.builders.literal(childTagName)),
|
|
1891
|
+
];
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
/*
|
|
1895
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1896
|
+
* All rights reserved.
|
|
1897
|
+
* SPDX-License-Identifier: MIT
|
|
1898
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1899
|
+
*/
|
|
1900
|
+
const bYieldFromDynamicComponentConstructorGenerator = (esTemplateWithYield `
|
|
1901
|
+
const Ctor = '${ /* lwcIs attribute value */estreeToolkit.is.expression}';
|
|
1902
|
+
if (Ctor) {
|
|
1903
|
+
if (typeof Ctor !== 'function' || !(Ctor.prototype instanceof LightningElement)) {
|
|
1904
|
+
throw new Error(\`Invalid constructor: "\${String(Ctor)}" is not a LightningElement constructor.\`)
|
|
1905
|
+
}
|
|
1906
|
+
const childProps = ${ /* child props */estreeToolkit.is.objectExpression};
|
|
1907
|
+
const childAttrs = ${ /* child attrs */estreeToolkit.is.objectExpression};
|
|
1908
|
+
/*
|
|
1909
|
+
If 'slotAttributeValue' is set, it references a slot that does not exist, and the 'slot' attribute should be set in the DOM. This behavior aligns with engine-server and engine-dom.
|
|
1910
|
+
See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example case.
|
|
1911
|
+
*/
|
|
1912
|
+
if (slotAttributeValue) {
|
|
1913
|
+
childAttrs.slot = slotAttributeValue;
|
|
1914
|
+
}
|
|
1915
|
+
${
|
|
1916
|
+
/*
|
|
1917
|
+
Slotted content is inserted here.
|
|
1918
|
+
Note that the slotted content will be stored in variables named
|
|
1919
|
+
`shadowSlottedContent`/`lightSlottedContentMap / scopedSlottedContentMap` which are used below
|
|
1920
|
+
when the child's generateMarkup function is invoked.
|
|
1921
|
+
*/
|
|
1922
|
+
estreeToolkit.is.statement}
|
|
1923
|
+
|
|
1924
|
+
const scopeToken = hasScopedStylesheets ? stylesheetScopeToken : undefined;
|
|
1925
|
+
|
|
1926
|
+
yield* Ctor[__SYMBOL__GENERATE_MARKUP](
|
|
1927
|
+
null,
|
|
1928
|
+
childProps,
|
|
1929
|
+
childAttrs,
|
|
1930
|
+
instance,
|
|
1931
|
+
scopeToken,
|
|
1932
|
+
contextfulParent,
|
|
1933
|
+
renderContext,
|
|
1934
|
+
shadowSlottedContent,
|
|
1935
|
+
lightSlottedContentMap,
|
|
1936
|
+
scopedSlottedContentMap
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
`);
|
|
1940
|
+
const LwcComponent = function LwcComponent(node, cxt) {
|
|
1941
|
+
const { directives } = node;
|
|
1942
|
+
const lwcIs = directives.find((directive) => directive.name === 'Is');
|
|
1943
|
+
if (!shared.isUndefined(lwcIs)) {
|
|
1944
|
+
cxt.import({
|
|
1945
|
+
LightningElement: undefined,
|
|
1946
|
+
SYMBOL__GENERATE_MARKUP: '__SYMBOL__GENERATE_MARKUP',
|
|
1947
|
+
});
|
|
1948
|
+
return bYieldFromDynamicComponentConstructorGenerator(
|
|
1949
|
+
// The template compiler has validation to prevent lwcIs.value from being a literal
|
|
1950
|
+
expressionIrToEs(lwcIs.value, cxt), getChildAttrsOrProps(node.properties, cxt), getChildAttrsOrProps(node.attributes, cxt), getSlottedContent(node, cxt));
|
|
1951
|
+
}
|
|
1952
|
+
else {
|
|
1953
|
+
return [];
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
/*
|
|
1958
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
1959
|
+
* All rights reserved.
|
|
1960
|
+
* SPDX-License-Identifier: MIT
|
|
1961
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
1962
|
+
*/
|
|
1963
|
+
const bYield = (expr) => estreeToolkit.builders.expressionStatement(estreeToolkit.builders.yieldExpression(expr));
|
|
1964
|
+
// TODO [#4714]: scope token renders as a suffix for literals, but prefix for expressions
|
|
1965
|
+
const bYieldDynamicValue = (esTemplateWithYield `
|
|
1966
|
+
{
|
|
1967
|
+
const attrName = ${ /* attribute name */estreeToolkit.is.literal};
|
|
1968
|
+
let attrValue = ${ /* attribute value expression */estreeToolkit.is.expression};
|
|
1969
|
+
const isHtmlBooleanAttr = ${ /* isHtmlBooleanAttr */estreeToolkit.is.literal};
|
|
1970
|
+
|
|
1971
|
+
// Global HTML boolean attributes are specially coerced into booleans
|
|
1972
|
+
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/template-compiler/src/codegen/index.ts#L450-L454
|
|
1973
|
+
if (isHtmlBooleanAttr) {
|
|
1974
|
+
attrValue = attrValue ? '' : undefined;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// Global HTML "tabindex" attribute is specially massaged into a stringified number
|
|
1978
|
+
// This follows the historical behavior in api.ts:
|
|
1979
|
+
// https://github.com/salesforce/lwc/blob/f34a347/packages/%40lwc/engine-core/src/framework/api.ts#L193-L211
|
|
1980
|
+
if (attrName === 'tabindex') {
|
|
1981
|
+
const shouldNormalize = attrValue > 0 && typeof attrValue !== 'boolean';
|
|
1982
|
+
attrValue = shouldNormalize ? 0 : attrValue;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// Backwards compatibility with historical patchStyleAttribute() behavior:
|
|
1986
|
+
// https://github.com/salesforce/lwc/blob/59e2c6c/packages/%40lwc/engine-core/src/framework/modules/computed-style-attr.ts#L40
|
|
1987
|
+
if (attrName === 'style' && (typeof attrValue !== 'string' || attrValue === '')) {
|
|
1988
|
+
attrValue = undefined;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (attrValue !== undefined && attrValue !== null) {
|
|
1992
|
+
yield ' ' + attrName;
|
|
1993
|
+
|
|
1994
|
+
if (attrValue !== '') {
|
|
1995
|
+
yield \`="\${htmlEscape(String(attrValue), true)}"\`;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
`);
|
|
2000
|
+
const bYieldClassDynamicValue = (esTemplateWithYield `
|
|
2001
|
+
{
|
|
2002
|
+
const attrValue = normalizeClass(${ /* attribute value expression */estreeToolkit.is.expression});
|
|
2003
|
+
const shouldRenderScopeToken = hasScopedStylesheets || hasScopedStaticStylesheets(Cmp);
|
|
2004
|
+
|
|
2005
|
+
// Concatenate the scope token with the class attribute value as necessary.
|
|
2006
|
+
// If either is missing, render the other alone.
|
|
2007
|
+
let combinedValue = shouldRenderScopeToken ? stylesheetScopeToken : '';
|
|
2008
|
+
if (attrValue) {
|
|
2009
|
+
if (combinedValue) {
|
|
2010
|
+
combinedValue += ' ';
|
|
2011
|
+
}
|
|
2012
|
+
combinedValue += htmlEscape(String(attrValue), true);
|
|
2013
|
+
}
|
|
2014
|
+
if (combinedValue) {
|
|
2015
|
+
yield \` class="\${combinedValue}"\`;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
`);
|
|
2019
|
+
// TODO [#4714]: scope token renders as a suffix for literals, but prefix for expressions
|
|
2020
|
+
const bStringLiteralYield = (esTemplateWithYield `
|
|
2021
|
+
{
|
|
2022
|
+
const attrName = ${ /* attribute name */estreeToolkit.is.literal}
|
|
2023
|
+
const attrValue = ${ /* attribute value */estreeToolkit.is.literal};
|
|
2024
|
+
|
|
2025
|
+
const shouldRenderScopeToken = attrName === 'class' &&
|
|
2026
|
+
(hasScopedStylesheets || hasScopedStaticStylesheets(Cmp));
|
|
2027
|
+
const suffix = shouldRenderScopeToken ? ' ' + stylesheetScopeToken : '';
|
|
2028
|
+
|
|
2029
|
+
yield ' ' + attrName;
|
|
2030
|
+
if (attrValue !== '' || shouldRenderScopeToken) {
|
|
2031
|
+
yield '="' + attrValue + suffix + '"';
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
}
|
|
2035
|
+
`);
|
|
2036
|
+
const bConditionallyYieldScopeTokenClass = (esTemplateWithYield `
|
|
2037
|
+
if (hasScopedStylesheets || hasScopedStaticStylesheets(Cmp)) {
|
|
2038
|
+
yield \` class="\${stylesheetScopeToken}"\`;
|
|
2039
|
+
}
|
|
2040
|
+
`);
|
|
2041
|
+
/*
|
|
2042
|
+
If `slotAttributeValue` is set, it references a slot that does not exist, and the `slot` attribute should be set in the DOM. This behavior aligns with engine-server and engine-dom.
|
|
2043
|
+
See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example case.
|
|
2044
|
+
*/
|
|
2045
|
+
const bConditionallyYieldDanglingSlotName = (esTemplateWithYield `
|
|
2046
|
+
if (slotAttributeValue) {
|
|
2047
|
+
yield \` slot="\${slotAttributeValue}"\`;
|
|
2048
|
+
}
|
|
2049
|
+
`);
|
|
2050
|
+
const bYieldSanitizedHtml = esTemplateWithYield `
|
|
2051
|
+
yield sanitizeHtmlContent(${ /* lwc:inner-html content */estreeToolkit.is.expression})
|
|
2052
|
+
`;
|
|
2053
|
+
function yieldAttrOrPropLiteralValue(name, valueNode) {
|
|
2054
|
+
const { value, type } = valueNode;
|
|
2055
|
+
if (typeof value === 'string') {
|
|
2056
|
+
let yieldedValue;
|
|
2057
|
+
if (name === 'style') {
|
|
2058
|
+
yieldedValue = shared.normalizeStyleAttributeValue(value);
|
|
2059
|
+
}
|
|
2060
|
+
else if (name === 'class') {
|
|
2061
|
+
yieldedValue = normalizeClassAttributeValue(value);
|
|
2062
|
+
if (yieldedValue === '') {
|
|
2063
|
+
return [];
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
else if (name === 'spellcheck') {
|
|
2067
|
+
// `spellcheck` string values are specially handled to massage them into booleans.
|
|
2068
|
+
// https://github.com/salesforce/lwc/blob/fe4e95f/packages/%40lwc/template-compiler/src/codegen/index.ts#L445-L448
|
|
2069
|
+
yieldedValue = String(value.toLowerCase() !== 'false');
|
|
2070
|
+
}
|
|
2071
|
+
else {
|
|
2072
|
+
yieldedValue = value;
|
|
2073
|
+
}
|
|
2074
|
+
return [bStringLiteralYield(estreeToolkit.builders.literal(name), estreeToolkit.builders.literal(yieldedValue))];
|
|
2075
|
+
}
|
|
2076
|
+
else if (typeof value === 'boolean') {
|
|
2077
|
+
if (name === 'class') {
|
|
2078
|
+
return [];
|
|
2079
|
+
}
|
|
2080
|
+
return [bYield(estreeToolkit.builders.literal(` ${name}`))];
|
|
2081
|
+
}
|
|
2082
|
+
throw new Error(`Unknown attr/prop literal: ${type}`);
|
|
2083
|
+
}
|
|
2084
|
+
function yieldAttrOrPropDynamicValue(elementName, name, value, cxt) {
|
|
2085
|
+
cxt.import('htmlEscape');
|
|
2086
|
+
const scopedExpression = getScopedExpression(value, cxt);
|
|
2087
|
+
switch (name) {
|
|
2088
|
+
case 'class':
|
|
2089
|
+
cxt.import('normalizeClass');
|
|
2090
|
+
return [bYieldClassDynamicValue(scopedExpression)];
|
|
2091
|
+
default:
|
|
2092
|
+
return [
|
|
2093
|
+
bYieldDynamicValue(estreeToolkit.builders.literal(name), scopedExpression, estreeToolkit.builders.literal(shared.isBooleanAttribute(name, elementName))),
|
|
2094
|
+
];
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
function reorderAttributes(attrs, props) {
|
|
2098
|
+
let classAttr = null;
|
|
2099
|
+
let styleAttr = null;
|
|
2100
|
+
let slotAttr = null;
|
|
2101
|
+
const boringAttrs = attrs.filter((attr) => {
|
|
2102
|
+
if (attr.name === 'class') {
|
|
2103
|
+
classAttr = attr;
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
else if (attr.name === 'style') {
|
|
2107
|
+
styleAttr = attr;
|
|
2108
|
+
return false;
|
|
2109
|
+
}
|
|
2110
|
+
else if (attr.name === 'slot') {
|
|
2111
|
+
slotAttr = attr;
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
return true;
|
|
2115
|
+
});
|
|
2116
|
+
return [classAttr, styleAttr, ...boringAttrs, ...props, slotAttr].filter((el) => el !== null);
|
|
2117
|
+
}
|
|
2118
|
+
const Element = function Element(node, cxt) {
|
|
2119
|
+
const innerHtmlDirective = node.type === 'Element' && node.directives.find((dir) => dir.name === 'InnerHTML');
|
|
2120
|
+
const attrsAndProps = reorderAttributes(node.attributes, node.properties);
|
|
2121
|
+
let hasClassAttribute = false;
|
|
2122
|
+
const yieldAttrsAndProps = attrsAndProps
|
|
2123
|
+
.filter(({ name }) => {
|
|
2124
|
+
// `<input checked>`/`<input value>` is treated as a property, not an attribute,
|
|
2125
|
+
// so should never be SSR'd. See https://github.com/salesforce/lwc/issues/4763
|
|
2126
|
+
return !(node.name === 'input' && (name === 'value' || name === 'checked'));
|
|
2127
|
+
})
|
|
2128
|
+
.flatMap(({ name, value, type }) => {
|
|
2129
|
+
if (type === 'Attribute' && (name === 'inner-h-t-m-l' || name === 'outer-h-t-m-l')) {
|
|
2130
|
+
throw new Error(`Cannot set attribute "${name}" on <${node.name}>.`);
|
|
2131
|
+
}
|
|
2132
|
+
let result;
|
|
2133
|
+
if (value.type === 'Literal') {
|
|
2134
|
+
result = yieldAttrOrPropLiteralValue(name, value);
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
result = yieldAttrOrPropDynamicValue(node.name, name, value, cxt);
|
|
2138
|
+
}
|
|
2139
|
+
if (result.length > 0 && name === 'class') {
|
|
2140
|
+
// actually yielded a class attribute value
|
|
2141
|
+
hasClassAttribute = true;
|
|
2142
|
+
}
|
|
2143
|
+
return result;
|
|
2144
|
+
});
|
|
2145
|
+
let childContent;
|
|
2146
|
+
// An element can have children or lwc:inner-html, but not both
|
|
2147
|
+
// If it has both, the template compiler will throw an error before reaching here
|
|
2148
|
+
if (node.children.length) {
|
|
2149
|
+
childContent = irChildrenToEs(node.children, cxt);
|
|
2150
|
+
}
|
|
2151
|
+
else if (innerHtmlDirective) {
|
|
2152
|
+
const value = innerHtmlDirective.value;
|
|
2153
|
+
const unsanitizedHtmlExpression = value.type === 'Literal' ? estreeToolkit.builders.literal(value.value) : expressionIrToEs(value, cxt);
|
|
2154
|
+
childContent = [bYieldSanitizedHtml(unsanitizedHtmlExpression)];
|
|
2155
|
+
cxt.import('sanitizeHtmlContent');
|
|
2156
|
+
}
|
|
2157
|
+
else {
|
|
2158
|
+
childContent = [];
|
|
2159
|
+
}
|
|
2160
|
+
const isForeignSelfClosingElement = node.namespace !== shared.HTML_NAMESPACE && childContent.length === 0;
|
|
2161
|
+
const isSelfClosingElement = shared.isVoidElement(node.name, shared.HTML_NAMESPACE) || isForeignSelfClosingElement;
|
|
2162
|
+
return [
|
|
2163
|
+
bYield(estreeToolkit.builders.literal(`<${node.name}`)),
|
|
2164
|
+
bConditionallyYieldDanglingSlotName(),
|
|
2165
|
+
// If we haven't already prefixed the scope token to an existing class, add an explicit class here
|
|
2166
|
+
...(hasClassAttribute ? [] : [bConditionallyYieldScopeTokenClass()]),
|
|
2167
|
+
...yieldAttrsAndProps,
|
|
2168
|
+
bYield(estreeToolkit.builders.literal(isForeignSelfClosingElement ? `/>` : `>`)),
|
|
2169
|
+
...(isSelfClosingElement ? [] : [...childContent, bYield(estreeToolkit.builders.literal(`</${node.name}>`))]),
|
|
2170
|
+
].filter(Boolean);
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
/*
|
|
2174
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2175
|
+
* All rights reserved.
|
|
2176
|
+
* SPDX-License-Identifier: MIT
|
|
2177
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2178
|
+
*/
|
|
2179
|
+
const bForOfYieldFrom$1 = (esTemplate `
|
|
2180
|
+
for (let [${estreeToolkit.is.identifier}, ${estreeToolkit.is.identifier}] of Object.entries(${estreeToolkit.is.expression} ?? {})) {
|
|
2181
|
+
${estreeToolkit.is.statement};
|
|
2182
|
+
}
|
|
2183
|
+
`);
|
|
2184
|
+
const ForEach = function ForEach(node, cxt) {
|
|
2185
|
+
const forItemId = node.item.name;
|
|
2186
|
+
const forIndexId = node.index?.name ?? '__unused__';
|
|
2187
|
+
cxt.pushLocalVars([forItemId, forIndexId]);
|
|
2188
|
+
const forEachStatements = irChildrenToEs(node.children, cxt);
|
|
2189
|
+
cxt.popLocalVars();
|
|
2190
|
+
const expression = node.expression;
|
|
2191
|
+
const iterable = getScopedExpression(expression, cxt);
|
|
2192
|
+
return [
|
|
2193
|
+
bForOfYieldFrom$1(estreeToolkit.builders.identifier(forIndexId), estreeToolkit.builders.identifier(forItemId), iterable, optimizeAdjacentYieldStmts(forEachStatements)),
|
|
2194
|
+
];
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
/*
|
|
2198
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2199
|
+
* All rights reserved.
|
|
2200
|
+
* SPDX-License-Identifier: MIT
|
|
2201
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2202
|
+
*/
|
|
2203
|
+
function getRootMemberExpression(node) {
|
|
2204
|
+
return node.object.type === 'MemberExpression' ? getRootMemberExpression(node.object) : node;
|
|
2205
|
+
}
|
|
2206
|
+
function getRootIdentifier(node) {
|
|
2207
|
+
const rootMemberExpression = getRootMemberExpression(node);
|
|
2208
|
+
return estreeToolkit.is.identifier(rootMemberExpression?.object) ? rootMemberExpression.object : null;
|
|
2209
|
+
}
|
|
2210
|
+
const bForOfYieldFrom = (esTemplate `
|
|
2211
|
+
for (let ${estreeToolkit.is.identifier} of toIteratorDirective(${estreeToolkit.is.expression} ?? [])) {
|
|
2212
|
+
${estreeToolkit.is.statement};
|
|
2213
|
+
}
|
|
2214
|
+
`);
|
|
2215
|
+
const ForOf = function ForEach(node, cxt) {
|
|
2216
|
+
const id = node.iterator.name;
|
|
2217
|
+
cxt.pushLocalVars([id]);
|
|
2218
|
+
const forEachStatements = irChildrenToEs(node.children, cxt);
|
|
2219
|
+
cxt.popLocalVars();
|
|
2220
|
+
const expression = node.expression;
|
|
2221
|
+
const scopeReferencedId = estreeToolkit.is.memberExpression(expression)
|
|
2222
|
+
? getRootIdentifier(expression)
|
|
2223
|
+
: null;
|
|
2224
|
+
const iterable = cxt.isLocalVar(scopeReferencedId?.name)
|
|
2225
|
+
? node.expression
|
|
2226
|
+
: estreeToolkit.builders.memberExpression(estreeToolkit.builders.identifier('instance'), node.expression);
|
|
2227
|
+
cxt.import('toIteratorDirective');
|
|
2228
|
+
return [
|
|
2229
|
+
bForOfYieldFrom(estreeToolkit.builders.identifier(id), iterable, optimizeAdjacentYieldStmts(forEachStatements)),
|
|
2230
|
+
];
|
|
2231
|
+
};
|
|
2232
|
+
|
|
2233
|
+
/*
|
|
2234
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2235
|
+
* All rights reserved.
|
|
2236
|
+
* SPDX-License-Identifier: MIT
|
|
2237
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2238
|
+
*/
|
|
2239
|
+
const LegacyIf = function If(node, cxt) {
|
|
2240
|
+
const { modifier: trueOrFalseAsStr, condition, children } = node;
|
|
2241
|
+
const trueOrFalse = trueOrFalseAsStr === 'true';
|
|
2242
|
+
const test = trueOrFalse
|
|
2243
|
+
? expressionIrToEs(condition, cxt)
|
|
2244
|
+
: estreeToolkit.builders.unaryExpression('!', expressionIrToEs(condition, cxt));
|
|
2245
|
+
const childStatements = irChildrenToEs(children, cxt);
|
|
2246
|
+
const block = estreeToolkit.builders.blockStatement(optimizeAdjacentYieldStmts(childStatements));
|
|
2247
|
+
return [estreeToolkit.builders.ifStatement(test, block)];
|
|
2248
|
+
};
|
|
2249
|
+
|
|
2250
|
+
/*
|
|
2251
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2252
|
+
* All rights reserved.
|
|
2253
|
+
* SPDX-License-Identifier: MIT
|
|
2254
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2255
|
+
*/
|
|
2256
|
+
const bConditionalSlot = (esTemplateWithYield `
|
|
2257
|
+
if (isLightDom) {
|
|
2258
|
+
const isScopedSlot = ${ /* isScopedSlot */estreeToolkit.is.literal};
|
|
2259
|
+
const isSlotted = ${ /* isSlotted */estreeToolkit.is.literal};
|
|
2260
|
+
const slotName = ${ /* slotName */estreeToolkit.is.expression} ?? "";
|
|
2261
|
+
const lightGenerators = lightSlottedContent?.[slotName];
|
|
2262
|
+
const scopedGenerators = scopedSlottedContent?.[slotName];
|
|
2263
|
+
const mismatchedSlots = isScopedSlot ? lightGenerators : scopedGenerators;
|
|
2264
|
+
const generators = isScopedSlot ? scopedGenerators : lightGenerators;
|
|
2265
|
+
/*
|
|
2266
|
+
If a slotAttributeValue is present, it should be provided for assignment to any slotted content. This behavior aligns with v1 and engine-dom.
|
|
2267
|
+
See: engine-server/src/__tests__/fixtures/slot-forwarding/slots/dangling/ for example.
|
|
2268
|
+
Note the slot mapping does not work for scoped slots, so the slot name is not rendered in this case.
|
|
2269
|
+
See: engine-server/src/__tests__/fixtures/slot-forwarding/scoped-slots for example.
|
|
2270
|
+
*/
|
|
2271
|
+
const danglingSlotName = !isScopedSlot ? ${ /* slotAttributeValue */estreeToolkit.is.expression} || slotAttributeValue : null;
|
|
2272
|
+
// start bookend HTML comment for light DOM slot vfragment
|
|
2273
|
+
if (!isSlotted) {
|
|
2274
|
+
yield '<!---->';
|
|
2275
|
+
|
|
2276
|
+
// If there is slot data, scoped slot factory has its own vfragment hence its own bookend
|
|
2277
|
+
if (isScopedSlot && generators) {
|
|
2278
|
+
yield '<!---->';
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
if (generators) {
|
|
2283
|
+
for (let i = 0; i < generators.length; i++) {
|
|
2284
|
+
yield* generators[i](contextfulParent, ${ /* scoped slot data */isNullableOf(estreeToolkit.is.expression)}, danglingSlotName);
|
|
2285
|
+
// Scoped slotted data is separated by bookends. Final bookends are added outside of the loop below.
|
|
2286
|
+
if (isScopedSlot && i < generators.length - 1) {
|
|
2287
|
+
yield '<!---->';
|
|
2288
|
+
yield '<!---->';
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
/*
|
|
2292
|
+
If there were mismatched slots, do not fallback to the default. This is required for parity with
|
|
2293
|
+
engine-core which resets children to an empty array when there are children (mismatched or not).
|
|
2294
|
+
Because the child nodes are reset, the default slotted content is not rendered in the mismatched slot case.
|
|
2295
|
+
See https://github.com/salesforce/lwc/blob/master/packages/%40lwc/engine-core/src/framework/api.ts#L238
|
|
2296
|
+
*/
|
|
2297
|
+
} else if (!mismatchedSlots) {
|
|
2298
|
+
// If we're in this else block, then the generator _must_ have yielded
|
|
2299
|
+
// something. It's impossible for a slottedContent["foo"] to exist
|
|
2300
|
+
// without the generator yielding at least a text node / element.
|
|
2301
|
+
${ /* slot fallback content */estreeToolkit.is.statement}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
// end bookend HTML comment for light DOM slot vfragment
|
|
2305
|
+
if (!isSlotted) {
|
|
2306
|
+
yield '<!---->';
|
|
2307
|
+
|
|
2308
|
+
// If there is slot data, scoped slot factory has its own vfragment hence its own bookend
|
|
2309
|
+
if (isScopedSlot && generators) {
|
|
2310
|
+
yield '<!---->';
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
} else {
|
|
2314
|
+
${ /* slot element AST */estreeToolkit.is.statement}
|
|
2315
|
+
}
|
|
2316
|
+
`);
|
|
2317
|
+
const Slot = function Slot(node, ctx) {
|
|
2318
|
+
const slotBindDirective = node.directives.find((dir) => dir.name === 'SlotBind');
|
|
2319
|
+
const slotBound = slotBindDirective?.value
|
|
2320
|
+
? getScopedExpression(slotBindDirective.value, ctx)
|
|
2321
|
+
: null;
|
|
2322
|
+
const slotName = bAttributeValue(node, 'name');
|
|
2323
|
+
// FIXME: avoid serializing the slot's children twice
|
|
2324
|
+
const slotAst = Element(node, ctx);
|
|
2325
|
+
const slotChildren = irChildrenToEs(node.children, ctx);
|
|
2326
|
+
const isScopedSlot = estreeToolkit.builders.literal(Boolean(slotBound));
|
|
2327
|
+
const isSlotted = estreeToolkit.builders.literal(Boolean(ctx.isSlotted));
|
|
2328
|
+
const slotAttributeValue = bAttributeValue(node, 'slot');
|
|
2329
|
+
return [
|
|
2330
|
+
bConditionalSlot(isScopedSlot, isSlotted, slotName, slotAttributeValue, slotBound, slotChildren, slotAst),
|
|
2331
|
+
];
|
|
2332
|
+
};
|
|
2333
|
+
|
|
2334
|
+
/*
|
|
2335
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2336
|
+
* All rights reserved.
|
|
2337
|
+
* SPDX-License-Identifier: MIT
|
|
2338
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2339
|
+
*/
|
|
2340
|
+
const Text = function Text(node, cxt) {
|
|
2341
|
+
if (isLastConcatenatedNode(cxt)) {
|
|
2342
|
+
// render all concatenated content up to us
|
|
2343
|
+
return generateConcatenatedTextNodesExpressions(cxt);
|
|
2344
|
+
}
|
|
2345
|
+
// our last sibling is responsible for rendering our content, not us
|
|
2346
|
+
return [];
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
/*
|
|
2350
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2351
|
+
* All rights reserved.
|
|
2352
|
+
* SPDX-License-Identifier: MIT
|
|
2353
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2354
|
+
*/
|
|
2355
|
+
function createNewContext(templateOptions) {
|
|
2356
|
+
const importManager = new ImportManager();
|
|
2357
|
+
const localVarStack = [];
|
|
2358
|
+
const pushLocalVars = (vars) => {
|
|
2359
|
+
localVarStack.push(new Set(vars));
|
|
2360
|
+
};
|
|
2361
|
+
const popLocalVars = () => {
|
|
2362
|
+
localVarStack.pop();
|
|
2363
|
+
};
|
|
2364
|
+
const isLocalVar = (varName) => {
|
|
2365
|
+
if (!varName) {
|
|
2366
|
+
return false;
|
|
2367
|
+
}
|
|
2368
|
+
for (const stackFrame of localVarStack) {
|
|
2369
|
+
if (stackFrame.has(varName)) {
|
|
2370
|
+
return true;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return false;
|
|
2374
|
+
};
|
|
2375
|
+
// Unique local variable names across block scopes, we don't care about block scope order
|
|
2376
|
+
const getLocalVars = () => [
|
|
2377
|
+
...new Set(localVarStack.flatMap((varsSet) => Array.from(varsSet))),
|
|
2378
|
+
];
|
|
2379
|
+
const hoistedStatements = {
|
|
2380
|
+
module: [],
|
|
2381
|
+
templateFn: [],
|
|
2382
|
+
};
|
|
2383
|
+
const hoistedModuleDedupe = new Set();
|
|
2384
|
+
const hoistedTemplateDedupe = new Set();
|
|
2385
|
+
const hoist = {
|
|
2386
|
+
// Anything added here will be inserted at the top of the compiled template's
|
|
2387
|
+
// JS module.
|
|
2388
|
+
module(stmt, optionalDedupeKey) {
|
|
2389
|
+
if (optionalDedupeKey) {
|
|
2390
|
+
if (hoistedModuleDedupe.has(optionalDedupeKey)) {
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
hoistedModuleDedupe.add(optionalDedupeKey);
|
|
2394
|
+
}
|
|
2395
|
+
hoistedStatements.module.push(stmt);
|
|
2396
|
+
},
|
|
2397
|
+
// Anything added here will be inserted at the top of the JavaScript function
|
|
2398
|
+
// corresponding to the template (typically named `__lwcTmpl`).
|
|
2399
|
+
templateFn(stmt, optionalDedupeKey) {
|
|
2400
|
+
if (optionalDedupeKey) {
|
|
2401
|
+
if (hoistedTemplateDedupe.has(optionalDedupeKey)) {
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
hoistedTemplateDedupe.add(optionalDedupeKey);
|
|
2405
|
+
}
|
|
2406
|
+
hoistedStatements.templateFn.push(stmt);
|
|
2407
|
+
},
|
|
2408
|
+
};
|
|
2409
|
+
const shadowSlotToFnName = new Map();
|
|
2410
|
+
let fnNameUniqueId = 0;
|
|
2411
|
+
// At present, we only track shadow-slotted content. This is because the functions
|
|
2412
|
+
// corresponding to shadow-slotted content are deduped and hoisted to the top of
|
|
2413
|
+
// the template function, whereas light-dom-slotted content is inlined. It may be
|
|
2414
|
+
// desirable to also track light-dom-slotted content at some future point in time.
|
|
2415
|
+
const slots = {
|
|
2416
|
+
shadow: {
|
|
2417
|
+
isDuplicate(uniqueNodeId) {
|
|
2418
|
+
return shadowSlotToFnName.has(uniqueNodeId);
|
|
2419
|
+
},
|
|
2420
|
+
register(uniqueNodeId, kebabCmpName) {
|
|
2421
|
+
if (slots.shadow.isDuplicate(uniqueNodeId)) {
|
|
2422
|
+
return shadowSlotToFnName.get(uniqueNodeId);
|
|
2423
|
+
}
|
|
2424
|
+
const shadowSlotContentFnName = `__lwcGenerateShadowSlottedContent_${kebabCmpName}_${fnNameUniqueId++}`;
|
|
2425
|
+
shadowSlotToFnName.set(uniqueNodeId, shadowSlotContentFnName);
|
|
2426
|
+
return shadowSlotContentFnName;
|
|
2427
|
+
},
|
|
2428
|
+
getFnName(uniqueNodeId) {
|
|
2429
|
+
return shadowSlotToFnName.get(uniqueNodeId) ?? null;
|
|
2430
|
+
},
|
|
2431
|
+
},
|
|
2432
|
+
};
|
|
2433
|
+
return {
|
|
2434
|
+
getImports: () => importManager.getImportDeclarations(),
|
|
2435
|
+
cxt: {
|
|
2436
|
+
pushLocalVars,
|
|
2437
|
+
popLocalVars,
|
|
2438
|
+
isLocalVar,
|
|
2439
|
+
getLocalVars,
|
|
2440
|
+
templateOptions,
|
|
2441
|
+
hoist,
|
|
2442
|
+
hoistedStatements,
|
|
2443
|
+
slots,
|
|
2444
|
+
import: importManager.add.bind(importManager),
|
|
2445
|
+
siblings: undefined,
|
|
2446
|
+
currentNodeIndex: undefined,
|
|
2447
|
+
},
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/*
|
|
2452
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2453
|
+
* All rights reserved.
|
|
2454
|
+
* SPDX-License-Identifier: MIT
|
|
2455
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2456
|
+
*/
|
|
2457
|
+
// lwc:if/lwc:elseif/lwc:else use bookend comments due to VFragment vdom node using them
|
|
2458
|
+
// The bookends should surround the entire if/elseif/else series
|
|
2459
|
+
// Note: these should only be rendered if _something_ is rendered by a series of if/elseif/else's
|
|
2460
|
+
function bYieldBookendComment() {
|
|
2461
|
+
return estreeToolkit.builders.expressionStatement(estreeToolkit.builders.yieldExpression(estreeToolkit.builders.literal(`<!---->`)));
|
|
2462
|
+
}
|
|
2463
|
+
function bBlockStatement(childNodes, cxt) {
|
|
2464
|
+
const childStatements = irChildrenToEs(childNodes, cxt);
|
|
2465
|
+
// Due to `flattenFragmentsInChildren`, we have to remove bookends for all _top-level_ slotted
|
|
2466
|
+
// content. This applies to both light DOM and shadow DOM slots, although light DOM slots have
|
|
2467
|
+
// the additional wrinkle that they themselves are VFragments with their own bookends.
|
|
2468
|
+
// https://github.com/salesforce/lwc/blob/a33b390/packages/%40lwc/engine-core/src/framework/rendering.ts#L718-L753
|
|
2469
|
+
const statements = cxt.isSlotted
|
|
2470
|
+
? childStatements
|
|
2471
|
+
: [bYieldBookendComment(), ...childStatements, bYieldBookendComment()];
|
|
2472
|
+
return estreeToolkit.builders.blockStatement(optimizeAdjacentYieldStmts(statements));
|
|
2473
|
+
}
|
|
2474
|
+
function bIfStatement(ifElseIfNode, cxt) {
|
|
2475
|
+
const { children, condition, else: elseNode } = ifElseIfNode;
|
|
2476
|
+
let elseBlock = null;
|
|
2477
|
+
if (elseNode) {
|
|
2478
|
+
if (elseNode.type === 'ElseBlock') {
|
|
2479
|
+
elseBlock = bBlockStatement(elseNode.children, cxt);
|
|
2480
|
+
}
|
|
2481
|
+
else {
|
|
2482
|
+
elseBlock = bIfStatement(elseNode, cxt);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
return estreeToolkit.builders.ifStatement(expressionIrToEs(condition, cxt), bBlockStatement(children, cxt), elseBlock);
|
|
2486
|
+
}
|
|
2487
|
+
const IfBlock = function IfBlock(node, cxt) {
|
|
2488
|
+
return [bIfStatement(node, cxt)];
|
|
2489
|
+
};
|
|
2490
|
+
|
|
2491
|
+
/*
|
|
2492
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2493
|
+
* All rights reserved.
|
|
2494
|
+
* SPDX-License-Identifier: MIT
|
|
2495
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2496
|
+
*/
|
|
2497
|
+
const bThrowError = (esTemplate `
|
|
2498
|
+
throw new Error(${estreeToolkit.is.literal});
|
|
2499
|
+
`);
|
|
2500
|
+
const Root = function Root(node, cxt) {
|
|
2501
|
+
return irChildrenToEs(node.children, cxt);
|
|
2502
|
+
};
|
|
2503
|
+
const defaultTransformer = (node) => {
|
|
2504
|
+
throw new Error(`Unimplemented IR node: ${node_util.inspect(node)}`);
|
|
2505
|
+
};
|
|
2506
|
+
const transformers = {
|
|
2507
|
+
Comment,
|
|
2508
|
+
Component,
|
|
2509
|
+
Element,
|
|
2510
|
+
ExternalComponent: Element,
|
|
2511
|
+
ForEach,
|
|
2512
|
+
ForOf,
|
|
2513
|
+
If: LegacyIf,
|
|
2514
|
+
IfBlock,
|
|
2515
|
+
Root,
|
|
2516
|
+
Text,
|
|
2517
|
+
// lwc:elseif cannot exist without an lwc:if (IfBlock); this gets handled by that transformer
|
|
2518
|
+
ElseifBlock: defaultTransformer,
|
|
2519
|
+
// lwc:elseif cannot exist without an lwc:elseif (IfBlock); this gets handled by that transformer
|
|
2520
|
+
ElseBlock: defaultTransformer,
|
|
2521
|
+
ScopedSlotFragment: defaultTransformer,
|
|
2522
|
+
Slot,
|
|
2523
|
+
Lwc: LwcComponent,
|
|
2524
|
+
};
|
|
2525
|
+
function irChildrenToEs(children, cxt, cb) {
|
|
2526
|
+
const result = [];
|
|
2527
|
+
for (let i = 0; i < children.length; i++) {
|
|
2528
|
+
// must set the siblings inside the for loop due to nested children
|
|
2529
|
+
cxt.siblings = children;
|
|
2530
|
+
cxt.currentNodeIndex = i;
|
|
2531
|
+
const cleanUp = cb?.(children[i]);
|
|
2532
|
+
result.push(...irToEs(children[i], cxt));
|
|
2533
|
+
cleanUp?.();
|
|
2534
|
+
}
|
|
2535
|
+
// reset the context
|
|
2536
|
+
cxt.siblings = undefined;
|
|
2537
|
+
cxt.currentNodeIndex = undefined;
|
|
2538
|
+
return result;
|
|
2539
|
+
}
|
|
2540
|
+
function irToEs(node, cxt) {
|
|
2541
|
+
if ('directives' in node && node.directives.some((d) => d.name === 'Dynamic')) {
|
|
2542
|
+
return [
|
|
2543
|
+
bThrowError(estreeToolkit.builders.literal('The lwc:dynamic directive is not supported for SSR. Use <lwc:component> instead.')),
|
|
2544
|
+
];
|
|
2545
|
+
}
|
|
2546
|
+
const transformer = transformers[node.type];
|
|
2547
|
+
return transformer(node, cxt);
|
|
2548
|
+
}
|
|
2549
|
+
function templateIrToEsTree(node, contextOpts) {
|
|
2550
|
+
const { getImports, cxt } = createNewContext(contextOpts);
|
|
2551
|
+
const statements = irToEs(node, cxt);
|
|
2552
|
+
return {
|
|
2553
|
+
addImport: cxt.import,
|
|
2554
|
+
getImports,
|
|
2555
|
+
statements,
|
|
2556
|
+
cxt,
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/*
|
|
2561
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2562
|
+
* All rights reserved.
|
|
2563
|
+
* SPDX-License-Identifier: MIT
|
|
2564
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2565
|
+
*/
|
|
2566
|
+
// TODO [#4663]: Render mode mismatch between template and compiler should throw.
|
|
2567
|
+
const bExportTemplate = (esTemplate `
|
|
2568
|
+
export default async function* __lwcTmpl(
|
|
2569
|
+
shadowSlottedContent,
|
|
2570
|
+
lightSlottedContent,
|
|
2571
|
+
scopedSlottedContent,
|
|
2572
|
+
Cmp,
|
|
2573
|
+
instance,
|
|
2574
|
+
renderContext
|
|
2575
|
+
) {
|
|
2576
|
+
// Deliberately using let so we can mutate as many times as we want in the same scope.
|
|
2577
|
+
// These should be scoped to the "tmpl" function however, to avoid conflicts with other templates.
|
|
2578
|
+
let textContentBuffer = '';
|
|
2579
|
+
let didBufferTextContent = false;
|
|
2580
|
+
|
|
2581
|
+
// This will get overridden but requires initialization.
|
|
2582
|
+
const slotAttributeValue = null;
|
|
2583
|
+
|
|
2584
|
+
// Establishes a contextual relationship between two components for ContextProviders.
|
|
2585
|
+
// This variable will typically get overridden (shadowed) within slotted content.
|
|
2586
|
+
const contextfulParent = instance;
|
|
2587
|
+
|
|
2588
|
+
const isLightDom = Cmp.renderMode === 'light';
|
|
2589
|
+
if (!isLightDom) {
|
|
2590
|
+
yield \`<template shadowrootmode="open"\${Cmp.delegatesFocus ? ' shadowrootdelegatesfocus' : ''}>\`
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
const { stylesheets: staticStylesheets } = Cmp;
|
|
2594
|
+
if (defaultStylesheets || defaultScopedStylesheets || staticStylesheets) {
|
|
2595
|
+
yield renderStylesheets(
|
|
2596
|
+
renderContext,
|
|
2597
|
+
defaultStylesheets,
|
|
2598
|
+
defaultScopedStylesheets,
|
|
2599
|
+
staticStylesheets,
|
|
2600
|
+
stylesheetScopeToken,
|
|
2601
|
+
Cmp,
|
|
2602
|
+
hasScopedStylesheets,
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
${estreeToolkit.is.statement};
|
|
2607
|
+
|
|
2608
|
+
if (!isLightDom) {
|
|
2609
|
+
yield '</template>';
|
|
2610
|
+
if (shadowSlottedContent) {
|
|
2611
|
+
// instance must be passed in; this is used to establish the contextful relationship
|
|
2612
|
+
// between context provider (aka parent component) and context consumer (aka slotted content)
|
|
2613
|
+
yield* shadowSlottedContent(contextfulParent);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
`);
|
|
2618
|
+
function compileTemplate(src, filename, options, compilationMode) {
|
|
2619
|
+
const { root, warnings } = templateCompiler.parse(src, {
|
|
2620
|
+
// `options` is from @lwc/compiler, and may have flags that @lwc/template-compiler doesn't
|
|
2621
|
+
// know about, so we must explicitly extract the relevant props.
|
|
2622
|
+
name: options.name,
|
|
2623
|
+
namespace: options.namespace,
|
|
2624
|
+
customRendererConfig: options.customRendererConfig,
|
|
2625
|
+
experimentalComputedMemberExpression: options.experimentalComputedMemberExpression,
|
|
2626
|
+
experimentalComplexExpressions: options.experimentalComplexExpressions,
|
|
2627
|
+
enableDynamicComponents: options.enableDynamicComponents,
|
|
2628
|
+
enableLwcOn: options.enableLwcOn,
|
|
2629
|
+
preserveHtmlComments: options.preserveHtmlComments,
|
|
2630
|
+
enableStaticContentOptimization: options.enableStaticContentOptimization,
|
|
2631
|
+
instrumentation: options.instrumentation,
|
|
2632
|
+
apiVersion: options.apiVersion,
|
|
2633
|
+
disableSyntheticShadowSupport: options.disableSyntheticShadowSupport,
|
|
2634
|
+
// TODO [#3331]: remove usage of lwc:dynamic in 246
|
|
2635
|
+
experimentalDynamicDirective: options.experimentalDynamicDirective,
|
|
2636
|
+
});
|
|
2637
|
+
if (!root || warnings.length) {
|
|
2638
|
+
for (const warning of warnings) {
|
|
2639
|
+
// eslint-disable-next-line no-console
|
|
2640
|
+
console.error('Cannot compile:', warning.message);
|
|
2641
|
+
}
|
|
2642
|
+
// The legacy SSR implementation would not bail from compilation even if a
|
|
2643
|
+
// DiagnosticLevel.Fatal error was encountered. It would only fail if the
|
|
2644
|
+
// template parser failed to return a root node. That behavior is duplicated
|
|
2645
|
+
// here.
|
|
2646
|
+
if (!root) {
|
|
2647
|
+
throw new Error('Template compilation failure; see warnings in the console.');
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
const preserveComments = !!root.directives.find((directive) => directive.name === 'PreserveComments')?.value?.value;
|
|
2651
|
+
const experimentalComplexExpressions = Boolean(options.experimentalComplexExpressions);
|
|
2652
|
+
const apiVersion = Number(options.apiVersion);
|
|
2653
|
+
const { addImport, getImports, statements, cxt } = templateIrToEsTree(root, {
|
|
2654
|
+
preserveComments,
|
|
2655
|
+
experimentalComplexExpressions,
|
|
2656
|
+
apiVersion,
|
|
2657
|
+
});
|
|
2658
|
+
addImport(['renderStylesheets', 'hasScopedStaticStylesheets']);
|
|
2659
|
+
for (const [imports, source] of getStylesheetImports(filename)) {
|
|
2660
|
+
addImport(imports, source);
|
|
2661
|
+
}
|
|
2662
|
+
let tmplDecl = bExportTemplate(optimizeAdjacentYieldStmts([
|
|
2663
|
+
// Deep in the compiler, we may choose to hoist statements and declarations
|
|
2664
|
+
// to the top of the template function. After `templateIrToEsTree`, these
|
|
2665
|
+
// hoisted statements/declarations are prepended to the template function's
|
|
2666
|
+
// body.
|
|
2667
|
+
...cxt.hoistedStatements.templateFn,
|
|
2668
|
+
...statements,
|
|
2669
|
+
]));
|
|
2670
|
+
// Ideally, we'd just do ${LWC_VERSION_COMMENT} in the code template,
|
|
2671
|
+
// but placeholders have a special meaning for `esTemplate`.
|
|
2672
|
+
tmplDecl = immer.produce(tmplDecl, (draft) => {
|
|
2673
|
+
draft.declaration.body.trailingComments = [
|
|
2674
|
+
{
|
|
2675
|
+
type: 'Block',
|
|
2676
|
+
value: shared.LWC_VERSION_COMMENT,
|
|
2677
|
+
},
|
|
2678
|
+
];
|
|
2679
|
+
});
|
|
2680
|
+
let program = estreeToolkit.builders.program([
|
|
2681
|
+
// All import declarations come first...
|
|
2682
|
+
...getImports(),
|
|
2683
|
+
// ... followed by any statements or declarations that need to be hoisted
|
|
2684
|
+
// to the top of the module scope...
|
|
2685
|
+
...cxt.hoistedStatements.module,
|
|
2686
|
+
// ... followed by the template function declaration itself.
|
|
2687
|
+
tmplDecl,
|
|
2688
|
+
], 'module');
|
|
2689
|
+
addScopeTokenDeclarations(program, filename, options.namespace, options.name);
|
|
2690
|
+
if (compilationMode === 'async' || compilationMode === 'sync') {
|
|
2691
|
+
program = transmogrify(program, compilationMode);
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
code: astring.generate(program, {
|
|
2695
|
+
// The generated AST doesn't have comments; this just preserves the LWC version comment
|
|
2696
|
+
comments: true,
|
|
2697
|
+
}),
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
/*
|
|
2702
|
+
* Copyright (c) 2024, salesforce.com, inc.
|
|
2703
|
+
* All rights reserved.
|
|
2704
|
+
* SPDX-License-Identifier: MIT
|
|
2705
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
|
|
2706
|
+
*/
|
|
2707
|
+
function compileComponentForSSR(src, filename, options, mode = shared.DEFAULT_SSR_MODE) {
|
|
2708
|
+
const tagName = shared.generateCustomElementTagName(options.namespace, options.name);
|
|
2709
|
+
const { code } = compileJS(src, filename, tagName, options, mode);
|
|
2710
|
+
return { code, map: undefined };
|
|
2711
|
+
}
|
|
2712
|
+
function compileTemplateForSSR(src, filename, options, mode = shared.DEFAULT_SSR_MODE) {
|
|
2713
|
+
const { code } = compileTemplate(src, filename, options, mode);
|
|
2714
|
+
return { code, map: undefined };
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
exports.compileComponentForSSR = compileComponentForSSR;
|
|
2718
|
+
exports.compileTemplateForSSR = compileTemplateForSSR;
|
|
2719
|
+
/** version: 9.0.3 */
|
|
2720
|
+
//# sourceMappingURL=index.cjs.map
|