diff options
-rw-r--r-- | .electron-builder.config.cjs | 1 | ||||
-rw-r--r-- | config/srcPlugin.js | 85 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts | 39 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/types.ts | 2 | ||||
-rw-r--r-- | packages/main/src/initReactions.ts | 1 | ||||
-rw-r--r-- | packages/service-inject/esbuild.config.js | 2 | ||||
-rw-r--r-- | packages/service-inject/package.json | 2 | ||||
-rw-r--r-- | packages/service-preload/esbuild.config.js | 4 | ||||
-rw-r--r-- | packages/service-preload/package.json | 1 | ||||
-rw-r--r-- | packages/service-preload/src/index.ts | 35 | ||||
-rw-r--r-- | packages/service-preload/tsconfig.json | 7 | ||||
-rw-r--r-- | packages/service-preload/types/serviceInject.d.ts | 4 | ||||
-rw-r--r-- | packages/service-shared/src/index.ts | 2 | ||||
-rw-r--r-- | packages/service-shared/src/ipc.ts | 1 | ||||
-rw-r--r-- | packages/service-shared/src/schemas.ts | 12 | ||||
-rw-r--r-- | scripts/build.js | 9 | ||||
-rw-r--r-- | scripts/watch.js | 90 | ||||
-rw-r--r-- | yarn.lock | 3 |
18 files changed, 162 insertions, 138 deletions
diff --git a/.electron-builder.config.cjs b/.electron-builder.config.cjs index 4340623..efd9ef7 100644 --- a/.electron-builder.config.cjs +++ b/.electron-builder.config.cjs | |||
@@ -15,7 +15,6 @@ const config = { | |||
15 | 'packages/main/dist/**', | 15 | 'packages/main/dist/**', |
16 | 'packages/preload/dist/**', | 16 | 'packages/preload/dist/**', |
17 | 'packages/renderer/dist/**', | 17 | 'packages/renderer/dist/**', |
18 | 'packages/service-inject/dist/**', | ||
19 | 'packages/service-preload/dist/**', | 18 | 'packages/service-preload/dist/**', |
20 | 'locales/**', | 19 | 'locales/**', |
21 | // Do not ship with source maps. | 20 | // Do not ship with source maps. |
diff --git a/config/srcPlugin.js b/config/srcPlugin.js new file mode 100644 index 0000000..13bee7b --- /dev/null +++ b/config/srcPlugin.js | |||
@@ -0,0 +1,85 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import { readFile } from 'node:fs/promises'; | ||
22 | |||
23 | /** @type {string} */ | ||
24 | const PLUGIN_NAMESPACE = 'sophie-src'; | ||
25 | |||
26 | /** @type {RegExp} */ | ||
27 | // eslint-disable-next-line security/detect-non-literal-regexp -- Argument is const. | ||
28 | const FILTER_REGEXP = new RegExp(`^${PLUGIN_NAMESPACE}:(.+)$`); | ||
29 | |||
30 | /** | ||
31 | * An esbuild plugin that import the source of referenced file or package as a string. | ||
32 | * | ||
33 | * Prefix the imported file name with `sophie-src:` to import its source instead. | ||
34 | * | ||
35 | * @type { import('esbuild').Plugin } | ||
36 | */ | ||
37 | const srcPlugin = { | ||
38 | name: 'sophie-inline-service-inject-plugin', | ||
39 | setup(build) { | ||
40 | build.onResolve( | ||
41 | { | ||
42 | filter: FILTER_REGEXP, | ||
43 | }, | ||
44 | async ({ path: pathToResolve, importer, resolveDir }) => { | ||
45 | const match = FILTER_REGEXP.exec(pathToResolve); | ||
46 | if (match === null) { | ||
47 | return { | ||
48 | path: '', | ||
49 | namespace: PLUGIN_NAMESPACE, | ||
50 | errors: [ | ||
51 | { | ||
52 | text: `Could not resolve ${pathToResolve}`, | ||
53 | }, | ||
54 | ], | ||
55 | }; | ||
56 | } | ||
57 | const { path, errors, warnings } = await build.resolve(match[1], { | ||
58 | importer, | ||
59 | resolveDir, | ||
60 | kind: 'import-statement', | ||
61 | }); | ||
62 | return { | ||
63 | path, | ||
64 | namespace: PLUGIN_NAMESPACE, | ||
65 | errors, | ||
66 | warnings, | ||
67 | }; | ||
68 | }, | ||
69 | ); | ||
70 | build.onLoad( | ||
71 | { | ||
72 | filter: /./, | ||
73 | namespace: PLUGIN_NAMESPACE, | ||
74 | }, | ||
75 | async ({ path }) => { | ||
76 | const src = await readFile(path, 'utf8'); | ||
77 | return { | ||
78 | contents: `export default ${JSON.stringify(src)};`, | ||
79 | }; | ||
80 | }, | ||
81 | ); | ||
82 | }, | ||
83 | }; | ||
84 | |||
85 | export default srcPlugin; | ||
diff --git a/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts b/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts index 884643a..c72860d 100644 --- a/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts +++ b/packages/main/src/infrastructure/electron/impl/ElectronViewFactory.ts | |||
@@ -18,15 +18,9 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { readFile } from 'node:fs/promises'; | ||
22 | |||
23 | import { ServiceToMainIpcMessage } from '@sophie/service-shared'; | ||
24 | import { ipcMain, WebSource } from 'electron'; | ||
25 | |||
26 | import type MainStore from '../../../stores/MainStore'; | 21 | import type MainStore from '../../../stores/MainStore'; |
27 | import type Profile from '../../../stores/Profile'; | 22 | import type Profile from '../../../stores/Profile'; |
28 | import type Service from '../../../stores/Service'; | 23 | import type Service from '../../../stores/Service'; |
29 | import { getLogger } from '../../../utils/log'; | ||
30 | import type Resources from '../../resources/Resources'; | 24 | import type Resources from '../../resources/Resources'; |
31 | import type UserAgents from '../UserAgents'; | 25 | import type UserAgents from '../UserAgents'; |
32 | import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types'; | 26 | import type { MainWindow, Partition, ServiceView, ViewFactory } from '../types'; |
@@ -35,37 +29,17 @@ import ElectronMainWindow from './ElectronMainWindow'; | |||
35 | import ElectronPartition from './ElectronPartition'; | 29 | import ElectronPartition from './ElectronPartition'; |
36 | import ElectronServiceView from './ElectronServiceView'; | 30 | import ElectronServiceView from './ElectronServiceView'; |
37 | 31 | ||
38 | const log = getLogger('ElectronViewFactory'); | ||
39 | |||
40 | export default class ElectronViewFactory implements ViewFactory { | 32 | export default class ElectronViewFactory implements ViewFactory { |
41 | private readonly webContentsIdToServiceView = new Map< | 33 | private readonly webContentsIdToServiceView = new Map< |
42 | number, | 34 | number, |
43 | ElectronServiceView | 35 | ElectronServiceView |
44 | >(); | 36 | >(); |
45 | 37 | ||
46 | private serviceInjectSource: WebSource | undefined; | ||
47 | |||
48 | constructor( | 38 | constructor( |
49 | readonly userAgents: UserAgents, | 39 | readonly userAgents: UserAgents, |
50 | readonly resources: Resources, | 40 | readonly resources: Resources, |
51 | readonly devMode: boolean, | 41 | readonly devMode: boolean, |
52 | ) { | 42 | ) {} |
53 | ipcMain.handle(ServiceToMainIpcMessage.ApiExposedInMainWorld, (event) => { | ||
54 | if (!this.webContentsIdToServiceView.has(event.sender.id)) { | ||
55 | log.error( | ||
56 | 'Unexpected', | ||
57 | ServiceToMainIpcMessage.ApiExposedInMainWorld, | ||
58 | 'IPC message from webContents', | ||
59 | event.sender.id, | ||
60 | ); | ||
61 | throw new Error('Invalid IPC call'); | ||
62 | } | ||
63 | if (this.serviceInjectSource === undefined) { | ||
64 | log.error('Service inject source was not loaded'); | ||
65 | } | ||
66 | return this.serviceInjectSource; | ||
67 | }); | ||
68 | } | ||
69 | 43 | ||
70 | async createMainWindow(store: MainStore): Promise<MainWindow> { | 44 | async createMainWindow(store: MainStore): Promise<MainWindow> { |
71 | const mainWindow = new ElectronMainWindow(store, this); | 45 | const mainWindow = new ElectronMainWindow(store, this); |
@@ -94,23 +68,12 @@ export default class ElectronViewFactory implements ViewFactory { | |||
94 | throw new TypeError('Unexpected ProfileSession is not a WrappedSession'); | 68 | throw new TypeError('Unexpected ProfileSession is not a WrappedSession'); |
95 | } | 69 | } |
96 | 70 | ||
97 | async loadServiceInject(): Promise<void> { | ||
98 | const injectPackage = 'service-inject'; | ||
99 | const injectFile = 'index.js'; | ||
100 | const injectPath = this.resources.getPath(injectPackage, injectFile); | ||
101 | this.serviceInjectSource = { | ||
102 | code: await readFile(injectPath, 'utf8'), | ||
103 | url: this.resources.getFileURL(injectPackage, injectFile), | ||
104 | }; | ||
105 | } | ||
106 | |||
107 | dispose(): void { | 71 | dispose(): void { |
108 | if (this.webContentsIdToServiceView.size > 0) { | 72 | if (this.webContentsIdToServiceView.size > 0) { |
109 | throw new Error( | 73 | throw new Error( |
110 | 'Must dispose all ServiceView instances before disposing ViewFactory', | 74 | 'Must dispose all ServiceView instances before disposing ViewFactory', |
111 | ); | 75 | ); |
112 | } | 76 | } |
113 | ipcMain.removeHandler(ServiceToMainIpcMessage.ApiExposedInMainWorld); | ||
114 | } | 77 | } |
115 | 78 | ||
116 | unregisterServiceView(id: number): void { | 79 | unregisterServiceView(id: number): void { |
diff --git a/packages/main/src/infrastructure/electron/types.ts b/packages/main/src/infrastructure/electron/types.ts index e5b0fd6..1321048 100644 --- a/packages/main/src/infrastructure/electron/types.ts +++ b/packages/main/src/infrastructure/electron/types.ts | |||
@@ -31,8 +31,6 @@ export interface ViewFactory { | |||
31 | 31 | ||
32 | createServiceView(service: Service, partition: Partition): ServiceView; | 32 | createServiceView(service: Service, partition: Partition): ServiceView; |
33 | 33 | ||
34 | loadServiceInject(): Promise<void>; | ||
35 | |||
36 | dispose(): void; | 34 | dispose(): void; |
37 | } | 35 | } |
38 | 36 | ||
diff --git a/packages/main/src/initReactions.ts b/packages/main/src/initReactions.ts index 94b1f06..05bc205 100644 --- a/packages/main/src/initReactions.ts +++ b/packages/main/src/initReactions.ts | |||
@@ -71,7 +71,6 @@ export default async function initReactions( | |||
71 | await devToolsLoaded; | 71 | await devToolsLoaded; |
72 | return viewFactory.createMainWindow(store); | 72 | return viewFactory.createMainWindow(store); |
73 | })(); | 73 | })(); |
74 | await viewFactory.loadServiceInject(); | ||
75 | loadServices(store, viewFactory); | 74 | loadServices(store, viewFactory); |
76 | store.setMainWindow(await mainWindow); | 75 | store.setMainWindow(await mainWindow); |
77 | return () => { | 76 | return () => { |
diff --git a/packages/service-inject/esbuild.config.js b/packages/service-inject/esbuild.config.js index d8698ac..fbc3e55 100644 --- a/packages/service-inject/esbuild.config.js +++ b/packages/service-inject/esbuild.config.js | |||
@@ -10,4 +10,6 @@ export default getEsbuildConfig({ | |||
10 | platform: 'browser', | 10 | platform: 'browser', |
11 | target: chrome, | 11 | target: chrome, |
12 | sourcemap: 'inline', | 12 | sourcemap: 'inline', |
13 | // Absolute URL for displaying source map in the "Page" tab of devtools. | ||
14 | sourceRoot: 'sophie-internal://sophie/packages/service-inject/dist/', | ||
13 | }); | 15 | }); |
diff --git a/packages/service-inject/package.json b/packages/service-inject/package.json index c045500..a9c6e08 100644 --- a/packages/service-inject/package.json +++ b/packages/service-inject/package.json | |||
@@ -4,7 +4,7 @@ | |||
4 | "private": true, | 4 | "private": true, |
5 | "sideEffects": false, | 5 | "sideEffects": false, |
6 | "type": "module", | 6 | "type": "module", |
7 | "types": "dist-types/index.d.ts", | 7 | "main": "dist/index.js", |
8 | "scripts": { | 8 | "scripts": { |
9 | "typecheck:workspace": "yarn g:typecheck" | 9 | "typecheck:workspace": "yarn g:typecheck" |
10 | }, | 10 | }, |
diff --git a/packages/service-preload/esbuild.config.js b/packages/service-preload/esbuild.config.js index 87e91d8..fb7359b 100644 --- a/packages/service-preload/esbuild.config.js +++ b/packages/service-preload/esbuild.config.js | |||
@@ -1,6 +1,7 @@ | |||
1 | import { chrome } from '../../config/buildConstants.js'; | 1 | import { chrome } from '../../config/buildConstants.js'; |
2 | import fileUrlToDirname from '../../config/fileUrlToDirname.js'; | 2 | import fileUrlToDirname from '../../config/fileUrlToDirname.js'; |
3 | import getEsbuildConfig from '../../config/getEsbuildConfig.js'; | 3 | import getEsbuildConfig from '../../config/getEsbuildConfig.js'; |
4 | import srcPlugin from '../../config/srcPlugin.js'; | ||
4 | 5 | ||
5 | export default getEsbuildConfig({ | 6 | export default getEsbuildConfig({ |
6 | absWorkingDir: fileUrlToDirname(import.meta.url), | 7 | absWorkingDir: fileUrlToDirname(import.meta.url), |
@@ -10,5 +11,8 @@ export default getEsbuildConfig({ | |||
10 | platform: 'node', | 11 | platform: 'node', |
11 | target: chrome, | 12 | target: chrome, |
12 | sourcemap: 'inline', | 13 | sourcemap: 'inline', |
14 | // Absolute URL for displaying source map in the "Content scripts" tab of devtools. | ||
15 | sourceRoot: 'sophie-internal://sophie/packages/service-preload/dist/', | ||
13 | external: ['electron'], | 16 | external: ['electron'], |
17 | plugins: [srcPlugin], | ||
14 | }); | 18 | }); |
diff --git a/packages/service-preload/package.json b/packages/service-preload/package.json index 20925ae..9307c2f 100644 --- a/packages/service-preload/package.json +++ b/packages/service-preload/package.json | |||
@@ -8,6 +8,7 @@ | |||
8 | "typecheck:workspace": "yarn g:typecheck" | 8 | "typecheck:workspace": "yarn g:typecheck" |
9 | }, | 9 | }, |
10 | "dependencies": { | 10 | "dependencies": { |
11 | "@sophie/service-inject": "workspace:*", | ||
11 | "@sophie/service-shared": "workspace:*", | 12 | "@sophie/service-shared": "workspace:*", |
12 | "color-string": "^1.9.0", | 13 | "color-string": "^1.9.0", |
13 | "electron": "18.0.1" | 14 | "electron": "18.0.1" |
diff --git a/packages/service-preload/src/index.ts b/packages/service-preload/src/index.ts index a49a3a4..99d02ec 100644 --- a/packages/service-preload/src/index.ts +++ b/packages/service-preload/src/index.ts | |||
@@ -18,9 +18,10 @@ | |||
18 | * SPDX-License-Identifier: AGPL-3.0-only | 18 | * SPDX-License-Identifier: AGPL-3.0-only |
19 | */ | 19 | */ |
20 | 20 | ||
21 | import { ServiceToMainIpcMessage, WebSource } from '@sophie/service-shared'; | ||
22 | import colorString from 'color-string'; | 21 | import colorString from 'color-string'; |
23 | import { ipcRenderer, webFrame } from 'electron'; | 22 | import { webFrame } from 'electron'; |
23 | // eslint-disable-next-line import/no-unresolved -- Synthetic import provided by an eslint plugin. | ||
24 | import injectSource from 'sophie-src:@sophie/service-inject'; | ||
24 | 25 | ||
25 | const DEFAULT_BG_COLOR = '#fff'; | 26 | const DEFAULT_BG_COLOR = '#fff'; |
26 | 27 | ||
@@ -79,35 +80,31 @@ if (webFrame.parent === null) { | |||
79 | } | 80 | } |
80 | 81 | ||
81 | /** | 82 | /** |
82 | * Fetches and executes the service inject script in the isolated world. | 83 | * Executes the service inject script in the isolated world. |
83 | * | 84 | * |
84 | * The service inject script relies on exposed APIs, so this function can only | 85 | * The service inject script relies on exposed APIs, so this function can only |
85 | * be called after APIs have been exposed via `contextBridge` to the main world. | 86 | * be called after APIs have been exposed via `contextBridge` to the main world. |
86 | * | 87 | * |
87 | * We have to call `executeJavaScriptInIsolatedWorld` from the service preload script, | 88 | * We embed the source code of the inject script into the preload script |
88 | * beause there is no way currently (electron 16) to execute a script on a | 89 | * with an esbuild plugin, so there is no need to fetch it separately. |
89 | * `WebFrameMain` in the main process by specifying a `WebSource`. | 90 | * |
90 | * Calling `executeJavaScriptInInsolatedWorld` on a `WebContents` in the main process | ||
91 | * will always inject the script into the _top-level_ frame, but here we | ||
92 | * are injecting into the _current_ frame instead. | ||
93 | * As a tradeoff, the promise returned by `executeJavaScriptInIsolatedWorld` | 91 | * As a tradeoff, the promise returned by `executeJavaScriptInIsolatedWorld` |
94 | * will resolve to `unknown` (instead of rejecting) even if the injected script fails, | 92 | * will resolve to `unknown` (instead of rejecting) even if the injected script fails, |
95 | * because chromium doesn't dispatch main world errors to isolated worlds. | 93 | * because chromium doesn't dispatch main world errors to isolated worlds. |
96 | * | 94 | * |
97 | * @return A promise that only rejects if we fail to fetch the inject script. | 95 | * @return A promise that always resolves to `undefined`. |
98 | * @see https://www.electronjs.org/docs/latest/api/web-frame#webframeexecutejavascriptinisolatedworldworldid-scripts-usergesture-callback | ||
99 | * @see https://www.electronjs.org/docs/latest/api/web-frame-main#frameexecutejavascriptcode-usergesture | ||
100 | * @see https://www.electronjs.org/docs/latest/api/web-contents#contentsexecutejavascriptinisolatedworldworldid-scripts-usergesture | ||
101 | */ | 96 | */ |
102 | async function fetchAndExecuteInjectScript(): Promise<void> { | 97 | async function fetchAndExecuteInjectScript(): Promise<void> { |
103 | const apiExposedResponse: unknown = await ipcRenderer.invoke( | ||
104 | ServiceToMainIpcMessage.ApiExposedInMainWorld, | ||
105 | ); | ||
106 | const injectSource = WebSource.parse(apiExposedResponse); | ||
107 | // Isolated world 0 is the main world. | 98 | // Isolated world 0 is the main world. |
108 | await webFrame.executeJavaScriptInIsolatedWorld(0, [injectSource]); | 99 | await webFrame.executeJavaScriptInIsolatedWorld(0, [ |
100 | { | ||
101 | code: injectSource, | ||
102 | }, | ||
103 | ]); | ||
109 | } | 104 | } |
110 | 105 | ||
111 | fetchAndExecuteInjectScript().catch((error) => { | 106 | fetchAndExecuteInjectScript().catch((error) => { |
112 | console.error('Failed to fetch inject source:', error); | 107 | // This will never happen because of |
108 | // https://www.electronjs.org/docs/latest/api/web-frame#webframeexecutejavascriptinisolatedworldworldid-scripts-usergesture-callback | ||
109 | console.error('Failed to execute service inject:', error); | ||
113 | }); | 110 | }); |
diff --git a/packages/service-preload/tsconfig.json b/packages/service-preload/tsconfig.json index 33ce1de..d7fb8cb 100644 --- a/packages/service-preload/tsconfig.json +++ b/packages/service-preload/tsconfig.json | |||
@@ -9,5 +9,10 @@ | |||
9 | "path": "../service-shared/tsconfig.build.json" | 9 | "path": "../service-shared/tsconfig.build.json" |
10 | } | 10 | } |
11 | ], | 11 | ], |
12 | "include": ["src/**/*.ts", ".eslintrc.cjs", "esbuild.config.js"] | 12 | "include": [ |
13 | "src/**/*.ts", | ||
14 | "types/**/*.d.ts", | ||
15 | ".eslintrc.cjs", | ||
16 | "esbuild.config.js" | ||
17 | ] | ||
13 | } | 18 | } |
diff --git a/packages/service-preload/types/serviceInject.d.ts b/packages/service-preload/types/serviceInject.d.ts new file mode 100644 index 0000000..236973e --- /dev/null +++ b/packages/service-preload/types/serviceInject.d.ts | |||
@@ -0,0 +1,4 @@ | |||
1 | declare module 'sophie-src:@sophie/service-inject' { | ||
2 | const src: string; | ||
3 | export default src; | ||
4 | } | ||
diff --git a/packages/service-shared/src/index.ts b/packages/service-shared/src/index.ts index a2e5ee5..5165fe5 100644 --- a/packages/service-shared/src/index.ts +++ b/packages/service-shared/src/index.ts | |||
@@ -20,4 +20,4 @@ | |||
20 | 20 | ||
21 | export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; | 21 | export { MainToServiceIpcMessage, ServiceToMainIpcMessage } from './ipc'; |
22 | 22 | ||
23 | export { UnreadCount, WebSource } from './schemas'; | 23 | export { UnreadCount } from './schemas'; |
diff --git a/packages/service-shared/src/ipc.ts b/packages/service-shared/src/ipc.ts index e0a8755..4ead5bd 100644 --- a/packages/service-shared/src/ipc.ts +++ b/packages/service-shared/src/ipc.ts | |||
@@ -21,6 +21,5 @@ | |||
21 | export enum MainToServiceIpcMessage {} | 21 | export enum MainToServiceIpcMessage {} |
22 | 22 | ||
23 | export enum ServiceToMainIpcMessage { | 23 | export enum ServiceToMainIpcMessage { |
24 | ApiExposedInMainWorld = 'sophie-service-to-main:api-exposed-in-main-world', | ||
25 | SetUnreadCount = 'sophie-service-to-main:set-unread-count', | 24 | SetUnreadCount = 'sophie-service-to-main:set-unread-count', |
26 | } | 25 | } |
diff --git a/packages/service-shared/src/schemas.ts b/packages/service-shared/src/schemas.ts index bb1926f..799faac 100644 --- a/packages/service-shared/src/schemas.ts +++ b/packages/service-shared/src/schemas.ts | |||
@@ -31,15 +31,3 @@ export const UnreadCount = /* @__PURE__ */ (() => | |||
31 | Intentionally naming the type the same as the schema definition. | 31 | Intentionally naming the type the same as the schema definition. |
32 | */ | 32 | */ |
33 | export type UnreadCount = z.infer<typeof UnreadCount>; | 33 | export type UnreadCount = z.infer<typeof UnreadCount>; |
34 | |||
35 | export const WebSource = /* @__PURE__ */ (() => | ||
36 | z.object({ | ||
37 | code: z.string(), | ||
38 | url: z.string().nonempty(), | ||
39 | }))(); | ||
40 | |||
41 | /* | ||
42 | eslint-disable-next-line @typescript-eslint/no-redeclare -- | ||
43 | Intentionally naming the type the same as the schema definition. | ||
44 | */ | ||
45 | export type WebSource = z.infer<typeof WebSource>; | ||
diff --git a/scripts/build.js b/scripts/build.js index 66a1506..eb0e8a3 100644 --- a/scripts/build.js +++ b/scripts/build.js | |||
@@ -37,12 +37,9 @@ function buildAll() { | |||
37 | Promise.all([buildServiceShared, buildShared]).then(() => | 37 | Promise.all([buildServiceShared, buildShared]).then(() => |
38 | buildPackageEsbuild('main'), | 38 | buildPackageEsbuild('main'), |
39 | ), | 39 | ), |
40 | buildServiceShared.then(() => | 40 | buildServiceShared |
41 | Promise.all([ | 41 | .then(() => buildPackageEsbuild('service-inject')) |
42 | buildPackageEsbuild('service-inject'), | 42 | .then(() => buildPackageEsbuild('service-preload')), |
43 | buildPackageEsbuild('service-preload'), | ||
44 | ]), | ||
45 | ), | ||
46 | buildShared.then(() => | 43 | buildShared.then(() => |
47 | Promise.all([ | 44 | Promise.all([ |
48 | buildPackageEsbuild('preload'), | 45 | buildPackageEsbuild('preload'), |
diff --git a/scripts/watch.js b/scripts/watch.js index 9cd65c8..3c3a8fd 100644 --- a/scripts/watch.js +++ b/scripts/watch.js | |||
@@ -1,6 +1,5 @@ | |||
1 | import { spawn } from 'node:child_process'; | 1 | import { spawn } from 'node:child_process'; |
2 | import path from 'node:path'; | 2 | import path from 'node:path'; |
3 | import { exit, kill } from 'node:process'; | ||
4 | 3 | ||
5 | import { watch } from 'chokidar'; | 4 | import { watch } from 'chokidar'; |
6 | import electronPath from 'electron'; | 5 | import electronPath from 'electron'; |
@@ -21,6 +20,12 @@ const serviceSharedModule = path.join( | |||
21 | '../packages/service-shared/dist/index.mjs', | 20 | '../packages/service-shared/dist/index.mjs', |
22 | ); | 21 | ); |
23 | 22 | ||
23 | /** @type {string} */ | ||
24 | const serviceInjectModule = path.join( | ||
25 | thisDir, | ||
26 | '../packages/service-inject/dist/index.js', | ||
27 | ); | ||
28 | |||
24 | /** @type {RegExp[]} */ | 29 | /** @type {RegExp[]} */ |
25 | const stderrIgnorePatterns = [ | 30 | const stderrIgnorePatterns = [ |
26 | // warning about devtools extension | 31 | // warning about devtools extension |
@@ -111,50 +116,6 @@ async function setupEsbuildWatcher(packageName, extraPaths, callback) { | |||
111 | } | 116 | } |
112 | 117 | ||
113 | /** | 118 | /** |
114 | * @param {string} packageName | ||
115 | * @returns {Promise<import('vite').ViteDevServer>} | ||
116 | */ | ||
117 | async function setupDevServer(packageName) { | ||
118 | const viteDevServer = await createServer({ | ||
119 | build: { | ||
120 | watch: { | ||
121 | skipWrite: true, | ||
122 | clearScreen: false, | ||
123 | }, | ||
124 | }, | ||
125 | configFile: path.join(thisDir, `../packages/${packageName}/vite.config.js`), | ||
126 | }); | ||
127 | await viteDevServer.listen(); | ||
128 | return viteDevServer; | ||
129 | } | ||
130 | |||
131 | /** | ||
132 | * @param {(event: import('vite').HMRPayload) => void} sendEvent | ||
133 | * @returns {Promise<void>} | ||
134 | */ | ||
135 | function setupPreloadPackageWatcher(sendEvent) { | ||
136 | return setupEsbuildWatcher('preload', [sharedModule], () => { | ||
137 | sendEvent({ | ||
138 | type: 'full-reload', | ||
139 | }); | ||
140 | }); | ||
141 | } | ||
142 | |||
143 | /** | ||
144 | * @param {string} packageName | ||
145 | * @param {(event: import('vite').HMRPayload) => void} sendEvent | ||
146 | * @returns {Promise<void>} | ||
147 | */ | ||
148 | function setupServicePackageWatcher(packageName, sendEvent) { | ||
149 | return setupEsbuildWatcher(packageName, [serviceSharedModule], () => { | ||
150 | sendEvent({ | ||
151 | type: 'custom', | ||
152 | event: 'sophie:reload-services', | ||
153 | }); | ||
154 | }); | ||
155 | } | ||
156 | |||
157 | /** | ||
158 | * @param {import('vite').ViteDevServer} viteDevServer | 119 | * @param {import('vite').ViteDevServer} viteDevServer |
159 | * @returns {Promise<void>} | 120 | * @returns {Promise<void>} |
160 | */ | 121 | */ |
@@ -280,7 +241,16 @@ async function setupDevEnvironment() { | |||
280 | * @returns {Promise<void>} | 241 | * @returns {Promise<void>} |
281 | */ | 242 | */ |
282 | async function startDevServer() { | 243 | async function startDevServer() { |
283 | viteDevServer = await setupDevServer('renderer'); | 244 | viteDevServer = await createServer({ |
245 | build: { | ||
246 | watch: { | ||
247 | skipWrite: true, | ||
248 | clearScreen: false, | ||
249 | }, | ||
250 | }, | ||
251 | configFile: path.join(thisDir, `../packages/renderer/vite.config.js`), | ||
252 | }); | ||
253 | await viteDevServer.listen(); | ||
284 | } | 254 | } |
285 | 255 | ||
286 | /** | 256 | /** |
@@ -289,7 +259,11 @@ async function setupDevEnvironment() { | |||
289 | async function watchRendererPackages() { | 259 | async function watchRendererPackages() { |
290 | await setupEsbuildWatcher('shared'); | 260 | await setupEsbuildWatcher('shared'); |
291 | await Promise.all([ | 261 | await Promise.all([ |
292 | setupPreloadPackageWatcher(sendEvent), | 262 | setupEsbuildWatcher('preload', [sharedModule], () => |
263 | sendEvent({ | ||
264 | type: 'full-reload', | ||
265 | }), | ||
266 | ), | ||
293 | startDevServer(), | 267 | startDevServer(), |
294 | ]); | 268 | ]); |
295 | } | 269 | } |
@@ -299,21 +273,29 @@ async function setupDevEnvironment() { | |||
299 | */ | 273 | */ |
300 | async function watchServicePackages() { | 274 | async function watchServicePackages() { |
301 | await setupEsbuildWatcher('service-shared'); | 275 | await setupEsbuildWatcher('service-shared'); |
302 | await Promise.all([ | 276 | await setupEsbuildWatcher('service-inject', [serviceSharedModule]); |
303 | setupServicePackageWatcher('service-inject', sendEvent), | 277 | await setupEsbuildWatcher( |
304 | setupServicePackageWatcher('service-preload', sendEvent), | 278 | 'service-preload', |
305 | ]); | 279 | [serviceSharedModule, serviceInjectModule], |
280 | () => | ||
281 | sendEvent({ | ||
282 | type: 'custom', | ||
283 | event: 'sophie:reload-services', | ||
284 | }), | ||
285 | ); | ||
306 | } | 286 | } |
307 | 287 | ||
308 | await Promise.all([watchRendererPackages(), watchServicePackages()]); | 288 | await Promise.all([ |
289 | watchRendererPackages(), | ||
290 | watchServicePackages(), | ||
291 | setupTranslationsWatcher(sendEvent), | ||
292 | ]); | ||
309 | 293 | ||
310 | if (viteDevServer === undefined) { | 294 | if (viteDevServer === undefined) { |
311 | console.error('Failed to create vite dev server'); | 295 | console.error('Failed to create vite dev server'); |
312 | return; | 296 | return; |
313 | } | 297 | } |
314 | 298 | ||
315 | setupTranslationsWatcher(sendEvent); | ||
316 | |||
317 | console.log('\uD83C\uDF80 Sophie is starting up'); | 299 | console.log('\uD83C\uDF80 Sophie is starting up'); |
318 | await setupMainPackageWatcher(viteDevServer); | 300 | await setupMainPackageWatcher(viteDevServer); |
319 | } | 301 | } |
@@ -1356,7 +1356,7 @@ __metadata: | |||
1356 | languageName: unknown | 1356 | languageName: unknown |
1357 | linkType: soft | 1357 | linkType: soft |
1358 | 1358 | ||
1359 | "@sophie/service-inject@workspace:packages/service-inject": | 1359 | "@sophie/service-inject@workspace:*, @sophie/service-inject@workspace:packages/service-inject": |
1360 | version: 0.0.0-use.local | 1360 | version: 0.0.0-use.local |
1361 | resolution: "@sophie/service-inject@workspace:packages/service-inject" | 1361 | resolution: "@sophie/service-inject@workspace:packages/service-inject" |
1362 | dependencies: | 1362 | dependencies: |
@@ -1368,6 +1368,7 @@ __metadata: | |||
1368 | version: 0.0.0-use.local | 1368 | version: 0.0.0-use.local |
1369 | resolution: "@sophie/service-preload@workspace:packages/service-preload" | 1369 | resolution: "@sophie/service-preload@workspace:packages/service-preload" |
1370 | dependencies: | 1370 | dependencies: |
1371 | "@sophie/service-inject": "workspace:*" | ||
1371 | "@sophie/service-shared": "workspace:*" | 1372 | "@sophie/service-shared": "workspace:*" |
1372 | "@types/color-string": ^1.5.2 | 1373 | "@types/color-string": ^1.5.2 |
1373 | color-string: ^1.9.0 | 1374 | color-string: ^1.9.0 |