@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.
Files changed (67) hide show
  1. package/build/development/client/getInj.js +48 -37
  2. package/build/development/client/getInj.js.map +1 -1
  3. package/build/development/client/index.js +3 -2
  4. package/build/development/client/index.js.map +1 -1
  5. package/build/development/index.js +3 -3
  6. package/build/development/index.js.map +1 -1
  7. package/build/development/server/renderer.js +12 -30
  8. package/build/development/server/renderer.js.map +1 -1
  9. package/build/development/shared/components/Modal/index.js +2 -2
  10. package/build/development/shared/components/Modal/index.js.map +1 -1
  11. package/build/development/shared/utils/config.js +37 -9
  12. package/build/development/shared/utils/config.js.map +1 -1
  13. package/build/development/shared/utils/index.js +2 -2
  14. package/build/development/shared/utils/index.js.map +1 -1
  15. package/build/development/shared/utils/splitComponent.js +2 -1
  16. package/build/development/shared/utils/splitComponent.js.map +1 -1
  17. package/build/production/client/getInj.js +12 -10
  18. package/build/production/client/getInj.js.map +1 -1
  19. package/build/production/client/index.js +1 -1
  20. package/build/production/client/index.js.map +1 -1
  21. package/build/production/index.js +2 -1
  22. package/build/production/index.js.map +1 -1
  23. package/build/production/server/renderer.js +6 -9
  24. package/build/production/server/renderer.js.map +1 -1
  25. package/build/production/shared/components/Modal/index.js +2 -2
  26. package/build/production/shared/components/Modal/index.js.map +1 -1
  27. package/build/production/shared/utils/config.js +8 -4
  28. package/build/production/shared/utils/config.js.map +1 -1
  29. package/build/production/shared/utils/index.js +1 -1
  30. package/build/production/shared/utils/index.js.map +1 -1
  31. package/build/production/shared/utils/splitComponent.js +1 -1
  32. package/build/production/shared/utils/splitComponent.js.map +1 -1
  33. package/build/types-code/client/getInj.d.ts +1 -2
  34. package/build/types-code/client/index.d.ts +1 -1
  35. package/build/types-code/index.d.ts +1 -1
  36. package/build/types-code/shared/utils/config.d.ts +4 -2
  37. package/build/types-code/shared/utils/index.d.ts +2 -2
  38. package/build/types-code/shared/utils/splitComponent.d.ts +6 -4
  39. package/build/web/client/getInj.js +48 -37
  40. package/build/web/client/getInj.js.map +1 -1
  41. package/build/web/client/index.js +3 -2
  42. package/build/web/client/index.js.map +1 -1
  43. package/build/web/index.js +3 -3
  44. package/build/web/index.js.map +1 -1
  45. package/build/web/server/renderer.js +12 -30
  46. package/build/web/server/renderer.js.map +1 -1
  47. package/build/web/shared/components/Modal/index.js +2 -2
  48. package/build/web/shared/components/Modal/index.js.map +1 -1
  49. package/build/web/shared/utils/config.js +36 -10
  50. package/build/web/shared/utils/config.js.map +1 -1
  51. package/build/web/shared/utils/index.js +2 -2
  52. package/build/web/shared/utils/index.js.map +1 -1
  53. package/build/web/shared/utils/splitComponent.js +2 -1
  54. package/build/web/shared/utils/splitComponent.js.map +1 -1
  55. package/config/jest/setup.js +8 -1
  56. package/config/webpack/app-base.js +2 -2
  57. package/config/webpack/lib-base.js +0 -1
  58. package/package.json +2 -4
  59. package/src/client/getInj.ts +60 -40
  60. package/src/client/index.tsx +5 -3
  61. package/src/index.ts +3 -3
  62. package/src/server/renderer.tsx +16 -35
  63. package/src/shared/components/Modal/index.tsx +2 -2
  64. package/src/shared/utils/config.ts +48 -12
  65. package/src/shared/utils/index.ts +2 -2
  66. package/src/shared/utils/splitComponent.tsx +14 -2
  67. package/types.d.ts +1 -6
@@ -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
- // Safeguard is needed here, because the server-side version of Docusaurus docs
17
- // is compiled (at least now) with settings suggesting it is a client-side
18
- // environment, but there is no document.
19
- let inj: InjT = {};
20
-
21
- const metaElement: HTMLMetaElement | null = typeof document === 'undefined'
22
- ? null : document.querySelector('meta[itemprop="drpruinj"]');
23
-
24
- if (metaElement) {
25
- metaElement.remove();
26
- let data = forge.util.decode64(metaElement.content);
27
-
28
- const { key } = getBuildInfo();
29
- const d = forge.cipher.createDecipher('AES-CBC', key);
30
- d.start({ iv: data.slice(0, key.length) });
31
- d.update(forge.util.createBuffer(data.slice(key.length)));
32
- d.finish();
33
-
34
- data = forge.util.decodeUtf8(d.output.data);
35
-
36
- // TODO: Double-check, if there is a safer alternative to parse it?
37
- // eslint-disable-next-line no-eval
38
- inj = eval(`(${data})`) as InjT;
39
- } else if (typeof window !== 'undefined' && window.REACT_UTILS_INJECTION) {
40
- inj = window.REACT_UTILS_INJECTION;
41
- delete window.REACT_UTILS_INJECTION;
42
- } else {
43
- // Otherwise, a bunch of dependent stuff will easily fail in non-standard
44
- // environments, where no client-side initialization is performed. Like tests,
45
- // Docusaurus examples, etc.
46
- inj = {};
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
  }
@@ -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={getInj().ISTATE ?? options.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
- let dirname = import.meta.url;
20
- dirname = dirname.slice(5, dirname.lastIndexOf('/'));
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,
@@ -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, see
138
- * node-forge documentation, in case of doubts).
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
- async function prepareCipher(key: string): Promise<{
144
- cipher: forge.cipher.BlockCipher;
145
- iv: string;
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
- cipher.update(forge.util.createBuffer(payload, 'utf8'));
558
- cipher.finish();
559
- const INJ = forge.util.encode64(`${iv}${cipher.output.data}`);
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 ReactDom from 'react-dom';
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 ReactDom.createPortal(
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
- // TODO: The internal type casting is somewhat messed up here,
11
- // to be corrected later.
12
- const config: Record<string, unknown> = (
13
- IS_CLIENT_SIDE ? clientGetInj().CONFIG : requireWeak('config')
14
- ) as (Record<string, unknown> | undefined) ?? ({} as Record<string, unknown>);
15
-
16
- // The safeguard for "document" is necessary because in non-Node environments,
17
- // like React Native, IS_CLIENT_SIDE is "true", however "document" and a bunch
18
- // of other browser-world features are not available.
19
- if (IS_CLIENT_SIDE && typeof document !== 'undefined') {
20
- config.CSRF = parse(document.cookie).csrfToken;
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 default config;
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 config from './config';
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
- config,
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
- return getInj().CHUNK_GROUPS ?? {};
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 { children?: ReactNode; ref?: RefObject<unknown> },
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;