Skip to content

Commit 967c234

Browse files
johnjenkinsJohn Jenkins
andauthored
feat: new core decorators @PropSerialize & @AttrDeserialize (#6387)
* feat: new core decorators `@PropSerialize` & `@AttrDeserialize` * chore: prettier * fix-up unit tests * chore: fixup tests * chore: some tests * chore: let deserializers decide on if Prop has an attr * chore: more testing * chore: make tests great again * chore: update build files * chore: fix tests more * chore: we can do stuff before first render! * chore: revert async decl * chore: wdio test. Better typing * chore: more testing * chore: prettier --------- Co-authored-by: John Jenkins <john.jenkins@nanoporetech.com>
1 parent 680b12e commit 967c234

File tree

67 files changed

+1690
-258
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1690
-258
lines changed

src/app-data/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const BUILD: BuildConditionals = {
6060
vdomRender: true,
6161
vdomStyle: true,
6262
vdomText: true,
63-
watchCallback: true,
63+
propChangeCallback: true,
6464
taskQueue: true,
6565
hotModuleReplacement: false,
6666
isDebug: false,

src/client/client-host-ref.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const registerHost = (hostElement: d.HostElement, cmpMeta: d.ComponentRun
4949
$hostElement$: hostElement,
5050
$cmpMeta$: cmpMeta,
5151
$instanceValues$: new Map(),
52+
$serializerValues$: new Map(),
5253
};
5354
if (BUILD.isDev) {
5455
hostRef.$renderCount$ = 0;
@@ -61,6 +62,9 @@ export const registerHost = (hostElement: d.HostElement, cmpMeta: d.ComponentRun
6162
hostElement['s-p'] = [];
6263
hostElement['s-rc'] = [];
6364
}
65+
if (BUILD.lazyLoad) {
66+
hostRef.$fetchedCbList$ = [];
67+
}
6468

6569
const ref = hostRef;
6670
hostElement.__stencil__getHostRef = () => ref;

src/compiler/app-core/app-data.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const getBuildFeatures = (cmps: ComponentCompilerMeta[]): BuildFeatures =
2525
const f: BuildFeatures = {
2626
allRenderFn: cmps.every((c) => c.hasRenderFn),
2727
formAssociated: cmps.some((c) => c.formAssociated),
28-
28+
deserializer: cmps.some((c) => c.hasDeserializer),
2929
element: cmps.some((c) => c.hasElement),
3030
event: cmps.some((c) => c.hasEvent),
3131
hasRenderFn: cmps.some((c) => c.hasRenderFn),
@@ -41,14 +41,16 @@ export const getBuildFeatures = (cmps: ComponentCompilerMeta[]): BuildFeatures =
4141
method: cmps.some((c) => c.hasMethod),
4242
mode: cmps.some((c) => c.hasMode),
4343
modernPropertyDecls: cmps.some((c) => c.hasModernPropertyDecls),
44-
observeAttribute: cmps.some((c) => c.hasAttribute || c.hasWatchCallback),
44+
observeAttribute: cmps.some((c) => c.hasAttribute || c.hasWatchCallback || c.hasDeserializer),
4545
prop: cmps.some((c) => c.hasProp),
4646
propBoolean: cmps.some((c) => c.hasPropBoolean),
47+
propChangeCallback: cmps.some((c) => c.hasWatchCallback || c.hasDeserializer || c.hasSerializer),
4748
propNumber: cmps.some((c) => c.hasPropNumber),
4849
propString: cmps.some((c) => c.hasPropString),
4950
propMutable: cmps.some((c) => c.hasPropMutable),
50-
reflect: cmps.some((c) => c.hasReflect),
51+
reflect: cmps.some((c) => c.hasReflect || c.hasSerializer),
5152
scoped: cmps.some((c) => c.encapsulation === 'scoped'),
53+
serializer: cmps.some((c) => c.hasSerializer),
5254
shadowDom,
5355
shadowDelegatesFocus: shadowDom && cmps.some((c) => c.shadowDelegatesFocus),
5456
slot,
@@ -68,7 +70,6 @@ export const getBuildFeatures = (cmps: ComponentCompilerMeta[]): BuildFeatures =
6870
vdomRender: cmps.some((c) => c.hasVdomRender),
6971
vdomStyle: cmps.some((c) => c.hasVdomStyle),
7072
vdomText: cmps.some((c) => c.hasVdomText),
71-
watchCallback: cmps.some((c) => c.hasWatchCallback),
7273
taskQueue: true,
7374
};
7475
f.vdomAttribute = f.vdomAttribute || f.reflect;

src/compiler/transformers/component-build-conditionals.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export const setComponentBuildConditionals = (cmpMeta: d.ComponentCompilerMeta)
2121
cmpMeta.hasWatchCallback = true;
2222
}
2323

24+
if (cmpMeta.serializers.length > 0) {
25+
cmpMeta.hasSerializer = true;
26+
}
27+
28+
if (cmpMeta.deserializers.length > 0) {
29+
cmpMeta.hasDeserializer = true;
30+
}
31+
2432
if (cmpMeta.methods.length > 0) {
2533
cmpMeta.hasMethod = true;
2634
}

src/compiler/transformers/component-hydrate/hydrate-component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { addLazyElementGetter } from '../component-lazy/lazy-element-getter';
66
import { transformHostData } from '../host-data-transform';
77
import { removeStaticMetaProperties } from '../remove-static-meta-properties';
88
import { retrieveModifierLike } from '../transform-utils';
9-
import { addWatchers } from '../watcher-meta-transform';
9+
import { addReactivePropHandlers } from '../reactive-handler-meta-transform';
1010
import { addHydrateRuntimeCmpMeta } from './hydrate-runtime-cmp-meta';
1111

1212
export const updateHydrateComponentClass = (
@@ -33,7 +33,9 @@ const updateHydrateHostComponentMembers = (
3333

3434
updateLazyComponentConstructor(classMembers, classNode, moduleFile, cmp);
3535
addLazyElementGetter(classMembers, moduleFile, cmp);
36-
addWatchers(classMembers, cmp);
36+
addReactivePropHandlers(classMembers, cmp, 'watchers');
37+
addReactivePropHandlers(classMembers, cmp, 'serializers');
38+
addReactivePropHandlers(classMembers, cmp, 'deserializers');
3739
addHydrateRuntimeCmpMeta(classMembers, cmp);
3840
transformHostData(classMembers, moduleFile);
3941

src/compiler/transformers/component-lazy/lazy-component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { addStaticStylePropertyToClass } from '../add-static-style';
55
import { transformHostData } from '../host-data-transform';
66
import { removeStaticMetaProperties } from '../remove-static-meta-properties';
77
import { updateComponentClass } from '../update-component-class';
8-
import { addWatchers } from '../watcher-meta-transform';
8+
import { addReactivePropHandlers } from '../reactive-handler-meta-transform';
99
import { updateLazyComponentConstructor } from './lazy-constructor';
1010
import { addLazyElementGetter } from './lazy-element-getter';
1111

@@ -54,7 +54,9 @@ const updateLazyComponentMembers = (
5454

5555
updateLazyComponentConstructor(classMembers, classNode, moduleFile, cmp);
5656
addLazyElementGetter(classMembers, moduleFile, cmp);
57-
addWatchers(classMembers, cmp);
57+
addReactivePropHandlers(classMembers, cmp, 'watchers');
58+
addReactivePropHandlers(classMembers, cmp, 'serializers');
59+
addReactivePropHandlers(classMembers, cmp, 'deserializers');
5860
transformHostData(classMembers, moduleFile);
5961

6062
if (transformOpts.style === 'static') {

src/compiler/transformers/component-native/native-component.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { transformHostData } from '../host-data-transform';
77
import { removeStaticMetaProperties } from '../remove-static-meta-properties';
88
import { foundSuper, updateConstructor } from '../transform-utils';
99
import { updateComponentClass } from '../update-component-class';
10-
import { addWatchers } from '../watcher-meta-transform';
10+
import { addReactivePropHandlers } from '../reactive-handler-meta-transform';
1111
import { addNativeConnectedCallback } from './native-connected-callback';
1212
import { updateNativeConstructor } from './native-constructor';
1313
import { addNativeElementGetter } from './native-element-getter';
@@ -145,7 +145,9 @@ const updateNativeHostComponentMembers = (
145145
updateNativeConstructor(classMembers, moduleFile, cmp, classNode);
146146
addNativeConnectedCallback(classMembers, cmp);
147147
addNativeElementGetter(classMembers, cmp);
148-
addWatchers(classMembers, cmp);
148+
addReactivePropHandlers(classMembers, cmp, 'watchers');
149+
addReactivePropHandlers(classMembers, cmp, 'serializers');
150+
addReactivePropHandlers(classMembers, cmp, 'deserializers');
149151

150152
if (cmp.isPlain) {
151153
addNativeComponentMeta(classMembers, cmp);

src/compiler/transformers/decorators-to-static/convert-decorators.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { methodDecoratorsToStatic, validateMethods } from './method-decorator';
1515
import { propDecoratorsToStatic } from './prop-decorator';
1616
import { stateDecoratorsToStatic } from './state-decorator';
1717
import { watchDecoratorsToStatic } from './watch-decorator';
18+
import { serializeDecoratorsToStatic } from './serialize-decorators';
1819

1920
/**
2021
* Create a {@link ts.TransformerFactory} which will handle converting any
@@ -118,6 +119,22 @@ const visitClassDeclaration = (
118119
// stores a reference to fields that should be watched for changes
119120
// parse member decorators (Prop, State, Listen, Event, Method, Element and Watch)
120121
if (decoratedMembers.length > 0) {
122+
const serializers = serializeDecoratorsToStatic(
123+
typeChecker,
124+
decoratedMembers,
125+
filteredMethodsAndFields,
126+
importAliasMap.get('PropSerialize'),
127+
'PropSerialize',
128+
importAliasMap.get('Prop'),
129+
);
130+
const deserializers = serializeDecoratorsToStatic(
131+
typeChecker,
132+
decoratedMembers,
133+
filteredMethodsAndFields,
134+
importAliasMap.get('AttrDeserialize'),
135+
'AttrDeserialize',
136+
importAliasMap.get('Prop'),
137+
);
121138
propDecoratorsToStatic(
122139
config,
123140
diagnostics,
@@ -126,6 +143,8 @@ const visitClassDeclaration = (
126143
program,
127144
filteredMethodsAndFields,
128145
importAliasMap.get('Prop'),
146+
serializers,
147+
deserializers,
129148
);
130149
stateDecoratorsToStatic(decoratedMembers, filteredMethodsAndFields, typeChecker, importAliasMap.get('State'));
131150
eventDecoratorsToStatic(

src/compiler/transformers/decorators-to-static/decorators-constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
*/
44
export const STENCIL_DECORATORS = [
55
'AttachInternals',
6+
'AttrDeserialize',
7+
'PropSerialize',
68
'Component',
79
'Element',
810
'Event',
@@ -27,6 +29,8 @@ export const CLASS_DECORATORS_TO_REMOVE = ['Component'] as const satisfies reado
2729
*/
2830
export const MEMBER_DECORATORS_TO_REMOVE = [
2931
'AttachInternals',
32+
'AttrDeserialize',
33+
'PropSerialize',
3034
'Element',
3135
'Event',
3236
'Listen',
@@ -62,6 +66,8 @@ export const STATIC_GETTER_NAMES = [
6266
'styleUrls',
6367
'styles',
6468
'watchers',
69+
'serializers',
70+
'deserializers',
6571
] as const;
6672

6773
export type StencilStaticGetter = (typeof STATIC_GETTER_NAMES)[number];

src/compiler/transformers/decorators-to-static/prop-decorator.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { getDecoratorParameters, isDecoratorNamed } from './decorator-utils';
2828
* @param program a {@link ts.Program} object
2929
* @param newMembers a collection that parsed `@Prop` annotated class members should be pushed to as a side effect of calling this function
3030
* @param decoratorName the name of the decorator to look for
31+
* @param serializers a collection of serializers (from prop > attribute) used on `@Prop` annotated class members
3132
*/
3233
export const propDecoratorsToStatic = (
3334
config: d.ValidatedConfig,
@@ -37,10 +38,24 @@ export const propDecoratorsToStatic = (
3738
program: ts.Program,
3839
newMembers: ts.ClassElement[],
3940
decoratorName: string,
41+
serializers: d.ComponentCompilerChangeHandler[],
42+
deserializers: d.ComponentCompilerChangeHandler[],
4043
): void => {
4144
const properties = decoratedProps
4245
.filter((prop) => ts.isPropertyDeclaration(prop) || ts.isGetAccessor(prop))
43-
.map((prop) => parsePropDecorator(config, diagnostics, typeChecker, program, prop, decoratorName, newMembers))
46+
.map((prop) =>
47+
parsePropDecorator(
48+
config,
49+
diagnostics,
50+
typeChecker,
51+
program,
52+
prop,
53+
decoratorName,
54+
newMembers,
55+
serializers,
56+
deserializers,
57+
),
58+
)
4459
.filter((prop): prop is ts.PropertyAssignment => prop != null);
4560

4661
if (properties.length > 0) {
@@ -57,6 +72,8 @@ export const propDecoratorsToStatic = (
5772
* @param prop the TypeScript `PropertyDeclaration` to parse
5873
* @param decoratorName the name of the decorator to look for
5974
* @param newMembers a collection of parsed `@Prop` annotated class members. Used for `get()` decorated props to find a corresponding `set()`
75+
* @param serializers a collection of serializers (from prop > attribute) used on `@Prop` annotated class members
76+
* @param deserializers a collection of deserializers (from attribute > prop) used on `@Prop` annotated class members
6077
* @returns a property assignment expression to be added to the Stencil component's class
6178
*/
6279
const parsePropDecorator = (
@@ -67,6 +84,8 @@ const parsePropDecorator = (
6784
prop: ts.PropertyDeclaration | ts.GetAccessorDeclaration,
6885
decoratorName: string,
6986
newMembers: ts.ClassElement[],
87+
serializers: d.ComponentCompilerChangeHandler[],
88+
deserializers: d.ComponentCompilerChangeHandler[],
7089
): ts.PropertyAssignment | null => {
7190
const propDecorator = retrieveTsDecorators(prop)?.find(isDecoratorNamed(decoratorName));
7291
if (propDecorator == null) {
@@ -100,7 +119,6 @@ const parsePropDecorator = (
100119

101120
const propMeta: d.ComponentCompilerStaticProperty = {
102121
type: typeStr,
103-
attribute: getAttributeName(propName, propOptions),
104122
mutable: !!propOptions.mutable,
105123
complexType: getComplexType(typeChecker, prop, type, program),
106124
required: prop.exclamationToken !== undefined && propName !== 'mode',
@@ -113,9 +131,23 @@ const parsePropDecorator = (
113131
propMeta.ogPropName = ogPropName;
114132
}
115133

116-
// prop can have an attribute if type is NOT "unknown"
117-
if (typeStr !== 'unknown') {
118-
propMeta.reflect = getReflect(diagnostics, propDecorator, propOptions);
134+
const foundSerializer = !!serializers.find((s) => s.propName === propName);
135+
const foundDeserializer = !!deserializers.find((s) => s.propName === propName);
136+
137+
// a `@Prop` can reflect if the type is *not* `unknown` (i.e. string, number, boolean, any)
138+
// or `@Prop` has a serializer (a fn that can convert a complex type to a string)
139+
if (typeStr !== 'unknown' || foundSerializer) {
140+
const explicitReflect = getReflect(diagnostics, propDecorator, propOptions);
141+
// an explicit reflect argument always wins over inferred
142+
propMeta.reflect = explicitReflect === null ? foundSerializer : explicitReflect;
143+
}
144+
145+
// a `@Prop` is allowed to have an attribute if:
146+
// - the type is *not* `unknown` (i.e. string, number, boolean, any)
147+
// - the prop is reflected (because reflected props must have an attribute)
148+
// - a deserializer has been provided (it doesn't make sense to have a deserializer without an attribute)
149+
if (typeStr !== 'unknown' || propMeta.reflect || foundDeserializer) {
150+
propMeta.attribute = getAttributeName(propName, propOptions);
119151
}
120152

121153
// extract default value
@@ -179,7 +211,7 @@ const getAttributeName = (propName: string, propOptions: d.PropOptions): string
179211
* @param propOptions the options passed in to the `@Prop` call expression
180212
* @returns `true` if the prop should be reflected in the DOM, `false` otherwise
181213
*/
182-
const getReflect = (diagnostics: d.Diagnostic[], propDecorator: ts.Decorator, propOptions: d.PropOptions): boolean => {
214+
const getReflect = (diagnostics: d.Diagnostic[], propDecorator: ts.Decorator, propOptions: d.PropOptions) => {
183215
if (typeof propOptions.reflect === 'boolean') {
184216
return propOptions.reflect;
185217
}
@@ -188,9 +220,9 @@ const getReflect = (diagnostics: d.Diagnostic[], propDecorator: ts.Decorator, pr
188220
err.header = `Rename "reflectToAttr" to "reflect"`;
189221
err.messageText = `@Prop option "reflectToAttr" should be renamed to "reflect".`;
190222
augmentDiagnosticWithNode(err, propDecorator);
191-
return (propOptions as any).reflectToAttr;
223+
return (propOptions as any).reflectToAttr as boolean;
192224
}
193-
return false;
225+
return null;
194226
};
195227

196228
const getComplexType = (

0 commit comments

Comments
 (0)