Repository: remix-run/remix
Pull request #3059 (2022-05-02 12:37:28)

fix(remix-dev): support JSX in `.js` route files
We currently support JSX syntax in `.js` files *except* for route modules. This PR should fix that. 

- [x] Docs
- [x] Tests


Patch:
diff --git a/docs/api/conventions.md b/docs/api/conventions.md
index 26c0db30e..638f69cf9 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 2a2eda5e9..8e810b175 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.
 
-<docs-warning>To use JSX without TypeScript, you need to use the `.jsx` extension.</docs-warning>
-
-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/packages/remix-dev/cli/create.ts b/packages/remix-dev/cli/create.ts
index 83a3e8750..0264e41e3 100644
--- a/packages/remix-dev/cli/create.ts
+++ b/packages/remix-dev/cli/create.ts
@@ -502,69 +502,73 @@ export async function validateTemplate(input: string) {
     }
     case "remoteTarball": {
       let spinner = ora("Validating the template file…").start();
+      let response;
       try {
-        let response = await fetch(input, { method: "HEAD" });
-        spinner.stop();
-        switch (response.status) {
-          case 200:
-            return;
-          case 404:
-            throw Error(
-              "🚨 The template file could not be verified. Please double check " +
-                "the URL and try again."
-            );
-          default:
-            throw Error(
-              "🚨 The template file could not be verified. The server returned " +
-                `a response with a ${response.status} status. Please double ` +
-                "check the URL and try again."
-            );
-        }
-      } catch (err) {
-        spinner.stop();
+        response = await fetch(input, { method: "HEAD" });
+      } catch (_) {
         throw Error(
           "🚨 There was a problem verifying the template file. Please ensure " +
             "you are connected to the internet and try again later."
         );
+      } finally {
+        spinner.stop();
+      }
+
+      switch (response.status) {
+        case 200:
+          return;
+        case 404:
+          throw Error(
+            "🚨 The template file could not be verified. Please double check " +
+              "the URL and try again."
+          );
+        default:
+          throw Error(
+            "🚨 The template file could not be verified. The server returned " +
+              `a response with a ${response.status} status. Please double ` +
+              "check the URL and try again."
+          );
       }
     }
     case "repo": {
       let spinner = ora("Validating the template repo…").start();
       let { url, filePath } = getRepoInfo(input);
+      let response;
       try {
-        let response = await fetch(url, { method: "HEAD" });
-        spinner.stop();
-        switch (response.status) {
-          case 200:
-            return;
-          case 403:
-            throw Error(
-              "🚨 The template could not be verified because you do not have " +
-                "access to the repository. Please double check the access " +
-                "rights of this repo and try again."
-            );
-          case 404:
-            throw Error(
-              "🚨 The template could not be verified. Please double check that " +
-                "the template is a valid GitHub repository" +
-                (filePath && filePath !== "/"
-                  ? " and that the filepath points to a directory in the repo"
-                  : "") +
-                " and try again."
-            );
-          default:
-            throw Error(
-              "🚨 The template could not be verified. The server returned a " +
-                `response with a ${response.status} status. Please double check ` +
-                "that the template is a valid GitHub repository  and try again."
-            );
-        }
+        response = await fetch(url, { method: "HEAD" });
       } catch (_) {
-        spinner.stop();
         throw Error(
-          "🚨 There was a problem verifying the template. Please ensure you " +
+          "🚨 There was a problem fetching the template. Please ensure you " +
             "are connected to the internet and try again later."
         );
+      } finally {
+        spinner.stop();
+      }
+
+      switch (response.status) {
+        case 200:
+          return;
+        case 403:
+          throw Error(
+            "🚨 The template could not be verified because you do not have " +
+              "access to the repository. Please double check the access " +
+              "rights of this repo and try again."
+          );
+        case 404:
+          throw Error(
+            "🚨 The template could not be verified. Please double check that " +
+              "the template is a valid GitHub repository" +
+              (filePath && filePath !== "/"
+                ? " and that the filepath points to a directory in the repo"
+                : "") +
+              " and try again."
+          );
+        default:
+          throw Error(
+            "🚨 The template could not be verified. The server returned a " +
+              `response with a ${response.status} status. Please double check ` +
+              "that the template is a valid GitHub repository and try again."
+          );
       }
     }
     case "example":
@@ -576,34 +580,36 @@ export async function validateTemplate(input: string) {
       }
       let typeDir = templateType + "s";
       let templateUrl = `https://github.com/remix-run/remix/tree/main/${typeDir}/${name}`;
+      let response;
       try {
-        let response = await fetch(templateUrl, { method: "HEAD" });
-        spinner.stop();
-        switch (response.status) {
-          case 200:
-            return;
-          case 404:
-            throw Error(
-              "🚨 The template could not be verified. Please double check that " +
-                "the template is a valid project directory in " +
-                `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
-                "try again."
-            );
-          default:
-            throw Error(
-              "🚨 The template could not be verified. The server returned a " +
-                `response with a ${response.status} status. Please double ` +
-                "check that the template is a valid project directory in " +
-                `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
-                "try again."
-            );
-        }
+        response = await fetch(templateUrl, { method: "HEAD" });
       } catch (_) {
-        spinner.stop();
         throw Error(
           "🚨 There was a problem verifying the template. Please ensure you are " +
             "connected to the internet and try again later."
         );
+      } finally {
+        spinner.stop();
+      }
+
+      switch (response.status) {
+        case 200:
+          return;
+        case 404:
+          throw Error(
+            "🚨 The template could not be verified. Please double check that " +
+              "the template is a valid project directory in " +
+              `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
+              "try again."
+          );
+        default:
+          throw Error(
+            "🚨 The template could not be verified. The server returned a " +
+              `response with a ${response.status} status. Please double ` +
+              "check that the template is a valid project directory in " +
+              `https://github.com/remix-run/remix/tree/main/${typeDir} and ` +
+              "try again."
+          );
       }
     }
   }
diff --git a/packages/remix-dev/compiler/routes.ts b/packages/remix-dev/compiler/routes.ts
index 06b9f36a5..22764cfb7 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 35584354e..96b38c57b 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);


Test patch:
diff --git a/integration/js-routes-test.ts b/integration/js-routes-test.ts
new file mode 100644
index 000000000..4b3777218
--- /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 () => <div data-testid="route-js">Rendered with .js ext</div>;
+          `,
+          "app/routes/jsx.jsx": js`
+            export default () => <div data-testid="route-jsx">Rendered with .jsx ext</div>;
+          `,
+        },
+      })
+    );
+  });
+
+  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/__tests__/create-test.ts b/packages/remix-dev/__tests__/create-test.ts
index 74cd1fa67..9e2dda96b 100644
--- a/packages/remix-dev/__tests__/create-test.ts
+++ b/packages/remix-dev/__tests__/create-test.ts
@@ -542,6 +542,88 @@ describe("the create command", () => {
     expect(output).toContain(getOptOutOfInstallMessage("pnpm exec remix init"));
     process.env.npm_user_agent = originalUserAgent;
   });
+
+  describe("errors", () => {
+    it("identifies when a github repo is not accessible (403)", async () => {
+      let projectDir = await getProjectDir("repo");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "error-username/403",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template could not be verified because you do not have access to the repository. Please double check the access rights of this repo and try again.]`
+      );
+    });
+
+    it("identifies when a github repo does not exist (404)", async () => {
+      let projectDir = await getProjectDir("repo");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "error-username/404",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template could not be verified. Please double check that the template is a valid GitHub repository and try again.]`
+      );
+    });
+
+    it("identifies when something unknown goes wrong with the repo request (4xx)", async () => {
+      let projectDir = await getProjectDir("repo");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "error-username/400",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template could not be verified. The server returned a response with a 400 status. Please double check that the template is a valid GitHub repository and try again.]`
+      );
+    });
+
+    it("identifies when a remote tarball does not exist (404)", async () => {
+      let projectDir = await getProjectDir("remote-tarball");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "https://example.com/error/404/remix-stack.tar.gz",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template file could not be verified. Please double check the URL and try again.]`
+      );
+    });
+
+    it("identifies when a remote tarball does not exist (4xx)", async () => {
+      let projectDir = await getProjectDir("remote-tarball");
+      await expect(() =>
+        run([
+          "create",
+          projectDir,
+          "--template",
+          "https://example.com/error/400/remix-stack.tar.gz",
+          "--no-install",
+          "--typescript",
+        ])
+      ).rejects.toMatchInlineSnapshot(
+        `[Error: 🚨 The template file could not be verified. The server returned a response with a 400 status. Please double check the URL and try again.]`
+      );
+    });
+  });
 });
 
 function getSuccessMessage(projectDirectory: string) {
diff --git a/packages/remix-dev/__tests__/github-mocks.ts b/packages/remix-dev/__tests__/github-mocks.ts
index 56718f8c0..8e486be7b 100644
--- a/packages/remix-dev/__tests__/github-mocks.ts
+++ b/packages/remix-dev/__tests__/github-mocks.ts
@@ -76,6 +76,12 @@ let githubHandlers: Array<RequestHandler> = [
       return res(ctx.status(200));
     }
   ),
+  rest.head(
+    `https://github.com/error-username/:status`,
+    async (req, res, ctx) => {
+      return res(ctx.status(Number(req.params.status)));
+    }
+  ),
   rest.head(`https://github.com/:owner/:repo`, async (req, res, ctx) => {
     return res(ctx.status(200));
   }),
diff --git a/packages/remix-dev/__tests__/msw.ts b/packages/remix-dev/__tests__/msw.ts
index 409603c3d..b73408dd1 100644
--- a/packages/remix-dev/__tests__/msw.ts
+++ b/packages/remix-dev/__tests__/msw.ts
@@ -8,6 +8,12 @@ import { githubHandlers } from "./github-mocks";
 type RequestHandler = Parameters<typeof setupServer>[0];
 
 let miscHandlers: Array<RequestHandler> = [
+  rest.head(
+    "https://example.com/error/:status/remix-stack.tar.gz",
+    async (req, res, ctx) => {
+      return res(ctx.status(Number(req.params.status)));
+    }
+  ),
   rest.head("https://example.com/remix-stack.tar.gz", async (req, res, ctx) => {
     return res(ctx.status(200));
   }),