-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseVscodeState.ts
More file actions
105 lines (95 loc) · 3.37 KB
/
useVscodeState.ts
File metadata and controls
105 lines (95 loc) · 3.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ACT, type Patch, PATCH, type Action, type WebviewKey } from '../types/reducer';
import { isFnKey } from './ipcReducer';
import type { StateReducer, VsCodeApi } from './types';
type PostAction<A extends object> = Pick<Action<A>, 'key' | 'params'>;
function isMyPatchMessage<A extends object>(message: unknown, id: WebviewKey): message is Patch<A> {
return (
message !== null &&
message !== undefined &&
typeof message === 'object' &&
'providerId' in message &&
'type' in message &&
'key' in message &&
'patch' in message &&
message.type === PATCH &&
typeof message.providerId === 'string' &&
message.providerId === id
);
}
const dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);
export function useVscodeState<S, A extends object>(
vscode: VsCodeApi,
providerId: WebviewKey,
postReducer: StateReducer<S, A>,
initialState: S | (() => S)
): readonly [S, A] {
const [state, setState] = useState<S>(
typeof initialState === 'function' ? (initialState as () => S)() : initialState
);
const validKeys = useMemo(
() => new Set(Object.keys(postReducer).filter((k) => !dangerousKeys.has(k))),
[postReducer]
);
useEffect(() => {
const handler = (event: MessageEvent<unknown>) => {
const { data } = event;
if (isMyPatchMessage<A>(data, providerId)) {
if (
validKeys.has(String(data.key)) &&
Object.prototype.hasOwnProperty.call(postReducer, data.key) &&
typeof postReducer[data.key] === 'function'
) {
const patchFn = postReducer[data.key];
setState((prev) => patchFn(prev, data.patch));
} else {
throw new Error(`Could not find a function for ${String(data.key)} in postReducer`);
}
}
};
window.addEventListener('message', handler);
return () => {
window.removeEventListener('message', handler);
};
}, [postReducer, providerId, validKeys]);
const postAction = useCallback(
(arg: PostAction<A>) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, sonarjs/different-types-comparison
if (vscode === undefined) {
throw new Error('Vscode api is undefined');
}
vscode.postMessage({
type: ACT,
providerId: providerId,
key: arg.key,
params: arg.params,
} satisfies Action<A>);
},
[vscode, providerId]
);
const actor = new Proxy({} as A, {
// eslint-disable-next-line code-complete/enforce-meaningful-names
get(_, prop) {
if (typeof prop !== 'string' && typeof prop !== 'symbol') {
throw new TypeError(`Invalid action type: ${String(prop)}`);
}
if (typeof prop === 'string' && dangerousKeys.has(prop)) {
throw new Error(`Dangerous action key is blocked: ${prop}`);
}
if (!isFnKey(prop, postReducer)) {
throw new Error(`Unknown or invalid action: ${String(prop)}`);
}
return (...args: unknown[]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const params = args as A[typeof prop] extends (...args: unknown[]) => any
? Parameters<A[typeof prop]>
: never;
postAction({
key: prop,
params,
} satisfies PostAction<A>);
};
},
});
return [state, actor] as const;
}