Skip to content

Commit

Permalink
Add ESM support
Browse files Browse the repository at this point in the history
  • Loading branch information
G committed Aug 28, 2024
1 parent 3ff0cfd commit 39ab352
Show file tree
Hide file tree
Showing 22 changed files with 401 additions and 116 deletions.
54 changes: 28 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,26 @@ Add a file at `desktop/index.js` to run the electron app. The `initRemix` functi

```ts
// desktop/index.js
const { initRemix } = require("remix-electron")
const { app, BrowserWindow } = require("electron")
const { join } = require("node:path")
const { initRemix } = require("remix-electron");
const { app, BrowserWindow } = require("electron");
const { join } = require("node:path");

/** @type {BrowserWindow | undefined} */
let win
let win;

app.on("ready", async () => {
try {
const url = await initRemix({
serverBuild: join(__dirname, "../build/index.js"),
})
serverBuild: join(process.cwd(), "build/index.js"),
});

win = new BrowserWindow({ show: false })
await win.loadURL(url)
win.show()
win = new BrowserWindow({ show: false });
await win.loadURL(url);
win.show();
} catch (error) {
console.error(error)
console.error(error);
}
})
});
```

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

```ts
// app/electron.server.ts
import electron from "electron"
export default electron
import electron from "electron";
export default electron;
```

```ts
// app/routes/_index.tsx
import electron from "~/electron.server"
import electron from "~/electron.server";

export function loader() {
return {
userDataPath: electron.app.getPath("userData"),
}
};
}
```

Expand All @@ -82,7 +82,7 @@ function createWindow() {
webPreferences: {
nodeIntegration: true,
},
})
});
}
```

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

Options:

- `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.
- `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.

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

- `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()`.
- `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()`.

- `getLoadContext`: Use this to inject some value into all of your remix loaders, e.g. an API client. The loaders receive it as `context`.

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

<details>
<summary>Load context TS example</summary>

**app/context.ts**

```ts
import type * as remix from "@remix-run/node"
import type * as remix from "@remix-run/node";

// your context type
export type LoadContext = {
secret: string
}
secret: string;
};

// a custom data function args type to use for loaders/actions
export type DataFunctionArgs = Omit<remix.DataFunctionArgs, "context"> & {
context: LoadContext
}
context: LoadContext;
};
```

**desktop/main.js**
Expand All @@ -131,13 +133,13 @@ const url = await initRemix({
getLoadContext: () => ({
secret: "123",
}),
})
});
```

In a route file:

```ts
import type { DataFunctionArgs, LoadContext } from "~/context"
import type { DataFunctionArgs, LoadContext } from "~/context";

export async function loader({ context }: DataFunctionArgs) {
// do something with context
Expand Down
3 changes: 3 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"workspaces/tests": {},
"workspaces/test-app": {
"ignoreDependencies": ["isbot", "nodemon"]
},
"workspaces/test-app-esm": {
"ignoreDependencies": ["isbot", "nodemon"]
}
}
}
37 changes: 37 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 27 additions & 11 deletions workspaces/remix-electron/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as webFetch from "@remix-run/web-fetch"
// if we override everything else, we get errors caused by the mismatch of built-in types and remix types
global.File = webFetch.File

import { watch } from "node:fs/promises"
import { constants, access, watch } from "node:fs/promises"
import type { AppLoadContext, ServerBuild } from "@remix-run/node"
import { broadcastDevReady, createRequestHandler } from "@remix-run/node"
import { app, protocol } from "electron"
Expand All @@ -25,6 +25,7 @@ interface InitRemixOptions {
mode?: string
publicFolder?: string
getLoadContext?: GetLoadContextFunction
esm?: boolean
}

/**
Expand All @@ -38,18 +39,29 @@ export async function initRemix({
mode,
publicFolder: publicFolderOption = "public",
getLoadContext,
esm = typeof require === "undefined",
}: InitRemixOptions): Promise<string> {
const appRoot = app.getAppPath()
const publicFolder = asAbsolutePath(publicFolderOption, appRoot)
const publicFolder = asAbsolutePath(publicFolderOption, process.cwd())

if (
!(await access(publicFolder, constants.R_OK).then(
() => true,
() => false,
))
) {
throw new Error(
`Public folder ${publicFolder} does not exist. Make sure that the initRemix \`publicFolder\` option is configured correctly.`,
)
}

const buildPath =
typeof serverBuildOption === "string"
? require.resolve(serverBuildOption)
: undefined
typeof serverBuildOption === "string" ? serverBuildOption : undefined

let serverBuild =
typeof serverBuildOption === "string"
? /** @type {ServerBuild} */ require(serverBuildOption)
typeof buildPath === "string"
? /** @type {ServerBuild} */ await import(
esm ? `${buildPath}?${Date.now()}` : buildPath
)
: serverBuildOption

await app.whenReady()
Expand Down Expand Up @@ -95,9 +107,13 @@ export async function initRemix({
) {
void (async () => {
for await (const _event of watch(buildPath)) {
purgeRequireCache(buildPath)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
serverBuild = require(buildPath)
if (esm) {
serverBuild = await import(`${buildPath}?${Date.now()}`)
} else {
purgeRequireCache(buildPath)
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
serverBuild = require(buildPath)
}
await broadcastDevReady(serverBuild)
}
})()
Expand Down
3 changes: 3 additions & 0 deletions workspaces/test-app-esm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/build
/public/build
.cache
2 changes: 2 additions & 0 deletions workspaces/test-app-esm/app/electron.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import electron from "electron"
export default electron
32 changes: 32 additions & 0 deletions workspaces/test-app-esm/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { MetaFunction } from "@remix-run/node"
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react"

export const meta: MetaFunction = () => {
return [{ title: "New Remix App" }]
}

export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === "development" && <LiveReload />}
</body>
</html>
)
}
27 changes: 27 additions & 0 deletions workspaces/test-app-esm/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useLoaderData } from "@remix-run/react"
import { useState } from "react"
import electron from "~/electron.server"

export function loader() {
return {
userDataPath: electron.app.getPath("userData"),
}
}

export default function Index() {
const data = useLoaderData<typeof loader>()
const [count, setCount] = useState(0)
return (
<div style={{ fontFamily: "system-ui, sans-serif", lineHeight: "1.4" }}>
<h1>Welcome to Remix</h1>
<p data-testid="user-data-path">{data.userDataPath}</p>
<button
type="button"
data-testid="counter"
onClick={() => setCount(count + 1)}
>
{count}
</button>
</div>
)
}
36 changes: 36 additions & 0 deletions workspaces/test-app-esm/app/routes/multipart-uploads.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
type ActionFunctionArgs,
NodeOnDiskFile,
json,
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
} from "@remix-run/node"
import { Form, useActionData } from "@remix-run/react"

export async function action({ request }: ActionFunctionArgs) {
const formData = await unstable_parseMultipartFormData(
request,
unstable_createFileUploadHandler(),
)

const file = formData.get("file")
if (!(file instanceof NodeOnDiskFile)) {
throw new Error("No file uploaded")
}

const text = await file.text()
return json({ text })
}

export default function MultipartUploadsTest() {
const data = useActionData<typeof action>()
return (
<>
<Form method="post" encType="multipart/form-data">
<input type="file" name="file" />
<button type="submit">Submit</button>
</Form>
<p data-testid="result">{data?.text}</p>
</>
)
}
13 changes: 13 additions & 0 deletions workspaces/test-app-esm/app/routes/referrer-redirect.action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ActionFunction } from "@remix-run/node"
import { redirect } from "@remix-run/node"

export const action: ActionFunction = async ({ request }) => {
const { redirects } = Object.fromEntries(await request.formData())
const referrer = request.headers.get("referer")
if (!referrer) {
throw new Error("No referrer header")
}
const url = new URL(referrer)
url.searchParams.set("redirects", String(Number(redirects) + 1))
return redirect(url.toString())
}
21 changes: 21 additions & 0 deletions workspaces/test-app-esm/app/routes/referrer-redirect.form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useFetcher, useSearchParams } from "@remix-run/react"

export default function RedirectForm() {
const fetcher = useFetcher()
const [params] = useSearchParams()
const redirects = params.get("redirects")
return (
<>
<p data-testid="redirects">{redirects ?? 0}</p>
<fetcher.Form
action="/referrer-redirect/action"
method="post"
data-testid="referrer-form"
>
<button type="submit" name="redirects" value={redirects ?? 0}>
submit
</button>
</fetcher.Form>
</>
)
}
Loading

0 comments on commit 39ab352

Please sign in to comment.