Skip to content

Commit bb51f84

Browse files
authored
feat(engine): support imperative wire adapters (#5132)
* feat(engine): support imperative wire adapters * feat(engine): correct ts errors in tests
1 parent 7556d0c commit bb51f84

File tree

2 files changed

+267
-7
lines changed
  • packages/@lwc
    • engine-core/src/framework/decorators
    • integration-types/src/decorators

2 files changed

+267
-7
lines changed

packages/@lwc/engine-core/src/framework/decorators/wire.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ export default function wire<
6565
Class = LightningElement,
6666
>(
6767
// eslint-disable-next-line @typescript-eslint/no-unused-vars
68-
adapter: WireAdapterConstructor<ReplaceReactiveValues<ReactiveConfig, Class>, Value, Context>,
68+
adapter:
69+
| WireAdapterConstructor<ReplaceReactiveValues<ReactiveConfig, Class>, Value, Context>
70+
| {
71+
adapter: WireAdapterConstructor<
72+
ReplaceReactiveValues<ReactiveConfig, Class>,
73+
Value,
74+
Context
75+
>;
76+
},
6977
// eslint-disable-next-line @typescript-eslint/no-unused-vars
7078
config?: ReactiveConfig
7179
): WireDecorator<Value, Class> {

packages/@lwc/integration-types/src/decorators/wire.ts

Lines changed: 258 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ type DeepConfig = { deep: { config: number } };
1515
declare const testConfig: TestConfig;
1616
declare const testValue: TestValue;
1717
declare const TestAdapter: WireAdapterConstructor<TestConfig, TestValue, TestContext>;
18+
declare const TestAdapterWithImperative: {
19+
(config: TestConfig): TestValue;
20+
adapter: WireAdapterConstructor<TestConfig, TestValue, TestContext>;
21+
};
1822
declare const AnyAdapter: any;
1923
declare const InvalidAdapter: object;
2024
declare const DeepConfigAdapter: WireAdapterConstructor<DeepConfig, TestValue>;
@@ -39,7 +43,7 @@ export class PropertyDecorators extends LightningElement {
3943
// Valid - basic
4044
@wire(TestAdapter, { config: 'config' })
4145
basic?: TestValue;
42-
@wire(TestAdapter, { config: '$config' })
46+
@wire(TestAdapter, { config: '$configProp' })
4347
simpleReactive?: TestValue;
4448
@wire(TestAdapter, { config: '$nested.prop' })
4549
nestedReactive?: TestValue;
@@ -126,6 +130,72 @@ export class PropertyDecorators extends LightningElement {
126130
never?: never;
127131
}
128132

133+
/** Validations for decorated properties/fields */
134+
export class PropertyDecoratorsWithImperative extends LightningElement {
135+
// Helper props
136+
configProp = 'config' as const;
137+
nested = { prop: 'config', invalid: 123 } as const;
138+
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
139+
// uses the object above, rather than a weird prop name
140+
'nested.prop' = false;
141+
number = 123;
142+
// --- VALID --- //
143+
// Valid - basic
144+
@wire(TestAdapterWithImperative, { config: 'config' })
145+
basic?: TestValue;
146+
@wire(TestAdapterWithImperative, { config: '$configProp' })
147+
simpleReactive?: TestValue;
148+
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
149+
nestedReactive?: TestValue;
150+
// Valid - as const
151+
@wire(TestAdapterWithImperative, { config: 'config' } as const)
152+
basicAsConst?: TestValue;
153+
@wire(TestAdapterWithImperative, { config: '$configProp' } as const)
154+
simpleReactiveAsConst?: TestValue;
155+
// Valid - using `any`
156+
@wire(TestAdapterWithImperative, {} as any)
157+
configAsAny?: TestValue;
158+
@wire(TestAdapterWithImperative, { config: 'config' })
159+
propAsAny?: any;
160+
// Valid - prop assignment
161+
@wire(TestAdapterWithImperative, { config: 'config' })
162+
nonNullAssertion!: TestValue;
163+
@wire(TestAdapterWithImperative, { config: 'config' })
164+
explicitDefaultType: TestValue = testValue;
165+
@wire(TestAdapterWithImperative, { config: 'config' })
166+
implicitDefaultType = testValue;
167+
168+
// --- INVALID --- //
169+
// @ts-expect-error Too many wire parameters
170+
@wire(TestAdapterWithImperative, { config: 'config' }, {})
171+
tooManyWireParams?: TestValue;
172+
// @ts-expect-error Bad config type
173+
@wire(TestAdapterWithImperative, { bad: 'value' })
174+
badConfig?: TestValue;
175+
// @ts-expect-error Bad prop type
176+
@wire(TestAdapterWithImperative, { config: 'config' })
177+
badPropType?: { bad: 'value' };
178+
// @ts-expect-error Referenced reactive prop does not exist
179+
@wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const)
180+
nonExistentReactiveProp?: TestValue;
181+
182+
// --- AMBIGUOUS --- //
183+
// Passing a config is optional because adapters don't strictly need to use it.
184+
// Can we be smarter about the type and require a config, but only if the adapter does?
185+
@wire(TestAdapterWithImperative)
186+
noConfig?: TestValue;
187+
// Because the basic type `string` could be _any_ string, we can't narrow it and compare against
188+
// the component's props, so we must accept all string props, even if they're incorrect.
189+
// We could technically be strict, and enforce that all configs objects use `as const`, but very
190+
// few projects currently use it (there is no need) and the error reported is not simple to
191+
// understand.
192+
@wire(TestAdapterWithImperative, { config: 'incorrect' })
193+
wrongConfigButInferredAsString?: TestValue;
194+
// People shouldn't do this, and they probably never (heh) will. TypeScript allows it, though.
195+
@wire(TestAdapterWithImperative, { config: 'config' })
196+
never?: never;
197+
}
198+
129199
/** Validations for decorated methods */
130200
export class MethodDecorators extends LightningElement {
131201
// Helper props
@@ -141,13 +211,13 @@ export class MethodDecorators extends LightningElement {
141211
basic(_: TestValue) {}
142212
@wire(TestAdapter, { config: 'config' })
143213
async asyncMethod(_: TestValue) {}
144-
@wire(TestAdapter, { config: '$config' })
214+
@wire(TestAdapter, { config: '$configProp' })
145215
simpleReactive(_: TestValue) {}
146216
@wire(TestAdapter, { config: '$nested.prop' })
147217
nestedReactive(_: TestValue) {}
148-
@wire(TestAdapter, { config: '$config' })
218+
@wire(TestAdapter, { config: '$configProp' })
149219
optionalParam(_?: TestValue) {}
150-
@wire(TestAdapter, { config: '$config' })
220+
@wire(TestAdapter, { config: '$configProp' })
151221
noParam() {}
152222
// Valid - as const
153223
@wire(TestAdapter, { config: 'config' } as const)
@@ -222,6 +292,67 @@ export class MethodDecorators extends LightningElement {
222292
implicitDefaultType(_ = testValue) {}
223293
}
224294

295+
/** Validations for decorated methods */
296+
export class MethodDecoratorsWithImperative extends LightningElement {
297+
// Helper props
298+
configProp = 'config' as const;
299+
nested = { prop: 'config', invalid: 123 } as const;
300+
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
301+
// uses the object above, rather than a weird prop name
302+
'nested.prop' = false;
303+
number = 123;
304+
// --- VALID --- //
305+
// Valid - basic
306+
@wire(TestAdapterWithImperative, { config: 'config' })
307+
basic(_: TestValue) {}
308+
@wire(TestAdapterWithImperative, { config: 'config' })
309+
async asyncMethod(_: TestValue) {}
310+
@wire(TestAdapterWithImperative, { config: '$configProp' })
311+
simpleReactive(_: TestValue) {}
312+
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
313+
nestedReactive(_: TestValue) {}
314+
@wire(TestAdapterWithImperative, { config: '$configProp' })
315+
optionalParam(_?: TestValue) {}
316+
@wire(TestAdapterWithImperative, { config: '$configProp' })
317+
noParam() {}
318+
// Valid - as const
319+
@wire(TestAdapterWithImperative, { config: 'config' } as const)
320+
basicAsConst(_: TestValue) {}
321+
@wire(TestAdapterWithImperative, { config: '$configProp' } as const)
322+
simpleReactiveAsConst(_: TestValue) {}
323+
@wire(TestAdapterWithImperative, { config: '$nested.prop' } as const)
324+
nestedReactiveAsConst(_: TestValue) {}
325+
// Valid - using `any`
326+
@wire(TestAdapterWithImperative, {} as any)
327+
configAsAny(_: TestValue) {}
328+
@wire(TestAdapterWithImperative, { config: 'config' })
329+
paramAsAny(_: any) {}
330+
331+
// --- INVALID --- //
332+
// @ts-expect-error Too many wire parameters
333+
@wire(TestAdapterWithImperative, { config: 'config' }, {})
334+
tooManyWireParams(_: TestValue) {}
335+
// @ts-expect-error Too many method parameters
336+
@wire(TestAdapterWithImperative, { config: 'config' })
337+
tooManyParameters(_a: TestValue, _b: TestValue) {}
338+
339+
// --- AMBIGUOUS --- //
340+
// Passing a config is optional because adapters don't strictly need to use it.
341+
// Can we be smarter about the type and require a config, but only if the adapter does?
342+
@wire(TestAdapterWithImperative)
343+
noConfig(_: TestValue): void {}
344+
// Because the basic type `string` could be _any_ string, we can't narrow it and compare against
345+
// the component's props, so we must accept all string props, even if they're incorrect.
346+
// We could technically be strict, and enforce that all configs objects use `as const`, but very
347+
// few projects currently use it (there is no need) and the error reported is not simple to
348+
// understand.
349+
@wire(TestAdapterWithImperative, { config: 'incorrect' })
350+
wrongConfigButInferredAsString(_: TestValue): void {}
351+
// Wire adapters shouldn't use default params, but the type system doesn't know the difference
352+
@wire(TestAdapterWithImperative, { config: 'config' })
353+
implicitDefaultType(_ = testValue) {}
354+
}
355+
225356
/** Validations for decorated getters */
226357
export class GetterDecorators extends LightningElement {
227358
// Helper props
@@ -244,7 +375,7 @@ export class GetterDecorators extends LightningElement {
244375
// we must return something. Since we don't have any data to return, we return `undefined`
245376
return undefined;
246377
}
247-
@wire(TestAdapter, { config: '$config' })
378+
@wire(TestAdapter, { config: '$configProp' })
248379
get simpleReactive() {
249380
return testValue;
250381
}
@@ -341,6 +472,69 @@ export class GetterDecorators extends LightningElement {
341472
}
342473
}
343474

475+
/** Validations for decorated getters */
476+
export class GetterDecoratorsWithImperative extends LightningElement {
477+
// Helper props
478+
configProp = 'config' as const;
479+
nested = { prop: 'config', invalid: 123 } as const;
480+
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
481+
// uses the object above, rather than a weird prop name
482+
'nested.prop' = false;
483+
number = 123;
484+
// --- VALID --- //
485+
486+
// Valid - basic
487+
@wire(TestAdapterWithImperative, { config: 'config' })
488+
get basic() {
489+
return testValue;
490+
}
491+
@wire(TestAdapterWithImperative, { config: 'config' })
492+
get undefined() {
493+
// The function implementation of a wired getter is ignored, but TypeScript enforces that
494+
// we must return something. Since we don't have any data to return, we return `undefined`
495+
return undefined;
496+
}
497+
@wire(TestAdapterWithImperative, { config: '$configProp' })
498+
get simpleReactive() {
499+
return testValue;
500+
}
501+
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
502+
get nestedReactive() {
503+
return testValue;
504+
}
505+
// Valid - using `any`
506+
@wire(TestAdapterWithImperative, {} as any)
507+
get configAsAny() {
508+
return testValue;
509+
}
510+
@wire(TestAdapterWithImperative, { config: 'config' })
511+
get valueAsAny() {
512+
return null as any;
513+
}
514+
515+
// --- INVALID --- //
516+
// @ts-expect-error Too many wire parameters
517+
@wire(TestAdapterWithImperative, { config: 'config' }, {})
518+
get tooManyWireParams() {
519+
return testValue;
520+
}
521+
// @ts-expect-error Bad config type
522+
@wire(TestAdapterWithImperative, { bad: 'value' })
523+
get badConfig() {
524+
return testValue;
525+
}
526+
// @ts-expect-error Bad value type
527+
@wire(TestAdapterWithImperative, { config: 'config' })
528+
get badValueType() {
529+
return { bad: 'value' };
530+
}
531+
// @ts-expect-error Referenced reactive prop does not exist
532+
@wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const)
533+
get nonExistentReactiveProp() {
534+
return testValue;
535+
}
536+
}
537+
344538
/** Validations for decorated setters */
345539
export class Setter extends LightningElement {
346540
// Helper props
@@ -355,7 +549,7 @@ export class Setter extends LightningElement {
355549
// Valid - basic
356550
@wire(TestAdapter, { config: 'config' })
357551
set basic(_: TestValue) {}
358-
@wire(TestAdapter, { config: '$config' })
552+
@wire(TestAdapter, { config: '$configProp' })
359553
set simpleReactive(_: TestValue) {}
360554
@wire(TestAdapter, { config: '$nested.prop' })
361555
set nestedReactive(_: TestValue) {}
@@ -411,3 +605,61 @@ export class Setter extends LightningElement {
411605
@wire(DeepConfigAdapter, { deep: { config: '$number' } } as const)
412606
set deepReactive(_: TestValue) {}
413607
}
608+
609+
/** Validations for decorated setters */
610+
export class SetterWithImperative extends LightningElement {
611+
// Helper props
612+
configProp = 'config' as const;
613+
nested = { prop: 'config', invalid: 123 } as const;
614+
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
615+
// uses the object above, rather than a weird prop name
616+
'nested.prop' = false;
617+
number = 123;
618+
// --- VALID --- //
619+
620+
// Valid - basic
621+
@wire(TestAdapterWithImperative, { config: 'config' })
622+
set basic(_: TestValue) {}
623+
@wire(TestAdapterWithImperative, { config: '$configProp' })
624+
set simpleReactive(_: TestValue) {}
625+
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
626+
set nestedReactive(_: TestValue) {}
627+
// Valid - as const
628+
@wire(TestAdapterWithImperative, { config: 'config' } as const)
629+
set basicAsConst(_: TestValue) {}
630+
@wire(TestAdapterWithImperative, { config: '$configProp' } as const)
631+
set simpleReactiveAsConst(_: TestValue) {}
632+
@wire(TestAdapterWithImperative, { config: '$nested.prop' } as const)
633+
set nestedReactiveAsConst(_: TestValue) {}
634+
// Valid - using `any`
635+
@wire(TestAdapterWithImperative, {} as any)
636+
set configAsAny(_: TestValue) {}
637+
@wire(TestAdapterWithImperative, { config: 'config' })
638+
set valueAsAny(_: any) {}
639+
640+
// --- INVALID --- //
641+
// @ts-expect-error Too many wire parameters
642+
@wire(TestAdapterWithImperative, { config: 'config' }, {})
643+
set tooManyWireParams(_: TestValue) {}
644+
// @ts-expect-error Bad config type
645+
@wire(TestAdapterWithImperative, { bad: 'value' })
646+
set badConfig(_: TestValue) {}
647+
// @ts-expect-error Bad value type
648+
@wire(TestAdapterWithImperative, { config: 'config' })
649+
set badValueType(_: { bad: 'value' }) {}
650+
// @ts-expect-error Referenced reactive prop does not exist
651+
@wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const)
652+
set nonExistentReactiveProp(_: TestValue) {}
653+
// @ts-expect-error Referenced reactive prop is the wrong type
654+
@wire(TestAdapterWithImperative, { config: '$number' } as const)
655+
set numberReactiveProp(_: TestValue) {}
656+
// @ts-expect-error Referenced nested reactive prop does not exist
657+
@wire(TestAdapterWithImperative, { config: '$nested.nonexistent' } as const)
658+
set nonexistentNestedReactiveProp(_: TestValue) {}
659+
// @ts-expect-error Referenced nested reactive prop does not exist
660+
@wire(TestAdapterWithImperative, { config: '$nested.invalid' } as const)
661+
set invalidNestedReactiveProp(_: TestValue) {}
662+
// @ts-expect-error Incorrect non-reactive string literal type
663+
@wire(TestAdapterWithImperative, { config: 'not reactive' } as const)
664+
set nonReactiveStringLiteral(_: TestValue) {}
665+
}

0 commit comments

Comments
 (0)