diff --git a/docs/api/conventions.md b/docs/api/conventions.md
index 23b2d1e29a9..61194de06d0 100644
--- a/docs/api/conventions.md
+++ b/docs/api/conventions.md
@@ -178,7 +178,7 @@ There are a few conventions that Remix uses you should be aware of.
Setting up routes in Remix is as simple as creating files in your `app` directory. These are the conventions you should know to understand how routing in Remix works.
-Please note that you can use either `.jsx` or `.tsx` file extensions depending on whether or not you use TypeScript. We'll stick with `.tsx` in the examples to avoid duplication (and because we ❤️ TypeScript).
+Please note that you can use either `.js`, `.jsx` or `.tsx` file extensions depending on whether or not you use TypeScript. We'll stick with `.tsx` in the examples to avoid duplication (and because we ❤️ TypeScript).
#### Root Layout Route
diff --git a/docs/guides/typescript.md b/docs/guides/typescript.md
index 2a2eda5e960..8e810b1757e 100644
--- a/docs/guides/typescript.md
+++ b/docs/guides/typescript.md
@@ -6,9 +6,7 @@ title: TypeScript
Remix seamlessly supports both JavaScript and TypeScript. If you name a file with a `.ts` or `.tsx` extension, it will treat it as TypeScript (`.tsx` is for TypeScript files [with JSX](https://www.typescriptlang.org/docs/handbook/jsx.html) in them). But it isn't required. You can write all your files as `.js` files if you don't want TypeScript.
-To use JSX without TypeScript, you need to use the `.jsx` extension.
-
-The remix compiler will not do any type checking (it simply removes the types). If you want to do type checking, you'll want to use TypeScript's `tsc` CLI yourself. A common solution is to add a `typecheck` script to your package.json:
+The Remix compiler will not do any type checking (it simply removes the types). If you want to do type checking, you'll want to use TypeScript's `tsc` CLI yourself. A common solution is to add a `typecheck` script to your package.json:
```json filename=package.json lines=[11]
{
diff --git a/integration/js-routes-test.ts b/integration/js-routes-test.ts
new file mode 100644
index 00000000000..4b3777218b5
--- /dev/null
+++ b/integration/js-routes-test.ts
@@ -0,0 +1,42 @@
+import { test } from "@playwright/test";
+
+import { createAppFixture, createFixture, js } from "./helpers/create-fixture";
+import type { AppFixture } from "./helpers/create-fixture";
+import { PlaywrightFixture } from "./helpers/playwright-fixture";
+
+test.describe(".js route files", () => {
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ appFixture = await createAppFixture(
+ await createFixture({
+ files: {
+ "app/routes/js.js": js`
+ export default () =>
Rendered with .js ext
;
+ `,
+ "app/routes/jsx.jsx": js`
+ export default () => Rendered with .jsx ext
;
+ `,
+ },
+ })
+ );
+ });
+
+ test.afterAll(async () => {
+ await appFixture.close();
+ });
+
+ test("should render all .js routes", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/js");
+ await page.waitForSelector("[data-testid='route-js']");
+ test.expect(await page.content()).toContain("Rendered with .js ext");
+ });
+
+ test("should render all .jsx routes", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/jsx");
+ await page.waitForSelector("[data-testid='route-jsx']");
+ test.expect(await page.content()).toContain("Rendered with .jsx ext");
+ });
+});
diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts
index 06b9f36a595..22764cfb728 100644
--- a/packages/remix-dev/compiler/routes.ts
+++ b/packages/remix-dev/compiler/routes.ts
@@ -53,6 +53,9 @@ export async function getRouteModuleExports(
format: "esm",
metafile: true,
write: false,
+ loader: {
+ ".js": "jsx",
+ },
logLevel: "silent",
plugins: [mdxPlugin(config)],
});
diff --git a/rollup.config.js b/rollup.config.js
index 35584354ea7..96b38c57b71 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -887,9 +887,9 @@ function copyToPlaygrounds() {
await fse.copy(writtenDir, destDir);
// tickle live reload by touching the server entry
- let serverEntry = ["entry.server.tsx", "entry.server.jsx"].find(
- (entryPath) =>
- fse.existsSync(path.join(playgroundDir, "app", entryPath))
+ let serverEntry = ["tsx", "js", "jsx"].find(
+ (entryPathExtension) =>
+ fse.existsSync(path.join(playgroundDir, "app", `entry.server.${entryPathExtension}`))
);
let serverEntryPath = path.join(playgroundDir, "app", serverEntry);
let serverEntryContent = await fse.readFile(serverEntryPath);