Skip to content

Commit

Permalink
feat(engine): support imperative wire adapters (#5132)
Browse files Browse the repository at this point in the history
* feat(engine): support imperative wire adapters

* feat(engine): correct ts errors in tests
  • Loading branch information
aheber authored Jan 27, 2025
1 parent 7556d0c commit bb51f84
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 7 deletions.
10 changes: 9 additions & 1 deletion packages/@lwc/engine-core/src/framework/decorators/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,15 @@ export default function wire<
Class = LightningElement,
>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
adapter: WireAdapterConstructor<ReplaceReactiveValues<ReactiveConfig, Class>, Value, Context>,
adapter:
| WireAdapterConstructor<ReplaceReactiveValues<ReactiveConfig, Class>, Value, Context>
| {
adapter: WireAdapterConstructor<
ReplaceReactiveValues<ReactiveConfig, Class>,
Value,
Context
>;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config?: ReactiveConfig
): WireDecorator<Value, Class> {
Expand Down
264 changes: 258 additions & 6 deletions packages/@lwc/integration-types/src/decorators/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ type DeepConfig = { deep: { config: number } };
declare const testConfig: TestConfig;
declare const testValue: TestValue;
declare const TestAdapter: WireAdapterConstructor<TestConfig, TestValue, TestContext>;
declare const TestAdapterWithImperative: {
(config: TestConfig): TestValue;
adapter: WireAdapterConstructor<TestConfig, TestValue, TestContext>;
};
declare const AnyAdapter: any;
declare const InvalidAdapter: object;
declare const DeepConfigAdapter: WireAdapterConstructor<DeepConfig, TestValue>;
Expand All @@ -39,7 +43,7 @@ export class PropertyDecorators extends LightningElement {
// Valid - basic
@wire(TestAdapter, { config: 'config' })
basic?: TestValue;
@wire(TestAdapter, { config: '$config' })
@wire(TestAdapter, { config: '$configProp' })
simpleReactive?: TestValue;
@wire(TestAdapter, { config: '$nested.prop' })
nestedReactive?: TestValue;
Expand Down Expand Up @@ -126,6 +130,72 @@ export class PropertyDecorators extends LightningElement {
never?: never;
}

/** Validations for decorated properties/fields */
export class PropertyDecoratorsWithImperative extends LightningElement {
// Helper props
configProp = 'config' as const;
nested = { prop: 'config', invalid: 123 } as const;
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
// uses the object above, rather than a weird prop name
'nested.prop' = false;
number = 123;
// --- VALID --- //
// Valid - basic
@wire(TestAdapterWithImperative, { config: 'config' })
basic?: TestValue;
@wire(TestAdapterWithImperative, { config: '$configProp' })
simpleReactive?: TestValue;
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
nestedReactive?: TestValue;
// Valid - as const
@wire(TestAdapterWithImperative, { config: 'config' } as const)
basicAsConst?: TestValue;
@wire(TestAdapterWithImperative, { config: '$configProp' } as const)
simpleReactiveAsConst?: TestValue;
// Valid - using `any`
@wire(TestAdapterWithImperative, {} as any)
configAsAny?: TestValue;
@wire(TestAdapterWithImperative, { config: 'config' })
propAsAny?: any;
// Valid - prop assignment
@wire(TestAdapterWithImperative, { config: 'config' })
nonNullAssertion!: TestValue;
@wire(TestAdapterWithImperative, { config: 'config' })
explicitDefaultType: TestValue = testValue;
@wire(TestAdapterWithImperative, { config: 'config' })
implicitDefaultType = testValue;

// --- INVALID --- //
// @ts-expect-error Too many wire parameters
@wire(TestAdapterWithImperative, { config: 'config' }, {})
tooManyWireParams?: TestValue;
// @ts-expect-error Bad config type
@wire(TestAdapterWithImperative, { bad: 'value' })
badConfig?: TestValue;
// @ts-expect-error Bad prop type
@wire(TestAdapterWithImperative, { config: 'config' })
badPropType?: { bad: 'value' };
// @ts-expect-error Referenced reactive prop does not exist
@wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const)
nonExistentReactiveProp?: TestValue;

// --- AMBIGUOUS --- //
// Passing a config is optional because adapters don't strictly need to use it.
// Can we be smarter about the type and require a config, but only if the adapter does?
@wire(TestAdapterWithImperative)
noConfig?: TestValue;
// Because the basic type `string` could be _any_ string, we can't narrow it and compare against
// the component's props, so we must accept all string props, even if they're incorrect.
// We could technically be strict, and enforce that all configs objects use `as const`, but very
// few projects currently use it (there is no need) and the error reported is not simple to
// understand.
@wire(TestAdapterWithImperative, { config: 'incorrect' })
wrongConfigButInferredAsString?: TestValue;
// People shouldn't do this, and they probably never (heh) will. TypeScript allows it, though.
@wire(TestAdapterWithImperative, { config: 'config' })
never?: never;
}

/** Validations for decorated methods */
export class MethodDecorators extends LightningElement {
// Helper props
Expand All @@ -141,13 +211,13 @@ export class MethodDecorators extends LightningElement {
basic(_: TestValue) {}
@wire(TestAdapter, { config: 'config' })
async asyncMethod(_: TestValue) {}
@wire(TestAdapter, { config: '$config' })
@wire(TestAdapter, { config: '$configProp' })
simpleReactive(_: TestValue) {}
@wire(TestAdapter, { config: '$nested.prop' })
nestedReactive(_: TestValue) {}
@wire(TestAdapter, { config: '$config' })
@wire(TestAdapter, { config: '$configProp' })
optionalParam(_?: TestValue) {}
@wire(TestAdapter, { config: '$config' })
@wire(TestAdapter, { config: '$configProp' })
noParam() {}
// Valid - as const
@wire(TestAdapter, { config: 'config' } as const)
Expand Down Expand Up @@ -222,6 +292,67 @@ export class MethodDecorators extends LightningElement {
implicitDefaultType(_ = testValue) {}
}

/** Validations for decorated methods */
export class MethodDecoratorsWithImperative extends LightningElement {
// Helper props
configProp = 'config' as const;
nested = { prop: 'config', invalid: 123 } as const;
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
// uses the object above, rather than a weird prop name
'nested.prop' = false;
number = 123;
// --- VALID --- //
// Valid - basic
@wire(TestAdapterWithImperative, { config: 'config' })
basic(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: 'config' })
async asyncMethod(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$configProp' })
simpleReactive(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
nestedReactive(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$configProp' })
optionalParam(_?: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$configProp' })
noParam() {}
// Valid - as const
@wire(TestAdapterWithImperative, { config: 'config' } as const)
basicAsConst(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$configProp' } as const)
simpleReactiveAsConst(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$nested.prop' } as const)
nestedReactiveAsConst(_: TestValue) {}
// Valid - using `any`
@wire(TestAdapterWithImperative, {} as any)
configAsAny(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: 'config' })
paramAsAny(_: any) {}

// --- INVALID --- //
// @ts-expect-error Too many wire parameters
@wire(TestAdapterWithImperative, { config: 'config' }, {})
tooManyWireParams(_: TestValue) {}
// @ts-expect-error Too many method parameters
@wire(TestAdapterWithImperative, { config: 'config' })
tooManyParameters(_a: TestValue, _b: TestValue) {}

// --- AMBIGUOUS --- //
// Passing a config is optional because adapters don't strictly need to use it.
// Can we be smarter about the type and require a config, but only if the adapter does?
@wire(TestAdapterWithImperative)
noConfig(_: TestValue): void {}
// Because the basic type `string` could be _any_ string, we can't narrow it and compare against
// the component's props, so we must accept all string props, even if they're incorrect.
// We could technically be strict, and enforce that all configs objects use `as const`, but very
// few projects currently use it (there is no need) and the error reported is not simple to
// understand.
@wire(TestAdapterWithImperative, { config: 'incorrect' })
wrongConfigButInferredAsString(_: TestValue): void {}
// Wire adapters shouldn't use default params, but the type system doesn't know the difference
@wire(TestAdapterWithImperative, { config: 'config' })
implicitDefaultType(_ = testValue) {}
}

/** Validations for decorated getters */
export class GetterDecorators extends LightningElement {
// Helper props
Expand All @@ -244,7 +375,7 @@ export class GetterDecorators extends LightningElement {
// we must return something. Since we don't have any data to return, we return `undefined`
return undefined;
}
@wire(TestAdapter, { config: '$config' })
@wire(TestAdapter, { config: '$configProp' })
get simpleReactive() {
return testValue;
}
Expand Down Expand Up @@ -341,6 +472,69 @@ export class GetterDecorators extends LightningElement {
}
}

/** Validations for decorated getters */
export class GetterDecoratorsWithImperative extends LightningElement {
// Helper props
configProp = 'config' as const;
nested = { prop: 'config', invalid: 123 } as const;
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
// uses the object above, rather than a weird prop name
'nested.prop' = false;
number = 123;
// --- VALID --- //

// Valid - basic
@wire(TestAdapterWithImperative, { config: 'config' })
get basic() {
return testValue;
}
@wire(TestAdapterWithImperative, { config: 'config' })
get undefined() {
// The function implementation of a wired getter is ignored, but TypeScript enforces that
// we must return something. Since we don't have any data to return, we return `undefined`
return undefined;
}
@wire(TestAdapterWithImperative, { config: '$configProp' })
get simpleReactive() {
return testValue;
}
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
get nestedReactive() {
return testValue;
}
// Valid - using `any`
@wire(TestAdapterWithImperative, {} as any)
get configAsAny() {
return testValue;
}
@wire(TestAdapterWithImperative, { config: 'config' })
get valueAsAny() {
return null as any;
}

// --- INVALID --- //
// @ts-expect-error Too many wire parameters
@wire(TestAdapterWithImperative, { config: 'config' }, {})
get tooManyWireParams() {
return testValue;
}
// @ts-expect-error Bad config type
@wire(TestAdapterWithImperative, { bad: 'value' })
get badConfig() {
return testValue;
}
// @ts-expect-error Bad value type
@wire(TestAdapterWithImperative, { config: 'config' })
get badValueType() {
return { bad: 'value' };
}
// @ts-expect-error Referenced reactive prop does not exist
@wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const)
get nonExistentReactiveProp() {
return testValue;
}
}

/** Validations for decorated setters */
export class Setter extends LightningElement {
// Helper props
Expand All @@ -355,7 +549,7 @@ export class Setter extends LightningElement {
// Valid - basic
@wire(TestAdapter, { config: 'config' })
set basic(_: TestValue) {}
@wire(TestAdapter, { config: '$config' })
@wire(TestAdapter, { config: '$configProp' })
set simpleReactive(_: TestValue) {}
@wire(TestAdapter, { config: '$nested.prop' })
set nestedReactive(_: TestValue) {}
Expand Down Expand Up @@ -411,3 +605,61 @@ export class Setter extends LightningElement {
@wire(DeepConfigAdapter, { deep: { config: '$number' } } as const)
set deepReactive(_: TestValue) {}
}

/** Validations for decorated setters */
export class SetterWithImperative extends LightningElement {
// Helper props
configProp = 'config' as const;
nested = { prop: 'config', invalid: 123 } as const;
// 'nested.prop' is not directly used, but helps validate that the reactive config resolution
// uses the object above, rather than a weird prop name
'nested.prop' = false;
number = 123;
// --- VALID --- //

// Valid - basic
@wire(TestAdapterWithImperative, { config: 'config' })
set basic(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$configProp' })
set simpleReactive(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$nested.prop' })
set nestedReactive(_: TestValue) {}
// Valid - as const
@wire(TestAdapterWithImperative, { config: 'config' } as const)
set basicAsConst(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$configProp' } as const)
set simpleReactiveAsConst(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: '$nested.prop' } as const)
set nestedReactiveAsConst(_: TestValue) {}
// Valid - using `any`
@wire(TestAdapterWithImperative, {} as any)
set configAsAny(_: TestValue) {}
@wire(TestAdapterWithImperative, { config: 'config' })
set valueAsAny(_: any) {}

// --- INVALID --- //
// @ts-expect-error Too many wire parameters
@wire(TestAdapterWithImperative, { config: 'config' }, {})
set tooManyWireParams(_: TestValue) {}
// @ts-expect-error Bad config type
@wire(TestAdapterWithImperative, { bad: 'value' })
set badConfig(_: TestValue) {}
// @ts-expect-error Bad value type
@wire(TestAdapterWithImperative, { config: 'config' })
set badValueType(_: { bad: 'value' }) {}
// @ts-expect-error Referenced reactive prop does not exist
@wire(TestAdapterWithImperative, { config: '$nonexistentProp' } as const)
set nonExistentReactiveProp(_: TestValue) {}
// @ts-expect-error Referenced reactive prop is the wrong type
@wire(TestAdapterWithImperative, { config: '$number' } as const)
set numberReactiveProp(_: TestValue) {}
// @ts-expect-error Referenced nested reactive prop does not exist
@wire(TestAdapterWithImperative, { config: '$nested.nonexistent' } as const)
set nonexistentNestedReactiveProp(_: TestValue) {}
// @ts-expect-error Referenced nested reactive prop does not exist
@wire(TestAdapterWithImperative, { config: '$nested.invalid' } as const)
set invalidNestedReactiveProp(_: TestValue) {}
// @ts-expect-error Incorrect non-reactive string literal type
@wire(TestAdapterWithImperative, { config: 'not reactive' } as const)
set nonReactiveStringLiteral(_: TestValue) {}
}

0 comments on commit bb51f84

Please sign in to comment.