@dr.pogodin/react-utils 1.47.0-alpha.3 → 1.47.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/development/client/getInj.js +48 -37
- package/build/development/client/getInj.js.map +1 -1
- package/build/development/client/index.js +3 -2
- package/build/development/client/index.js.map +1 -1
- package/build/development/index.js +3 -3
- package/build/development/index.js.map +1 -1
- package/build/development/server/renderer.js +12 -30
- package/build/development/server/renderer.js.map +1 -1
- package/build/development/shared/components/Modal/index.js +2 -2
- package/build/development/shared/components/Modal/index.js.map +1 -1
- package/build/development/shared/utils/config.js +37 -9
- package/build/development/shared/utils/config.js.map +1 -1
- package/build/development/shared/utils/index.js +2 -2
- package/build/development/shared/utils/index.js.map +1 -1
- package/build/development/shared/utils/splitComponent.js +2 -1
- package/build/development/shared/utils/splitComponent.js.map +1 -1
- package/build/production/client/getInj.js +12 -10
- package/build/production/client/getInj.js.map +1 -1
- package/build/production/client/index.js +1 -1
- package/build/production/client/index.js.map +1 -1
- package/build/production/index.js +2 -1
- package/build/production/index.js.map +1 -1
- package/build/production/server/renderer.js +6 -9
- package/build/production/server/renderer.js.map +1 -1
- package/build/production/shared/components/Modal/index.js +2 -2
- package/build/production/shared/components/Modal/index.js.map +1 -1
- package/build/production/shared/utils/config.js +8 -4
- package/build/production/shared/utils/config.js.map +1 -1
- package/build/production/shared/utils/index.js +1 -1
- package/build/production/shared/utils/index.js.map +1 -1
- package/build/production/shared/utils/splitComponent.js +1 -1
- package/build/production/shared/utils/splitComponent.js.map +1 -1
- package/build/types-code/client/getInj.d.ts +1 -2
- package/build/types-code/client/index.d.ts +1 -1
- package/build/types-code/index.d.ts +1 -1
- package/build/types-code/shared/utils/config.d.ts +4 -2
- package/build/types-code/shared/utils/index.d.ts +2 -2
- package/build/types-code/shared/utils/splitComponent.d.ts +6 -4
- package/build/web/client/getInj.js +48 -37
- package/build/web/client/getInj.js.map +1 -1
- package/build/web/client/index.js +3 -2
- package/build/web/client/index.js.map +1 -1
- package/build/web/index.js +3 -3
- package/build/web/index.js.map +1 -1
- package/build/web/server/renderer.js +12 -30
- package/build/web/server/renderer.js.map +1 -1
- package/build/web/shared/components/Modal/index.js +2 -2
- package/build/web/shared/components/Modal/index.js.map +1 -1
- package/build/web/shared/utils/config.js +36 -10
- package/build/web/shared/utils/config.js.map +1 -1
- package/build/web/shared/utils/index.js +2 -2
- package/build/web/shared/utils/index.js.map +1 -1
- package/build/web/shared/utils/splitComponent.js +2 -1
- package/build/web/shared/utils/splitComponent.js.map +1 -1
- package/config/jest/setup.js +8 -1
- package/config/webpack/app-base.js +2 -2
- package/config/webpack/lib-base.js +0 -1
- package/package.json +2 -4
- package/src/client/getInj.ts +60 -40
- package/src/client/index.tsx +5 -3
- package/src/index.ts +3 -3
- package/src/server/renderer.tsx +16 -35
- package/src/shared/components/Modal/index.tsx +2 -2
- package/src/shared/utils/config.ts +48 -12
- package/src/shared/utils/index.ts +2 -2
- package/src/shared/utils/splitComponent.tsx +14 -2
- package/types.d.ts +1 -6
package/src/client/getInj.ts
CHANGED
|
@@ -2,50 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
/* global document */
|
|
4
4
|
|
|
5
|
-
// Note: this way, only required part of "node-forge": AES, and some utils,
|
|
6
|
-
// is bundled into client-side code.
|
|
7
|
-
import forge from 'node-forge/lib/forge.js';
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line import/no-unassigned-import
|
|
10
|
-
import 'node-forge/lib/aes.js';
|
|
11
|
-
|
|
12
5
|
import type { InjT } from 'utils/globalState';
|
|
13
6
|
|
|
14
7
|
import { getBuildInfo } from 'utils/isomorphy/buildInfo';
|
|
15
8
|
|
|
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
|
-
|
|
9
|
+
let inj: InjT | Promise<InjT> | undefined;
|
|
10
|
+
|
|
11
|
+
export default function getInj(): InjT | Promise<InjT> {
|
|
12
|
+
inj ??= (async () => {
|
|
13
|
+
const metaElement: HTMLMetaElement | null = typeof document === 'undefined'
|
|
14
|
+
? null : document.querySelector('meta[itemprop="drpruinj"]');
|
|
15
|
+
|
|
16
|
+
if (metaElement) {
|
|
17
|
+
metaElement.remove();
|
|
18
|
+
|
|
19
|
+
// NOTE: Since 2025 there is Uint8Array.fromBase64(), which should be
|
|
20
|
+
// preferred, but it is not supported by older environments yet.
|
|
21
|
+
const data = atob(metaElement.content);
|
|
22
|
+
|
|
23
|
+
// TODO: Our current handling of this encryption / decryption follows
|
|
24
|
+
// a legacy approach, and can be enhanced by using Crypto features.
|
|
25
|
+
// Though, this is not strictly intended to be secure (it is more
|
|
26
|
+
// to obfurscate injected data, rather than really keeping them
|
|
27
|
+
// secure), thus it is fine like this for now.
|
|
28
|
+
const { key } = getBuildInfo();
|
|
29
|
+
|
|
30
|
+
const code = (x: string) => x.charCodeAt(0);
|
|
31
|
+
const dataBuffer = Uint8Array.from(data.slice(16), code);
|
|
32
|
+
const ivBuffer = Uint8Array.from(data.slice(0, 16), code);
|
|
33
|
+
const keyBuffer = Uint8Array.from(atob(key), code);
|
|
34
|
+
|
|
35
|
+
const cKey = await window.crypto.subtle.importKey(
|
|
36
|
+
'raw',
|
|
37
|
+
keyBuffer,
|
|
38
|
+
{ name: 'AES-CBC' },
|
|
39
|
+
false,
|
|
40
|
+
['decrypt'],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const buffer = await window.crypto.subtle.decrypt({
|
|
44
|
+
iv: ivBuffer,
|
|
45
|
+
name: 'AES-CBC',
|
|
46
|
+
}, cKey, dataBuffer);
|
|
47
|
+
|
|
48
|
+
const decoder = new TextDecoder();
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line no-eval
|
|
51
|
+
const res = eval(`(${decoder.decode(buffer)})`) as InjT;
|
|
52
|
+
|
|
53
|
+
// NOTE: This is important, to be able to return the injection
|
|
54
|
+
// synchronously once it is initialized.
|
|
55
|
+
inj = res;
|
|
56
|
+
|
|
57
|
+
return res;
|
|
58
|
+
} else if (typeof window !== 'undefined' && window.REACT_UTILS_INJECTION) {
|
|
59
|
+
const res = window.REACT_UTILS_INJECTION;
|
|
60
|
+
delete window.REACT_UTILS_INJECTION;
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Otherwise, a bunch of dependent stuff will easily fail in non-standard
|
|
65
|
+
// environments, where no client-side initialization is performed. Like tests,
|
|
66
|
+
// Docusaurus examples, etc.
|
|
67
|
+
return {};
|
|
68
|
+
})();
|
|
48
69
|
|
|
49
|
-
export default function getInj(): InjT {
|
|
50
70
|
return inj;
|
|
51
71
|
}
|
package/src/client/index.tsx
CHANGED
|
@@ -20,14 +20,16 @@ type OptionsT = {
|
|
|
20
20
|
* @param Application Root application component
|
|
21
21
|
* @param [options={}] Optional. Additional settings.
|
|
22
22
|
*/
|
|
23
|
-
export default function Launch(
|
|
23
|
+
export default async function Launch(
|
|
24
24
|
Application: ComponentType,
|
|
25
25
|
options: OptionsT = {},
|
|
26
|
-
): void {
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const inj = await getInj();
|
|
28
|
+
|
|
27
29
|
const container = document.getElementById('react-view');
|
|
28
30
|
if (!container) throw Error('Failed to find container for React app');
|
|
29
31
|
const scene = (
|
|
30
|
-
<GlobalStateProvider initialState={
|
|
32
|
+
<GlobalStateProvider initialState={inj.ISTATE ?? options.initialState}>
|
|
31
33
|
<BrowserRouter>
|
|
32
34
|
<HelmetProvider>
|
|
33
35
|
<Application />
|
package/src/index.ts
CHANGED
|
@@ -16,8 +16,8 @@ if (global.REACT_UTILS_LIBRARY_LOADED) {
|
|
|
16
16
|
// TODO: This is a rapid workaround to get rid of __dirname. I guess, later
|
|
17
17
|
// we'll re-implement requireWeak() to accept import.meta.url directly, and
|
|
18
18
|
// this workaround won't be needed.
|
|
19
|
-
|
|
20
|
-
dirname =
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/prefer-destructuring
|
|
20
|
+
const dirname = import.meta.dirname;
|
|
21
21
|
|
|
22
22
|
const server = webpack.requireWeak<typeof ServerFactoryM>('./server', dirname);
|
|
23
23
|
|
|
@@ -54,10 +54,10 @@ export {
|
|
|
54
54
|
|
|
55
55
|
export {
|
|
56
56
|
assertEmptyObject,
|
|
57
|
-
config,
|
|
58
57
|
Barrier,
|
|
59
58
|
Cached,
|
|
60
59
|
Emitter,
|
|
60
|
+
getConfig,
|
|
61
61
|
isomorphy,
|
|
62
62
|
getSsrContext,
|
|
63
63
|
type Listener,
|
package/src/server/renderer.tsx
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* ExpressJS middleware for server-side rendering of a ReactJS app.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { Buffer } from 'node:buffer';
|
|
6
|
+
import { type Cipheriv, createCipheriv, randomBytes } from 'node:crypto';
|
|
5
7
|
import fs from 'node:fs';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import { Writable } from 'node:stream';
|
|
@@ -23,7 +25,6 @@ import {
|
|
|
23
25
|
} from 'lodash-es';
|
|
24
26
|
|
|
25
27
|
import config from 'config';
|
|
26
|
-
import forge from 'node-forge';
|
|
27
28
|
|
|
28
29
|
import { prerenderToNodeStream } from 'react-dom/static';
|
|
29
30
|
import { type HelmetDataContext, HelmetProvider } from '@dr.pogodin/react-helmet';
|
|
@@ -134,26 +135,15 @@ function readChunkGroupsJson(buildDir: string) {
|
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
137
|
* Prepares a new Cipher for data encryption.
|
|
137
|
-
* @param key Encryption key (32-bit random key is expected
|
|
138
|
-
*
|
|
139
|
-
* @return Resolves to the object with two fields:
|
|
138
|
+
* @param key Encryption key (32-bit random, Base64-encoded key is expected).
|
|
139
|
+
* @return Returns a tuple of:
|
|
140
140
|
* 1. cipher - a new Cipher, ready for encryption;
|
|
141
141
|
* 2. iv - initial vector used by the cipher.
|
|
142
142
|
*/
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
iv
|
|
146
|
-
|
|
147
|
-
return new Promise((resolve, reject) => {
|
|
148
|
-
forge.random.getBytes(32, (err, iv) => {
|
|
149
|
-
if (err) reject(err);
|
|
150
|
-
else {
|
|
151
|
-
const cipher = forge.cipher.createCipher('AES-CBC', key);
|
|
152
|
-
cipher.start({ iv });
|
|
153
|
-
resolve({ cipher, iv });
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
});
|
|
143
|
+
function prepareCipher(key: string): [cipher: Cipheriv, iv: Buffer] {
|
|
144
|
+
const iv = randomBytes(16);
|
|
145
|
+
const cipher = createCipheriv('AES-256-CBC', Buffer.from(key, 'base64'), iv);
|
|
146
|
+
return [cipher, iv];
|
|
157
147
|
}
|
|
158
148
|
|
|
159
149
|
/**
|
|
@@ -401,21 +391,9 @@ export default function factory(
|
|
|
401
391
|
}
|
|
402
392
|
|
|
403
393
|
const brr = ops.beforeRender!(req, sanitizedConfig as unknown as ConfigT);
|
|
394
|
+
const { configToInject, extraScripts, initialState } = await brr;
|
|
404
395
|
|
|
405
|
-
const [
|
|
406
|
-
configToInject,
|
|
407
|
-
extraScripts,
|
|
408
|
-
initialState,
|
|
409
|
-
}, {
|
|
410
|
-
cipher,
|
|
411
|
-
iv,
|
|
412
|
-
}] = await Promise.all([
|
|
413
|
-
// NOTE: Written this way to avoid triggering the "await-thenable"
|
|
414
|
-
// ESLint rule.
|
|
415
|
-
brr instanceof Promise ? brr : Promise.resolve(brr),
|
|
416
|
-
|
|
417
|
-
prepareCipher(buildInfo.key),
|
|
418
|
-
]);
|
|
396
|
+
const [cipher, iv] = prepareCipher(buildInfo.key);
|
|
419
397
|
|
|
420
398
|
let helmet: HelmetDataContext['helmet'];
|
|
421
399
|
|
|
@@ -554,9 +532,12 @@ export default function factory(
|
|
|
554
532
|
ignoreFunction: true,
|
|
555
533
|
unsafe: true,
|
|
556
534
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
535
|
+
|
|
536
|
+
const INJ = Buffer.concat([
|
|
537
|
+
iv,
|
|
538
|
+
cipher.update(payload, 'utf8'),
|
|
539
|
+
cipher.final(),
|
|
540
|
+
]).toString('base64');
|
|
560
541
|
|
|
561
542
|
const chunkSet = new Set<string>();
|
|
562
543
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
useRef,
|
|
8
8
|
} from 'react';
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import { createPortal } from 'react-dom';
|
|
11
11
|
import themed, { type Theme } from '@dr.pogodin/react-themes';
|
|
12
12
|
|
|
13
13
|
import baseTheme from './base-theme.scss';
|
|
@@ -96,7 +96,7 @@ const BaseModal: FunctionComponent<PropsT> = ({
|
|
|
96
96
|
/>
|
|
97
97
|
), []);
|
|
98
98
|
|
|
99
|
-
return
|
|
99
|
+
return createPortal(
|
|
100
100
|
(
|
|
101
101
|
<div>
|
|
102
102
|
{focusLast}
|
|
@@ -7,17 +7,53 @@ import clientGetInj from '../../client/getInj';
|
|
|
7
7
|
import { IS_CLIENT_SIDE } from './isomorphy/environment-check';
|
|
8
8
|
import { requireWeak } from './webpack';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
type ConfigT = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
let config: ConfigT | Promise<ConfigT> | undefined;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Injects CSRF token into config (on client-side only).
|
|
16
|
+
*
|
|
17
|
+
* BEWARE: It mutates the argument, and also returns it as the result,
|
|
18
|
+
* for chaining.
|
|
19
|
+
*/
|
|
20
|
+
function injectCsrfToken(cfg: ConfigT): ConfigT {
|
|
21
|
+
// The safeguard for "document" is necessary because in non-Node environments,
|
|
22
|
+
// like React Native, IS_CLIENT_SIDE is "true", however "document" and a bunch
|
|
23
|
+
// of other browser-world features are not available.
|
|
24
|
+
if (IS_CLIENT_SIDE && typeof document !== 'undefined') {
|
|
25
|
+
// eslint-disable-next-line no-param-reassign
|
|
26
|
+
cfg.CSRF = parse(document.cookie).csrfToken;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return cfg;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
export
|
|
32
|
+
export function getConfig(sync: true): ConfigT;
|
|
33
|
+
|
|
34
|
+
export function getConfig(sync?: boolean): ConfigT | Promise<ConfigT>;
|
|
35
|
+
|
|
36
|
+
export function getConfig(sync?: boolean): ConfigT | Promise<ConfigT> {
|
|
37
|
+
if (!config) {
|
|
38
|
+
if (IS_CLIENT_SIDE) {
|
|
39
|
+
const inj = clientGetInj();
|
|
40
|
+
config = inj instanceof Promise
|
|
41
|
+
? inj.then((injection) => {
|
|
42
|
+
const res = injectCsrfToken(injection.CONFIG ?? {});
|
|
43
|
+
config = res;
|
|
44
|
+
return res;
|
|
45
|
+
})
|
|
46
|
+
: injectCsrfToken(inj.CONFIG ?? {});
|
|
47
|
+
} else {
|
|
48
|
+
const weak = requireWeak<ConfigT>('config');
|
|
49
|
+
if (!weak) throw Error('Internal error');
|
|
50
|
+
config = weak;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sync && (config instanceof Promise)) {
|
|
55
|
+
throw Error('The config is not available yet');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
@@ -5,7 +5,7 @@ import themedImpl, {
|
|
|
5
5
|
ThemeProvider,
|
|
6
6
|
} from '@dr.pogodin/react-themes';
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { getConfig } from './config';
|
|
9
9
|
import * as isomorphy from './isomorphy';
|
|
10
10
|
import time from './time';
|
|
11
11
|
import * as webpack from './webpack';
|
|
@@ -36,7 +36,7 @@ themed.PRIORITY = PRIORITY;
|
|
|
36
36
|
|
|
37
37
|
export {
|
|
38
38
|
type Theme,
|
|
39
|
-
|
|
39
|
+
getConfig,
|
|
40
40
|
isomorphy,
|
|
41
41
|
themed,
|
|
42
42
|
ThemeProvider,
|
|
@@ -25,7 +25,9 @@ function getClientChunkGroups(): Promise<ChunkGroupsT> | undefined {
|
|
|
25
25
|
|
|
26
26
|
return (async () => {
|
|
27
27
|
const { default: getInj } = await import(/* webpackChunkName: "react-utils-client-side-code" */ '../../client/getInj');
|
|
28
|
-
|
|
28
|
+
|
|
29
|
+
const inj = await getInj();
|
|
30
|
+
return inj.CHUNK_GROUPS ?? {};
|
|
29
31
|
})();
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -171,6 +173,16 @@ type ComponentOrModule<PropsT> = ComponentType<PropsT> | {
|
|
|
171
173
|
default: ComponentType<PropsT>;
|
|
172
174
|
};
|
|
173
175
|
|
|
176
|
+
type GenericComponentPropsT = {
|
|
177
|
+
children?: ReactNode;
|
|
178
|
+
ref?: RefObject<unknown>;
|
|
179
|
+
|
|
180
|
+
// NOTE: This is necessary, as without it this type (with only optional
|
|
181
|
+
// fields) will be conisdered as "weak" by TypeScript, and it will be
|
|
182
|
+
// a error to assign to it any type that does not use "children", or "ref".
|
|
183
|
+
[propName: string]: unknown;
|
|
184
|
+
};
|
|
185
|
+
|
|
174
186
|
/**
|
|
175
187
|
* Given an async component retrieval function `getComponent()` it creates
|
|
176
188
|
* a special "code split" component, which uses <Suspense> to asynchronously
|
|
@@ -182,7 +194,7 @@ type ComponentOrModule<PropsT> = ComponentType<PropsT> | {
|
|
|
182
194
|
* @return {React.ElementType}
|
|
183
195
|
*/
|
|
184
196
|
export default function splitComponent<
|
|
185
|
-
ComponentPropsT extends
|
|
197
|
+
ComponentPropsT extends GenericComponentPropsT,
|
|
186
198
|
>({
|
|
187
199
|
chunkName,
|
|
188
200
|
getComponent,
|
package/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ declare var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
|
|
|
4
4
|
declare var REACT_UTILS_WEBPACK_BUNDLE: boolean | undefined;
|
|
5
5
|
declare var REACT_UTILS_LIBRARY_LOADED: boolean | undefined;
|
|
6
6
|
declare var REACT_UTILS_FORCE_CLIENT_SIDE: boolean | undefined;
|
|
7
|
+
declare var SCENE_INIT_PROMISE: Promise<void> | undefined;
|
|
7
8
|
/* eslint-enable no-var */
|
|
8
9
|
|
|
9
10
|
declare module '@babel/register/experimental-worker' {
|
|
@@ -19,12 +20,6 @@ declare module '@babel/register/experimental-worker' {
|
|
|
19
20
|
export default register;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
declare module 'node-forge/lib/forge.js' {
|
|
23
|
-
import F from 'node-forge';
|
|
24
|
-
|
|
25
|
-
export default F;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
23
|
declare module '*.png' {
|
|
29
24
|
const path: string;
|
|
30
25
|
export default path;
|