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

refactor(decorators): update decorator types @W-16373548@ #4429

Merged
merged 46 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8766cfc
chore: hushsky
wjhsf Jul 24, 2024
ebf1940
feat(wire): ensure prop type matches adapter value
wjhsf Jul 25, 2024
6e442e9
fix(wire): account for reactive prop replacement
wjhsf Jul 25, 2024
c1674a8
Merge branch 'master' into wjh/wire-decorator-type
wjhsf Jul 26, 2024
a23aeff
Merge branch 'master' into wjh/modern-decorators
wjhsf Jul 30, 2024
267d45a
feat(wire): drop experimental decorator signature
wjhsf Jul 30, 2024
2c33102
refactor(api): only support modern decorators
wjhsf Jul 30, 2024
20b5fe7
refactor(track): only support modern decorators
wjhsf Jul 30, 2024
a028807
refactor(wire): only support modern decorators
wjhsf Jul 30, 2024
989941c
refactor(integration-types): remove tests for experimental decorators
wjhsf Jul 30, 2024
dee4a56
docs: add comment explaining assertions
wjhsf Jul 30, 2024
4a1e94d
refactor(playground): remove experimental decorator support
wjhsf Jul 30, 2024
946e9c1
refactor(integration-types): split decorator tests into multiple files
wjhsf Jul 30, 2024
cb385fe
refactor(track): make function signature work as decorator and non-de…
wjhsf Jul 30, 2024
7908b53
refactor(wire): allow values to be undefined
wjhsf Jul 30, 2024
c2b350d
test(wire): expand type validations
wjhsf Jul 30, 2024
caba78b
docs: simplify comment
wjhsf Jul 30, 2024
c523656
test(wire): expand type validations
wjhsf Jul 30, 2024
8f64c15
feat(wire): properly type reactive values
wjhsf Jul 30, 2024
e29812c
test(wire): expand type validations
wjhsf Jul 30, 2024
feed5b4
test(decorators): validate type error when used on non-components
wjhsf Jul 31, 2024
0c038ed
refactor(wire): method param is never explicit undefined
wjhsf Jul 31, 2024
5dc02f2
test(wire): split type validations into separate classes
wjhsf Jul 31, 2024
4916d51
refactor(wire): @wire cannot be used on getters/setters
wjhsf Jul 31, 2024
2af0a13
test(wire): validate that nested props are not reactive
wjhsf Jul 31, 2024
d322402
fix(test): use correct context type
wjhsf Aug 2, 2024
d894e23
docs: clarify comment
wjhsf Aug 2, 2024
ac7a2d3
revert(track): check arguments instead of undefined
wjhsf Aug 5, 2024
17b91e5
test(wire): validate wire doesn't work on non-component with superclass
wjhsf Aug 5, 2024
6360fc8
fix(wire): make wire work with adapters typed as `any`
wjhsf Aug 5, 2024
8d832f1
test(types): assert decorators don't work on non-LightningElement cla…
wjhsf Aug 5, 2024
f32a4a0
refactor(wire): remove type error if extending a non-component
wjhsf Aug 6, 2024
51cf2e1
feat(wire): update signature to allow decorating getters/setters
wjhsf Aug 6, 2024
8686d2d
docs(wire): add comments to decorator type
wjhsf Aug 7, 2024
0127a6c
refactor(wire): update type def to always split on "."
wjhsf Aug 7, 2024
a718efd
test(wire): update type validations for prop/method decorators
wjhsf Aug 7, 2024
083d8be
test(wire): add type validations for decorated getters
wjhsf Aug 7, 2024
ac3928f
test(wire): add type validations for decorated setters
wjhsf Aug 7, 2024
e47bf1a
feat(wire): allow undefined in wired getter
wjhsf Aug 7, 2024
23c7502
refactor(decorators): relax type constraint restricting to LightningE…
wjhsf Aug 7, 2024
4bee4c9
chore(decorators): remove useless generic type param
wjhsf Aug 7, 2024
b18b0f2
docs(test): clarify use of nested.prop
wjhsf Aug 8, 2024
641f2f9
Merge branch 'master' into wjh/modern-decorators
wjhsf Aug 9, 2024
257fe17
chore: prettier
wjhsf Aug 9, 2024
5799424
Merge branch 'master' into wjh/modern-decorators
wjhsf Sep 4, 2024
d721eb7
Merge branch 'master' into wjh/modern-decorators
wjhsf Sep 4, 2024
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
4 changes: 2 additions & 2 deletions packages/@lwc/engine-core/src/framework/decorators/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { assert, isFunction, isNull, toString } from '@lwc/shared';
import { logError } from '../../shared/logger';
import { isInvokingRender, isBeingConstructed } from '../invoker';
import { componentValueObserved, componentValueMutated } from '../mutation-tracker';
import { LightningElement } from '../base-lightning-element';
import { getAssociatedVM } from '../vm';
import { isUpdatingTemplate, getVMBeingRendered } from '../template';
import type { LightningElement } from '../base-lightning-element';

/**
* The `@api` decorator marks public fields and public methods in
Expand All @@ -21,7 +21,7 @@ export default function api(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
value: unknown,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
context: ClassMemberDecoratorContext | string | symbol
context: ClassMemberDecoratorContext
): void {
if (process.env.NODE_ENV !== 'production') {
assert.fail(`@api decorator can only be used as a decorator function.`);
Expand Down
12 changes: 7 additions & 5 deletions packages/@lwc/engine-core/src/framework/decorators/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,23 @@ import { componentValueObserved } from '../mutation-tracker';
import { isInvokingRender } from '../invoker';
import { getAssociatedVM } from '../vm';
import { getReactiveProxy } from '../membrane';
import { LightningElement } from '../base-lightning-element';
import { isUpdatingTemplate, getVMBeingRendered } from '../template';
import { updateComponentValue } from '../update-component-value';
import { logError } from '../../shared/logger';
import type { LightningElement } from '../base-lightning-element';

/**
* The `@track` decorator function marks field values as reactive in
* LWC Components. This function can also be invoked directly
* with any value to obtain the trackable version of the value.
*/
export default function track(target: undefined, context: ClassFieldDecoratorContext): void;
export default function track<T>(target: T, context?: never): T;
export default function track(
value: unknown,
context: ClassMemberDecoratorContext | string | symbol
): void;
export default function track<T>(target: T): T {
target: unknown,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
context?: ClassFieldDecoratorContext
): unknown {
if (arguments.length === 1) {
return getReactiveProxy(target);
}
Expand Down
53 changes: 47 additions & 6 deletions packages/@lwc/engine-core/src/framework/decorators/wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,47 @@
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/
import { assert } from '@lwc/shared';
import { LightningElement } from '../base-lightning-element';
import { componentValueObserved } from '../mutation-tracker';
import { getAssociatedVM } from '../vm';
import { WireAdapterConstructor } from '../wiring';
import { updateComponentValue } from '../update-component-value';
import type { LightningElement } from '../base-lightning-element';
import type {
ConfigValue,
ContextValue,
ReplaceReactiveValues,
WireAdapterConstructor,
} from '../wiring';

/**
* The decorator returned by `@wire()`; not the `wire` function.
*
* For TypeScript users:
* - If you are seeing an unclear error message, ensure that both the type of the decorated prop and
* the config used match the types expected by the wire adapter.
* - String literal types in the config are resolved to the corresponding prop on the component.
* For example, a component with `id = 555` and `@wire(getBook, {id: "$id"} as const) book` will
* have `"$id"` resolve to type `number`.
*/
interface WireDecorator<Value, Class> {
(
target: unknown,
context: // A wired prop doesn't have any data on creation, so we must allow `undefined`
| ClassFieldDecoratorContext<Class, Value | undefined>
| ClassMethodDecoratorContext<
Class,
// When a wire adapter is typed as `WireAdapterConstructor`, then this `Value`
// generic is inferred as the value used by the adapter for all decorator contexts
// (field/method/getter/setter). But when the adapter is typed as `any`, then
// decorated methods have `Value` inferred as the full method. (I'm not sure why.)
// This conditional checks `Value` so that we get the correct decorator context.
Value extends (value: any) => any ? Value : (this: Class, value: Value) => void
>
// The implementation of a wired getter/setter is ignored; they are treated identically
// to wired props. Wired props don't have data on creation, so we must allow `undefined`
| ClassGetterDecoratorContext<Class, Value | undefined>
| ClassSetterDecoratorContext<Class, Value>
): void;
}

/**
* Decorator factory to wire a property or method to a wire adapter data source.
Expand All @@ -22,12 +58,17 @@ import { updateComponentValue } from '../update-component-value';
* \@wire(getBook, { id: '$bookId'}) book;
* }
*/
export default function wire(
export default function wire<
ReactiveConfig extends ConfigValue = ConfigValue,
Value = any,
Context extends ContextValue = ContextValue,
Class = LightningElement,
>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
adapter: WireAdapterConstructor,
adapter: WireAdapterConstructor<ReplaceReactiveValues<ReactiveConfig, Class>, Value, Context>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config?: Record<string, any>
): (value: unknown, context: ClassMemberDecoratorContext | string | symbol) => void {
config?: ReactiveConfig
): WireDecorator<Value, Class> {
if (process.env.NODE_ENV !== 'production') {
assert.fail('@wire(adapter, config?) may only be used as a decorator.');
}
Expand Down
1 change: 1 addition & 0 deletions packages/@lwc/engine-core/src/framework/wiring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
ContextProvider,
ContextValue,
DataCallback,
ReplaceReactiveValues,
WireAdapter,
WireAdapterConstructor,
WireAdapterSchemaValue,
Expand Down
36 changes: 36 additions & 0 deletions packages/@lwc/engine-core/src/framework/wiring/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,39 @@ export type RegisterContextProviderFn = (
adapterContextToken: string,
onContextSubscription: WireContextSubscriptionCallback
) => void;

/** Resolves a property chain to the corresponding value on the target type. */
type ResolveReactiveValue<
/** The object to search for properties; initially the component. */
Target,
/** A string representing a chain of of property keys, e.g. "data.user.name". */
Keys extends string,
> = Keys extends `${infer FirstKey}.${infer Rest}`
? // If the string is "a.b.c", check if "a" is a prop on the target object
FirstKey extends keyof Target
? // If "a" exists on the target, check `target["a"]` for "b.c"
ResolveReactiveValue<Target[FirstKey], Rest>
: undefined
: // The string has no ".", use the full string as the key (e.g. we've reached "c" in "a.b.c")
Keys extends keyof Target
? Target[Keys]
: undefined;

/**
* Detects if the `Value` type is a property chain starting with "$". If so, it resolves the
* properties to the corresponding value on the target type.
*/
type ResolveValueIfReactive<Value, Target> = Value extends string
? string extends Value // `Value` is type `string`
? // Workaround for not being able to enforce `as const` assertions -- we don't know if this
// is a true string value (e.g. `@wire(adapter, {val: 'str'})`) or if it's a reactive prop
// (e.g. `@wire(adapter, {val: '$number'})`), so we have to go broad to avoid type errors.
any
: Value extends `$${infer Keys}` // String literal starting with "$", e.g. `$prop`
? ResolveReactiveValue<Target, Keys>
: Value // String literal *not* starting with "$", e.g. `"hello world"`
: Value; // non-string type

export type ReplaceReactiveValues<Config extends ConfigValue, Component> = {
[K in keyof Config]: ResolveValueIfReactive<Config[K], Component>;
};
3 changes: 1 addition & 2 deletions packages/@lwc/integration-types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
"description": "Type validation for LWC packages",
"type": "module",
"scripts": {
"test": "node ./scripts/update-paths.js --check && tsc && yarn run test:experimental-decorators",
"test:experimental-decorators": "tsc -p tsconfig.experimental-decorators.json",
"test": "node ./scripts/update-paths.js --check && tsc",
"playground": "rollup -c src/playground/rollup.config.js --watch"
},
"dependencies": {
Expand Down
25 changes: 0 additions & 25 deletions packages/@lwc/integration-types/src/decorators.ts

This file was deleted.

22 changes: 22 additions & 0 deletions packages/@lwc/integration-types/src/decorators/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { LightningElement, api } from 'lwc';

// @ts-expect-error bare decorator cannot be used
api();

// @ts-expect-error decorator doesn't work on classes
@api
export default class Test extends LightningElement {
@api optionalProperty?: string;
@api propertyWithDefault = true;
@api nonNullAssertedProperty!: object;
@api method() {}
@api getter(): undefined {}
@api setter(_: string) {}
}
31 changes: 31 additions & 0 deletions packages/@lwc/integration-types/src/decorators/track.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
*/

import { LightningElement, track } from 'lwc';

// This is okay! track has a non-decorator signature
track(123);
// This is okay because we treat implicit and explicit `undefined` identically
track(123, undefined);
// @ts-expect-error wrong number of arguments
track();
// @ts-expect-error wrong number of arguments
track({}, {});

// @ts-expect-error doesn't work on classes
@track
export default class Test extends LightningElement {
@track optionalProperty?: string;
@track propertyWithDefault = true;
@track nonNullAssertedProperty!: object;
// @ts-expect-error cannot be used on methods
@track method() {}
// @ts-expect-error cannot be used on getters
@track getter(): undefined {}
// @ts-expect-error cannot be used on setters
@track setter(_: string) {}
}
Loading
Loading