Skip to content
This repository was archived by the owner on Jan 17, 2025. It is now read-only.

Commit e45343d

Browse files
author
G
committed
Add ESM support
1 parent 3ff0cfd commit e45343d

21 files changed

+402
-116
lines changed

README.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,26 @@ Add a file at `desktop/index.js` to run the electron app. The `initRemix` functi
2424

2525
```ts
2626
// desktop/index.js
27-
const { initRemix } = require("remix-electron")
28-
const { app, BrowserWindow } = require("electron")
29-
const { join } = require("node:path")
27+
const { initRemix } = require("remix-electron");
28+
const { app, BrowserWindow } = require("electron");
29+
const { join } = require("node:path");
3030

3131
/** @type {BrowserWindow | undefined} */
32-
let win
32+
let win;
3333

3434
app.on("ready", async () => {
3535
try {
3636
const url = await initRemix({
37-
serverBuild: join(__dirname, "../build/index.js"),
38-
})
37+
serverBuild: join(process.cwd(), "build/index.js"),
38+
});
3939

40-
win = new BrowserWindow({ show: false })
41-
await win.loadURL(url)
42-
win.show()
40+
win = new BrowserWindow({ show: false });
41+
await win.loadURL(url);
42+
win.show();
4343
} catch (error) {
44-
console.error(error)
44+
console.error(error);
4545
}
46-
})
46+
});
4747
```
4848

4949
Build the app with `npm run build`, then run `npx electron desktop/index.js` to start the app! 🚀
@@ -56,18 +56,18 @@ To circumvent this, create a `electron.server.ts` file, which re-exports from el
5656

5757
```ts
5858
// app/electron.server.ts
59-
import electron from "electron"
60-
export default electron
59+
import electron from "electron";
60+
export default electron;
6161
```
6262

6363
```ts
6464
// app/routes/_index.tsx
65-
import electron from "~/electron.server"
65+
import electron from "~/electron.server";
6666

6767
export function loader() {
6868
return {
6969
userDataPath: electron.app.getPath("userData"),
70-
}
70+
};
7171
}
7272
```
7373

@@ -82,7 +82,7 @@ function createWindow() {
8282
webPreferences: {
8383
nodeIntegration: true,
8484
},
85-
})
85+
});
8686
}
8787
```
8888

@@ -94,31 +94,33 @@ Initializes remix-electron. Returns a promise with a url to load in the browser
9494

9595
Options:
9696

97-
- `serverBuild`: The path to your server build (e.g. `path.join(__dirname, 'build')`), or the server build itself (e.g. required from `@remix-run/dev/server-build`). Updates on refresh are only supported when passing a path string.
97+
- `serverBuild`: The path to your server build (e.g. `path.join(process.cwd(), 'build')`), or the server build itself (e.g. required from `@remix-run/dev/server-build`). Updates on refresh are only supported when passing a path string.
9898

9999
- `mode`: The mode the app is running in. Can be `"development"` or `"production"`. Defaults to `"production"` when packaged, otherwise uses `process.env.NODE_ENV`.
100100

101-
- `publicFolder`: The folder where static assets are served from, including your browser build. Defaults to `"public"`. Non-relative paths are resolved relative to `app.getAppPath()`.
101+
- `publicFolder`: The folder where static assets are served from, including your browser build. Defaults to `"public"`. Non-absolute paths are resolved relative to `process.cwd()`.
102+
103+
- `getLoadContext`: Use this to inject some value into all of your remix loaders, e.g. an API client. The loaders receive it as `context`.
102104

103-
- `getLoadContext`: Use this to inject some value into all of your remix loaders, e.g. an API client. The loaders receive it as `context`
105+
- `esm`: Set this to `true` to use remix-electron in an ESM application.
104106

105107
<details>
106108
<summary>Load context TS example</summary>
107109

108110
**app/context.ts**
109111

110112
```ts
111-
import type * as remix from "@remix-run/node"
113+
import type * as remix from "@remix-run/node";
112114

113115
// your context type
114116
export type LoadContext = {
115-
secret: string
116-
}
117+
secret: string;
118+
};
117119

118120
// a custom data function args type to use for loaders/actions
119121
export type DataFunctionArgs = Omit<remix.DataFunctionArgs, "context"> & {
120-
context: LoadContext
121-
}
122+
context: LoadContext;
123+
};
122124
```
123125

124126
**desktop/main.js**
@@ -131,13 +133,13 @@ const url = await initRemix({
131133
getLoadContext: () => ({
132134
secret: "123",
133135
}),
134-
})
136+
});
135137
```
136138

137139
In a route file:
138140

139141
```ts
140-
import type { DataFunctionArgs, LoadContext } from "~/context"
142+
import type { DataFunctionArgs, LoadContext } from "~/context";
141143

142144
export async function loader({ context }: DataFunctionArgs) {
143145
// do something with context

pnpm-lock.yaml

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

workspaces/remix-electron/src/index.mts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as webFetch from "@remix-run/web-fetch"
44
// if we override everything else, we get errors caused by the mismatch of built-in types and remix types
55
global.File = webFetch.File
66

7-
import { watch } from "node:fs/promises"
7+
import { constants, access, watch } from "node:fs/promises"
88
import type { AppLoadContext, ServerBuild } from "@remix-run/node"
99
import { broadcastDevReady, createRequestHandler } from "@remix-run/node"
1010
import { app, protocol } from "electron"
@@ -25,6 +25,7 @@ interface InitRemixOptions {
2525
mode?: string
2626
publicFolder?: string
2727
getLoadContext?: GetLoadContextFunction
28+
esm?: boolean
2829
}
2930

3031
/**
@@ -38,18 +39,29 @@ export async function initRemix({
3839
mode,
3940
publicFolder: publicFolderOption = "public",
4041
getLoadContext,
42+
esm = typeof require === "undefined",
4143
}: InitRemixOptions): Promise<string> {
42-
const appRoot = app.getAppPath()
43-
const publicFolder = asAbsolutePath(publicFolderOption, appRoot)
44+
const publicFolder = asAbsolutePath(publicFolderOption, process.cwd())
45+
46+
if (
47+
!(await access(publicFolder, constants.R_OK).then(
48+
() => true,
49+
() => false,
50+
))
51+
) {
52+
throw new Error(
53+
`Public folder ${publicFolder} does not exist. Make sure that the initRemix \`publicFolder\` option is configured correctly.`,
54+
)
55+
}
4456

4557
const buildPath =
46-
typeof serverBuildOption === "string"
47-
? require.resolve(serverBuildOption)
48-
: undefined
58+
typeof serverBuildOption === "string" ? serverBuildOption : undefined
4959

5060
let serverBuild =
51-
typeof serverBuildOption === "string"
52-
? /** @type {ServerBuild} */ require(serverBuildOption)
61+
typeof buildPath === "string"
62+
? /** @type {ServerBuild} */ await import(
63+
esm ? `${buildPath}?${Date.now()}` : buildPath
64+
)
5365
: serverBuildOption
5466

5567
await app.whenReady()
@@ -95,9 +107,13 @@ export async function initRemix({
95107
) {
96108
void (async () => {
97109
for await (const _event of watch(buildPath)) {
98-
purgeRequireCache(buildPath)
99-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
100-
serverBuild = require(buildPath)
110+
if (esm) {
111+
serverBuild = await import(`${buildPath}?${Date.now()}`)
112+
} else {
113+
purgeRequireCache(buildPath)
114+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
115+
serverBuild = require(buildPath)
116+
}
101117
await broadcastDevReady(serverBuild)
102118
}
103119
})()

workspaces/test-app-esm/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/build
2+
/public/build
3+
.cache
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import electron from "electron"
2+
export default electron

workspaces/test-app-esm/app/root.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { MetaFunction } from "@remix-run/node"
2+
import {
3+
Links,
4+
LiveReload,
5+
Meta,
6+
Outlet,
7+
Scripts,
8+
ScrollRestoration,
9+
} from "@remix-run/react"
10+
11+
export const meta: MetaFunction = () => {
12+
return [{ title: "New Remix App" }]
13+
}
14+
15+
export default function App() {
16+
return (
17+
<html lang="en">
18+
<head>
19+
<meta charSet="utf8" />
20+
<meta name="viewport" content="width=device-width,initial-scale=1" />
21+
<Meta />
22+
<Links />
23+
</head>
24+
<body>
25+
<Outlet />
26+
<ScrollRestoration />
27+
<Scripts />
28+
{process.env.NODE_ENV === "development" && <LiveReload />}
29+
</body>
30+
</html>
31+
)
32+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useLoaderData } from "@remix-run/react"
2+
import { useState } from "react"
3+
import electron from "~/electron.server"
4+
5+
export function loader() {
6+
return {
7+
userDataPath: electron.app.getPath("userData"),
8+
}
9+
}
10+
11+
export default function Index() {
12+
const data = useLoaderData<typeof loader>()
13+
const [count, setCount] = useState(0)
14+
return (
15+
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
16+
<h1>Welcome to Remix</h1>
17+
<p data-testid="user-data-path">{data.userDataPath}</p>
18+
<button
19+
type="button"
20+
data-testid="counter"
21+
onClick={() => setCount(count + 1)}
22+
>
23+
{count}
24+
</button>
25+
</div>
26+
)
27+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {
2+
type ActionFunctionArgs,
3+
NodeOnDiskFile,
4+
json,
5+
unstable_createFileUploadHandler,
6+
unstable_parseMultipartFormData,
7+
} from "@remix-run/node"
8+
import { Form, useActionData } from "@remix-run/react"
9+
10+
export async function action({ request }: ActionFunctionArgs) {
11+
const formData = await unstable_parseMultipartFormData(
12+
request,
13+
unstable_createFileUploadHandler(),
14+
)
15+
16+
const file = formData.get("file")
17+
if (!(file instanceof NodeOnDiskFile)) {
18+
throw new Error("No file uploaded")
19+
}
20+
21+
const text = await file.text()
22+
return json({ text })
23+
}
24+
25+
export default function MultipartUploadsTest() {
26+
const data = useActionData<typeof action>()
27+
return (
28+
<>
29+
<Form method="post" encType="multipart/form-data">
30+
<input type="file" name="file" />
31+
<button type="submit">Submit</button>
32+
</Form>
33+
<p data-testid="result">{data?.text}</p>
34+
</>
35+
)
36+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { ActionFunction } from "@remix-run/node"
2+
import { redirect } from "@remix-run/node"
3+
4+
export const action: ActionFunction = async ({ request }) => {
5+
const { redirects } = Object.fromEntries(await request.formData())
6+
const referrer = request.headers.get("referer")
7+
if (!referrer) {
8+
throw new Error("No referrer header")
9+
}
10+
const url = new URL(referrer)
11+
url.searchParams.set("redirects", String(Number(redirects) + 1))
12+
return redirect(url.toString())
13+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useFetcher, useSearchParams } from "@remix-run/react"
2+
3+
export default function RedirectForm() {
4+
const fetcher = useFetcher()
5+
const [params] = useSearchParams()
6+
const redirects = params.get("redirects")
7+
return (
8+
<>
9+
<p data-testid="redirects">{redirects ?? 0}</p>
10+
<fetcher.Form
11+
action="/referrer-redirect/action"
12+
method="post"
13+
data-testid="referrer-form"
14+
>
15+
<button type="submit" name="redirects" value={redirects ?? 0}>
16+
submit
17+
</button>
18+
</fetcher.Form>
19+
</>
20+
)
21+
}

0 commit comments

Comments
 (0)