Skip to content

Commit 6283e34

Browse files
wjhsfnolanlawsonjhefferman-sfdc
authored
feat(ssr): enable @wire on getters/setters (#4986)
* fix(ssr): adjacent text in `<template>`s Co-Authored-By: Will Harney <[email protected]> Co-Authored-By: John Hefferman <[email protected]> * test: fix expected failures * test(snapshots): add helpfuler message when error/expected swap * fix(ssr): support @wire-decorated getters/setters * chore: add test for edge case * test(ssr): add test for differently-wired getter/setter * Update packages/@lwc/ssr-compiler/src/compile-js/index.ts Co-authored-by: Nolan Lawson <[email protected]> * fix(ssr): use structured clone when converting wired getter to prop * test: add tests * chore: add comment --------- Co-authored-by: Nolan Lawson <[email protected]> Co-authored-by: John Hefferman <[email protected]>
1 parent 71b929e commit 6283e34

File tree

32 files changed

+236
-10
lines changed

32 files changed

+236
-10
lines changed

packages/@lwc/engine-server/src/__tests__/fixtures/wire/collision/get-set/error.txt

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<x-wire>
2+
<template shadowrootmode="open">
3+
<div>
4+
Wired? set
5+
</div>
6+
</template>
7+
</x-wire>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-wire';
2+
export { default } from 'x/wire';
3+
export * from 'x/wire';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class Adapter {
2+
constructor(dataCallback) {
3+
this.callback = dataCallback;
4+
}
5+
6+
connect() {}
7+
8+
update(cfg) {
9+
this.callback(cfg.value);
10+
}
11+
12+
disconnect() {}
13+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
<div>Wired? {wired}</div>
3+
</template>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { wire, LightningElement } from 'lwc';
2+
import { Adapter } from 'x/adapter';
3+
export default class Test extends LightningElement {
4+
@wire(Adapter, { value: 'get' })
5+
get wired() {
6+
throw new Error('get wired!');
7+
}
8+
9+
@wire(Adapter, { value: 'set' })
10+
set wired(val) {
11+
throw new Error('set wired!');
12+
}
13+
}

packages/@lwc/engine-server/src/__tests__/fixtures/wire/collision/method-prop/error.txt

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<x-wire>
2+
<template shadowrootmode="open">
3+
propAndMethod
4+
</template>
5+
</x-wire>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-wire';
2+
export { default } from 'x/wire';
3+
export * from 'x/wire';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class adapter {
2+
constructor(dataCallback) {
3+
this.dc = dataCallback;
4+
}
5+
6+
connect() {}
7+
8+
update(config) {
9+
this.dc(config.name);
10+
}
11+
12+
disconnect() {}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
{propAndMethod}
3+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { LightningElement, wire } from 'lwc';
2+
3+
import { adapter } from './adapter';
4+
5+
export default class Wire extends LightningElement {
6+
propAndMethod() {
7+
throw new Error('should not be called');
8+
}
9+
10+
@wire(adapter, { name: 'propAndMethod' })
11+
propAndMethod;
12+
}

packages/@lwc/engine-server/src/__tests__/fixtures/wire/collision/prop-method/error.txt

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<x-wire>
2+
<template shadowrootmode="open">
3+
propAndMethod
4+
</template>
5+
</x-wire>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-wire';
2+
export { default } from 'x/wire';
3+
export * from 'x/wire';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class adapter {
2+
constructor(dataCallback) {
3+
this.dc = dataCallback;
4+
}
5+
6+
connect() {}
7+
8+
update(config) {
9+
this.dc(config.name);
10+
}
11+
12+
disconnect() {}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
{propAndMethod}
3+
</template>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { LightningElement, wire } from 'lwc';
2+
3+
import { adapter } from './adapter';
4+
5+
export default class Wire extends LightningElement {
6+
@wire(adapter, { name: 'propAndMethod' })
7+
propAndMethod;
8+
9+
propAndMethod() {
10+
throw new Error('should not be called');
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cannot assign to read only property 'wired' of object '[object Object]'

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-when-colliding-prop-then-method/expected.html

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-wire';
2+
export { default } from 'x/wire';
3+
export * from 'x/wire';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class Adapter {
2+
constructor(dataCallback) {
3+
this.callback = dataCallback;
4+
}
5+
6+
connect() {}
7+
8+
update() {
9+
this.callback(true);
10+
}
11+
12+
disconnect() {}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template>
2+
<div>Wired? {wiredProp}</div>
3+
<div>Using method? {methodUsed}</div>
4+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { wire, LightningElement } from 'lwc';
2+
import { Adapter } from 'x/adapter';
3+
export default class Test extends LightningElement {
4+
methodUsed = false;
5+
6+
@wire(Adapter, { key1: '$prop1', key2: ['fixed', 'array'] })
7+
wired;
8+
9+
@wire(Adapter, { key1: '$prop1', key2: ['fixed', 'array'] })
10+
wired(val) {
11+
this.methodUsed = val;
12+
}
13+
}

packages/@lwc/engine-server/src/__tests__/fixtures/wire/get-set/error.txt

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<x-wire>
2+
<template shadowrootmode="open">
3+
getterOnly setterOnly getterWithSetter setterWithGetter bothDecorated
4+
</template>
5+
</x-wire>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const tagName = 'x-wire';
2+
export { default } from 'x/wire';
3+
export * from 'x/wire';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class adapter {
2+
constructor(dataCallback) {
3+
this.dc = dataCallback;
4+
}
5+
6+
connect() {}
7+
8+
update(config) {
9+
this.dc(config.name);
10+
}
11+
12+
disconnect() {}
13+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<template>
2+
{getterOnly} {setterOnly} {getterWithSetter} {setterWithGetter} {bothDecorated}
3+
</template>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { LightningElement, wire } from 'lwc';
2+
3+
import { adapter } from './adapter';
4+
5+
export default class Wire extends LightningElement {
6+
@wire(adapter, { name: 'getterOnly' })
7+
get getterOnly() {
8+
throw new Error('get getterOnly should not be called');
9+
}
10+
11+
@wire(adapter, { name: 'setterOnly' })
12+
set setterOnly(v) {
13+
throw new Error('set setterOnly should not be called');
14+
}
15+
16+
@wire(adapter, { name: 'getterWithSetter' })
17+
get getterWithSetter() {
18+
throw new Error('get getterWithSetter should not be called');
19+
}
20+
set getterWithSetter(v) {
21+
throw new Error('set getterWithSetter should not be called');
22+
}
23+
24+
@wire(adapter, { name: 'setterWithGetter' })
25+
get setterWithGetter() {
26+
throw new Error('get setterWithGetter should not be called');
27+
}
28+
set setterWithGetter(v) {
29+
throw new Error('set setterWithGetter should not be called');
30+
}
31+
32+
@wire(adapter, { name: 'bothDecorated' })
33+
get bothDecorated() {
34+
throw new Error('get bothDecorated should not be called');
35+
}
36+
37+
@wire(adapter, { name: 'bothDecorated' })
38+
set bothDecorated(v) {
39+
throw new Error('set bothDecorated should not be called');
40+
}
41+
}

packages/@lwc/ssr-compiler/src/__tests__/utils/expected-failures.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export const expectedFailures = new Set([
3434
'superclass/render-in-superclass/unused-default-in-subclass/index.js',
3535
'superclass/render-in-superclass/unused-default-in-superclass/index.js',
3636
'svgs/index.js',
37+
'wire/errors/throws-on-computed-key/index.js',
38+
'wire/errors/throws-when-colliding-prop-then-method/index.js',
3739
'wire/errors/throws-when-computed-prop-is-expression/index.js',
3840
'wire/errors/throws-when-computed-prop-is-let-variable/index.js',
3941
'wire/errors/throws-when-computed-prop-is-regexp-literal/index.js',

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

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,9 @@ import { addGenerateMarkupFunction } from './generate-markup';
1717
import { catalogWireAdapters } from './wire';
1818

1919
import { removeDecoratorImport } from './remove-decorator-import';
20-
import type { Identifier as EsIdentifier, Program as EsProgram, Expression } from 'estree';
20+
import type { Identifier as EsIdentifier, Program as EsProgram } from 'estree';
2121
import type { Visitors, ComponentMetaState } from './types';
2222
import type { CompilationMode } from '@lwc/shared';
23-
import type {
24-
PropertyDefinition as DecoratatedPropertyDefinition,
25-
MethodDefinition as DecoratatedMethodDefinition,
26-
} from 'meriyah/dist/src/estree';
2723

2824
const visitors: Visitors = {
2925
$: { scope: true },
@@ -72,8 +68,8 @@ const visitors: Visitors = {
7268
return;
7369
}
7470

75-
const decorators = (node as DecoratatedPropertyDefinition).decorators;
76-
const decoratedExpression = decorators?.[0]?.expression as Expression;
71+
const { decorators } = node;
72+
const decoratedExpression = decorators?.[0]?.expression;
7773
if (is.identifier(decoratedExpression) && decoratedExpression.name === 'api') {
7874
state.publicFields.push(node.key.name);
7975
} else if (
@@ -111,14 +107,32 @@ const visitors: Visitors = {
111107
return;
112108
}
113109

114-
const decorators = (node as DecoratatedMethodDefinition).decorators;
115-
const decoratedExpression = decorators?.[0]?.expression as Expression;
110+
const { decorators } = node;
111+
// The real type is a subset of `Expression`, which doesn't work with the `is` validators
112+
const decoratedExpression = decorators?.[0]?.expression;
116113
if (
117114
is.callExpression(decoratedExpression) &&
118115
is.identifier(decoratedExpression.callee) &&
119116
decoratedExpression.callee.name === 'wire'
120117
) {
121-
catalogWireAdapters(path, state);
118+
// Getters and setters are methods in the AST, but treated as properties by @wire
119+
// Note that this means that their implementations are ignored!
120+
if (node.kind === 'get' || node.kind === 'set') {
121+
const methodAsProp = b.propertyDefinition(
122+
structuredClone(node.key),
123+
null,
124+
node.computed,
125+
node.static
126+
);
127+
methodAsProp.decorators = structuredClone(decorators);
128+
path.replaceWith(methodAsProp);
129+
// We do not need to call `catalogWireAdapters()` because, by replacing the current
130+
// node, `traverse()` will visit it again automatically, so we will just call
131+
// `catalogWireAdapters()` later anyway.
132+
return;
133+
} else {
134+
catalogWireAdapters(path, state);
135+
}
122136
}
123137

124138
switch (node.key.name) {

0 commit comments

Comments
 (0)