Skip to content

Commit

Permalink
Merge pull request #33 from giuseppeg/esm
Browse files Browse the repository at this point in the history
Add ESM support
  • Loading branch information
itsMapleLeaf authored Sep 4, 2024
2 parents 6c4a6e6 + d524f0c commit 1ba118a
Show file tree
Hide file tree
Showing 22 changed files with 393 additions and 107 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ 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 })
Expand Down Expand Up @@ -94,13 +94,15 @@ 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>
Expand Down
3 changes: 3 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"workspaces/tests": {},
"workspaces/test-app": {
"ignoreDependencies": ["isbot", "nodemon"]
},
"workspaces/test-app-esm": {
"ignoreDependencies": ["isbot", "nodemon"]
}
}
}
63 changes: 50 additions & 13 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())
}
Loading

0 comments on commit 1ba118a

Please sign in to comment.