@witchcraft/nuxt-electron 0.0.12 → 0.2.0
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 +160 -73
- package/dist/module.json +1 -1
- package/dist/module.mjs +33 -13
- package/dist/runtime/electron/createPriviledgedProtocolScheme.d.ts +25 -0
- package/dist/runtime/electron/createPriviledgedProtocolScheme.js +16 -0
- package/dist/runtime/electron/createPrivilegedProtocolScheme.d.ts +25 -0
- package/dist/runtime/electron/createPrivilegedProtocolScheme.js +16 -0
- package/dist/runtime/electron/createProxiedProtocolHandler.d.ts +31 -0
- package/dist/runtime/electron/createProxiedProtocolHandler.js +158 -0
- package/dist/runtime/electron/getPaths.d.ts +7 -15
- package/dist/runtime/electron/getPaths.js +18 -12
- package/dist/runtime/electron/index.d.ts +2 -1
- package/dist/runtime/electron/index.js +2 -1
- package/genDevDesktop.js +50 -3
- package/package.json +8 -6
- package/src/module.ts +42 -16
- package/src/runtime/electron/createPriviledgedProtocolScheme.ts +40 -0
- package/src/runtime/electron/createPrivilegedProtocolScheme.ts +40 -0
- package/src/runtime/electron/createProxiedProtocolHandler.ts +229 -0
- package/src/runtime/electron/getPaths.ts +32 -27
- package/src/runtime/electron/index.ts +2 -1
- package/dist/runtime/electron/createNuxtFileProtocolHandler.d.ts +0 -14
- package/dist/runtime/electron/createNuxtFileProtocolHandler.js +0 -20
- package/src/runtime/electron/createNuxtFileProtocolHandler.ts +0 -42
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { keys } from "@alanscodelog/utils/keys"
|
|
2
|
+
import { pick } from "@alanscodelog/utils/pick"
|
|
3
|
+
import type { Protocol } from "electron"
|
|
4
|
+
import { net } from "electron"
|
|
5
|
+
import fs from "node:fs/promises"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import { pathToFileURL } from "node:url"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
const cache = new Map<string, string | undefined>()
|
|
11
|
+
|
|
12
|
+
async function getPathToServe(
|
|
13
|
+
originalPath: string,
|
|
14
|
+
pathWithIndexHtml: string,
|
|
15
|
+
/** Usually the request url */
|
|
16
|
+
key: string,
|
|
17
|
+
logger?: {
|
|
18
|
+
trace: (...args: any[]) => void
|
|
19
|
+
}
|
|
20
|
+
) {
|
|
21
|
+
if (cache.has(key)) {
|
|
22
|
+
const result = cache.get(key)
|
|
23
|
+
if (logger) {
|
|
24
|
+
logger.trace({
|
|
25
|
+
ns: "main:createProxiedProtocolHandler:cacheHit",
|
|
26
|
+
requestUrl: key,
|
|
27
|
+
result: cache.get(key)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
const stats = await fs.stat(originalPath).catch(() => undefined)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if (logger) {
|
|
36
|
+
logger.trace({
|
|
37
|
+
ns: "main:createProxiedProtocolHandler:stats",
|
|
38
|
+
key,
|
|
39
|
+
stats,
|
|
40
|
+
isFile: stats?.isFile(),
|
|
41
|
+
isDirectory: stats?.isDirectory()
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
// if file exists get the file, if path/index.html exists get that
|
|
45
|
+
const pathToServe = stats !== undefined
|
|
46
|
+
? stats.isFile()
|
|
47
|
+
? originalPath
|
|
48
|
+
: stats.isDirectory()
|
|
49
|
+
? (await fs.stat(pathWithIndexHtml).catch(() => undefined))?.isFile()
|
|
50
|
+
? pathWithIndexHtml
|
|
51
|
+
: undefined
|
|
52
|
+
: undefined
|
|
53
|
+
: undefined
|
|
54
|
+
cache.set(key, pathToServe)
|
|
55
|
+
return pathToServe
|
|
56
|
+
}
|
|
57
|
+
export function createProxiedProtocolHandler(
|
|
58
|
+
/** `protocol` or `session.protocol` depending on if you're using paritions with your windows or not, regular `protocol` only registers the handler for the default parition. */
|
|
59
|
+
protocol: Protocol,
|
|
60
|
+
protocolName: string = "app",
|
|
61
|
+
basePath: string,
|
|
62
|
+
/**
|
|
63
|
+
* If any route starts with one of these keys, it will get rerouted to the given value.
|
|
64
|
+
*
|
|
65
|
+
* At least `/api` should be added for a basic nuxt app to work correctly.
|
|
66
|
+
*
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* routeProxies: {
|
|
70
|
+
* "/api": "http://localhost:3000/api",
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
routeProxies: Record<string, string>,
|
|
75
|
+
{
|
|
76
|
+
logger,
|
|
77
|
+
errorPage = "404.html"
|
|
78
|
+
}: {
|
|
79
|
+
/**
|
|
80
|
+
* Optional logger. It's suggested you not pass this unless you're traceging in dev mode as a lot of requests can be made.
|
|
81
|
+
*/
|
|
82
|
+
logger?: {
|
|
83
|
+
trace: (...args: any[]) => void
|
|
84
|
+
error: (...args: any[]) => void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The path to the error page. Defaults to "/404.html".
|
|
89
|
+
*
|
|
90
|
+
* The module forces nuxt to generate it as it's not rendered in build mode. This is only used for 404 errors. The 400 error for bad requests still just returns Bad Request for now. This might become more flexible in the future.
|
|
91
|
+
*/
|
|
92
|
+
errorPage?: string
|
|
93
|
+
|
|
94
|
+
} = {}
|
|
95
|
+
): void {
|
|
96
|
+
const routeProxyKeys = keys(routeProxies)
|
|
97
|
+
if (protocol.isProtocolHandled(protocolName)) {
|
|
98
|
+
throw new Error(`Protocol ${protocolName} is already handled.`)
|
|
99
|
+
}
|
|
100
|
+
if (routeProxyKeys.length > 0) {
|
|
101
|
+
if (!process.env.PUBLIC_SERVER_URL && !process.env.VITE_DEV_URL && !process.env.PUBLIC_SERVER_URL) {
|
|
102
|
+
throw new Error("You defined proxy routes but didn't set PUBLIC_SERVER_URL or VITE_DEV_URL set. This is required for the /api routes to work.")
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let errorPage404: string | undefined
|
|
107
|
+
// note that while it would be nice to do protocol.isProtocolRegistered
|
|
108
|
+
// to check the user correctly registered it
|
|
109
|
+
// it's deprecated for some reason and also it doesn't work (so maybe because of that)!
|
|
110
|
+
protocol.handle(protocolName, async request => {
|
|
111
|
+
errorPage404 ??= await getPathToServe(path.join(basePath, errorPage), "404.html", "404.html", logger)
|
|
112
|
+
|
|
113
|
+
if (!errorPage404) {
|
|
114
|
+
throw new Error(`Error page ${path.join(basePath, errorPage)} does not exist. Did you override the routeRules for it?`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parsedUrl = new URL(request.url)
|
|
118
|
+
// we can ignore the host, as getPaths sets it to bundle (e.g. protocol://bundle/path/to/file)
|
|
119
|
+
const requestPath = decodeURIComponent(parsedUrl.pathname)
|
|
120
|
+
|
|
121
|
+
const proxyKey = routeProxyKeys.find(key => requestPath.startsWith(key))
|
|
122
|
+
|
|
123
|
+
if (proxyKey) {
|
|
124
|
+
const proxyUrl = routeProxies[proxyKey]
|
|
125
|
+
const finalPath = proxyUrl + requestPath
|
|
126
|
+
if (logger) {
|
|
127
|
+
logger.trace({
|
|
128
|
+
ns: "main:createProxiedProtocolHandler:fetchingViaProxy",
|
|
129
|
+
requestUrl: request.url,
|
|
130
|
+
proxyKey,
|
|
131
|
+
route: proxyUrl,
|
|
132
|
+
requestPath,
|
|
133
|
+
finalPath
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
return net.fetch(finalPath, {
|
|
137
|
+
...pick(request, ["headers", "destination", "referrer", "referrerPolicy", "mode", "credentials", "cache", "redirect", "integrity", "keepalive"])
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const originalPath = path.join(basePath, requestPath)
|
|
142
|
+
const pathWithIndexHtml = path.join(originalPath, "index.html")
|
|
143
|
+
|
|
144
|
+
// the safety checks are modified from this example
|
|
145
|
+
// https://www.electronjs.org/docs/latest/api/protocol#protocolhandlescheme-handler
|
|
146
|
+
const relativePath = path.relative(basePath, originalPath)
|
|
147
|
+
const isSafe = !relativePath.startsWith("..") && !path.isAbsolute(relativePath)
|
|
148
|
+
|
|
149
|
+
if (!isSafe) {
|
|
150
|
+
if (logger) {
|
|
151
|
+
logger.error({
|
|
152
|
+
ns: "main:createProxiedProtocolHandler:badRequest",
|
|
153
|
+
|
|
154
|
+
request: pick(request, [
|
|
155
|
+
"url", "headers", "destination", "referrer", "referrerPolicy", "mode", "credentials", "cache", "redirect", "integrity", "keepalive",
|
|
156
|
+
// these also seem to exist
|
|
157
|
+
...(["isReloadNavigation", "isHistoryNavigation"] as any)
|
|
158
|
+
]),
|
|
159
|
+
requestPath,
|
|
160
|
+
pathWithIndexHtml,
|
|
161
|
+
relativePath,
|
|
162
|
+
originalPath,
|
|
163
|
+
isSafe
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return new Response(JSON.stringify({
|
|
168
|
+
error: "Bad Request - Unsafe Path"
|
|
169
|
+
}), {
|
|
170
|
+
headers: { "content-type": "application/json" },
|
|
171
|
+
status: 400
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
const pathToServe = await getPathToServe(originalPath, pathWithIndexHtml, request.url, logger)
|
|
177
|
+
|
|
178
|
+
if (!pathToServe) {
|
|
179
|
+
if (logger) {
|
|
180
|
+
logger.error({
|
|
181
|
+
ns: "main:createProxiedProtocolHandler:noFileFound",
|
|
182
|
+
requestUrl: request.url,
|
|
183
|
+
originalPath,
|
|
184
|
+
requestPath,
|
|
185
|
+
pathWithIndexHtml,
|
|
186
|
+
pathToServe,
|
|
187
|
+
errorPath: pathToFileURL(errorPage404).toString()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
const res = await net.fetch(pathToFileURL(errorPage404).toString(), {
|
|
191
|
+
// see below
|
|
192
|
+
bypassCustomProtocolHandlers: true
|
|
193
|
+
})
|
|
194
|
+
.catch(err => err)
|
|
195
|
+
return res
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const finalPath = pathToFileURL(pathToServe).toString()
|
|
199
|
+
if (logger) {
|
|
200
|
+
logger.trace({
|
|
201
|
+
ns: "main:createProxiedProtocolHandler:fetchingFile",
|
|
202
|
+
requestUrl: request.url,
|
|
203
|
+
finalPath,
|
|
204
|
+
requestPath,
|
|
205
|
+
originalPath
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const response = await net.fetch(finalPath, {
|
|
210
|
+
// avoid infinite loop if protocolName protocol is "file"
|
|
211
|
+
// or file is otherwise handled
|
|
212
|
+
bypassCustomProtocolHandlers: true
|
|
213
|
+
}).catch(err => {
|
|
214
|
+
if (logger) {
|
|
215
|
+
logger.error({
|
|
216
|
+
ns: "main:createProxiedProtocolHandler:fetchError",
|
|
217
|
+
requestUrl: request.url,
|
|
218
|
+
finalPath,
|
|
219
|
+
requestPath,
|
|
220
|
+
originalPath,
|
|
221
|
+
err
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
return err
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return response
|
|
228
|
+
})
|
|
229
|
+
}
|
|
@@ -1,49 +1,47 @@
|
|
|
1
1
|
import { unreachable } from "@alanscodelog/utils/unreachable"
|
|
2
|
-
import { app } from "electron"
|
|
3
2
|
import path from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
4
|
|
|
5
5
|
import { STATIC } from "./static.js"
|
|
6
6
|
|
|
7
|
-
export function forceRelativePath(filepath: string): string {
|
|
8
|
-
return path.join(`.${path.sep}`, filepath)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
7
|
/**
|
|
12
8
|
* Calculates the correct paths for the app to run in electron.
|
|
13
9
|
*
|
|
14
|
-
* Note: For serverUrl to not be overridable in production, you should set `electron.additionalElectronVariables.publicServerUrl` to a quoted string.
|
|
15
10
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* additionalElectronVariables: {
|
|
23
|
-
* publicServerUrl: process.env.NODE_ENV === "production"
|
|
24
|
-
* ? `"mysite.com"`
|
|
25
|
-
* : `undefined`
|
|
26
|
-
* }
|
|
27
|
-
* }
|
|
11
|
+
* Paths are not overridable in production unless you pass the variables that can override them.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const paths = getPaths("app", {
|
|
15
|
+
* windowUrl: process.env.OVERRIDE_WINDOW_URL,
|
|
16
|
+
* publicServerUrl: process.env.OVERRIDE_PUBLIC_SERVER_URL
|
|
28
17
|
* })
|
|
29
18
|
* ```
|
|
30
19
|
*/
|
|
31
|
-
export function getPaths(
|
|
20
|
+
export function getPaths(
|
|
21
|
+
protocolName: string = "app",
|
|
22
|
+
overridingEnvs: Record<string, string | undefined> = {
|
|
23
|
+
windowUrl: undefined,
|
|
24
|
+
publicServerUrl: undefined
|
|
25
|
+
}
|
|
26
|
+
): {
|
|
32
27
|
windowUrl: string
|
|
33
28
|
publicServerUrl: string
|
|
34
29
|
nuxtPublicDir: string
|
|
35
30
|
preloadPath: string
|
|
36
31
|
} {
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// this will be the same in dev and prod and makes things simpler
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
34
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
35
|
+
|
|
36
|
+
const nuxtPublicDir = path.join(__dirname, STATIC.ELECTRON_NUXT_PUBLIC_DIR!)
|
|
39
37
|
|
|
40
|
-
const preloadPath = path.
|
|
38
|
+
const preloadPath = path.join(__dirname, STATIC.ELECTRON_BUILD_DIR!, "./preload.cjs")
|
|
41
39
|
|
|
42
40
|
const base = {
|
|
43
41
|
nuxtPublicDir,
|
|
44
42
|
preloadPath,
|
|
45
|
-
publicServerUrl: (
|
|
46
|
-
|
|
43
|
+
publicServerUrl: (
|
|
44
|
+
overridingEnvs.publicServerUrl
|
|
47
45
|
?? process.env.PUBLIC_SERVER_URL
|
|
48
46
|
?? process.env.VITE_DEV_SERVER_URL)!
|
|
49
47
|
}
|
|
@@ -51,17 +49,24 @@ export function getPaths(): {
|
|
|
51
49
|
throw new Error("publicServerUrl could not be determined.")
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
if (
|
|
52
|
+
if (overridingEnvs.windowUrl) {
|
|
53
|
+
return {
|
|
54
|
+
...base,
|
|
55
|
+
windowUrl: `${overridingEnvs.windowUrl}${STATIC.ELECTRON_ROUTE}`
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (process.env.NODE_ENV === "production" && process.env.VITE_DEV_SERVER_URL) {
|
|
55
60
|
return {
|
|
56
61
|
...base,
|
|
57
62
|
windowUrl: `${process.env.VITE_DEV_SERVER_URL}${STATIC.ELECTRON_ROUTE}`
|
|
58
63
|
}
|
|
59
64
|
// this will always be defined in production since they are defined by vite
|
|
60
|
-
} else if (STATIC.ELECTRON_PROD_URL && STATIC.ELECTRON_BUILD_DIR) {
|
|
65
|
+
} else if (STATIC.ELECTRON_PROD_URL !== undefined && STATIC.ELECTRON_BUILD_DIR !== undefined) {
|
|
61
66
|
return {
|
|
62
67
|
...base,
|
|
63
68
|
// careful, do not use path.join, it will remove extra slashes
|
|
64
|
-
windowUrl:
|
|
69
|
+
windowUrl: `${protocolName}://bundle/${STATIC.ELECTRON_PROD_URL}`
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
unreachable()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from "./types"
|
|
2
2
|
// note adding file endings breaks build types
|
|
3
3
|
export { getPaths } from "./getPaths"
|
|
4
|
-
export {
|
|
4
|
+
export { createProxiedProtocolHandler } from "./createProxiedProtocolHandler"
|
|
5
|
+
export { createPrivilegedProtocolScheme } from "./createPrivilegedProtocolScheme"
|
|
5
6
|
export { createWindowControlsApi } from "./createWindowControlsApi"
|
|
6
7
|
export { createWindowControlsApiHandler } from "./createWindowControlsApiHandler"
|
|
7
8
|
export { useNuxtRuntimeConfig } from "./useNuxtRuntimeConfig"
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export declare function createNuxtFileProtocolHandler(session: Electron.Session, basePath: string,
|
|
2
|
-
/**
|
|
3
|
-
* If any route starts with one of these keys, it will get rerouted to the given value.
|
|
4
|
-
*
|
|
5
|
-
* At least `/api` should be added for a basic nuxt app to work correctly.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* ```ts
|
|
9
|
-
* routeProxies: {
|
|
10
|
-
* "/api": "http://localhost:3000/api",
|
|
11
|
-
* }
|
|
12
|
-
* ```
|
|
13
|
-
*/
|
|
14
|
-
routeProxies: Record<string, string>): void;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { keys } from "@alanscodelog/utils/keys";
|
|
2
|
-
import { net } from "electron";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
export function createNuxtFileProtocolHandler(session, basePath, routeProxies) {
|
|
5
|
-
const routeProxyKeys = keys(routeProxies);
|
|
6
|
-
session.protocol.handle("file", async (request) => {
|
|
7
|
-
const url = decodeURIComponent(request.url.slice(7));
|
|
8
|
-
const newUrl = path.isAbsolute(url) ? path.resolve(basePath, url.slice(1)) : path.resolve(basePath, url);
|
|
9
|
-
const proxyKey = routeProxyKeys.find((key) => url.startsWith(key));
|
|
10
|
-
if (proxyKey) {
|
|
11
|
-
const proxyUrl = routeProxies[proxyKey];
|
|
12
|
-
return net.fetch(proxyUrl + url);
|
|
13
|
-
}
|
|
14
|
-
const res = await net.fetch(`file://${newUrl}`, {
|
|
15
|
-
// avoid infinite loop
|
|
16
|
-
bypassCustomProtocolHandlers: true
|
|
17
|
-
}).catch((err) => new Response(err, { status: 404 }));
|
|
18
|
-
return res;
|
|
19
|
-
});
|
|
20
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
}
|