@witchcraft/nuxt-electron 0.0.1
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/README.md +299 -0
- package/dist/module.d.mts +117 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +362 -0
- package/dist/runtime/components/ElectronWindowControls.d.vue.ts +31 -0
- package/dist/runtime/components/ElectronWindowControls.vue +67 -0
- package/dist/runtime/components/ElectronWindowControls.vue.d.ts +31 -0
- package/dist/runtime/components/WindowControls/CloseButton.d.vue.ts +16 -0
- package/dist/runtime/components/WindowControls/CloseButton.vue +54 -0
- package/dist/runtime/components/WindowControls/CloseButton.vue.d.ts +16 -0
- package/dist/runtime/components/WindowControls/MaximizeButton.d.vue.ts +16 -0
- package/dist/runtime/components/WindowControls/MaximizeButton.vue +33 -0
- package/dist/runtime/components/WindowControls/MaximizeButton.vue.d.ts +16 -0
- package/dist/runtime/components/WindowControls/MinimizeButton.d.vue.ts +16 -0
- package/dist/runtime/components/WindowControls/MinimizeButton.vue +40 -0
- package/dist/runtime/components/WindowControls/MinimizeButton.vue.d.ts +16 -0
- package/dist/runtime/components/WindowControls/PinButton.d.vue.ts +27 -0
- package/dist/runtime/components/WindowControls/PinButton.vue +51 -0
- package/dist/runtime/components/WindowControls/PinButton.vue.d.ts +27 -0
- package/dist/runtime/electron/apiBuilder.d.ts +8 -0
- package/dist/runtime/electron/apiBuilder.js +9 -0
- package/dist/runtime/electron/createBroadcastHandlers.d.ts +4 -0
- package/dist/runtime/electron/createBroadcastHandlers.js +30 -0
- package/dist/runtime/electron/createBroadcaster.d.ts +2 -0
- package/dist/runtime/electron/createBroadcaster.js +7 -0
- package/dist/runtime/electron/createNuxtFileProtocolHandler.d.ts +14 -0
- package/dist/runtime/electron/createNuxtFileProtocolHandler.js +20 -0
- package/dist/runtime/electron/createWindowControlsApi.d.ts +19 -0
- package/dist/runtime/electron/createWindowControlsApi.js +6 -0
- package/dist/runtime/electron/createWindowControlsApiHandler.d.ts +4 -0
- package/dist/runtime/electron/createWindowControlsApiHandler.js +24 -0
- package/dist/runtime/electron/getEventWindow.d.ts +11 -0
- package/dist/runtime/electron/getEventWindow.js +8 -0
- package/dist/runtime/electron/getPaths.d.ts +27 -0
- package/dist/runtime/electron/getPaths.js +33 -0
- package/dist/runtime/electron/getPreloadMeta.d.ts +25 -0
- package/dist/runtime/electron/getPreloadMeta.js +7 -0
- package/dist/runtime/electron/index.d.ts +16 -0
- package/dist/runtime/electron/index.js +16 -0
- package/dist/runtime/electron/promisifyApi.d.ts +40 -0
- package/dist/runtime/electron/promisifyApi.js +41 -0
- package/dist/runtime/electron/promisifyReply.d.ts +8 -0
- package/dist/runtime/electron/promisifyReply.js +27 -0
- package/dist/runtime/electron/registerDevtoolsShortcuts.d.ts +2 -0
- package/dist/runtime/electron/registerDevtoolsShortcuts.js +10 -0
- package/dist/runtime/electron/static.d.ts +12 -0
- package/dist/runtime/electron/static.js +8 -0
- package/dist/runtime/electron/types.d.ts +1 -0
- package/dist/runtime/electron/types.js +0 -0
- package/dist/runtime/electron/useDevDataDir.d.ts +1 -0
- package/dist/runtime/electron/useDevDataDir.js +8 -0
- package/dist/runtime/electron/useNuxtRuntimeConfig.d.ts +2 -0
- package/dist/runtime/electron/useNuxtRuntimeConfig.js +4 -0
- package/dist/runtime/utils/isElectron.d.ts +9 -0
- package/dist/runtime/utils/isElectron.js +3 -0
- package/dist/types.d.mts +3 -0
- package/genDevDesktop.js +47 -0
- package/package.json +93 -0
- package/src/module.ts +549 -0
- package/src/runtime/components/ElectronWindowControls.vue +94 -0
- package/src/runtime/components/WindowControls/CloseButton.vue +56 -0
- package/src/runtime/components/WindowControls/MaximizeButton.vue +35 -0
- package/src/runtime/components/WindowControls/MinimizeButton.vue +42 -0
- package/src/runtime/components/WindowControls/PinButton.vue +56 -0
- package/src/runtime/electron/apiBuilder.ts +27 -0
- package/src/runtime/electron/createBroadcastHandlers.ts +36 -0
- package/src/runtime/electron/createBroadcaster.ts +11 -0
- package/src/runtime/electron/createNuxtFileProtocolHandler.ts +42 -0
- package/src/runtime/electron/createWindowControlsApi.ts +27 -0
- package/src/runtime/electron/createWindowControlsApiHandler.ts +34 -0
- package/src/runtime/electron/getEventWindow.ts +19 -0
- package/src/runtime/electron/getPaths.ts +68 -0
- package/src/runtime/electron/getPreloadMeta.ts +36 -0
- package/src/runtime/electron/index.ts +17 -0
- package/src/runtime/electron/promisifyApi.ts +102 -0
- package/src/runtime/electron/promisifyReply.ts +49 -0
- package/src/runtime/electron/registerDevtoolsShortcuts.ts +12 -0
- package/src/runtime/electron/static.ts +14 -0
- package/src/runtime/electron/types.ts +1 -0
- package/src/runtime/electron/useDevDataDir.ts +8 -0
- package/src/runtime/electron/useNuxtRuntimeConfig.ts +7 -0
- package/src/runtime/utils/isElectron.ts +11 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<WButton
|
|
3
|
+
:border="false"
|
|
4
|
+
aria-label="Toggle Maximize"
|
|
5
|
+
class="
|
|
6
|
+
p-0
|
|
7
|
+
[&:hover_.default-icon]:border-accent-500
|
|
8
|
+
[&:hover_.default-icon]:shadow-xs
|
|
9
|
+
[&:hover_.default-icon]:shadow-fg/50
|
|
10
|
+
|
|
11
|
+
"
|
|
12
|
+
@click="emit('action', 'toggleMaximize')"
|
|
13
|
+
>
|
|
14
|
+
<slot>
|
|
15
|
+
<div
|
|
16
|
+
class="
|
|
17
|
+
default-icon
|
|
18
|
+
border-fg
|
|
19
|
+
dark:border-bg
|
|
20
|
+
hover:border-accent-500
|
|
21
|
+
border-[length:var(--electron-wc-border)]
|
|
22
|
+
rounded-(--electron-wc-rounded)
|
|
23
|
+
w-[var(--electron-wc-size)]
|
|
24
|
+
h-[var(--electron-wc-size)]
|
|
25
|
+
"
|
|
26
|
+
/>
|
|
27
|
+
</slot>
|
|
28
|
+
</WButton>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script lang="ts" setup>
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
(e: "action", action: "toggleMaximize"): void
|
|
34
|
+
}>()
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<WButton
|
|
3
|
+
:border="false"
|
|
4
|
+
aria-label="Minimize"
|
|
5
|
+
class="
|
|
6
|
+
p-0
|
|
7
|
+
[&:hover_.default-icon:after]:bg-accent-500
|
|
8
|
+
[&:hover_.default-icon:after]:shadow-xs
|
|
9
|
+
[&:hover_.default-icon:after]:shadow-fg/50
|
|
10
|
+
"
|
|
11
|
+
@click="emit('action', 'minimize')"
|
|
12
|
+
>
|
|
13
|
+
<slot>
|
|
14
|
+
<div
|
|
15
|
+
class="
|
|
16
|
+
default-icon
|
|
17
|
+
border-fg
|
|
18
|
+
dark:border-bg
|
|
19
|
+
w-[var(--electron-wc-size)]
|
|
20
|
+
h-[var(--electron-wc-size)]
|
|
21
|
+
relative
|
|
22
|
+
after:absolute
|
|
23
|
+
after:content-['']
|
|
24
|
+
after:bottom-0
|
|
25
|
+
after:left-0
|
|
26
|
+
after:w-[var(--electron-wc-size)]
|
|
27
|
+
after:h-[var(--electron-wc-border)]
|
|
28
|
+
after:rounded-(--electron-wc-rounded)
|
|
29
|
+
after:bg-fg
|
|
30
|
+
dark:after:bg-bg
|
|
31
|
+
|
|
32
|
+
"
|
|
33
|
+
/>
|
|
34
|
+
</slot>
|
|
35
|
+
</WButton>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<script lang="ts" setup>
|
|
39
|
+
const emit = defineEmits<{
|
|
40
|
+
(e: "action", action: "minimize"): void
|
|
41
|
+
}>()
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<WButton
|
|
3
|
+
aria-label="Toggle Always On Top"
|
|
4
|
+
auto-title-from-aria
|
|
5
|
+
:border="false"
|
|
6
|
+
:class="twMerge(`
|
|
7
|
+
p-0
|
|
8
|
+
hover:text-accent-500
|
|
9
|
+
`,
|
|
10
|
+
alwaysOnTop && `
|
|
11
|
+
[&_.default-icon_svg]:drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]
|
|
12
|
+
`,
|
|
13
|
+
!alwaysOnTop && `
|
|
14
|
+
text-neutral-400
|
|
15
|
+
dark:text-neutral-600
|
|
16
|
+
`
|
|
17
|
+
)"
|
|
18
|
+
|
|
19
|
+
@click="emit('action', 'togglePin')"
|
|
20
|
+
>
|
|
21
|
+
<slot v-bind="{ alwaysOnTop }">
|
|
22
|
+
<WIcon
|
|
23
|
+
:class="twMerge(
|
|
24
|
+
`
|
|
25
|
+
default-icon
|
|
26
|
+
w-[var(--electron-wc-size)]
|
|
27
|
+
h-[var(--electron-wc-size)]
|
|
28
|
+
flex items-center justify-center
|
|
29
|
+
scale-105
|
|
30
|
+
`
|
|
31
|
+
)
|
|
32
|
+
"
|
|
33
|
+
>
|
|
34
|
+
<i-octicon-pin-16/>
|
|
35
|
+
</WIcon>
|
|
36
|
+
</slot>
|
|
37
|
+
</WButton>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script lang="ts" setup>
|
|
41
|
+
import { twMerge } from "#imports"
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
(e: "action", action: "togglePin"): void
|
|
45
|
+
}>()
|
|
46
|
+
/**
|
|
47
|
+
* If there is a `window.electron.on` api that allows listening to events and main sends an `always-on-top-changed`, it will update the state of the pin.
|
|
48
|
+
*/
|
|
49
|
+
const alwaysOnTop = defineModel<boolean>({ default: false })
|
|
50
|
+
|
|
51
|
+
if (import.meta.client && (window as any)?.electron?.on) {
|
|
52
|
+
(window as any).electron.on("always-on-top-changed", (val: boolean) => {
|
|
53
|
+
alwaysOnTop.value = val
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { DeepPartial } from "@alanscodelog/utils"
|
|
2
|
+
import defu from "defu"
|
|
3
|
+
import type { IpcRenderer } from "electron"
|
|
4
|
+
/**
|
|
5
|
+
* Helper for building an api object.
|
|
6
|
+
*
|
|
7
|
+
* @experimental
|
|
8
|
+
*/
|
|
9
|
+
export function apiBuilder<
|
|
10
|
+
TPreloadMeta extends Record<string, any>,
|
|
11
|
+
TRawApi extends Record<string, any> = Record<string, any>,
|
|
12
|
+
TBuilders extends
|
|
13
|
+
((meta: DeepPartial<TPreloadMeta>, ipcRenderer: IpcRenderer) => any)[] = ((meta: DeepPartial<TPreloadMeta>, ipcRenderer: IpcRenderer) => any)[],
|
|
14
|
+
TBuilderOutput extends ReturnType<TBuilders[number]> = ReturnType<TBuilders[number]>
|
|
15
|
+
>(
|
|
16
|
+
ipcRenderer: IpcRenderer,
|
|
17
|
+
api: TRawApi,
|
|
18
|
+
meta: TPreloadMeta,
|
|
19
|
+
builders: TBuilders
|
|
20
|
+
): TRawApi & TBuilderOutput {
|
|
21
|
+
const resolutions: object[] = []
|
|
22
|
+
resolutions.push(api)
|
|
23
|
+
for (const builder of builders) {
|
|
24
|
+
resolutions.push(builder(meta, ipcRenderer))
|
|
25
|
+
}
|
|
26
|
+
return (defu as any)(...resolutions.reverse()) as any
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ipcRenderer } from "electron"
|
|
2
|
+
|
|
3
|
+
export function createBroadcastHandlers<TEvents extends Record<string, (...args: any) => any>>(
|
|
4
|
+
key: string
|
|
5
|
+
): {
|
|
6
|
+
on: <T extends keyof TEvents>(event: T, listener: TEvents[T]) => void
|
|
7
|
+
off: <T extends keyof TEvents>(event: T, listener: TEvents[T]) => void
|
|
8
|
+
} {
|
|
9
|
+
const listeners = new Map<keyof TEvents, (TEvents[keyof TEvents])[]>()
|
|
10
|
+
ipcRenderer.on(key, (_e, eventName, ...args: any[]) => {
|
|
11
|
+
const cbs = listeners.get(eventName)
|
|
12
|
+
if (cbs) {
|
|
13
|
+
for (const cb of cbs) {
|
|
14
|
+
cb(...args)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
function on<T extends keyof TEvents>(event: T, listener: TEvents[T]): void {
|
|
19
|
+
const cbs = listeners.get(event)
|
|
20
|
+
if (cbs) {
|
|
21
|
+
cbs.push(listener)
|
|
22
|
+
} else {
|
|
23
|
+
listeners.set(event, [listener])
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function off<T extends keyof TEvents>(event: T, listener: TEvents[T]): void {
|
|
28
|
+
const cbs = listeners.get(event)
|
|
29
|
+
if (cbs) {
|
|
30
|
+
cbs.splice(cbs.indexOf(listener), 1)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
on, off
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BrowserWindow } from "electron"
|
|
2
|
+
|
|
3
|
+
export function createBroadcaster<
|
|
4
|
+
TEvents extends Record<string, (...args: any) => any>
|
|
5
|
+
>(key: string, getWindows: () => BrowserWindow[]) {
|
|
6
|
+
return function broadcast<T extends keyof TEvents>(event: T, ...args: Parameters<TEvents[T]>) {
|
|
7
|
+
for (const win of getWindows()) {
|
|
8
|
+
win.webContents.send(key, event, ...args)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { keys } from "@alanscodelog/utils/keys"
|
|
2
|
+
import { net } from "electron"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
export function createNuxtFileProtocolHandler(
|
|
6
|
+
session: Electron.Session,
|
|
7
|
+
basePath: string,
|
|
8
|
+
/**
|
|
9
|
+
* If any route starts with one of these keys, it will get rerouted to the given value.
|
|
10
|
+
*
|
|
11
|
+
* At least `/api` should be added for a basic nuxt app to work correctly.
|
|
12
|
+
*
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* routeProxies: {
|
|
16
|
+
* "/api": "http://localhost:3000/api",
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
routeProxies: Record<string, string>
|
|
21
|
+
): void {
|
|
22
|
+
const routeProxyKeys = keys(routeProxies)
|
|
23
|
+
session.protocol.handle("file", async request => {
|
|
24
|
+
const url = decodeURIComponent(request.url.slice(7))
|
|
25
|
+
const newUrl = path.isAbsolute(url)
|
|
26
|
+
? path.resolve(basePath, url.slice(1))
|
|
27
|
+
: path.resolve(basePath, url)
|
|
28
|
+
|
|
29
|
+
const proxyKey = routeProxyKeys.find(key => url.startsWith(key))
|
|
30
|
+
|
|
31
|
+
if (proxyKey) {
|
|
32
|
+
const proxyUrl = routeProxies[proxyKey]
|
|
33
|
+
return net.fetch(proxyUrl + url)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const res = await net.fetch(`file://${newUrl}`, {
|
|
37
|
+
// avoid infinite loop
|
|
38
|
+
bypassCustomProtocolHandlers: true
|
|
39
|
+
}).catch(err => new Response(err, { status: 404 }))
|
|
40
|
+
return res
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IpcRenderer } from "electron"
|
|
2
|
+
|
|
3
|
+
import { promisifyApi } from "./promisifyApi.js"
|
|
4
|
+
import type { WindowControlsApi } from "./types.js"
|
|
5
|
+
|
|
6
|
+
const key = "window-control-action"
|
|
7
|
+
/**
|
|
8
|
+
* Creates the apis to control the window from the preload script / client.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // the default handler expects it to exist under `electron.api.ui.windowAction`
|
|
12
|
+
* // but you can configure it to use a different path
|
|
13
|
+
* constextBridge.exposeInMainWorld("electron", {
|
|
14
|
+
* api: {
|
|
15
|
+
* ui: {
|
|
16
|
+
* ...createWindowControlsApi("windowAction")
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function createWindowControlsApi(ipcRenderer: IpcRenderer, name: string): Record<string, WindowControlsApi> {
|
|
23
|
+
return promisifyApi<typeof name,
|
|
24
|
+
WindowControlsApi
|
|
25
|
+
>(ipcRenderer, name, key)
|
|
26
|
+
}
|
|
27
|
+
export const windowControlsMessageKey = key
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BrowserWindow } from "electron"
|
|
2
|
+
|
|
3
|
+
import { windowControlsMessageKey } from "./createWindowControlsApi.js"
|
|
4
|
+
import { promisifyReply } from "./promisifyReply.js"
|
|
5
|
+
import type { WindowControlsApi } from "./types.js"
|
|
6
|
+
|
|
7
|
+
export function createWindowControlsApiHandler(
|
|
8
|
+
/** Mostly for logging. Does not replace the action. */
|
|
9
|
+
cb?: (win: BrowserWindow | undefined, action: "close" | "minimize" | "toggleMaximize" | "togglePin") => void
|
|
10
|
+
) {
|
|
11
|
+
promisifyReply<
|
|
12
|
+
WindowControlsApi
|
|
13
|
+
>(windowControlsMessageKey, async (win, action) => {
|
|
14
|
+
switch (action) {
|
|
15
|
+
case "close":
|
|
16
|
+
win?.close()
|
|
17
|
+
break
|
|
18
|
+
case "minimize":
|
|
19
|
+
win?.minimize()
|
|
20
|
+
break
|
|
21
|
+
case "toggleMaximize":
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.log(win?.isMaximized())
|
|
24
|
+
win?.isMaximized() ? win?.unmaximize() : win?.maximize()
|
|
25
|
+
break
|
|
26
|
+
case "togglePin":
|
|
27
|
+
win?.setAlwaysOnTop(!win?.isAlwaysOnTop())
|
|
28
|
+
break
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Invalid action: ${action}`)
|
|
31
|
+
}
|
|
32
|
+
cb?.(win, action)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BrowserWindow } from "electron"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets the window that sent the event if it exists, otherwise tries to use the focused window.
|
|
5
|
+
*
|
|
6
|
+
* Set `defaultToFocused` to false to only use the sender window.
|
|
7
|
+
*
|
|
8
|
+
* Returns undefined if no window is found.
|
|
9
|
+
*/
|
|
10
|
+
export function getEventWindow(
|
|
11
|
+
e: Electron.IpcMainEvent,
|
|
12
|
+
{ defaultToFocused = true } = {}
|
|
13
|
+
): BrowserWindow | undefined {
|
|
14
|
+
const senderWindow = BrowserWindow.fromId(e.sender.id)
|
|
15
|
+
if (defaultToFocused) {
|
|
16
|
+
return senderWindow ?? BrowserWindow.getFocusedWindow() ?? undefined
|
|
17
|
+
}
|
|
18
|
+
return senderWindow ?? undefined
|
|
19
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { unreachable } from "@alanscodelog/utils/unreachable"
|
|
2
|
+
import { app } from "electron"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
import { STATIC } from "./static.js"
|
|
6
|
+
|
|
7
|
+
export function forceRelativePath(filepath: string): string {
|
|
8
|
+
return path.join(`.${path.sep}`, filepath)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculates the correct paths for the app to run in electron.
|
|
13
|
+
*
|
|
14
|
+
* Note: For serverUrl to not be overridable in production, you should set `electron.additionalElectronVariables.publicServerUrl` to a quoted string.
|
|
15
|
+
*
|
|
16
|
+
* ```ts[nuxt.config.ts]
|
|
17
|
+
* export default defineNuxtConfig({
|
|
18
|
+
* modules: [
|
|
19
|
+
* "@witchcraft/nuxt-electron",
|
|
20
|
+
* ],
|
|
21
|
+
* electron: {
|
|
22
|
+
* additionalElectronVariables: {
|
|
23
|
+
* publicServerUrl: process.env.NODE_ENV === "production"
|
|
24
|
+
* ? `"mysite.com"`
|
|
25
|
+
* : `undefined`
|
|
26
|
+
* }
|
|
27
|
+
* }
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function getPaths(): {
|
|
32
|
+
windowUrl: string
|
|
33
|
+
publicServerUrl: string
|
|
34
|
+
nuxtPublicDir: string
|
|
35
|
+
preloadPath: string
|
|
36
|
+
} {
|
|
37
|
+
const rootDir = app.getAppPath()
|
|
38
|
+
const nuxtPublicDir = path.resolve(rootDir, forceRelativePath(STATIC.ELECTRON_NUXT_PUBLIC_DIR!))
|
|
39
|
+
|
|
40
|
+
const preloadPath = path.resolve(rootDir, forceRelativePath(STATIC.ELECTRON_BUILD_DIR!), "./preload.cjs")
|
|
41
|
+
|
|
42
|
+
const base = {
|
|
43
|
+
nuxtPublicDir,
|
|
44
|
+
preloadPath,
|
|
45
|
+
publicServerUrl: (process.env.PUBLIC_SERVER_URL
|
|
46
|
+
// allows us to override it when previewing (VITE_DEV_URL is not available then)
|
|
47
|
+
?? process.env.PUBLIC_SERVER_URL
|
|
48
|
+
?? process.env.VITE_DEV_SERVER_URL)!
|
|
49
|
+
}
|
|
50
|
+
if (!base.publicServerUrl) {
|
|
51
|
+
throw new Error("publicServerUrl could not be determined.")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (process.env.VITE_DEV_SERVER_URL) {
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
windowUrl: `${process.env.VITE_DEV_SERVER_URL}${STATIC.ELECTRON_ROUTE}`
|
|
58
|
+
}
|
|
59
|
+
// this will always be defined in production since they are defined by vite
|
|
60
|
+
} else if (STATIC.ELECTRON_PROD_URL && STATIC.ELECTRON_BUILD_DIR) {
|
|
61
|
+
return {
|
|
62
|
+
...base,
|
|
63
|
+
// careful, do not use path.join, it will remove extra slashes
|
|
64
|
+
windowUrl: `file://${STATIC.ELECTRON_PROD_URL}`
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
unreachable()
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preload script can be passed arguments using the `webPreferences.additionalArguments` option.
|
|
3
|
+
*
|
|
4
|
+
* ````ts [main.ts]
|
|
5
|
+
* const meta = { ... }
|
|
6
|
+
*
|
|
7
|
+
* const win = new BrowserWindow({
|
|
8
|
+
* webPreferences: {
|
|
9
|
+
* additionalArguments: [
|
|
10
|
+
* // you can change which key is used
|
|
11
|
+
* `--metadata=${JSON.stringify(meta)}`,
|
|
12
|
+
* ],
|
|
13
|
+
* },
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ````ts [preload.ts]
|
|
18
|
+
* const meta = getPreloadMeta<Meta>("metadata")
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* This function can be used to get the metadata and type it in the preload script.
|
|
22
|
+
*
|
|
23
|
+
* Note that if it can't find the argument or there is an error parsing the json, it will throw.
|
|
24
|
+
*/
|
|
25
|
+
export function getPreloadMeta<T>(
|
|
26
|
+
key: string
|
|
27
|
+
): T {
|
|
28
|
+
const meta = process.argv
|
|
29
|
+
.find(arg => arg.startsWith(`--${key}=`))
|
|
30
|
+
?.split("=")?.[1]
|
|
31
|
+
|
|
32
|
+
if (!meta) {
|
|
33
|
+
throw new Error("No metadata passed to renderer.")
|
|
34
|
+
}
|
|
35
|
+
return JSON.parse(meta)
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from "./types"
|
|
2
|
+
// note adding file endings breaks build types
|
|
3
|
+
export { getPaths } from "./getPaths"
|
|
4
|
+
export { createNuxtFileProtocolHandler } from "./createNuxtFileProtocolHandler"
|
|
5
|
+
export { createWindowControlsApi } from "./createWindowControlsApi"
|
|
6
|
+
export { createWindowControlsApiHandler } from "./createWindowControlsApiHandler"
|
|
7
|
+
export { useNuxtRuntimeConfig } from "./useNuxtRuntimeConfig"
|
|
8
|
+
export { STATIC } from "./static"
|
|
9
|
+
export { apiBuilder } from "./apiBuilder"
|
|
10
|
+
export { useDevDataDir } from "./useDevDataDir"
|
|
11
|
+
export { registerDevtoolsShortcuts } from "./registerDevtoolsShortcuts"
|
|
12
|
+
export { promisifyApi } from "./promisifyApi"
|
|
13
|
+
export { getPreloadMeta } from "./getPreloadMeta"
|
|
14
|
+
export { getEventWindow } from "./getEventWindow"
|
|
15
|
+
export { promisifyReply } from "./promisifyReply"
|
|
16
|
+
export { createBroadcaster } from "./createBroadcaster"
|
|
17
|
+
export { createBroadcastHandlers } from "./createBroadcastHandlers"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { IpcRenderer } from "electron"
|
|
2
|
+
|
|
3
|
+
const promiseResolveMap = new Map<string, {
|
|
4
|
+
resolve: (value: any) => void
|
|
5
|
+
reject: (reason?: any) => void
|
|
6
|
+
}>()
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Promisify an electron api and make it type safe so it can be awaited client side. Note that promisifyReply will throw if it can't find a window.
|
|
10
|
+
*
|
|
11
|
+
* ```ts[type.st]
|
|
12
|
+
* export type ElectronApi = {
|
|
13
|
+
* api: {
|
|
14
|
+
* someApi: (apiParam: string, apiParam2: number) => Promise<void>
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
* ```ts [preload.ts]
|
|
19
|
+
* contextBridge.exposeInMainWorld("electron", {
|
|
20
|
+
* api: {
|
|
21
|
+
* ...promisifyApi<"someApi", ElectronApi["api"]["someApi"]>(ipcRenderer, "someApi", MESSAGE.UNIQUE_KEY),
|
|
22
|
+
* },
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ```ts [main.ts]
|
|
27
|
+
* import { promisifyReply } from "@witchcraft/nuxt-electron/runtime/electron"
|
|
28
|
+
* promisifyReply<
|
|
29
|
+
* ElectronApi["api"]["someApi"]
|
|
30
|
+
* >(ipcRenderer, MESSAGE.UNIQUE_KEY, (win, apiParam, apiParam2) => {
|
|
31
|
+
* // this doesn't have to be async, though on the client side,
|
|
32
|
+
* // it will always be async
|
|
33
|
+
* return electronSideOfApi(apiParam, apiParam2)
|
|
34
|
+
* })
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* ```ts [renderer.ts]
|
|
38
|
+
* const result = await electron.api.someApi(apiParam, apiParam2)
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* By default, calls will timeout and reject after 10 seconds. This can be changed by changing the timeout option.
|
|
42
|
+
*/
|
|
43
|
+
export function promisifyApi<
|
|
44
|
+
TKey extends string,
|
|
45
|
+
TFunction extends (...args: any) => Promise<any>,
|
|
46
|
+
TArgs extends Parameters<TFunction> = Parameters<TFunction>
|
|
47
|
+
>(
|
|
48
|
+
ipcRenderer: IpcRenderer,
|
|
49
|
+
key: TKey,
|
|
50
|
+
messageKey: string,
|
|
51
|
+
modifyArgs?: (args: TArgs) => any,
|
|
52
|
+
{ debug, timeout }: { debug?: boolean, timeout?: number } = { debug: true, timeout: 10000 }
|
|
53
|
+
): Record<TKey, TFunction> {
|
|
54
|
+
return {
|
|
55
|
+
[key]: async (...args: TArgs) => new Promise((resolve, reject) => {
|
|
56
|
+
// preload script should have access to crypto
|
|
57
|
+
// can't seem to get node crypto to import even renamed
|
|
58
|
+
const promiseId = crypto.randomUUID()
|
|
59
|
+
promiseResolveMap.set(promiseId, { resolve, reject })
|
|
60
|
+
if (modifyArgs) {
|
|
61
|
+
args = modifyArgs(args)
|
|
62
|
+
}
|
|
63
|
+
if (debug) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log("promisifyApi:sent", messageKey, promiseId, args)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const timer: number | NodeJS.Timeout = setTimeout(() => {
|
|
69
|
+
const resolver = promiseResolveMap.get(promiseId)
|
|
70
|
+
if (resolver) {
|
|
71
|
+
promiseResolveMap.delete(promiseId)
|
|
72
|
+
ipcRenderer.off(messageKey, listener)
|
|
73
|
+
resolver.reject(new Error(`promisifyApi: Timeout for ${messageKey}`))
|
|
74
|
+
}
|
|
75
|
+
}, timeout)
|
|
76
|
+
function listener(
|
|
77
|
+
_event: Electron.IpcRendererEvent,
|
|
78
|
+
resPromiseId: string,
|
|
79
|
+
res: any,
|
|
80
|
+
info?: { isError?: boolean }
|
|
81
|
+
): void {
|
|
82
|
+
if (debug) {
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log("promiseApi:received", resPromiseId, res)
|
|
85
|
+
}
|
|
86
|
+
const resolver = promiseResolveMap.get(resPromiseId)
|
|
87
|
+
if (resolver) {
|
|
88
|
+
clearTimeout(timer)
|
|
89
|
+
promiseResolveMap.delete(resPromiseId)
|
|
90
|
+
ipcRenderer.off(messageKey, listener)
|
|
91
|
+
if (info?.isError) {
|
|
92
|
+
resolver.reject(res)
|
|
93
|
+
} else {
|
|
94
|
+
resolver.resolve(res)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
ipcRenderer.on(messageKey, listener)
|
|
99
|
+
ipcRenderer.send(messageKey, promiseId, ...args)
|
|
100
|
+
})
|
|
101
|
+
} as any
|
|
102
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type BrowserWindow, ipcMain } from "electron"
|
|
2
|
+
|
|
3
|
+
import { getEventWindow } from "./getEventWindow.js"
|
|
4
|
+
|
|
5
|
+
/** See {@link promisifyApi} for more info. */
|
|
6
|
+
export function promisifyReply<
|
|
7
|
+
TFunction extends ((...args: any[]) => any),
|
|
8
|
+
TKey extends string = string,
|
|
9
|
+
TArgs extends Parameters<TFunction> = Parameters<TFunction>,
|
|
10
|
+
TReturn extends ReturnType<TFunction> = ReturnType<TFunction>
|
|
11
|
+
>(
|
|
12
|
+
key: TKey,
|
|
13
|
+
cb: (win?: BrowserWindow, ...args: TArgs) => Promise<TReturn> | TReturn,
|
|
14
|
+
/** See {@link getEventWindow}. */
|
|
15
|
+
{
|
|
16
|
+
defaultToFocused = true,
|
|
17
|
+
debug = false
|
|
18
|
+
}: {
|
|
19
|
+
defaultToFocused?: boolean
|
|
20
|
+
debug?: boolean
|
|
21
|
+
} = {}
|
|
22
|
+
): void {
|
|
23
|
+
ipcMain.on(key, (
|
|
24
|
+
e,
|
|
25
|
+
promiseId: string,
|
|
26
|
+
...args: any[]
|
|
27
|
+
) => {
|
|
28
|
+
const win = getEventWindow(e, { defaultToFocused })
|
|
29
|
+
if (!win) {
|
|
30
|
+
throw new Error("No window to send reply to.")
|
|
31
|
+
}
|
|
32
|
+
if (debug) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log({ key, promiseId, args })
|
|
35
|
+
}
|
|
36
|
+
const res = cb(win, ...args as TArgs)
|
|
37
|
+
if (res instanceof Promise) {
|
|
38
|
+
res.then((innerRes: any) => {
|
|
39
|
+
win.webContents.send(key, promiseId, innerRes)
|
|
40
|
+
}).catch(err => {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.error(err, { key, promiseId, args })
|
|
43
|
+
win.webContents.send(key, promiseId, err, { isError: true })
|
|
44
|
+
})
|
|
45
|
+
} else {
|
|
46
|
+
win.webContents.send(key, promiseId, res)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Registers F12 and Ctrl+Shift+I as shortcuts to open the devtools. */
|
|
2
|
+
export function registerDevtoolsShortcuts(win: Electron.BrowserWindow): void {
|
|
3
|
+
win.webContents.on("before-input-event", (event, input) => {
|
|
4
|
+
if (input.type === "keyDown") {
|
|
5
|
+
if ((input.shift && input.control && input.key.toLowerCase() === "i")
|
|
6
|
+
|| (input.key === "F12")) {
|
|
7
|
+
win.webContents.openDevTools()
|
|
8
|
+
event.preventDefault()
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PublicRuntimeConfig } from "nuxt/schema"
|
|
2
|
+
|
|
3
|
+
// Note that we must write them out like this (with the full path to the env variable) for vite to find them.
|
|
4
|
+
/**
|
|
5
|
+
* Group of the module's vite's injected variables.
|
|
6
|
+
*/
|
|
7
|
+
export const STATIC = {
|
|
8
|
+
ELECTRON_ROUTE: process.env.ELECTRON_ROUTE!,
|
|
9
|
+
ELECTRON_PROD_URL: process.env.ELECTRON_PROD_URL!,
|
|
10
|
+
ELECTRON_NUXT_DIR: process.env.ELECTRON_NUXT_DIR!,
|
|
11
|
+
ELECTRON_NUXT_PUBLIC_DIR: process.env.ELECTRON_NUXT_PUBLIC_DIR!,
|
|
12
|
+
ELECTRON_BUILD_DIR: process.env.ELECTRON_BUILD_DIR!,
|
|
13
|
+
ELECTRON_RUNTIME_CONFIG: (process.env.ELECTRON_RUNTIME_CONFIG ? JSON.parse(process.env.ELECTRON_RUNTIME_CONFIG) : undefined) as PublicRuntimeConfig
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type WindowControlsApi = (action: "close" | "minimize" | "toggleMaximize" | "togglePin") => Promise<void>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This requires electron's preload script set the `electron` property on the window:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // preload.ts
|
|
6
|
+
* contextBridge.exposeInMainWorld("electron", { })
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export function isElectron(): boolean {
|
|
10
|
+
return typeof window !== "undefined" && "electron" in window && typeof window.electron !== "undefined"
|
|
11
|
+
}
|