Skip to content

Commit e38beed

Browse files
authored
Merge pull request #7621 from QwikDev/v2-throw-on-serializing-function
feat: throw on serializing function
2 parents af71080 + 9500039 commit e38beed

File tree

5 files changed

+118
-126
lines changed

5 files changed

+118
-126
lines changed

.changeset/rare-candies-join.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: skip serialize functions wrapped with the `noSerialize`

packages/qwik/src/core/shared/error/error.ts

Lines changed: 62 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -9,53 +9,37 @@ export const codeToText = (code: number, ...parts: any[]): string => {
99
'Scheduler not found', // 1
1010
'track() received object, without prop to track', // 2
1111
'Only primitive and object literals can be serialized. {{0}}', // 3
12-
'', // 4 unused
13-
'You can render over a existing q:container. Skipping render().', // 5
14-
'', // 6 unused
15-
'', // 7 unused
16-
'', // 8 unused
17-
'', // 9 unused
18-
'QRL is not a function', // 10
19-
'Dynamic import not found', // 11
20-
'Unknown type argument', // 12
21-
`Actual value for useContext({{0}}) can not be found, make sure some ancestor component has set a value using useContextProvider(). In the browser make sure that the context was used during SSR so its state was serialized.`, // 13
22-
"Invoking 'use*()' method outside of invocation context.", // 14
23-
'', // 15 unused
24-
'', // 16 unused
25-
'', // 17 unused
26-
'', // 18 unused
27-
'', // 19 unused
28-
`Calling a 'use*()' method outside 'component$(() => { HERE })' is not allowed. 'use*()' methods provide hooks to the 'component$' state and lifecycle, ie 'use' hooks can only be called synchronously within the 'component$' function or another 'use' method.\nSee https://qwik.dev/docs/components/tasks/#use-method-rules`, // 20
29-
'', // 21 unused
30-
'', // 22 unused
31-
'', // 23 unused
32-
'', // 24 unused
33-
'', // 25 unused
34-
'', // 26 unused
35-
'', // 27 unused
36-
'The provided Context reference "{{0}}" is not a valid context created by createContextId()', // 28
37-
'SsrError(tag): {{0}}', // 29
38-
'QRLs can not be resolved because it does not have an attached container. This means that the QRL does not know where it belongs inside the DOM, so it cant dynamically import() from a relative path.', // 30
39-
'QRLs can not be dynamically resolved, because it does not have a chunk path', // 31
40-
'{{0}}\nThe JSX ref attribute must be a Signal', // 32
41-
'Serialization Error: Deserialization of data type {{0}} is not implemented', // 33
42-
'Serialization Error: Expected vnode for ref prop, but got {{0}}', // 34
43-
'Serialization Error: Cannot allocate data type {{0}}', // 35
44-
'Serialization Error: Missing root id for {{0}}', // 36
45-
'Serialization Error: Serialization of data type {{0}} is not implemented', // 37
46-
'Serialization Error: Unvisited {{0}}', // 38
47-
'Serialization Error: Missing QRL chunk for {{0}}', // 39
48-
'{{0}}\nThe value of the textarea must be a string found {{1}}', // 40
49-
'Unable to find q:container', // 41
50-
"Element must have 'q:container' attribute.", // 42
51-
'Unknown vnode type {{0}}.', // 43
52-
'Materialize error: missing element: {{0}} {{1}} {{2}}', // 44
53-
'Cannot coerce a Signal, use `.value` instead', // 45
54-
'useComputed$ QRL {{0}} {{1}} cannot return a Promise', // 46
55-
'ComputedSignal is read-only', // 47
56-
'WrappedSignal is read-only', // 48
57-
'Attribute value is unsafe for SSR', // 49
58-
'SerializerSymbol function returned rejected promise', // 50
12+
'You can render over a existing q:container. Skipping render().', // 4
13+
'QRL is not a function', // 5
14+
'Dynamic import not found', // 6
15+
'Unknown type argument', // 7
16+
`Actual value for useContext({{0}}) can not be found, make sure some ancestor component has set a value using useContextProvider(). In the browser make sure that the context was used during SSR so its state was serialized.`, // 8
17+
"Invoking 'use*()' method outside of invocation context.", // 9
18+
`Calling a 'use*()' method outside 'component$(() => { HERE })' is not allowed. 'use*()' methods provide hooks to the 'component$' state and lifecycle, ie 'use' hooks can only be called synchronously within the 'component$' function or another 'use' method.\nSee https://qwik.dev/docs/components/tasks/#use-method-rules`, // 10
19+
'The provided Context reference "{{0}}" is not a valid context created by createContextId()', // 11
20+
'SsrError(tag): {{0}}', // 12
21+
'QRLs can not be resolved because it does not have an attached container. This means that the QRL does not know where it belongs inside the DOM, so it cant dynamically import() from a relative path.', // 13
22+
'QRLs can not be dynamically resolved, because it does not have a chunk path', // 14
23+
'{{0}}\nThe JSX ref attribute must be a Signal', // 15
24+
'Serialization Error: Deserialization of data type {{0}} is not implemented', // 16
25+
'Serialization Error: Expected vnode for ref prop, but got {{0}}', // 17
26+
'Serialization Error: Cannot allocate data type {{0}}', // 18
27+
'Serialization Error: Missing root id for {{0}}', // 19
28+
'Serialization Error: Serialization of data type {{0}} is not implemented', // 20
29+
'Serialization Error: Unvisited {{0}}', // 21
30+
'Serialization Error: Missing QRL chunk for {{0}}', // 22
31+
'{{0}}\nThe value of the textarea must be a string found {{1}}', // 23
32+
'Unable to find q:container', // 24
33+
"Element must have 'q:container' attribute.", // 25
34+
'Unknown vnode type {{0}}.', // 26
35+
'Materialize error: missing element: {{0}} {{1}} {{2}}', // 27
36+
'Cannot coerce a Signal, use `.value` instead', // 28
37+
'useComputed$ QRL {{0}} {{1}} cannot return a Promise', // 29
38+
'ComputedSignal is read-only', // 30
39+
'WrappedSignal is read-only', // 31
40+
'Attribute value is unsafe for SSR', // 32
41+
'SerializerSymbol function returned rejected promise', // 33
42+
'Serialization Error: Cannot serialize function: {{0}}', // 34
5943
];
6044
let text = MAP[code] ?? '';
6145
if (parts.length) {
@@ -79,53 +63,37 @@ export const enum QError {
7963
schedulerNotFound = 1,
8064
trackObjectWithoutProp = 2,
8165
verifySerializable = 3,
82-
UNUSED_4 = 4,
83-
cannotRenderOverExistingContainer = 5,
84-
UNUSED_6 = 6,
85-
UNUSED_7 = 7,
86-
UNUSED_8 = 8,
87-
UNUSED_9 = 9,
88-
qrlIsNotFunction = 10,
89-
dynamicImportFailed = 11,
90-
unknownTypeArgument = 12,
91-
notFoundContext = 13,
92-
useMethodOutsideContext = 14,
93-
UNUSED_15 = 15,
94-
UNUSED_16 = 16,
95-
UNUSED_17 = 17,
96-
UNUSED_18 = 18,
97-
UNUSED_19 = 19,
98-
useInvokeContext = 20,
99-
UNUSED_21 = 21,
100-
UNUSED_22 = 22,
101-
UNUSED_23 = 23,
102-
UNUSED_24 = 24,
103-
UNUSED_25 = 25,
104-
UNUSED_26 = 26,
105-
UNUSED_27 = 27,
106-
invalidContext = 28,
107-
tagError = 29,
108-
qrlMissingContainer = 30,
109-
qrlMissingChunk = 31,
110-
invalidRefValue = 32,
111-
serializeErrorNotImplemented = 33,
112-
serializeErrorExpectedVNode = 34,
113-
serializeErrorCannotAllocate = 35,
114-
serializeErrorMissingRootId = 36,
115-
serializeErrorUnknownType = 37,
116-
serializeErrorUnvisited = 38,
117-
serializeErrorMissingChunk = 39,
118-
wrongTextareaValue = 40,
119-
containerNotFound = 41,
120-
elementWithoutContainer = 42,
121-
invalidVNodeType = 43,
122-
materializeVNodeDataError = 44,
123-
cannotCoerceSignal = 45,
124-
computedNotSync = 46,
125-
computedReadOnly = 47,
126-
wrappedReadOnly = 48,
127-
unsafeAttr = 49,
128-
serializerSymbolRejectedPromise = 50,
66+
cannotRenderOverExistingContainer = 4,
67+
qrlIsNotFunction = 5,
68+
dynamicImportFailed = 6,
69+
unknownTypeArgument = 7,
70+
notFoundContext = 8,
71+
useMethodOutsideContext = 9,
72+
useInvokeContext = 10,
73+
invalidContext = 11,
74+
tagError = 12,
75+
qrlMissingContainer = 13,
76+
qrlMissingChunk = 14,
77+
invalidRefValue = 15,
78+
serializeErrorNotImplemented = 16,
79+
serializeErrorExpectedVNode = 17,
80+
serializeErrorCannotAllocate = 18,
81+
serializeErrorMissingRootId = 19,
82+
serializeErrorUnknownType = 20,
83+
serializeErrorUnvisited = 21,
84+
serializeErrorMissingChunk = 22,
85+
wrongTextareaValue = 23,
86+
containerNotFound = 24,
87+
elementWithoutContainer = 25,
88+
invalidVNodeType = 26,
89+
materializeVNodeDataError = 27,
90+
cannotCoerceSignal = 28,
91+
computedNotSync = 29,
92+
computedReadOnly = 30,
93+
wrappedReadOnly = 31,
94+
unsafeAttr = 32,
95+
serializerSymbolRejectedPromise = 33,
96+
serializeErrorCannotSerializeFunction = 34,
12997
}
13098

13199
export const qError = (code: number, errorMessageArgs: any[] = []): Error => {

packages/qwik/src/core/shared/shared-serialization.ts

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -933,20 +933,21 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
933933
serializationContext.$addRoot$(qrl, null);
934934
};
935935

936-
const outputRootRef = (value: unknown, elseCallback: () => void) => {
936+
const outputRootRef = (value: unknown, rootDepth = 0) => {
937937
const seen = $wasSeen$(value);
938938
const rootRefPath = $pathMap$.get(value);
939-
if (isRootObject() && seen && seen.$parent$ !== null && rootRefPath) {
939+
if (rootDepth === depth && seen && seen.$parent$ !== null && rootRefPath) {
940940
output(TypeIds.RootRef, rootRefPath);
941-
} else if (depth > 0 && seen && seen.$rootIndex$ !== -1) {
941+
return true;
942+
} else if (depth > rootDepth && seen && seen.$rootIndex$ !== -1) {
942943
output(TypeIds.RootRef, seen.$rootIndex$);
943-
} else {
944-
elseCallback();
944+
return true;
945945
}
946+
return false;
946947
};
947948

948949
const writeValue = (value: unknown) => {
949-
if (fastSkipSerialize(value as object)) {
950+
if (fastSkipSerialize(value as object | Function)) {
950951
output(TypeIds.Constant, Constants.Undefined);
951952
} else if (typeof value === 'bigint') {
952953
output(TypeIds.BigInt, value.toString());
@@ -958,7 +959,7 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
958959
} else if (value === Fragment) {
959960
output(TypeIds.Constant, Constants.Fragment);
960961
} else if (isQrl(value)) {
961-
outputRootRef(value, () => {
962+
if (!outputRootRef(value)) {
962963
const qrl = qrlToString(serializationContext, value);
963964
const type = preloadQrls.has(value) ? TypeIds.PreloadQRL : TypeIds.QRL;
964965
if (isRootObject()) {
@@ -967,15 +968,13 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
967968
const id = serializationContext.$addRoot$(qrl);
968969
output(type, id);
969970
}
970-
});
971+
}
971972
} else if (isQwikComponent(value)) {
972973
const [qrl]: [QRLInternal] = (value as any)[SERIALIZABLE_STATE];
973974
serializationContext.$renderSymbols$.add(qrl.$symbol$);
974975
output(TypeIds.Component, [qrl]);
975976
} else {
976-
// TODO this happens for inline components with render props like Resource
977-
console.error('Cannot serialize function (ignoring for now): ' + value.toString());
978-
output(TypeIds.Constant, Constants.Undefined);
977+
throw qError(QError.serializeErrorCannotSerializeFunction, [value.toString()]);
979978
}
980979
} else if (typeof value === 'number') {
981980
if (Number.isNaN(value)) {
@@ -1013,9 +1012,9 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
10131012
if (value.length === 0) {
10141013
output(TypeIds.Constant, Constants.EmptyString);
10151014
} else {
1016-
outputRootRef(value, () => {
1015+
if (!outputRootRef(value)) {
10171016
output(TypeIds.String, value);
1018-
});
1017+
}
10191018
}
10201019
} else if (typeof value === 'undefined') {
10211020
output(TypeIds.Constant, Constants.Undefined);
@@ -1033,28 +1032,15 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
10331032
* The object writer outputs an array object (without type prefix) and this increases the depth
10341033
* for the objects within (depth 1).
10351034
*/
1036-
const isRootObject = depth === 1;
10371035
// Objects are the only way to create circular dependencies.
10381036
// So the first thing to to is to see if we have a circular dependency.
10391037
// (NOTE: For root objects we need to serialize them regardless if we have seen
10401038
// them before, otherwise the root object reference will point to itself.)
10411039
// Also note that depth will be 1 for objects in root
1042-
if (isRootObject) {
1043-
const seen = $wasSeen$(value);
1044-
const rootPath = $pathMap$.get(value);
1045-
if (rootPath && seen && seen.$parent$ !== null) {
1046-
output(TypeIds.RootRef, rootPath);
1047-
return;
1048-
}
1049-
} else if (depth > 1) {
1050-
const seen = $wasSeen$(value);
1051-
if (seen && seen.$rootIndex$ !== -1) {
1052-
// We have seen this object before, so we can serialize it as a reference.
1053-
// Otherwise serialize as normal
1054-
output(TypeIds.RootRef, seen.$rootIndex$);
1055-
return;
1056-
}
1040+
if (outputRootRef(value, 1)) {
1041+
return;
10571042
}
1043+
10581044
if (isPropsProxy(value)) {
10591045
const varProps = value[_VAR_PROPS];
10601046
const constProps = value[_CONST_PROPS];

packages/qwik/src/core/shared/shared-serialization.unit.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { isQrl } from './qrl/qrl-utils';
2626
import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils';
2727
import { SubscriptionData } from '../reactive-primitives/subscription-data';
2828
import { StoreFlags } from '../reactive-primitives/types';
29+
import { QError } from './error/error';
2930

3031
const DEBUG = false;
3132

@@ -942,6 +943,32 @@ describe('shared-serialization', () => {
942943
(5 chars)"
943944
`);
944945
});
946+
it('should ignore functions in noSerialize set', async () => {
947+
const obj = { hi: true, ignore: noSerialize(() => console.warn()) };
948+
const state = await serialize(obj);
949+
expect(dumpState(state)).toMatchInlineSnapshot(`
950+
"
951+
0 Object [
952+
String "hi"
953+
Constant true
954+
]
955+
(17 chars)"
956+
`);
957+
});
958+
it('should ignore functions with NoSerializeSymbol', async () => {
959+
const ignore = () => console.warn();
960+
(ignore as any)[NoSerializeSymbol] = true;
961+
const obj = { hi: true, ignore };
962+
const state = await serialize(obj);
963+
expect(dumpState(state)).toMatchInlineSnapshot(`
964+
"
965+
0 Object [
966+
String "hi"
967+
Constant true
968+
]
969+
(17 chars)"
970+
`);
971+
});
945972
it('should ignore NoSerializeSymbol', async () => {
946973
const obj = { hi: true, [NoSerializeSymbol]: true };
947974
const state = await serialize(obj);
@@ -1047,7 +1074,9 @@ describe('shared-serialization', () => {
10471074
throw 'oh no';
10481075
}
10491076
}
1050-
await expect(serialize(new Foo())).rejects.toThrow('Q50');
1077+
await expect(serialize(new Foo())).rejects.toThrow(
1078+
'Q' + QError.serializerSymbolRejectedPromise
1079+
);
10511080
expect(consoleSpy).toHaveBeenCalledWith('oh no');
10521081
consoleSpy.mockRestore();
10531082
});

packages/qwik/src/core/shared/utils/serialize-utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,12 @@ export const shouldSerialize = (obj: unknown): boolean => {
9999
return true;
100100
};
101101

102-
export const fastSkipSerialize = (obj: object): boolean => {
103-
return typeof obj === 'object' && obj && (NoSerializeSymbol in obj || noSerializeSet.has(obj));
102+
export const fastSkipSerialize = (obj: object | Function): boolean => {
103+
return (
104+
obj &&
105+
(typeof obj === 'object' || typeof obj === 'function') &&
106+
(NoSerializeSymbol in obj || noSerializeSet.has(obj))
107+
);
104108
};
105109

106110
export const fastWeakSerialize = (obj: object): boolean => {

0 commit comments

Comments
 (0)