Skip to content

Commit 81273be

Browse files
committed
feat: add webshellStrictMode prop to raise errors on inconsistencies
By default, warning messages will be logged when `webshellDebug` is set to `true`, which defaults to `__DEV__`. From now on, errors will be raised if `webshellStrictMode` is set to `true`. Defaults to `false`.
1 parent 69a67b5 commit 81273be

9 files changed

+183
-80
lines changed

packages/core/etc/webshell.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export interface WebshellInvariantProps {
393393
onWebFeatureError?: (featureIdentifier: string, error: string) => void;
394394
webHandle?: Ref<WebHandle>;
395395
webshellDebug?: boolean;
396+
webshellStrictMode?: boolean;
396397
}
397398

398399
// @public

packages/core/src/Reporter.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
export type ErrorCode =
2+
| 'WEBSH_MISSING_SHELL_HANDLER'
3+
| 'WEBSH_SCRIPT_ERROR'
4+
| 'WEBSH_WEBVIEW_MISSING_MEMBER'
5+
| 'WEBSH_FEAT_MISSING_WEB_HANDLER'
6+
| 'WEBSH_FEAT_MISSING_IN_SHELL';
7+
8+
function describe(template: TemplateStringsArray, ...args: any[]) {
9+
return (
10+
'[Webshell]: ' +
11+
template
12+
.reduce((buffer, currentSnippet, index) => {
13+
buffer.push(currentSnippet, args[index] || '');
14+
return buffer;
15+
}, [] as string[])
16+
.join('')
17+
);
18+
}
19+
20+
interface ErrorDefinition<E extends ErrorCode> {
21+
code: E;
22+
verbose: (...args: any[]) => string;
23+
}
24+
25+
const ErrorCodes: Record<ErrorCode, ErrorDefinition<ErrorCode>> = {
26+
WEBSH_MISSING_SHELL_HANDLER: {
27+
code: 'WEBSH_MISSING_SHELL_HANDLER',
28+
verbose: function (identifier, handlerId) {
29+
return describe`Web script from feature "${identifier}" sent an event for "${handlerId}" shell handler, but there is no shell handler registered for this feature. Use FeatureBuilder.withShellHandler to register that handler, or make sure its name is not misspell in the DOM script.`;
30+
}
31+
},
32+
WEBSH_FEAT_MISSING_WEB_HANDLER: {
33+
code: 'WEBSH_FEAT_MISSING_WEB_HANDLER',
34+
verbose: function (identifier, handlerId) {
35+
return describe`Feature "${identifier}" has no Web handler with ID "${handlerId}".`;
36+
}
37+
},
38+
WEBSH_FEAT_MISSING_IN_SHELL: {
39+
code: 'WEBSH_FEAT_MISSING_IN_SHELL',
40+
verbose: function (identifier) {
41+
return describe`Feature ${identifier} has not be instantiated in this shell.`;
42+
}
43+
},
44+
WEBSH_SCRIPT_ERROR: {
45+
code: 'WEBSH_SCRIPT_ERROR',
46+
verbose: function (identifier, body = 'empty error') {
47+
return describe`Web script from feature "${identifier}" raised an error: ${body}`;
48+
}
49+
},
50+
WEBSH_WEBVIEW_MISSING_MEMBER: {
51+
code: 'WEBSH_WEBVIEW_MISSING_MEMBER',
52+
verbose: function () {
53+
return describe`The WebView element you passed is missing "injectJavaScript" method.`;
54+
}
55+
}
56+
};
57+
58+
export class Reporter {
59+
private readonly webshellDebug: boolean;
60+
private readonly strict: boolean;
61+
constructor(webshellDebug: boolean, strict: boolean) {
62+
this.webshellDebug = webshellDebug;
63+
this.strict = strict;
64+
}
65+
dispatchError(code: ErrorCode, ...args: any[]) {
66+
if (!this.webshellDebug) {
67+
return;
68+
}
69+
const message = ErrorCodes[code].verbose(...args);
70+
if (this.strict) {
71+
throw new Error(message);
72+
} else {
73+
console.warn(message);
74+
}
75+
}
76+
77+
dispatchWebLog(
78+
severity: 'warn' | 'info',
79+
identifier: string,
80+
message: string
81+
) {
82+
if (!this.webshellDebug) {
83+
return;
84+
}
85+
if (severity === 'warn') {
86+
console.warn(`[Webshell(${identifier})]: ${message}`);
87+
} else if (severity === 'info') {
88+
console.info(`[Webshell(${identifier})]: ${message}`);
89+
}
90+
}
91+
}

packages/core/src/__tests__/make-webshell.test.tsx

+19-14
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@ import dummyHandleridScript from './feat/DummyHandlerid.webjs';
1010
import dummyReceiverScript from './feat/DummyReceiver.webjs';
1111
import { makeWebshell } from '../make-webshell';
1212
import { FeatureBuilder } from '../FeatureBuilder';
13-
import { MinimalWebViewProps, WebHandle } from '../types';
13+
import {
14+
MinimalWebViewProps,
15+
WebHandle,
16+
WebshellInvariantProps
17+
} from '../types';
1418
import { act } from 'react-test-renderer';
1519

1620
const { waitForErsatz } = makeErsatzTesting(Ersatz);
1721

22+
const defaultWebshellProps: WebshellInvariantProps = {
23+
webshellDebug: true,
24+
webshellStrictMode: true
25+
};
26+
1827
const DummyWebView = React.forwardRef(({}: MinimalWebViewProps, ref) => {
1928
React.useImperativeHandle(ref, () => ({
2029
injectJavaScript() {}
@@ -66,7 +75,7 @@ const ReceiverFeature = new FeatureBuilder({
6675
describe('Webshell component', () => {
6776
it('sould mount without error', () => {
6877
const Webshell = makeWebshell(DummyWebView, new HelloFeature());
69-
const { UNSAFE_getByType } = render(<Webshell />);
78+
const { UNSAFE_getByType } = render(<Webshell {...defaultWebshellProps} />);
7079
const webshell = UNSAFE_getByType(Webshell);
7180
expect(webshell).toBeTruthy();
7281
});
@@ -80,18 +89,11 @@ describe('Webshell component', () => {
8089
);
8190
expect(onDOMDummyHello).toHaveBeenCalledWith('Hello world!');
8291
});
83-
it('should handle feature failures', async () => {
84-
const onDOMDummyFailure = jest.fn();
92+
it('should handle feature failures with onWebFeatureError', async () => {
8593
const onFailure = jest.fn();
8694
const Webshell = makeWebshell(Ersatz, new FailingFeature());
8795
await waitForErsatz(
88-
render(
89-
<Webshell
90-
webshellDebug={false}
91-
onDOMDummyFailure={onDOMDummyFailure}
92-
onWebFeatureError={onFailure}
93-
/>
94-
)
96+
render(<Webshell webshellDebug={false} onWebFeatureError={onFailure} />)
9597
);
9698
expect(onFailure).toHaveBeenCalledWith(
9799
FailingFeature.identifier,
@@ -103,7 +105,10 @@ describe('Webshell component', () => {
103105
const Webshell = makeWebshell(Ersatz, new OptionFeature({ foo: 'bar' }));
104106
await waitForErsatz(
105107
render(
106-
<Webshell webshellDebug={false} onDOMDummyOption={onDOMDummyOption} />
108+
<Webshell
109+
{...defaultWebshellProps}
110+
onDOMDummyOption={onDOMDummyOption}
111+
/>
107112
)
108113
);
109114
expect(onDOMDummyOption).toHaveBeenCalledWith({ foo: 'bar' });
@@ -114,7 +119,7 @@ describe('Webshell component', () => {
114119
await waitForErsatz(
115120
render(
116121
<Webshell
117-
webshellDebug={false}
122+
{...defaultWebshellProps}
118123
onDOMDummyOption={onHandlerIdDummyOption}
119124
/>
120125
)
@@ -130,8 +135,8 @@ describe('Webshell component', () => {
130135
render(
131136
<Webshell
132137
webHandle={webHandle}
133-
webshellDebug={false}
134138
onWebFeedback={onWebFeedback}
139+
{...defaultWebshellProps}
135140
/>
136141
)
137142
);

packages/core/src/make-webshell.tsx

+24-34
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
import { FeatureRegistry } from './FeatureRegistry';
1818
import { BufferedWebRMIHandle } from './web/BufferedWebRMIHandle';
1919
import { WebFeaturesLoader } from './web/WebFeaturesLoader';
20+
import { Reporter } from './Reporter';
2021

2122
interface WebViewMessage {
2223
data: string;
@@ -49,6 +50,7 @@ function isPostMessageObject(o: unknown): o is PostMessage {
4950

5051
function useWebMessageBus(
5152
registry: FeatureRegistry<any>,
53+
reporter: Reporter,
5254
{
5355
webshellDebug,
5456
onWebFeatureError,
@@ -74,47 +76,26 @@ function useWebMessageBus(
7476
if (type === 'feature') {
7577
const propDef = registry.getPropDefFromId(identifier, handlerId);
7678
if (!propDef) {
77-
console.warn(
78-
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there is ` +
79-
'no handler registered for this feature. ' +
80-
'Use FeatureBuilder.withShellHandler to register that handler, or make ' +
81-
'sure its name is not misspell in the DOM script.'
79+
reporter.dispatchError(
80+
'WEBSH_MISSING_SHELL_HANDLER',
81+
identifier,
82+
handlerId
8283
);
8384
return;
8485
}
8586
const handlerName = propDef.name;
8687
const handler =
8788
typeof handlerId === 'string' ? domHandlers[handlerName] : null;
88-
if (registry.getPropDefFromHandlerName(handlerName)) {
89-
if (typeof handler === 'function') {
90-
handler(body);
91-
} else {
92-
webshellDebug &&
93-
console.info(
94-
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there ` +
95-
`is no handler prop named "${handlerName}" attached to the shell.`
96-
);
97-
}
98-
} else {
99-
console.warn(
100-
`[Webshell]: script from feature "${identifier}" sent an event towards ${handlerId} handler, but there is ` +
101-
`no handler named "${handlerName}" defined with this handler ID. ` +
102-
'Use FeatureBuilder.withShellHandler to register that handler, or make ' +
103-
'sure its name is not misspell in the DOM script.'
104-
);
89+
if (typeof handler === 'function') {
90+
handler(body);
10591
}
10692
} else if (type === 'error') {
10793
// Handle as an error message
10894
typeof onWebFeatureError === 'function' &&
10995
onWebFeatureError(identifier, body);
110-
webshellDebug &&
111-
console.warn(
112-
`[Webshell]: script from feature "${identifier}" raised an error: ${body}`
113-
);
114-
return;
96+
reporter.dispatchError('WEBSH_SCRIPT_ERROR', identifier, body);
11597
} else if (type === 'log') {
116-
webshellDebug && severity === 'warn' && console.warn(body);
117-
webshellDebug && severity === 'info' && console.info(body);
98+
reporter.dispatchWebLog(severity, identifier, body);
11899
}
119100
} else {
120101
typeof onMessage === 'function' && onMessage(nativeEvent);
@@ -131,11 +112,13 @@ function useWebMessageBus(
131112

132113
function useWebHandle(
133114
webViewRef: React.RefObject<any>,
134-
registry: FeatureRegistry<any>
115+
registry: FeatureRegistry<any>,
116+
reporter: Reporter
135117
) {
136118
return React.useMemo(
137-
(): BufferedWebRMIHandle => new BufferedWebRMIHandle(webViewRef, registry),
138-
[webViewRef, registry]
119+
(): BufferedWebRMIHandle =>
120+
new BufferedWebRMIHandle(webViewRef, registry, reporter),
121+
[webViewRef, registry, reporter]
139122
);
140123
}
141124

@@ -200,14 +183,20 @@ export function makeWebshell<
200183
webViewRef,
201184
injectedJavaScript: userInjectedJavaScript,
202185
webshellDebug,
186+
webshellStrictMode,
203187
...webViewProps
204188
} = props;
189+
const reporter = React.useMemo(
190+
() => new Reporter(webshellDebug, webshellStrictMode),
191+
[webshellDebug, webshellStrictMode]
192+
);
205193
const { handleOnWebMessage, isLoaded } = useWebMessageBus(
206194
registry,
195+
reporter,
207196
otherProps
208197
);
209198
const injectedJavaScript = useJavaScript(loader, userInjectedJavaScript);
210-
const webHandle = useWebHandle(webViewRef, registry);
199+
const webHandle = useWebHandle(webViewRef, registry, reporter);
211200

212201
React.useImperativeHandle(webHandleRef, () => webHandle);
213202
React.useEffect(() => {
@@ -229,7 +218,8 @@ export function makeWebshell<
229218
);
230219
};
231220
Webshell.defaultProps = {
232-
webshellDebug: __DEV__
221+
webshellDebug: __DEV__,
222+
webshellStrict: false
233223
};
234224
return React.forwardRef<
235225
ElementRef<C>,

packages/core/src/types.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,16 @@ export interface WebshellInvariantProps {
214214
/**
215215
* Report Web error messages from features in the console.
216216
*
217-
* @defaultvalue `__DEV__`
217+
* @defaultvalue `__DEV__` (`true` in development, `false` otherwise)
218218
*/
219219
webshellDebug?: boolean;
220+
/**
221+
* If this prop is `true` and `webshellDebug` is `true`, errors will be
222+
* thrown when inconsistencies are identified.
223+
*
224+
* @defaultvalue false
225+
*/
226+
webshellStrictMode?: boolean;
220227
/**
221228
* Pass a reference to send messages to the Web environment.
222229
*/

packages/core/src/web/BufferedWebRMIHandle.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { FeatureRegistry } from '../FeatureRegistry';
3+
import { Reporter } from '../Reporter';
34
import { WebHandle } from '../types';
45
import { WebRMIHandle } from './WebRMIHandle';
56

@@ -17,9 +18,10 @@ export class BufferedWebRMIHandle implements WebHandle {
1718

1819
constructor(
1920
webViewRef: React.RefObject<any>,
20-
registry: FeatureRegistry<any>
21+
registry: FeatureRegistry<any>,
22+
webshellDebug: Reporter
2123
) {
22-
this.handle = new WebRMIHandle(webViewRef, registry);
24+
this.handle = new WebRMIHandle(webViewRef, registry, webshellDebug);
2325
this.postMessageToWeb = this.proxify('postMessageToWeb');
2426
this.setDebug = this.proxify('setDebug');
2527
}

packages/core/src/web/WebRMIController.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RefObject } from 'react';
2+
import { Reporter } from '../Reporter';
23

34
function javaScript(snippets: TemplateStringsArray, ...args: any[]) {
45
return snippets
@@ -13,15 +14,15 @@ export class WebRMIController {
1314
private webViewRef: RefObject<{
1415
injectJavaScript: (js: string) => void;
1516
}>;
16-
constructor(webViewRef: RefObject<any>) {
17+
protected reporter: Reporter;
18+
constructor(webViewRef: RefObject<any>, reporter: Reporter) {
1719
this.webViewRef = webViewRef;
20+
this.reporter = reporter;
1821
}
1922

2023
protected injectJavaScript(snippets: TemplateStringsArray, ...args: any[]) {
2124
if (this.webViewRef.current && !this.webViewRef.current.injectJavaScript) {
22-
console.warn(
23-
'[Webshell]: The WebView element you passed is missing injectJavaScript method.'
24-
);
25+
this.reporter.dispatchError('WEBSH_WEBVIEW_MISSING_MEMBER');
2526
return;
2627
}
2728
this.webViewRef.current?.injectJavaScript(javaScript(snippets, ...args));

0 commit comments

Comments
 (0)