Skip to content

Commit 0716123

Browse files
authored
feat(ssr-compiler): only mutate things if we're in a component @W-17748892 (#5310)
* chore: convert comments to JSDoc for tooltips * feat(ssr-compiler): change __load to use trusted identifier __lwcLoad * feat(ssr-compiler): only do stuff to components
1 parent 03ca787 commit 0716123

File tree

2 files changed

+78
-55
lines changed

2 files changed

+78
-55
lines changed

packages/@lwc/ssr-compiler/src/compile-js/index.ts

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -74,47 +74,63 @@ const visitors: Visitors = {
7474
return;
7575
}
7676
const source = path.node!.source!;
77-
// 1. insert `import { load as __load } from '${loader}'` at top of program
78-
importManager.add({ load: '__load' }, loader);
79-
// 2. replace this import with `__load(${source})`
80-
path.replaceWith(b.callExpression(b.identifier('__load'), [structuredClone(source)]));
77+
// 1. insert `import { load as __lwcLoad } from '${loader}'` at top of program
78+
importManager.add({ load: '__lwcLoad' }, loader);
79+
// 2. replace this `import(source)` with `__lwcLoad(source)`
80+
const load = b.identifier('__lwcLoad');
81+
state.trustedLwcIdentifiers.add(load);
82+
path.replaceWith(b.callExpression(load, [structuredClone(source)]));
8183
},
82-
ClassDeclaration(path, state) {
83-
const { node } = path;
84-
if (
85-
node?.superClass &&
86-
// export default class extends LightningElement {}
87-
(is.exportDefaultDeclaration(path.parentPath) ||
88-
// class Cmp extends LightningElement {}; export default Cmp
89-
path.scope
90-
?.getBinding(node.id.name)
91-
?.references.some((ref) => is.exportDefaultDeclaration(ref.parent)))
92-
) {
93-
// If it's a default-exported class with a superclass, then it's an LWC component!
94-
state.isLWC = true;
95-
if (node.id) {
96-
state.lwcClassName = node.id.name;
97-
} else {
98-
node.id = b.identifier('DefaultComponentName');
99-
state.lwcClassName = 'DefaultComponentName';
100-
}
84+
ClassDeclaration: {
85+
enter(path, state) {
86+
const { node } = path;
87+
if (
88+
node?.superClass &&
89+
// export default class extends LightningElement {}
90+
(is.exportDefaultDeclaration(path.parentPath) ||
91+
// class Cmp extends LightningElement {}; export default Cmp
92+
path.scope
93+
?.getBinding(node.id.name)
94+
?.references.some((ref) => is.exportDefaultDeclaration(ref.parent)))
95+
) {
96+
// If it's a default-exported class with a superclass, then it's an LWC component!
97+
state.isLWC = true;
98+
state.currentComponent = node;
99+
if (node.id) {
100+
state.lwcClassName = node.id.name;
101+
} else {
102+
node.id = b.identifier('DefaultComponentName');
103+
state.lwcClassName = 'DefaultComponentName';
104+
}
101105

102-
// There's no builder for comment nodes :\
103-
const lwcVersionComment: EsComment = {
104-
type: 'Block',
105-
value: LWC_VERSION_COMMENT,
106-
};
106+
// There's no builder for comment nodes :\
107+
const lwcVersionComment: EsComment = {
108+
type: 'Block',
109+
value: LWC_VERSION_COMMENT,
110+
};
107111

108-
// Add LWC version comment to end of class body
109-
const { body } = node;
110-
if (body.trailingComments) {
111-
body.trailingComments.push(lwcVersionComment);
112-
} else {
113-
body.trailingComments = [lwcVersionComment];
112+
// Add LWC version comment to end of class body
113+
const { body } = node;
114+
if (body.trailingComments) {
115+
body.trailingComments.push(lwcVersionComment);
116+
} else {
117+
body.trailingComments = [lwcVersionComment];
118+
}
114119
}
115-
}
120+
},
121+
leave(path, state) {
122+
// Indicate that we're no longer traversing an LWC component
123+
if (state.currentComponent && path.node === state.currentComponent) {
124+
state.currentComponent = null;
125+
}
126+
},
116127
},
117128
PropertyDefinition(path, state) {
129+
// Don't do anything unless we're in a component
130+
if (!state.currentComponent) {
131+
return;
132+
}
133+
118134
const node = path.node;
119135
if (!node?.key) {
120136
// Seems to occur for `@wire() [symbol];` -- not sure why
@@ -155,7 +171,7 @@ const visitors: Visitors = {
155171
}
156172
// If we mutate any class-methods that are piped through this compiler, then we'll be
157173
// inadvertently mutating things like Wire adapters.
158-
if (!state.isLWC) {
174+
if (!state.currentComponent) {
159175
return;
160176
}
161177

@@ -218,7 +234,7 @@ const visitors: Visitors = {
218234
Super(path, state) {
219235
// If we mutate any super calls that are piped through this compiler, then we'll be
220236
// inadvertently mutating things like Wire adapters.
221-
if (!state.isLWC) {
237+
if (!state.currentComponent) {
222238
return;
223239
}
224240

@@ -246,9 +262,9 @@ const visitors: Visitors = {
246262
}
247263
},
248264
},
249-
Identifier(path, _state) {
265+
Identifier(path, state) {
250266
const { node } = path;
251-
if (node?.name.startsWith('__lwc')) {
267+
if (node?.name.startsWith('__lwc') && !state.trustedLwcIdentifiers.has(node)) {
252268
throw generateError(node, SsrCompilerErrors.RESERVED_IDENTIFIER_PREFIX);
253269
}
254270
},
@@ -271,6 +287,7 @@ export default function compileJS(
271287

272288
const state: ComponentMetaState = {
273289
isLWC: false,
290+
currentComponent: null,
274291
hasConstructor: false,
275292
hasConnectedCallback: false,
276293
hadRenderedCallback: false,
@@ -286,6 +303,7 @@ export default function compileJS(
286303
wireAdapters: [],
287304
experimentalDynamicComponent: options.experimentalDynamicComponent,
288305
importManager: new ImportManager(),
306+
trustedLwcIdentifiers: new WeakSet(),
289307
};
290308

291309
traverse(ast, visitors, state);

packages/@lwc/ssr-compiler/src/compile-js/types.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { type traverse } from 'estree-toolkit';
99
import type { ImportManager } from '../imports';
1010
import type { ComponentTransformOptions } from '../shared';
1111
import type {
12+
ClassDeclaration,
1213
Identifier,
1314
MemberExpression,
1415
MethodDefinition,
@@ -26,36 +27,40 @@ export interface WireAdapter {
2627
}
2728

2829
export interface ComponentMetaState {
29-
// indicates whether the LightningElement subclass is found in the JS being traversed
30+
/** indicates whether a subclass of LightningElement is found in the JS being traversed */
3031
isLWC: boolean;
31-
// indicates whether the LightningElement subclass includes a constructor method
32+
/** the class declaration currently being traversed, if it is an LWC component */
33+
currentComponent: ClassDeclaration | null;
34+
/** indicates whether the LightningElement subclass includes a constructor method */
3235
hasConstructor: boolean;
33-
// indicates whether the subclass has a connectedCallback method
36+
/** indicates whether the subclass has a connectedCallback method */
3437
hasConnectedCallback: boolean;
35-
// indicates whether the subclass has a renderedCallback method
38+
/** indicates whether the subclass has a renderedCallback method */
3639
hadRenderedCallback: boolean;
37-
// indicates whether the subclass has a disconnectedCallback method
40+
/** indicates whether the subclass has a disconnectedCallback method */
3841
hadDisconnectedCallback: boolean;
39-
// indicates whether the subclass has a errorCallback method
42+
/** indicates whether the subclass has a errorCallback method */
4043
hadErrorCallback: boolean;
41-
// the local name corresponding to the `LightningElement` import
44+
/** the local name corresponding to the `LightningElement` import */
4245
lightningElementIdentifier: string | null;
43-
// the class name of the subclass
46+
/** the class name of the subclass */
4447
lwcClassName: string | null;
45-
// ties local variable names to explicitly-imported HTML templates
48+
/** ties local variable names to explicitly-imported HTML templates */
4649
tmplExplicitImports: Map<string, string> | null;
47-
// ties local variable names to explicitly-imported CSS files
50+
/** ties local variable names to explicitly-imported CSS files */
4851
cssExplicitImports: Map<string, string> | null;
49-
// the set of variable names associated with explicitly imported CSS files
52+
/** the set of variable names associated with explicitly imported CSS files */
5053
staticStylesheetIds: Set<string> | null;
51-
// the public (`@api`-annotated) properties of the component class
54+
/** the public (`@api`-annotated) properties of the component class */
5255
publicProperties: Map<string, (MethodDefinition | PropertyDefinition) & { key: Identifier }>;
53-
// the private properties of the component class
56+
/** the private properties of the component class */
5457
privateProperties: Set<string>;
55-
// indicates whether the LightningElement has any wired props
58+
/** indicates whether the LightningElement has any wired props */
5659
wireAdapters: WireAdapter[];
57-
// dynamic imports configuration
60+
/** dynamic imports configuration */
5861
experimentalDynamicComponent: ComponentTransformOptions['experimentalDynamicComponent'];
59-
// imports to add to the top of the program after parsing
62+
/** imports to add to the top of the program after parsing */
6063
importManager: ImportManager;
64+
/** identifiers starting with __lwc that we added */
65+
trustedLwcIdentifiers: WeakSet<Identifier>;
6166
}

0 commit comments

Comments
 (0)