Skip to content

Commit 476bb41

Browse files
authored
fix(ssr): throw compile-time error on @wire-decorated getter/setter/method (#5175)
* fix(ssr): throw compile-time error on @wire-decorated getter/setter/method * fix(ssr): disallow @wire on computed props
1 parent 1193957 commit 476bb41

File tree

39 files changed

+169
-16
lines changed

39 files changed

+169
-16
lines changed

packages/@lwc/engine-server/src/__tests__/fixtures.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { rollup } from 'rollup';
1111
import lwcRollupPlugin from '@lwc/rollup-plugin';
1212
import { testFixtureDir, formatHTML } from '@lwc/test-utils-lwc-internals';
1313
import { setFeatureFlagForTest } from '../index';
14+
import type { FeatureFlagName } from '@lwc/features/dist/types';
1415
import type { RollupLwcOptions } from '@lwc/rollup-plugin';
1516
import type * as lwc from '../index';
1617

@@ -128,16 +129,17 @@ function testFixtures(options?: RollupLwcOptions) {
128129
// the LightningElement. Therefor the compiled module should also be evaluated in the
129130
// same sandbox registry as the engine.
130131
const lwcEngineServer = await import('../index');
131-
const module = (await import(compiledFixturePath)) as FixtureModule;
132-
133-
const features = module!.features ?? [];
134-
features.forEach((flag) => {
135-
lwcEngineServer!.setFeatureFlagForTest(flag, true);
136-
});
137132

138133
let result;
139134
let err;
135+
let features: FeatureFlagName[] = [];
140136
try {
137+
const module = (await import(compiledFixturePath)) as FixtureModule;
138+
139+
features = module!.features ?? [];
140+
features.forEach((flag) => {
141+
lwcEngineServer!.setFeatureFlagForTest(flag, true);
142+
});
141143
result = formatHTML(
142144
lwcEngineServer!.renderComponent(
143145
module!.tagName,
@@ -146,10 +148,10 @@ function testFixtures(options?: RollupLwcOptions) {
146148
)
147149
);
148150
} catch (_err: any) {
149-
if (_err.name === 'AssertionError') {
151+
if (_err?.name === 'AssertionError') {
150152
throw _err;
151153
}
152-
err = _err.message;
154+
err = _err?.message || 'An empty error occurred?!';
153155
}
154156

155157
features.forEach((flag) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ssrFiles": {
3+
"error": "error-ssr.txt",
4+
"expected": "expected-ssr.html"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Unknown state: property definition has no key

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/computed-prop-updates-incorrectly/expected-ssr.html

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<x-wire>
2+
<template shadowrootmode="open">
3+
<p>
4+
Prop named symbol that should not be used: wire data
5+
</p>
6+
<p>
7+
Prop with symbol key that should be used: unset
8+
</p>
9+
</template>
10+
</x-wire>
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.value);
10+
}
11+
12+
disconnect() {}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<template>
2+
<p>Prop named symbol that should not be used: {symbol}</p>
3+
<p>Prop with symbol key that should be used: {symbolValue}</p>
4+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LightningElement, wire } from 'lwc';
2+
import { adapter } from './adapter';
3+
4+
const symbol = Symbol("I'm a symbol!");
5+
export default class extends LightningElement {
6+
symbol = 'accidentally overwritten';
7+
8+
@wire(adapter, { value: 'wire data' })
9+
[symbol] = 'unset';
10+
11+
get symbolValue() {
12+
return this[symbol] ?? 'unset';
13+
}
14+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ssrFiles": {
3+
"error": "error-ssr.txt",
4+
"expected": "expected-ssr.html"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@wire cannot be used on computed properties in SSR context.

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-on-computed-getter/expected-ssr.html

Whitespace-only changes.

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-on-computed-getter/expected.html

Whitespace-only changes.
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: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,6 @@ export default class Wire extends LightningElement {
88
throw new Error('getter should not be called');
99
}
1010

11-
@wire(adapter, { name: 'symbol' })
12-
set [sym](v) {
13-
throw new Error('setter should not be called');
14-
}
15-
1611
get exposedSymbol() {
1712
return this[sym];
1813
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ssrFiles": {
3+
"error": "error-ssr.txt",
4+
"expected": "expected-ssr.html"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[7:13]: Unexpected token: '{'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
An empty error occurred?!

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-on-computed-method/expected-ssr.html

Whitespace-only changes.

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-on-computed-method/expected.html

Whitespace-only changes.
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';
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+
{exposedSymbol}
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { LightningElement, wire } from 'lwc';
2+
import { adapter } from './adapter';
3+
const sym = Symbol('computed prop');
4+
5+
export default class Wire extends LightningElement {
6+
@wire(adapter, { name: 'symbol' })
7+
[sym]() {
8+
return 'john was here';
9+
}
10+
11+
get exposedSymbol() {
12+
return this[sym]();
13+
}
14+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ssrFiles": {
3+
"error": "error-ssr.txt",
4+
"expected": "expected-ssr.html"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@wire cannot be used on computed properties in SSR context.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
setter should not be called

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-on-computed-setter/expected-ssr.html

Whitespace-only changes.

packages/@lwc/engine-server/src/__tests__/fixtures/wire/errors/throws-on-computed-setter/expected.html

Whitespace-only changes.
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';
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+
{exposedSymbol}
3+
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { LightningElement, wire } from 'lwc';
2+
import { adapter } from './adapter';
3+
const sym = Symbol('computed prop');
4+
5+
export default class Wire extends LightningElement {
6+
@wire(adapter, { name: 'symbol' })
7+
set [sym](_) {
8+
throw new Error('setter should not be called');
9+
}
10+
11+
get exposedSymbol() {
12+
this[sym] = 123;
13+
return 123;
14+
}
15+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,5 @@ export const expectedFailures = new Set([
1212
'attribute-global-html/as-component-prop/without-@api/index.js',
1313
'exports/component-as-default/index.js',
1414
'known-boolean-attributes/default-def-html-attributes/static-on-component/index.js',
15-
'wire/errors/throws-on-computed-key/index.js',
1615
'wire/errors/throws-when-colliding-prop-then-method/index.js',
1716
]);

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ const visitors: Visitors = {
9393
},
9494
PropertyDefinition(path, state) {
9595
const node = path.node;
96-
if (!is.identifier(node?.key)) {
96+
if (!node?.key) {
97+
// Seems to occur for `@wire() [symbol];` -- not sure why
98+
throw new Error('Unknown state: property definition has no key');
99+
}
100+
if (!is.identifier(node.key)) {
97101
return;
98102
}
99103

@@ -107,6 +111,10 @@ const visitors: Visitors = {
107111
is.identifier(decoratedExpression.callee) &&
108112
decoratedExpression.callee.name === 'wire'
109113
) {
114+
if (node.computed) {
115+
// TODO [#5032]: Harmonize errors thrown in `@lwc/ssr-compiler`
116+
throw new Error('@wire cannot be used on computed properties in SSR context.');
117+
}
110118
catalogWireAdapters(path, state);
111119
state.privateFields.push(node.key.name);
112120
} else {
@@ -146,9 +154,17 @@ const visitors: Visitors = {
146154
is.identifier(decoratedExpression.callee) &&
147155
decoratedExpression.callee.name === 'wire'
148156
) {
157+
// not a getter/setter
158+
const isRealMethod = node.kind === 'method';
159+
if (node.computed) {
160+
// TODO [#5032]: Harmonize errors thrown in `@lwc/ssr-compiler`
161+
throw new Error(
162+
`@wire cannot be used on computed ${isRealMethod ? 'method' : 'properties'} in SSR context.`
163+
);
164+
}
149165
// Getters and setters are methods in the AST, but treated as properties by @wire
150166
// Note that this means that their implementations are ignored!
151-
if (node.kind === 'get' || node.kind === 'set') {
167+
if (!isRealMethod) {
152168
const methodAsProp = b.propertyDefinition(
153169
structuredClone(node.key),
154170
null,

0 commit comments

Comments
 (0)