Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(engine): support imperative wire adapters #5132

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
268 changes: 252 additions & 16 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 @@ -114,18 +118,76 @@ export class PropertyDecorators extends LightningElement {
// Can we be smarter about the type and require a config, but only if the adapter does?
@wire(TestAdapter)
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.
// @ts-expect-error must be 'config' or reactive
@wire(TestAdapter, { config: 'incorrect' })
wrongConfigButInferredAsString?: TestValue;
// People shouldn't do this, and they probably never (heh) will. TypeScript allows it, though.
@wire(TestAdapter, { config: 'config' })
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;
// @ts-expect-error config limited to specific string or valid reactive prop
@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 +203,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 @@ -210,18 +272,71 @@ export class MethodDecorators extends LightningElement {
// Can we be smarter about the type and require a config, but only if the adapter does?
@wire(TestAdapter)
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.
// @ts-expect-error Must be 'config' or reactive
@wire(TestAdapter, { config: 'incorrect' })
wrongConfigButInferredAsString(_: TestValue): void {}
// Wire adapters shouldn't use default params, but the type system doesn't know the difference
@wire(TestAdapter, { config: 'config' })
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 {}
// @ts-expect-error config limited to specific string or valid reactive prop
@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 +359,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 +456,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 +533,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 +589,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) {}
}
Loading