Repository: remix-run/remix (ts)

Issue:
Build fails if there's a broken tsconfig.json file outside the project
### What version of Remix are you using?

latest

### Steps to Reproduce

- npx create-remix@latest (just the basics, typescript)
- cd my-remix-app
- touch ../tsconfig.json
- npm run dev

### Expected Behavior

The build should work fine, and the tsconfig.json file outside my project should be ignored

### Actual Behavior

🍺  ~/my-remix-app npm run dev

> dev
> remix dev

Watching Remix app in development mode...

✘ [ERROR] Unexpected end of file

    ../tsconfig.json:1:0:
      1 │
        ╵ ^


Build failed with 1 error:
../tsconfig.json:1:0: ERROR: Unexpected end of file

Pull request description:
feat(remix-dev): add `tsconfigPath` config option to RemixConfig
This patch addresses an issue[1] in which Remix looks for a tsconfig
outside the root directory.

We fix this behavior by looking for a tsconfig where it should be
(in the root directory itself) and passing its absolute path to
`tsconfig-paths`. `tsconfig-paths` will then treat the given path as a
tsconfig file path and won't crawl further. If no tsconfig file is
found by us in our root directory, we simply won't call `tsconfig-paths`
at all. Note that crawling up is `tsconfig-path`s intended behavior,
but not Remix's.

A secondary bug that causes this same issue is due to esbuild[2]. When
no tsconfig is passed to esbuild, it will crawl the filesystem up looking
for any tsconfig available and will load it. When tsconfig is explicitly
set to `undefined`, it does the crawling anyways. Unfortunately,
this is a fix that has to be done in `esbuild`, but a few alternatives
to manage this unwanted behavior for now can be:

1. Create an empty tsconfig in the build directory and pass it to esbuild
   so it doesn't crawl outside rootDir;
2. Show a warning to let the user know why the crawling is happening.

Closes #3210

[1] https://github.com/remix-run/remix/issues/3210
[2] https://github.com/evanw/esbuild/issues/2440

Signed-off-by: Juliana Oliveira <juliana@uma.ni>


Patch:
diff --git a/contributors.yml b/contributors.yml
index 4c632d504..4164fec2a 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -209,6 +209,7 @@
 - juwiragiye
 - jveldridge
 - jvnm-dev
+- jwnx
 - kalch
 - kanermichael
 - karimsan
diff --git a/decisions/0004-streaming-apis.md b/decisions/0004-streaming-apis.md
index bf629a92b..0aeb82654 100644
--- a/decisions/0004-streaming-apis.md
+++ b/decisions/0004-streaming-apis.md
@@ -58,23 +58,23 @@ We decided _not_ to support naked objects due to the ambiguity that would be int
 // been introduced had we chosen to support naked objects :)
 
 // This would NOT be streamed
-function loader() {
+function exampleLoader1() {
   return Promise.resolve(5);
 }
 
 // This WOULD be streamed
-function loader() {
+function exampleLoader2() {
   return {
     value: Promise.resolve(5),
   };
 }
 
 // This would NOT be streamed
-function loader() {
+function exampleLoader3() {
   return {
     value: {
       nested: Promise.resolve(5),
-    }
+    },
   };
 }
 ```
diff --git a/packages/remix-dev/compiler.ts b/packages/remix-dev/compiler.ts
index a2425c7ca..b677af3ca 100644
--- a/packages/remix-dev/compiler.ts
+++ b/packages/remix-dev/compiler.ts
@@ -368,6 +368,10 @@ async function createBrowserBuild(
     sourcemap: options.sourcemap,
     metafile: true,
     incremental: options.incremental,
+    // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to
+    // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted
+    // behavior can only be avoided by creating an empty tsconfig file in the root directory.
+    tsconfig: config.tsconfigPath,
     mainFields: ["browser", "module", "main"],
     treeShaking: true,
     minify: options.mode === BuildMode.Production,
@@ -458,6 +462,10 @@ function createServerBuild(
       loader: loaders,
       bundle: true,
       logLevel: "silent",
+      // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to
+      // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted
+      // behavior can only be avoided by creating an empty tsconfig file in the root directory.
+      tsconfig: config.tsconfigPath,
       incremental: options.incremental,
       sourcemap: options.sourcemap, // use linked (true) to fix up .map file
       // The server build needs to know how to generate asset URLs for imports
diff --git a/packages/remix-dev/compiler/plugins/mdx.ts b/packages/remix-dev/compiler/plugins/mdx.ts
index 4ff210ec9..49e137b8a 100644
--- a/packages/remix-dev/compiler/plugins/mdx.ts
+++ b/packages/remix-dev/compiler/plugins/mdx.ts
@@ -17,7 +17,7 @@ export function mdxPlugin(config: RemixConfig): esbuild.Plugin {
       ]);
 
       build.onResolve({ filter: /\.mdx?$/ }, (args) => {
-        let matchPath = createMatchPath();
+        let matchPath = createMatchPath(config.tsconfigPath);
         // Resolve paths according to tsconfig paths property
         function resolvePath(id: string) {
           if (!matchPath) {
diff --git a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
index 4e9fcffe1..d5124b0a7 100644
--- a/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
+++ b/packages/remix-dev/compiler/plugins/serverBareModulesPlugin.ts
@@ -23,7 +23,7 @@ export function serverBareModulesPlugin(
   let isDenoRuntime = remixConfig.serverBuildTarget === "deno";
 
   // Resolve paths according to tsconfig paths property
-  let matchPath = isDenoRuntime ? undefined : createMatchPath();
+  let matchPath = isDenoRuntime ? undefined : createMatchPath(remixConfig.tsconfigPath);
   function resolvePath(id: string) {
     if (!matchPath) {
       return id;
diff --git a/packages/remix-dev/compiler/utils/tsconfig/index.ts b/packages/remix-dev/compiler/utils/tsconfig/index.ts
index 8836e7a47..559a34e88 100644
--- a/packages/remix-dev/compiler/utils/tsconfig/index.ts
+++ b/packages/remix-dev/compiler/utils/tsconfig/index.ts
@@ -2,8 +2,17 @@ import tsConfigPaths from "tsconfig-paths";
 
 import { writeConfigDefaults } from "./write-config-defaults";
 
-export function createMatchPath() {
-  let configLoaderResult = tsConfigPaths.loadConfig();
+export function createMatchPath(tsconfigPath: string | undefined) {
+  // There is no tsconfig to match paths against.
+  if (!tsconfigPath) {
+    return undefined;
+  }
+
+  // When passing a absolute path, loadConfig assumes that the path contains
+  // a tsconfig file.
+  // Ref.: https://github.com/dividab/tsconfig-paths/blob/v4.0.0/src/__tests__/config-loader.test.ts#L74
+  let configLoaderResult = tsConfigPaths.loadConfig(tsconfigPath);
+
   if (configLoaderResult.resultType === "failed") {
     if (configLoaderResult.message === "Missing baseUrl in compilerOptions") {
       throw new Error(
diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts
index 6c2a110ed..39d87a89f 100644
--- a/packages/remix-dev/config.ts
+++ b/packages/remix-dev/config.ts
@@ -269,6 +269,11 @@ export interface RemixConfig {
    * A list of directories to watch.
    */
   watchPaths: string[];
+
+  /**
+   * The path for the tsconfig file, if present on the root directory.
+   */
+  tsconfigPath: string | undefined;
 }
 
 /**
@@ -450,6 +455,18 @@ export async function readConfig(
 
   let serverDependenciesToBundle = appConfig.serverDependenciesToBundle || [];
 
+  // When tsconfigPath is undefined, the default "tsconfig.json" is not
+  // found in the root directory.
+  let tsconfigPath: string | undefined;
+  let rootTsconfig = path.resolve(rootDirectory, "tsconfig.json");
+  let rootJsConfig = path.resolve(rootDirectory, "jsconfig.json");
+
+  if (fse.existsSync(rootTsconfig)) {
+    tsconfigPath = rootTsconfig;
+  } else if (fse.existsSync(rootJsConfig)) {
+    tsconfigPath = rootJsConfig;
+  }
+
   return {
     appDirectory,
     cacheDirectory,
@@ -472,6 +489,7 @@ export async function readConfig(
     serverDependenciesToBundle,
     mdx,
     watchPaths,
+    tsconfigPath,
   };
 }
 


Test patch:
diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts
index ddfed1ba5..1b101f886 100644
--- a/packages/remix-dev/__tests__/readConfig-test.ts
+++ b/packages/remix-dev/__tests__/readConfig-test.ts
@@ -21,6 +21,7 @@ describe("readConfig", () => {
         serverBuildPath: expect.any(String),
         assetsBuildDirectory: expect.any(String),
         relativeAssetsBuildDirectory: expect.any(String),
+        tsconfigPath: expect.any(String)
       },
       `
       Object {
@@ -50,6 +51,7 @@ describe("readConfig", () => {
         "serverMode": "production",
         "serverModuleFormat": "cjs",
         "serverPlatform": "node",
+        "tsconfigPath": Any<String>,
         "watchPaths": Array [],
       }
     `