@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.
@@ -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
- * ```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
- * }
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
- const rootDir = app.getAppPath()
38
- const nuxtPublicDir = path.resolve(rootDir, forceRelativePath(STATIC.ELECTRON_NUXT_PUBLIC_DIR!))
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.resolve(rootDir, forceRelativePath(STATIC.ELECTRON_BUILD_DIR!), "./preload.cjs")
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: (process.env.PUBLIC_SERVER_URL
46
- // allows us to override it when previewing (VITE_DEV_URL is not available then)
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 (process.env.VITE_DEV_SERVER_URL) {
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: `file://${STATIC.ELECTRON_PROD_URL}`
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 { createNuxtFileProtocolHandler } from "./createNuxtFileProtocolHandler"
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
- }