From b534db7907a57549ff514ec1ae843167ca047b98 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 7 Dec 2023 11:57:52 -0500 Subject: [PATCH 1/4] feat(vite): error messages when .server files are referenced by client --- integration/package.json | 2 +- integration/vite-dot-server-test.ts | 363 ++++++++++++++++------------ packages/remix-dev/vite/plugin.ts | 88 ++++++- yarn.lock | 8 +- 4 files changed, 286 insertions(+), 175 deletions(-) diff --git a/integration/package.json b/integration/package.json index 99f6ff48453..cd226fc5470 100644 --- a/integration/package.json +++ b/integration/package.json @@ -35,6 +35,6 @@ "tailwindcss": "^3.3.0", "type-fest": "^4.0.0", "typescript": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1" + "vite-tsconfig-paths": "^4.2.2" } } diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts index ae53192e3f8..5480afaa272 100644 --- a/integration/vite-dot-server-test.ts +++ b/integration/vite-dot-server-test.ts @@ -1,144 +1,62 @@ import * as path from "node:path"; import { test, expect } from "@playwright/test"; +import stripAnsi from "strip-ansi"; import { createProject, grep, viteBuild } from "./helpers/vite.js"; -let files = { - "app/utils.server.ts": String.raw` - export const dotServerFile = "SERVER_ONLY_FILE"; - export default dotServerFile; - `, - "app/.server/utils.ts": String.raw` - export const dotServerDir = "SERVER_ONLY_DIR"; - export default dotServerDir; - `, -}; - -test("Vite / .server file / named import in client fails with expected error", async () => { - let cwd = await createProject({ - ...files, - "app/routes/fail-server-file-in-client.tsx": String.raw` - import { dotServerFile } from "~/utils.server"; +let serverOnlyModule = String.raw` + export const serverOnly = "SERVER_ONLY"; + export default serverOnly; +`; + +let tsconfig = (aliases: Record) => String.raw` + { + "include": ["env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": ${JSON.stringify(aliases)}, + "noEmit": true + } + } +`; - export default function() { - console.log(dotServerFile); - return

Fail: Server file included in client

- } - `, - }); - let result = viteBuild({ cwd }); - let stderr = result.stderr.toString("utf8"); - expect(stderr).toMatch( - `"dotServerFile" is not exported by "app/utils.server.ts"` - ); -}); - -test("Vite / .server file / namespace import in client fails with expected error", async () => { - let cwd = await createProject({ - ...files, - "app/routes/fail-server-file-in-client.tsx": String.raw` - import * as utils from "~/utils.server"; - - export default function() { - console.log(utils.dotServerFile); - return

Fail: Server file included in client

- } - `, - }); - let result = viteBuild({ cwd }); - let stderr = result.stderr.toString("utf8"); - expect(stderr).toMatch( - `"dotServerFile" is not exported by "app/utils.server.ts"` - ); -}); - -test("Vite / .server file / default import in client fails with expected error", async () => { - let cwd = await createProject({ - ...files, - "app/routes/fail-server-file-in-client.tsx": String.raw` - import dotServerFile from "~/utils.server"; - - export default function() { - console.log(dotServerFile); - return

Fail: Server file included in client

- } - `, - }); - let result = viteBuild({ cwd }); - let stderr = result.stderr.toString("utf8"); - expect(stderr).toMatch(`"default" is not exported by "app/utils.server.ts"`); -}); - -test("Vite / .server dir / named import in client fails with expected error", async () => { - let cwd = await createProject({ - ...files, - "app/routes/fail-server-dir-in-client.tsx": String.raw` - import { dotServerDir } from "~/.server/utils"; - - export default function() { - console.log(dotServerDir); - return

Fail: Server directory included in client

- } - `, - }); - let result = viteBuild({ cwd }); - let stderr = result.stderr.toString("utf8"); - expect(stderr).toMatch( - `"dotServerDir" is not exported by "app/.server/utils.ts"` - ); -}); - -test("Vite / .server dir / namespace import in client fails with expected error", async () => { +test("Vite / dead-code elimination for server exports", async () => { let cwd = await createProject({ - ...files, - "app/routes/fail-server-dir-in-client.tsx": String.raw` - import * as utils from "~/.server/utils"; - - export default function() { - console.log(utils.dotServerDir); - return

Fail: Server directory included in client

- } - `, - }); - let result = viteBuild({ cwd }); - let stderr = result.stderr.toString("utf8"); - expect(stderr).toMatch( - `"dotServerDir" is not exported by "app/.server/utils.ts"` - ); -}); + "app/utils.server.ts": serverOnlyModule, + "app/.server/utils.ts": serverOnlyModule, + "app/routes/remove-server-exports-and-dce.tsx": String.raw` + import fs from "node:fs"; + import { json } from "@remix-run/node"; + import { useLoaderData } from "@remix-run/react"; -test("Vite / .server dir / default import in client fails with expected error", async () => { - let cwd = await createProject({ - ...files, - "app/routes/fail-server-dir-in-client.tsx": String.raw` - import dotServerDir from "~/.server/utils"; + import { serverOnly as serverOnlyFile } from "../utils.server"; + import { serverOnly as serverOnlyDir } from "../.server/utils"; - export default function() { - console.log(dotServerDir); - return

Fail: Server directory included in client

+ export const loader = () => { + let contents = fs.readFileSync("server_only.txt"); + return json({ serverOnlyFile, serverOnlyDir, contents }) } - `, - }); - let result = viteBuild({ cwd }); - let stderr = result.stderr.toString("utf8"); - expect(stderr).toMatch(`"default" is not exported by "app/.server/utils.ts"`); -}); -test("Vite / `handle` with dynamic imports as an escape hatch for server-only code", async () => { - let cwd = await createProject({ - ...files, - "app/routes/handle-server-only.tsx": String.raw` - export const handle = { - // Sharp knife alert: you probably should avoid doing this, but you can! - serverOnlyEscapeHatch: async () => { - let { dotServerFile } = await import("~/utils.server"); - let dotServerDir = await import("~/.server/utils"); - return { dotServerFile, dotServerDir }; - } + export const action = () => { + let contents = fs.readFileSync("server_only.txt"); + console.log({ serverOnlyFile, serverOnlyDir, contents }); + return null; } export default function() { - return

This should work

+ let { data } = useLoaderData(); + return
{JSON.stringify(data)}
; } `, }); @@ -147,50 +65,177 @@ test("Vite / `handle` with dynamic imports as an escape hatch for server-only co let lines = grep( path.join(cwd, "build/client"), - /SERVER_ONLY_FILE|SERVER_ONLY_DIR/ + /SERVER_ONLY|SERVER_ONLY|node:fs/ ); expect(lines).toHaveLength(0); }); -test("Vite / dead-code elimination for server exports", async () => { - let cwd = await createProject({ - ...files, - "app/routes/remove-server-exports-and-dce.tsx": String.raw` - import fs from "node:fs"; - import { json } from "@remix-run/node"; - import { useLoaderData } from "@remix-run/react"; - - import { dotServerFile } from "../utils.server"; - import { dotServerDir } from "../.server/utils"; +test.describe("Vite / route / server-only module referenced by client", () => { + let matrix = [ + { type: "file", path: "app/utils.server.ts", specifier: `~/utils.server` }, + { type: "dir", path: "app/.server/utils.ts", specifier: `~/.server/utils` }, + + { + type: "file alias", + path: "app/utils.server.ts", + specifier: `#dot-server-file`, + }, + { + type: "dir alias", + path: "app/.server/utils.ts", + specifier: `#dot-server-dir/utils`, + }, + ]; + + let cases = matrix.flatMap(({ type, path, specifier }) => [ + { + name: `default import / .server ${type}`, + path, + specifier, + route: ` + import serverOnly from "${specifier}"; + export default () =>

{serverOnly}

; + `, + }, + { + name: `named import / .server ${type}`, + path, + specifier, + route: ` + import { serverOnly } from "${specifier}" + export default () =>

{serverOnly}

; + `, + }, + { + name: `namespace import / .server ${type}`, + path, + specifier, + route: ` + import * as utils from "${specifier}" + export default () =>

{utils.serverOnly}

; + `, + }, + ]); + + for (let { name, path, specifier, route } of cases) { + test(name, async () => { + let cwd = await createProject({ + "tsconfig.json": tsconfig({ + "~/*": ["app/*"], + "#dot-server-file": ["app/utils.server.ts"], + "#dot-server-dir/*": ["app/.server/*"], + }), + [path]: serverOnlyModule, + "app/routes/_index.tsx": route, + }); + let result = viteBuild({ cwd }); + let stderr = result.stderr.toString("utf8"); + [ + "Server-only module referenced by client", + + ` '${specifier}' imported by route 'app/routes/_index.tsx'`, + + " The only route exports that can reference server-only modules are: `loader`, `action`, `headers`", + ` but other route exports in 'app/routes/_index.tsx' depend on '${specifier}'.`, + + " For more see https://remix.run/docs/en/main/discussion/server-vs-client", + ].forEach(expect(stderr).toMatch); + }); + } +}); - export const loader = () => { - let contents = fs.readFileSync("blah"); - let data = dotServerFile + dotServerDir + serverOnly + contents; - return json({ data }); - } +test.describe("Vite / non-route / server-only module referenced by client", () => { + let matrix = [ + { type: "file", path: "app/utils.server.ts", specifier: `~/utils.server` }, + { type: "dir", path: "app/.server/utils.ts", specifier: `~/.server/utils` }, + ]; + + let cases = matrix.flatMap(({ type, path, specifier }) => [ + { + name: `default import / .server ${type}`, + path, + specifier, + nonroute: ` + import serverOnly from "${specifier}"; + export const getServerOnly = () => serverOnly; + `, + }, + { + name: `named import / .server ${type}`, + path, + specifier, + nonroute: ` + import { serverOnly } from "${specifier}"; + export const getServerOnly = () => serverOnly; + `, + }, + { + name: `namespace import / .server ${type}`, + path, + specifier, + nonroute: ` + import * as utils from "${specifier}"; + export const getServerOnly = () => utils.serverOnly; + `, + }, + ]); + + for (let { name, path, specifier, nonroute } of cases) { + test(name, async () => { + let cwd = await createProject({ + [path]: serverOnlyModule, + "app/reexport-server-only.ts": nonroute, + "app/routes/_index.tsx": String.raw` + import { serverOnly } from "~/reexport-server-only" + export default () =>

{serverOnly}

; + `, + }); + let result = viteBuild({ cwd }); + let stderr = stripAnsi(result.stderr.toString("utf8")); + + [ + `Server-only module referenced by client`, + + ` '${specifier}' imported by 'app/reexport-server-only.ts'`, + + " * If all code in 'app/reexport-server-only.ts' is server-only:", + + " Rename it to 'app/reexport-server-only.server.ts'", + + " * Otherwise:", + + " Keep client-safe code in 'app/reexport-server-only.ts'", + " and move server-only code:", + + " - Into a `.server` directory e.g. 'app/.server/utils.ts'", + " - Or into a `.server` file e.g. 'app/reexport-server-only.server.ts'", + + "For more, see https://remix.run/docs/en/main/future/vite#server-code-not-tree-shaken-in-development", + ].forEach(expect(stderr).toMatch); + }); + } +}); - export const action = () => { - console.log(dotServerFile, dotServerDir, serverOnly); - return null; +test("Vite / `handle` with dynamic imports as an escape hatch for server-only code", async () => { + let cwd = await createProject({ + "app/utils.server.ts": serverOnlyModule, + "app/.server/utils.ts": serverOnlyModule, + "app/routes/handle-server-only.tsx": String.raw` + export const handle = { + escapeHatch: !import.meta.env.SSR ? undefined : + async () => { + let { serverOnly: serverOnlyFile } = await import("~/utils.server"); + let serverOnlyDir = await import("~/.server/utils"); + return { serverOnlyFile, serverOnlyDir }; + } } - export default function() { - let { data } = useLoaderData(); - return ( - <> -

Index

-

{data}

- - ); - } + export default () =>

This should work

; `, }); let { status } = viteBuild({ cwd }); expect(status).toBe(0); - let lines = grep( - path.join(cwd, "build/client"), - /SERVER_ONLY_FILE|SERVER_ONLY_DIR|node:fs/ - ); + let lines = grep(path.join(cwd, "build/client"), /SERVER_ONLY/); expect(lines).toHaveLength(0); }); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index b7488408ddd..ce142e62a34 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -61,6 +61,8 @@ const ROUTE_EXPORTS = new Set([ "shouldRevalidate", ]); +const SERVER_ONLY_EXPORTS = ["loader", "action", "headers"]; + // We need to provide different JSDoc comments in some cases due to differences // between the Remix config and the Vite plugin. type RemixConfigJsdocOverrides = { @@ -927,22 +929,88 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { }, }, { - name: "remix-empty-server-modules", + name: "remix-dot-server", enforce: "pre", - async transform(_code, id, options) { + async resolveId(id, importer, options) { if (options?.ssr) return; + + let isResolving = options?.custom?.["remix-dot-server"] ?? false; + if (isResolving) return; + options.custom = { ...options.custom, "remix-dot-server": true }; + let resolved = await this.resolve(id, importer, options); + if (!resolved) return; + let serverFileRE = /\.server(\.[cm]?[jt]sx?)?$/; let serverDirRE = /\/\.server\//; - if (serverFileRE.test(id) || serverDirRE.test(id)) { - return { - code: "export {}", - map: null, - }; + let isDotServer = + serverFileRE.test(resolved!.id) || serverDirRE.test(resolved!.id); + if (!isDotServer) return; + + if (!importer) throw Error(`Importer not found: ${id}`); + + let pluginConfig = await resolvePluginConfig(); + let importerShort = path.relative(pluginConfig.rootDirectory, importer); + let isRoute = getRoute(pluginConfig, importer); + + if (isRoute) { + let serverOnlyExports = SERVER_ONLY_EXPORTS.map( + (xport) => `\`${xport}\`` + ).join(", "); + throw Error( + [ + colors.red(`Server-only module referenced by client`), + "", + ` '${id}' imported by route '${importerShort}'`, + "", + ` The only route exports that can reference server-only modules are: ${serverOnlyExports}`, + ` but other route exports in '${importerShort}' depend on '${id}'.`, + "", + " For more see https://remix.run/docs/en/main/discussion/server-vs-client", + "", + ].join("\n") + ); } + + let importedBy = path.parse(importerShort); + let ext = importedBy.ext === ".jsx" ? ".js" : ".ts"; + let dotServerDir = path + .join( + path.basename(pluginConfig.appDirectory), + ".server", + "utils" + ext + ) + .replace(/\.jsx$/, ".js"); + let dotServerFile = path.join( + importedBy.dir, + importedBy.name + ".server" + ext + ); + + throw Error( + [ + colors.red(`Server-only module referenced by client`), + "", + ` '${id}' imported by '${importerShort}'`, + "", + ` * If all code in '${importerShort}' is server-only:`, + "", + ` Rename it to '${dotServerFile}'`, + "", + ` * Otherwise:`, + "", + ` Keep client-safe code in '${importerShort}'`, + ` and move server-only code:`, + "", + ` - Into a \`.server\` directory e.g. '${dotServerDir}'`, + ` - Or into a \`.server\` file e.g. '${dotServerFile}'`, + "", + " For more, see https://remix.run/docs/en/main/future/vite#server-code-not-tree-shaken-in-development", + "", + ].join("\n") + ); }, }, { - name: "remix-empty-client-modules", + name: "remix-dot-client", enforce: "post", async transform(code, id, options) { if (!options?.ssr) return; @@ -991,10 +1059,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { throw Error(message); } - let serverExports = ["loader", "action", "headers"]; - return { - code: removeExports(code, serverExports), + code: removeExports(code, SERVER_ONLY_EXPORTS), map: null, }; }, diff --git a/yarn.lock b/yarn.lock index 065bfc47898..0366f307f6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13093,10 +13093,10 @@ vite-node@^0.28.5: source-map-support "^0.5.21" vite "^3.0.0 || ^4.0.0" -vite-tsconfig-paths@^4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.1.tgz#e53b89096b91d31a6d1e26f75999ea8c336a89ed" - integrity sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ== +vite-tsconfig-paths@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.2.tgz#fee5a59c885687ae046e1d5a394bdcfdb12d9361" + integrity sha512-dq0FjyxHHDnp0uS3P12WEOX2W7NeuLzX9AWP38D7Zw2CTbFErapwQVlCiT5DMJcVWKQ1MMdTe92PZl/rBQ7qcw== dependencies: debug "^4.1.1" globrex "^0.1.2" From a6dff9a869ba7dea5a3124f3ff9fa6608d9b3c58 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 11 Dec 2023 22:42:15 -0500 Subject: [PATCH 2/4] add changeset --- .changeset/breezy-grapes-roll.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .changeset/breezy-grapes-roll.md diff --git a/.changeset/breezy-grapes-roll.md b/.changeset/breezy-grapes-roll.md new file mode 100644 index 00000000000..281cbe42bc0 --- /dev/null +++ b/.changeset/breezy-grapes-roll.md @@ -0,0 +1,18 @@ +--- +"@remix-run/dev": minor +--- + +Vite: Error messages when .server files are referenced by client + +Previously, referencing a `.server` module from client code resulted in an error message like: + +```txt +The requested module '/app/models/answer.server.ts' does not provide an export named 'isDateType' +``` + +Which was confusing because `answer.server.ts` _does_ provide the `isDateType` export, +but Remix was replacing `.server` modules with empty modules (`export {}`) for the client build. + +Now, Remix explicitly fails at compile time when a `.server` module is referenced from client code +and includes dedicated error messages depending on whether the import occurs in a route or a non-route +module. The error messages also include links to relevant documentation. From 27826f050fc1f9d1533f78be24722a7bbaf36fb7 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 11 Dec 2023 23:05:58 -0500 Subject: [PATCH 3/4] address pr feedback: better error message wording --- packages/remix-dev/vite/plugin.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index ce142e62a34..8c1f4e3c57a 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -962,8 +962,10 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { "", ` '${id}' imported by route '${importerShort}'`, "", - ` The only route exports that can reference server-only modules are: ${serverOnlyExports}`, - ` but other route exports in '${importerShort}' depend on '${id}'.`, + ` The only route exports that can reference server-only modules are:`, + ` ${serverOnlyExports}`, + "", + ` But other route exports in '${importerShort}' depend on '${id}'.`, "", " For more see https://remix.run/docs/en/main/discussion/server-vs-client", "", @@ -973,13 +975,6 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { let importedBy = path.parse(importerShort); let ext = importedBy.ext === ".jsx" ? ".js" : ".ts"; - let dotServerDir = path - .join( - path.basename(pluginConfig.appDirectory), - ".server", - "utils" + ext - ) - .replace(/\.jsx$/, ".js"); let dotServerFile = path.join( importedBy.dir, importedBy.name + ".server" + ext @@ -991,17 +986,19 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { "", ` '${id}' imported by '${importerShort}'`, "", + ` * If all code in '${importerShort}' is server-only:`, "", ` Rename it to '${dotServerFile}'`, "", ` * Otherwise:`, "", - ` Keep client-safe code in '${importerShort}'`, - ` and move server-only code:`, + ` - Keep client-safe code in '${importerShort}'`, + ` - And move server-only code to a \`.server\` file`, + ` e.g. '${dotServerFile}'`, "", - ` - Into a \`.server\` directory e.g. '${dotServerDir}'`, - ` - Or into a \`.server\` file e.g. '${dotServerFile}'`, + " If you have lots of `.server` files, try using", + " a `.server` directory e.g. 'app/.server'", "", " For more, see https://remix.run/docs/en/main/future/vite#server-code-not-tree-shaken-in-development", "", From 0ca4b4f4264218279e6c067d24ebdbd0d812cfda Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 11 Dec 2023 23:18:55 -0500 Subject: [PATCH 4/4] test(vite): update checks for error messages for .server imports --- integration/vite-dot-server-test.ts | 15 +++++++++------ packages/remix-dev/vite/plugin.ts | 14 +++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts index 5480afaa272..03c10c7b2d7 100644 --- a/integration/vite-dot-server-test.ts +++ b/integration/vite-dot-server-test.ts @@ -135,8 +135,10 @@ test.describe("Vite / route / server-only module referenced by client", () => { ` '${specifier}' imported by route 'app/routes/_index.tsx'`, - " The only route exports that can reference server-only modules are: `loader`, `action`, `headers`", - ` but other route exports in 'app/routes/_index.tsx' depend on '${specifier}'.`, + " The only route exports that can reference server-only modules are:", + " `loader`, `action`, `headers`", + + ` But other route exports in 'app/routes/_index.tsx' depend on '${specifier}'.`, " For more see https://remix.run/docs/en/main/discussion/server-vs-client", ].forEach(expect(stderr).toMatch); @@ -204,11 +206,12 @@ test.describe("Vite / non-route / server-only module referenced by client", () = " * Otherwise:", - " Keep client-safe code in 'app/reexport-server-only.ts'", - " and move server-only code:", + ` - Keep client-safe code in 'app/reexport-server-only.ts'`, + ` - And move server-only code to a \`.server\` file`, + ` e.g. 'app/reexport-server-only.server.ts'`, - " - Into a `.server` directory e.g. 'app/.server/utils.ts'", - " - Or into a `.server` file e.g. 'app/reexport-server-only.server.ts'", + " If you have lots of `.server` files, try using", + " a `.server` directory e.g. 'app/.server'", "For more, see https://remix.run/docs/en/main/future/vite#server-code-not-tree-shaken-in-development", ].forEach(expect(stderr).toMatch); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 8c1f4e3c57a..f5598bc460a 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -948,8 +948,11 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { if (!importer) throw Error(`Importer not found: ${id}`); + let vite = importViteEsmSync(); let pluginConfig = await resolvePluginConfig(); - let importerShort = path.relative(pluginConfig.rootDirectory, importer); + let importerShort = vite.normalizePath( + path.relative(pluginConfig.rootDirectory, importer) + ); let isRoute = getRoute(pluginConfig, importer); if (isRoute) { @@ -974,10 +977,11 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => { } let importedBy = path.parse(importerShort); - let ext = importedBy.ext === ".jsx" ? ".js" : ".ts"; - let dotServerFile = path.join( - importedBy.dir, - importedBy.name + ".server" + ext + let dotServerFile = vite.normalizePath( + path.join( + importedBy.dir, + importedBy.name + ".server" + importedBy.ext + ) ); throw Error(