Repository: vercel/next.js
Pull request: Fix(turbopack): Resolve experimentalDecorators from extended tsconfig

Turbopack was not correctly resolving the `experimentalDecorators` compiler option from extended `tsconfig.json` files. This caused errors when using decorators in projects with such a configuration.

What caused this issue?
We are trying to read 3 different values on a single accessor from `read_from_tsconfigs` if we return any value from the accessor it stop looping through the extended files

This commit fixes the issue by:
1. read the values from `tsconfigs` independently to make sure we are reading correct value from extended configs
2. construct the final return value same as before

Fixes #78782

Changed files:
filename: crates/next-core/src/transform_options.rs
patch: @@ -66,51 +66,62 @@ pub async fn get_decorators_transform_options(
 ) -> Result<Vc<DecoratorsOptions>> {
     let tsconfig = get_typescript_options(project_path).await?;
 
-    let decorators_transform_options = if let Some(tsconfig) = tsconfig {
-        read_from_tsconfigs(&tsconfig, |json, _| {
-            let decorators_kind = if json["compilerOptions"]["experimentalDecorators"]
-                .as_bool()
-                .unwrap_or(false)
-            {
-                Some(DecoratorsKind::Legacy)
-            } else {
-                // ref: https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/#differences-with-experimental-legacy-decorators
-                // `without the flag, decorators will now be valid syntax for all new code.
-                // Outside of --experimentalDecorators, they will be type-checked and emitted
-                // differently with ts 5.0, new ecma decorators will be enabled
-                // if legacy decorators are not enabled
-                Some(DecoratorsKind::Ecma)
-            };
-
-            let emit_decorators_metadata = if let Some(decorators_kind) = &decorators_kind {
-                match decorators_kind {
-                    DecoratorsKind::Legacy => {
-                        // ref: This new decorators proposal is not compatible with
-                        // --emitDecoratorMetadata, and it does not allow decorating parameters.
-                        // Future ECMAScript proposals may be able to help bridge that gap
-                        json["compilerOptions"]["emitDecoratorMetadata"]
-                            .as_bool()
-                            .unwrap_or(false)
-                    }
-                    DecoratorsKind::Ecma => false,
-                }
-            } else {
-                false
-            };
-
-            Some(DecoratorsOptions {
-                decorators_kind,
-                emit_decorators_metadata,
-                use_define_for_class_fields: json["compilerOptions"]["useDefineForClassFields"]
-                    .as_bool()
-                    .unwrap_or(false),
-                ..Default::default()
-            })
+    let experimental_decorators = if let Some(ref tsconfig) = tsconfig {
+        read_from_tsconfigs(tsconfig, |json, _| {
+            json["compilerOptions"]["experimentalDecorators"].as_bool()
         })
         .await?
-        .unwrap_or_default()
+        .unwrap_or(false)
+    } else {
+        false
+    };
+
+    let decorators_kind = if experimental_decorators {
+        Some(DecoratorsKind::Legacy)
     } else {
-        Default::default()
+        // ref: https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/#differences-with-experimental-legacy-decorators
+        // `without the flag, decorators will now be valid syntax for all new code.
+        // Outside of --experimentalDecorators, they will be type-checked and emitted
+        // differently with ts 5.0, new ecma decorators will be enabled
+        // if legacy decorators are not enabled
+        Some(DecoratorsKind::Ecma)
+    };
+
+    let emit_decorators_metadata = if let Some(ref tsconfig) = tsconfig {
+        read_from_tsconfigs(tsconfig, |json, _| {
+            json["compilerOptions"]["emitDecoratorMetadata"].as_bool()
+        })
+        .await?
+        .unwrap_or(false)
+    } else {
+        false
+    };
+
+    let use_define_for_class_fields = if let Some(ref tsconfig) = tsconfig {
+        read_from_tsconfigs(tsconfig, |json, _| {
+            json["compilerOptions"]["useDefineForClassFields"].as_bool()
+        })
+        .await?
+        .unwrap_or(false)
+    } else {
+        false
+    };
+
+    let decorators_transform_options = DecoratorsOptions {
+        decorators_kind: decorators_kind.clone(),
+        emit_decorators_metadata: if let Some(ref decorators_kind) = decorators_kind {
+            match decorators_kind {
+                DecoratorsKind::Legacy => emit_decorators_metadata,
+                // ref: This new decorators proposal is not compatible with
+                // --emitDecoratorMetadata, and it does not allow decorating parameters.
+                // Future ECMAScript proposals may be able to help bridge that gap
+                DecoratorsKind::Ecma => false,
+            }
+        } else {
+            false
+        },
+        use_define_for_class_fields,
+        ..Default::default()
     };
 
     Ok(decorators_transform_options.cell())

filename: test/development/basic/legacy-decorators.test.ts
patch: @@ -5,39 +5,81 @@ import { NextInstance } from 'e2e-utils'
 import { check } from 'next-test-utils'
 
 describe('Legacy decorators SWC option', () => {
-  let next: NextInstance
+  describe('with extended tsconfig', () => {
+    let next: NextInstance
 
-  beforeAll(async () => {
-    next = await createNext({
-      files: {
-        'jsconfig.json': new FileRef(
-          join(__dirname, 'legacy-decorators/jsconfig.json')
-        ),
-        pages: new FileRef(join(__dirname, 'legacy-decorators/pages')),
-      },
-      dependencies: {
-        mobx: '6.3.7',
-        'mobx-react': '7.2.1',
-      },
+    beforeAll(async () => {
+      next = await createNext({
+        files: {
+          'tsconfig.json': new FileRef(
+            join(__dirname, 'legacy-decorators/tsconfig-extended.json')
+          ),
+          'tsconfig-base.json': new FileRef(
+            join(__dirname, 'legacy-decorators/jsconfig.json')
+          ),
+          pages: new FileRef(join(__dirname, 'legacy-decorators/pages')),
+        },
+        dependencies: {
+          mobx: '6.3.7',
+          'mobx-react': '7.2.1',
+        },
+      })
+    })
+    afterAll(() => next.destroy())
+
+    it('should compile with legacy decorators enabled from extended config', async () => {
+      let browser
+      try {
+        browser = await webdriver(next.url, '/')
+        const text = await browser.elementByCss('#count').text()
+        expect(text).toBe('Current number: 0')
+        await browser.elementByCss('#increase').click()
+        await check(
+          () => browser.elementByCss('#count').text(),
+          /Current number: 1/
+        )
+      } finally {
+        if (browser) {
+          await browser.close()
+        }
+      }
     })
   })
-  afterAll(() => next.destroy())
 
-  it('should compile with legacy decorators enabled', async () => {
-    let browser
-    try {
-      browser = await webdriver(next.url, '/')
-      const text = await browser.elementByCss('#count').text()
-      expect(text).toBe('Current number: 0')
-      await browser.elementByCss('#increase').click()
-      await check(
-        () => browser.elementByCss('#count').text(),
-        /Current number: 1/
-      )
-    } finally {
-      if (browser) {
-        await browser.close()
+  describe('with base config', () => {
+    let next: NextInstance
+    beforeAll(async () => {
+      next = await createNext({
+        files: {
+          'jsconfig.json': new FileRef(
+            join(__dirname, 'legacy-decorators/jsconfig.json')
+          ),
+          pages: new FileRef(join(__dirname, 'legacy-decorators/pages')),
+        },
+        dependencies: {
+          mobx: '6.3.7',
+          'mobx-react': '7.2.1',
+        },
+      })
+    })
+    afterAll(() => next.destroy())
+
+    it('should compile with legacy decorators enabled', async () => {
+      let browser
+      try {
+        browser = await webdriver(next.url, '/')
+        const text = await browser.elementByCss('#count').text()
+        expect(text).toBe('Current number: 0')
+        await browser.elementByCss('#increase').click()
+        await check(
+          () => browser.elementByCss('#count').text(),
+          /Current number: 1/
+        )
+      } finally {
+        if (browser) {
+          await browser.close()
+        }
       }
-    }
+    })
   })
 })

filename: test/development/basic/legacy-decorators/tsconfig-extended.json
patch: @@ -0,0 +1,3 @@
+{
+  "extends": "./tsconfig-base.json"
+}