@kirill.konshin/utils 0.0.8 → 0.0.9

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 (101) hide show
  1. package/.ctirc +10 -0
  2. package/.storybook/main.ts +21 -0
  3. package/.storybook/preview.ts +17 -0
  4. package/.turbo/turbo-build.log +67 -0
  5. package/.turbo/turbo-test.log +118 -0
  6. package/CHANGELOG.md +6 -0
  7. package/README.md +59 -0
  8. package/builders/.swcrc +18 -0
  9. package/builders/build.config.ts +11 -0
  10. package/builders/bun.build.js +23 -0
  11. package/builders/package.json +209 -0
  12. package/builders/perf.mjs +63 -0
  13. package/builders/rollup.config.mjs +50 -0
  14. package/builders/rslib.config.ts +26 -0
  15. package/builders/tsconfig.json +21 -0
  16. package/builders/tsup.config.ts +20 -0
  17. package/builders/turbo.json +24 -0
  18. package/builders/vite.config.ts +37 -0
  19. package/demo/cache-demo.ts +2 -1
  20. package/package.json +166 -22
  21. package/src/bootstrap/adaptiveContainer.tsx +12 -0
  22. package/src/bootstrap/controls.stories.tsx +62 -0
  23. package/src/bootstrap/controls.tsx +119 -0
  24. package/src/bootstrap/error.stories.tsx +34 -0
  25. package/src/bootstrap/error.tsx +40 -0
  26. package/src/bootstrap/field.stories.tsx +42 -0
  27. package/src/bootstrap/field.tsx +35 -0
  28. package/src/bootstrap/footer.tsx +49 -0
  29. package/src/bootstrap/globalLoading.stories.tsx +25 -0
  30. package/src/bootstrap/globalLoading.tsx +17 -0
  31. package/src/bootstrap/index.ts +12 -0
  32. package/src/bootstrap/loading.stories.tsx +35 -0
  33. package/src/bootstrap/loading.tsx +22 -0
  34. package/src/bootstrap/main.scss +86 -0
  35. package/src/bootstrap/responsiveHelper.tsx +50 -0
  36. package/src/bootstrap/screen.stories.tsx +160 -0
  37. package/src/bootstrap/screen.tsx +144 -0
  38. package/src/bootstrap/toaster.stories.tsx +32 -0
  39. package/src/bootstrap/toaster.tsx +38 -0
  40. package/src/bootstrap/useModal.stories.tsx +51 -0
  41. package/src/bootstrap/useModal.tsx +88 -0
  42. package/src/bootstrap/useWrappedForm.ts +40 -0
  43. package/src/core/cache.test.ts +121 -0
  44. package/src/electron/README.md +13 -0
  45. package/src/electron/createWindow.ts +234 -0
  46. package/src/electron/index.ts +2 -0
  47. package/src/electron/updater.ts +54 -0
  48. package/src/electron-builder/builder.ts +150 -0
  49. package/src/electron-builder/images.ts +33 -0
  50. package/src/electron-builder/index.ts +2 -0
  51. package/src/mui/README.md +3 -0
  52. package/src/mui/formControlFieldset.stories.tsx +71 -0
  53. package/src/mui/formControlFieldset.tsx +26 -0
  54. package/src/mui/formLabelLegend.tsx +11 -0
  55. package/src/mui/genericControl.tsx +20 -0
  56. package/src/mui/index.ts +4 -0
  57. package/src/mui/readOnly.tsx +6 -0
  58. package/src/next/appLink.tsx +39 -0
  59. package/src/next/index.ts +5 -0
  60. package/src/next/measure.ts +7 -0
  61. package/src/next/noSSR.tsx +17 -0
  62. package/src/next/redirect.tsx +13 -0
  63. package/src/next/useIsInner.ts +13 -0
  64. package/src/react/apiCall.ts +25 -0
  65. package/src/react/form/client.tsx +62 -0
  66. package/src/react/form/form.tsx +109 -0
  67. package/src/react/form/index.ts +2 -0
  68. package/src/react/index.ts +4 -0
  69. package/src/react/useFetch.ts +29 -0
  70. package/src/react/useFetcher.ts +49 -0
  71. package/src/react-native/index.ts +3 -0
  72. package/src/react-native/share.ts +31 -0
  73. package/src/react-native/update.tsx +40 -0
  74. package/src/react-native/useAppState.ts +18 -0
  75. package/src/tailwind/fullpage.tsx +11 -0
  76. package/src/tailwind/index.ts +2 -0
  77. package/src/tailwind/responsiveHelper.tsx +16 -0
  78. package/src-todo/auth0.tsx +177 -0
  79. package/tsconfig.json +15 -18
  80. package/turbo.json +2 -11
  81. package/vite.config.ts +47 -0
  82. package/vite.exports.ts +121 -0
  83. package/build/cache.d.ts +0 -120
  84. package/build/cache.js +0 -191
  85. package/build/errors.d.ts +0 -1
  86. package/build/errors.js +0 -14
  87. package/build/index.d.ts +0 -5
  88. package/build/index.js +0 -5
  89. package/build/measure.d.ts +0 -34
  90. package/build/measure.js +0 -48
  91. package/build/mutex.d.ts +0 -5
  92. package/build/mutex.js +0 -23
  93. package/build/worker.d.ts +0 -83
  94. package/build/worker.js +0 -250
  95. package/tsconfig.tsbuildinfo +0 -1
  96. package/src/{cache.ts → core/cache.ts} +0 -0
  97. package/src/{errors.ts → core/errors.ts} +0 -0
  98. package/src/{index.ts → core/index.ts} +1 -1
  99. /package/src/{measure.ts → core/measure.ts} +0 -0
  100. /package/src/{mutex.ts → core/mutex.ts} +0 -0
  101. /package/src/{worker.ts → core/worker.ts} +0 -0
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { fn } from '@storybook/test';
3
+
4
+ import { Toaster } from './toaster';
5
+
6
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7
+ const meta = {
8
+ title: 'Bootstrap / Toaster',
9
+ component: Toaster,
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ tags: ['autodocs'],
14
+ argTypes: {},
15
+ args: { onClick: fn() },
16
+ } satisfies Meta<typeof Toaster>;
17
+
18
+ export default meta;
19
+
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ export const Default: Story = {
23
+ args: {
24
+ toast: 'Hello, World!',
25
+ },
26
+ };
27
+
28
+ export const Object: Story = {
29
+ args: {
30
+ toast: { message: 'Toast', variant: 'dark' },
31
+ },
32
+ };
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { Toast, ToastProps } from 'react-bootstrap';
5
+ // import { useToaster } from '../redux/toasterSlice';
6
+
7
+ export function Toaster({
8
+ toast,
9
+ }: { toast: string | { message: string; title?: string; variant?: 'dark' | 'success' | 'danger' } } & ToastProps) {
10
+ //const { toast, setToast } = useToaster();
11
+ const [dismissed, setDismissed] = useState(false);
12
+
13
+ const msg = typeof toast === 'string' ? toast : toast?.message;
14
+ const title = typeof toast === 'string' ? 'Notification' : toast?.title;
15
+ const variant = typeof toast === 'string' ? '' : toast?.variant;
16
+
17
+ return (
18
+ <Toast
19
+ onClose={() => setDismissed(true)}
20
+ show={!dismissed}
21
+ delay={10000}
22
+ autohide
23
+ bg={variant?.toLowerCase()}
24
+ className="position-fixed top-50 right-50 min-w-200px z-3"
25
+ >
26
+ <Toast.Header>
27
+ <strong className="me-auto">Notification</strong>
28
+ </Toast.Header>
29
+ <Toast.Body
30
+ className={
31
+ ['dark', 'success', 'danger'].includes((variant as string)?.toLowerCase()) ? 'text-white' : ''
32
+ }
33
+ >
34
+ {msg}
35
+ </Toast.Body>
36
+ </Toast>
37
+ );
38
+ }
@@ -0,0 +1,51 @@
1
+ import React, { FC } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { useModal, UseModalProps } from './useModal';
4
+ import { Button, ButtonGroup } from 'react-bootstrap';
5
+
6
+ // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
7
+ const meta = {
8
+ title: 'Bootstrap / useModal',
9
+ parameters: {
10
+ layout: 'centered',
11
+ },
12
+ tags: ['autodocs'],
13
+ argTypes: {
14
+ children: { control: 'text' },
15
+ title: { control: 'text' },
16
+ cancelButton: { control: 'text' },
17
+ submitButton: { control: 'text' },
18
+ offcanvas: { control: 'check' },
19
+ },
20
+ args: {
21
+ children: 'Modal Content',
22
+ title: 'Modal Title',
23
+ },
24
+ render: function Demo(args) {
25
+ const { open, show, close, ModalDialog } = useModal();
26
+ return (
27
+ <>
28
+ <Button onClick={open}>Open Modal</Button>
29
+ <ModalDialog {...args} show={show}>
30
+ {args.children}
31
+ <Button onClick={close}>Close Modal</Button>
32
+ </ModalDialog>
33
+ </>
34
+ );
35
+ },
36
+ // args: { onSubmit: (e) => e.preventDefault() },
37
+ } satisfies Meta<FC<UseModalProps>>;
38
+
39
+ export default meta;
40
+
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ export const Default: Story = {
44
+ args: {},
45
+ };
46
+
47
+ export const Offcanvas: Story = {
48
+ args: {
49
+ offcanvas: true,
50
+ },
51
+ };
@@ -0,0 +1,88 @@
1
+ import React, { useCallback, useMemo, useState } from 'react';
2
+ import { Button, Modal, ModalProps, Offcanvas, OffcanvasProps, Stack } from 'react-bootstrap';
3
+
4
+ export type UseModalArgs = { onClose?: any; showOnMount?: boolean };
5
+
6
+ export type UseModalProps = {
7
+ children?: any;
8
+ show?: boolean;
9
+ onSubmit?: (e: any) => void;
10
+ submitButton?: string | any;
11
+ cancelButton?: string | any;
12
+ offcanvas?: boolean;
13
+ title?: string | any;
14
+ containerProps?: ModalProps | OffcanvasProps;
15
+ };
16
+
17
+ export function useModal({ onClose, showOnMount }: UseModalArgs = {}) {
18
+ const [show, setShow] = useState(showOnMount);
19
+
20
+ const close = useCallback(() => {
21
+ setShow(false);
22
+ onClose?.();
23
+ }, [onClose]);
24
+
25
+ const open = useCallback(() => setShow(true), []);
26
+
27
+ const defaultOnSubmit = useCallback(
28
+ (e) => {
29
+ e.preventDefault();
30
+ close();
31
+ },
32
+ [close],
33
+ );
34
+
35
+ const ModalDialog = useMemo(
36
+ () =>
37
+ function ModalDialog({
38
+ children,
39
+ show,
40
+ onSubmit = defaultOnSubmit,
41
+ submitButton = 'Done',
42
+ cancelButton = 'Cancel',
43
+ offcanvas = false,
44
+ title = null as any,
45
+ containerProps = {},
46
+ }: ModalProps) {
47
+ const Container = offcanvas ? Offcanvas : Modal;
48
+ const containerDefaultProps = offcanvas ? { style: { maxWidth: '85%' } } : {};
49
+ const Body = offcanvas ? Offcanvas.Body : Modal.Body;
50
+ const Header = offcanvas ? Offcanvas.Header : Modal.Header;
51
+ const Title = offcanvas ? Offcanvas.Title : Modal.Title;
52
+
53
+ return (
54
+ <Container {...{ ...containerDefaultProps, ...containerProps }} show={show} onHide={close}>
55
+ <form onSubmit={onSubmit}>
56
+ {title && (
57
+ <Header closeButton>
58
+ <Title>{title}</Title>
59
+ </Header>
60
+ )}
61
+
62
+ <Body as={Stack} gap={3}>
63
+ {show && children}
64
+ </Body>
65
+
66
+ {!offcanvas && (submitButton || cancelButton) && (
67
+ <Modal.Footer>
68
+ {cancelButton && (
69
+ <Button variant="outline-secondary" onClick={close}>
70
+ {cancelButton}
71
+ </Button>
72
+ )}
73
+ {submitButton && (
74
+ <Button variant="secondary" type="submit">
75
+ {submitButton}
76
+ </Button>
77
+ )}
78
+ </Modal.Footer>
79
+ )}
80
+ </form>
81
+ </Container>
82
+ );
83
+ },
84
+ [close, defaultOnSubmit],
85
+ );
86
+
87
+ return { show, close, open, ModalDialog };
88
+ }
@@ -0,0 +1,40 @@
1
+ import { useForm, FormState } from 'react-hook-form';
2
+ import { useCallback } from 'react';
3
+
4
+ export const isFormLoading = (formState: FormState<any>) => formState.isLoading || formState.isSubmitting;
5
+
6
+ export const useWrappedForm: typeof useForm = (options) => {
7
+ const form = useForm(options);
8
+
9
+ const handleSubmit: typeof form.handleSubmit = useCallback(
10
+ (onSubmit, onError) => {
11
+ const wrappedSubmit: typeof onSubmit = async (data) => {
12
+ try {
13
+ await onSubmit(data);
14
+ } catch (e) {
15
+ form.setError('root', { type: 'server', message: e.message });
16
+ console.error('Error submitting form', e);
17
+ }
18
+ };
19
+ return form.handleSubmit(wrappedSubmit, onError);
20
+ },
21
+ [form],
22
+ );
23
+
24
+ return { ...form, handleSubmit };
25
+ };
26
+
27
+ /**
28
+ * Issue #3: Submit event still propagates to parent when using portal
29
+ * https://reactjs.org/docs/portals.html#event-bubbling-through-portals
30
+ * Event bubbling goes through React DOM instead of HTML DOM
31
+ * Portals don't have an effect on this one, we need to stop event propagation
32
+ * This should be our default form handling method
33
+ * https://codesandbox.io/s/react-hook-form-nested-portal-6x3fvy?file=/src/App.tsx:1905-2261
34
+ * https://github.com/react-hook-form/react-hook-form/issues/1005#issuecomment-988380981
35
+ */
36
+ export const stopPropagation = (callback) => (e) => {
37
+ e.stopPropagation();
38
+ e.preventDefault();
39
+ callback(e);
40
+ };
@@ -0,0 +1,121 @@
1
+ import { expect, describe, test, vi } from 'vitest';
2
+ import { ANY, MaybePromise, memo, TransformerMap } from './cache';
3
+
4
+ describe('memo', async () => {
5
+ test('simple', async () => {
6
+ const random = vi.fn(() => Math.random());
7
+ const memoized = memo(random);
8
+
9
+ const first = await memoized();
10
+ const second = await memoized();
11
+
12
+ expect(first.hit).toBeFalsy();
13
+ expect(second.hit).toBeTruthy();
14
+ expect(first.value).toBe(second.value);
15
+ expect(memoized.size()).toBe(1);
16
+
17
+ memoized.clear();
18
+ expect(memoized.size()).toBe(0);
19
+
20
+ const third = await memoized();
21
+ expect(third.hit).toBeFalsy();
22
+ });
23
+
24
+ test('with key', async () => {
25
+ const random = vi.fn((arg) => arg + Math.random().toString());
26
+ const memoized = memo(random);
27
+
28
+ const first = await memoized(1);
29
+ const second = await memoized(2);
30
+
31
+ expect(first.hit).toBeFalsy();
32
+ expect(second.hit).toBeFalsy();
33
+ expect(first.value).not.toBe(second.value);
34
+ expect(memoized.size()).toBe(2);
35
+
36
+ const first2 = await memoized(1);
37
+ const second2 = await memoized(2);
38
+
39
+ expect(first2.hit).toBeTruthy();
40
+ expect(first.value).toBe(first2.value);
41
+ expect(second2.hit).toBeTruthy();
42
+ expect(second.value).toBe(second2.value);
43
+
44
+ memoized.clear(1);
45
+ expect(memoized.size()).toBe(1);
46
+ memoized.clear(ANY);
47
+ expect(memoized.size()).toBe(0);
48
+ });
49
+ });
50
+
51
+ describe('TransformerMap', async () => {
52
+ test('simple', async () => {
53
+ const dispose = vi.fn();
54
+
55
+ class Cache extends TransformerMap<number, any> {
56
+ write(key, value, oldValue) {
57
+ value.cached = true;
58
+ value.read = 0;
59
+ return value;
60
+ }
61
+ read(value, key) {
62
+ value.read++;
63
+ return value;
64
+ }
65
+ dispose(value, key) {
66
+ dispose(value, key);
67
+ }
68
+ }
69
+
70
+ const cache = new Cache('test');
71
+
72
+ const obj = { a: 1 };
73
+
74
+ const x1 = await cache.memo(1, obj);
75
+ expect(x1).toStrictEqual({ a: 1, cached: true, read: 0 });
76
+
77
+ const x2 = await cache.memo(1, obj);
78
+ expect(x2).toStrictEqual({ a: 1, cached: true, read: 1 });
79
+
80
+ const x3 = await cache.memo(1, { a: 2 });
81
+ expect(x3).toStrictEqual({ a: 2, cached: true, read: 0 });
82
+ expect(cache.size).toEqual(1);
83
+ expect(dispose).toBeCalledTimes(1);
84
+ expect(dispose).toBeCalledWith({ a: 1, cached: true, read: 1 }, 1);
85
+
86
+ const x4 = await cache.memo(2, { a: 20 });
87
+
88
+ expect(x4).toStrictEqual({ a: 20, cached: true, read: 0 });
89
+ expect(cache.size).toEqual(2);
90
+ expect(dispose).toBeCalledTimes(1);
91
+ });
92
+
93
+ test('key transform', async () => {
94
+ const dispose = vi.fn();
95
+
96
+ class Cache extends TransformerMap<number, any> {
97
+ write(key, value, oldValue) {
98
+ return { key, read: 0 };
99
+ }
100
+ read(value, key) {
101
+ value.read++;
102
+ return value;
103
+ }
104
+ dispose(value, key) {
105
+ dispose(value, key);
106
+ }
107
+ }
108
+
109
+ const cache = new Cache('test');
110
+
111
+ const x1 = await cache.memo(1);
112
+ expect(x1).toStrictEqual({ key: 1, read: 0 });
113
+
114
+ const x2 = await cache.memo(1);
115
+ expect(x2).toStrictEqual({ key: 1, read: 1 });
116
+
117
+ cache.clear();
118
+ expect(dispose).toBeCalledTimes(1);
119
+ expect(dispose).toBeCalledWith({ key: 1, read: 1 }, 1);
120
+ });
121
+ });
@@ -0,0 +1,13 @@
1
+ # Electron Utils
2
+
3
+ # `createWindow`
4
+
5
+ ```js
6
+ import { createWindow } from '@kirill.konshin/utils/electron';
7
+
8
+ const window = createWindow({
9
+ title: 'My App',
10
+ width: 800,
11
+ height: 600,
12
+ });
13
+ ```
@@ -0,0 +1,234 @@
1
+ import path from 'node:path';
2
+ import https from 'node:https';
3
+ import http from 'node:http';
4
+ import fs from 'node:fs';
5
+ import fsp from 'node:fs/promises';
6
+ import url from 'node:url';
7
+ import { Menu, app, BrowserWindow, shell, protocol, Rectangle } from 'electron';
8
+ import defaultMenu from 'electron-default-menu';
9
+ import Store from 'electron-store';
10
+ import { checkForUpdates } from './updater.js';
11
+
12
+ export const isDev = process.env['NODE_ENV'] === 'development';
13
+ export const appPath = app.getAppPath();
14
+
15
+ // See https://cs.chromium.org/chromium/src/net/base/net_error_list.h
16
+ const FILE_NOT_FOUND = -6;
17
+
18
+ const getPath = async (path_, file = '') => {
19
+ try {
20
+ const result = await fsp.stat(path_);
21
+
22
+ if (result.isFile()) return path_;
23
+
24
+ if (result.isDirectory()) return getPath(path.join(path_, `${file}.html`));
25
+ } catch {
26
+ /* empty */
27
+ }
28
+ };
29
+
30
+ export function createWindow({
31
+ width = 1000,
32
+ height = 1000,
33
+ disableSecurity = true,
34
+ disableSecurityWarnings = true,
35
+ quitOnClose = true,
36
+ onOpen,
37
+ onClose,
38
+ localhostUrl = 'http://localhost:3000',
39
+ productionUrl = '', // in this mode Electron acts as a browser for remote app
40
+ webPath = path.resolve(appPath, 'web'), // must conform to what is in electron-builder.js files section
41
+ useStaticInDev = false,
42
+ updater = false,
43
+ rememberBounds = true,
44
+ }: {
45
+ width?: number;
46
+ height?: number;
47
+ disableSecurity?: boolean;
48
+ disableSecurityWarnings?: boolean;
49
+ quitOnClose?: boolean;
50
+ onOpen?: (win: BrowserWindow) => void;
51
+ onClose?: (win: BrowserWindow) => void;
52
+ localhostUrl?: string;
53
+ productionUrl?: string;
54
+ webPath?: string;
55
+ useStaticInDev?: boolean;
56
+ updater?: boolean;
57
+ rememberBounds?: boolean;
58
+ } = {}) {
59
+ let mainWindow: BrowserWindow | null = null;
60
+
61
+ console.log('[APP] Starting Electron', {
62
+ isDev,
63
+ appPath,
64
+ useStaticInDev,
65
+ localhostUrl,
66
+ productionUrl,
67
+ webPath,
68
+ updater,
69
+ rememberBounds,
70
+ quitOnClose,
71
+ disableSecurity,
72
+ disableSecurityWarnings,
73
+ });
74
+
75
+ if (!productionUrl && !webPath) {
76
+ throw new Error('productionUrl or webPath must be defined');
77
+ }
78
+
79
+ if (productionUrl && webPath) {
80
+ throw new Error('productionUrl and webPath cannot be used together');
81
+ }
82
+
83
+ if (!localhostUrl) {
84
+ throw new Error('localhostUrl must be defined');
85
+ }
86
+
87
+ const store = new Store<{ bounds: Rectangle }>({
88
+ schema: {
89
+ bounds: {
90
+ type: 'object',
91
+ properties: {
92
+ // Docs: https://electronjs.org/docs/api/structures/rectangle
93
+ height: { type: 'number' },
94
+ width: { type: 'number' },
95
+ x: { type: 'number' },
96
+ y: { type: 'number' },
97
+ },
98
+ },
99
+ },
100
+ });
101
+
102
+ if (disableSecurityWarnings) process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
103
+ process.env['ELECTRON_ENABLE_LOGGING'] = 'true';
104
+
105
+ process.on('SIGTERM', () => process.exit(0));
106
+ process.on('SIGINT', () => process.exit(0));
107
+
108
+ const openDevTools = () => {
109
+ if (!mainWindow) return;
110
+ mainWindow.setSize(width + 800, height);
111
+ mainWindow.center();
112
+ mainWindow.webContents.openDevTools();
113
+ };
114
+
115
+ const createWindow = async () => {
116
+ mainWindow = new BrowserWindow({
117
+ width,
118
+ height,
119
+ icon: path.resolve(appPath, 'assets/icon.png'),
120
+ webPreferences: {
121
+ nodeIntegration: false, // is default value after Electron v5
122
+ contextIsolation: true, // protect against prototype pollution
123
+ webSecurity: !disableSecurity,
124
+ devTools: true,
125
+ },
126
+ ...(rememberBounds ? store.get('bounds') : {}),
127
+ });
128
+
129
+ mainWindow.on('move', () => rememberBounds && mainWindow && store.set('bounds', mainWindow.getBounds()));
130
+
131
+ mainWindow.once('ready-to-show', () => isDev && openDevTools());
132
+
133
+ // Static interceptor
134
+
135
+ let interceptor: (() => void) | null = null;
136
+
137
+ if (webPath && (!isDev || useStaticInDev)) {
138
+ console.log(`[APP] Static Server Enabled, ${localhostUrl} will be intercepted to ${webPath}`);
139
+
140
+ //TODO https://localhost:3000
141
+ // @see https://github.com/sindresorhus/electron-serve/blob/main/index.js
142
+ protocol.interceptStreamProtocol('http', (request, callback) => {
143
+ if (request.url.startsWith(localhostUrl)) {
144
+ let fileUrl = url.parse(request.url, false).pathname;
145
+
146
+ if (!fileUrl) return callback({ error: FILE_NOT_FOUND });
147
+
148
+ if (fileUrl === '/') fileUrl = 'index.html';
149
+
150
+ const filePath = path.join(webPath, fileUrl);
151
+
152
+ const relativePath = path.relative(webPath, fileUrl);
153
+
154
+ const isSafe = !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
155
+
156
+ if (!isSafe && !isDev) return callback({ error: FILE_NOT_FOUND });
157
+
158
+ // const finalPath = await getPath(filePath, options.file);
159
+ // const fileExtension = path.extname(filePath);
160
+
161
+ console.log('[APP] Static Server', fileUrl, '->', filePath);
162
+
163
+ // return {path: path.join(webPath, fileUrl)};
164
+ callback(fs.createReadStream(filePath));
165
+
166
+ return true;
167
+ }
168
+
169
+ // return {url: request.url};
170
+ (request.url.startsWith('https') ? https : http).get(request.url, callback);
171
+
172
+ return true;
173
+ });
174
+
175
+ interceptor = () => protocol.uninterceptProtocol('http');
176
+ }
177
+
178
+ mainWindow.on('closed', () => {
179
+ interceptor?.();
180
+ onClose?.(mainWindow as never);
181
+ mainWindow = null;
182
+ interceptor = null;
183
+ });
184
+
185
+ // External URLs
186
+
187
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
188
+ shell.openExternal(url).catch((e) => console.error(e));
189
+ return { action: 'deny' };
190
+ });
191
+
192
+ // Menu
193
+
194
+ const menu = defaultMenu(app, shell);
195
+
196
+ if (updater) {
197
+ (menu.at(-1)!.submenu as any).push(
198
+ { type: 'separator' },
199
+ {
200
+ label: 'Check For Updates',
201
+ click: checkForUpdates,
202
+ },
203
+ );
204
+ }
205
+
206
+ Menu.setApplicationMenu(Menu.buildFromTemplate(menu)); //TODO onMenu
207
+
208
+ // Should be last, after all listeners and menu
209
+
210
+ await app.whenReady();
211
+
212
+ onOpen?.(mainWindow);
213
+
214
+ if (!onOpen && localhostUrl) {
215
+ await mainWindow.loadURL(productionUrl && !isDev ? productionUrl : `${localhostUrl}/`);
216
+ }
217
+
218
+ console.log('[APP] Main Window Open');
219
+ };
220
+
221
+ app.on('ready', createWindow);
222
+
223
+ app.on('window-all-closed', () => {
224
+ if (process.platform !== 'darwin' || quitOnClose) app.quit();
225
+ });
226
+
227
+ app.on('activate', () => BrowserWindow.getAllWindows().length === 0 && !mainWindow && createWindow());
228
+
229
+ return {
230
+ get mainWindow() {
231
+ return mainWindow;
232
+ },
233
+ };
234
+ }
@@ -0,0 +1,2 @@
1
+ export * from './createWindow';
2
+ export * from './updater';
@@ -0,0 +1,54 @@
1
+ // https://github.com/electron-userland/electron-builder/blob/docs-deprecated/encapsulated%20manual%20update%20via%20menu.js
2
+ import { dialog } from 'electron';
3
+ import updater from 'electron-updater';
4
+ const { autoUpdater } = updater;
5
+
6
+ let updateButton;
7
+
8
+ autoUpdater.autoDownload = false;
9
+
10
+ autoUpdater.on('error', (error) => {
11
+ dialog.showErrorBox('Error: ', !error ? 'unknown' : (error.stack || error).toString());
12
+ });
13
+
14
+ autoUpdater.on('update-available', async () => {
15
+ const { response } = await dialog.showMessageBox({
16
+ type: 'info',
17
+ title: 'Found Updates',
18
+ message: 'Found updates, do you want update now?',
19
+ buttons: ['Yes', 'No'],
20
+ });
21
+
22
+ updateButton.enabled = true;
23
+ updateButton = null;
24
+
25
+ if (response === 0) await autoUpdater.downloadUpdate();
26
+ });
27
+
28
+ autoUpdater.on('update-not-available', async () => {
29
+ try {
30
+ await dialog.showMessageBox({
31
+ title: 'No Updates',
32
+ message: 'Current version is up-to-date.',
33
+ });
34
+ } catch (e) {
35
+ dialog.showErrorBox('Error: ', e.toString());
36
+ }
37
+ updateButton.enabled = true;
38
+ updateButton = null;
39
+ });
40
+
41
+ autoUpdater.on('update-downloaded', async () => {
42
+ await dialog.showMessageBox({
43
+ title: 'Install Update',
44
+ message: 'Update downloaded, application will quit for update...',
45
+ });
46
+
47
+ autoUpdater.quitAndInstall();
48
+ });
49
+
50
+ export function checkForUpdates(menuItem, focusedWindow, event) {
51
+ updateButton = menuItem;
52
+ updateButton.enabled = false;
53
+ autoUpdater.checkForUpdates().catch((e) => console.error('Update error', e));
54
+ }