Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: auto-install @types/react only if react is installed #68928

Conversation

devjiwonchoi
Copy link
Member

@devjiwonchoi devjiwonchoi commented Aug 15, 2024

Why?

If react is not installed in the project, it does not require @types/react either.
This could happen when the user tries to run a headless Next.js app that does not require react.

Also, we are working on adding a create-next-app template for the case above.

x-ref: #68118

Closes NDX-220

@ijjk ijjk added created-by: Next.js team PRs by the Next.js team. type: next labels Aug 15, 2024
@ijjk
Copy link
Member

ijjk commented Aug 15, 2024

Stats from current PR

Default Build (Increase detected ⚠️)
General
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
buildDuration 17.3s 17.3s N/A
buildDurationCached 8.5s 7.7s N/A
nodeModulesSize 356 MB 356 MB N/A
nextStartRea..uration (ms) 432ms 429ms N/A
Client Bundles (main, webpack)
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
4958.HASH.js gzip 169 B 168 B N/A
6405-HASH.js gzip 5.19 kB 5.19 kB N/A
8711-HASH.js gzip 37.9 kB 37.9 kB N/A
d09468e9-HASH.js gzip 51.9 kB 51.9 kB N/A
framework-HASH.js gzip 56.8 kB 56.7 kB N/A
main-app-HASH.js gzip 223 B 225 B N/A
main-HASH.js gzip 32.5 kB 32.5 kB N/A
webpack-HASH.js gzip 1.71 kB 1.71 kB N/A
Overall change 0 B 0 B
Legacy Client Bundles (polyfills)
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
polyfills-HASH.js gzip 31 kB 31 kB
Overall change 31 kB 31 kB
Client Pages
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
_app-HASH.js gzip 194 B 192 B N/A
_error-HASH.js gzip 191 B 192 B N/A
amp-HASH.js gzip 512 B 511 B N/A
css-HASH.js gzip 342 B 344 B N/A
dynamic-HASH.js gzip 1.84 kB 1.84 kB N/A
edge-ssr-HASH.js gzip 265 B 266 B N/A
head-HASH.js gzip 361 B 364 B N/A
hooks-HASH.js gzip 390 B 393 B N/A
image-HASH.js gzip 4.4 kB 4.4 kB N/A
index-HASH.js gzip 268 B 267 B N/A
link-HASH.js gzip 2.82 kB 2.81 kB N/A
routerDirect..HASH.js gzip 328 B 328 B
script-HASH.js gzip 397 B 397 B
withRouter-HASH.js gzip 324 B 324 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 1.16 kB 1.16 kB
Client Build Manifests
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
_buildManifest.js gzip 747 B 749 B N/A
Overall change 0 B 0 B
Rendered Page Sizes
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
index.html gzip 524 B 521 B N/A
link.html gzip 538 B 538 B
withRouter.html gzip 520 B 520 B
Overall change 1.06 kB 1.06 kB
Edge SSR bundle Size
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
edge-ssr.js gzip 128 kB 127 kB N/A
page.js gzip 173 kB 173 kB N/A
Overall change 0 B 0 B
Middleware size
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
middleware-b..fest.js gzip 668 B 667 B N/A
middleware-r..fest.js gzip 156 B 156 B
middleware.js gzip 29.7 kB 29.6 kB N/A
edge-runtime..pack.js gzip 844 B 844 B
Overall change 1 kB 1 kB
Next Runtimes
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
928-experime...dev.js gzip 322 B 322 B
928.runtime.dev.js gzip 314 B 314 B
app-page-exp...dev.js gzip 309 kB 307 kB N/A
app-page-exp..prod.js gzip 122 kB 121 kB N/A
app-page-tur..prod.js gzip 135 kB 133 kB N/A
app-page-tur..prod.js gzip 130 kB 129 kB N/A
app-page.run...dev.js gzip 298 kB 297 kB N/A
app-page.run..prod.js gzip 118 kB 117 kB N/A
app-route-ex...dev.js gzip 30.7 kB 30.7 kB N/A
app-route-ex..prod.js gzip 20.7 kB 20.7 kB N/A
app-route-tu..prod.js gzip 20.7 kB 20.7 kB N/A
app-route-tu..prod.js gzip 20.5 kB 20.5 kB N/A
app-route.ru...dev.js gzip 32.4 kB 32.3 kB N/A
app-route.ru..prod.js gzip 20.5 kB 20.5 kB N/A
pages-api-tu..prod.js gzip 9.62 kB 9.61 kB N/A
pages-api.ru...dev.js gzip 11.5 kB 11.4 kB N/A
pages-api.ru..prod.js gzip 9.61 kB 9.6 kB N/A
pages-turbo...prod.js gzip 21.6 kB 21.6 kB N/A
pages.runtim...dev.js gzip 27.5 kB 27.5 kB N/A
pages.runtim..prod.js gzip 21.6 kB 21.6 kB N/A
server.runti..prod.js gzip 57 kB 56.9 kB N/A
Overall change 636 B 636 B
build cache Overall increase ⚠️
vercel/next.js canary vercel/next.js refac/install-types-react-only-if-react-is-installed Change
0.pack gzip 1.49 MB 1.49 MB ⚠️ +3.93 kB
index.pack gzip 126 kB 127 kB ⚠️ +771 B
Overall change 1.62 MB 1.62 MB ⚠️ +4.7 kB
Diff details
Diff for page.js

Diff too large to display

Diff for middleware.js

Diff too large to display

Diff for edge-ssr.js

Diff too large to display

Diff for image-HASH.js
@@ -1,7 +1,7 @@
 (self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([
   [8358],
   {
-    /***/ 9842: /***/ (
+    /***/ 5684: /***/ (
       __unused_webpack_module,
       __unused_webpack_exports,
       __webpack_require__
@@ -9,7 +9,7 @@
       (window.__NEXT_P = window.__NEXT_P || []).push([
         "/image",
         function () {
-          return __webpack_require__(8722);
+          return __webpack_require__(2665);
         },
       ]);
       if (false) {
@@ -18,7 +18,7 @@
       /***/
     },
 
-    /***/ 5040: /***/ (module, exports, __webpack_require__) => {
+    /***/ 3608: /***/ (module, exports, __webpack_require__) => {
       "use strict";
       /* __next_internal_client_entry_do_not_use__  cjs */
       Object.defineProperty(exports, "__esModule", {
@@ -40,17 +40,17 @@
         __webpack_require__(2957)
       );
       const _head = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(5187)
+        __webpack_require__(7569)
       );
-      const _getimgprops = __webpack_require__(1332);
-      const _imageconfig = __webpack_require__(3384);
-      const _imageconfigcontextsharedruntime = __webpack_require__(8875);
-      const _warnonce = __webpack_require__(4516);
-      const _routercontextsharedruntime = __webpack_require__(5002);
+      const _getimgprops = __webpack_require__(4429);
+      const _imageconfig = __webpack_require__(2950);
+      const _imageconfigcontextsharedruntime = __webpack_require__(6587);
+      const _warnonce = __webpack_require__(8719);
+      const _routercontextsharedruntime = __webpack_require__(2062);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(5145)
+        __webpack_require__(232)
       );
-      const _usemergedref = __webpack_require__(4926);
+      const _usemergedref = __webpack_require__(9935);
       // This is replaced by webpack define plugin
       const configEnv = {
         deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
@@ -371,7 +371,7 @@
       /***/
     },
 
-    /***/ 4926: /***/ (module, exports, __webpack_require__) => {
+    /***/ 9935: /***/ (module, exports, __webpack_require__) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -440,7 +440,7 @@
       /***/
     },
 
-    /***/ 1332: /***/ (
+    /***/ 4429: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -456,9 +456,9 @@
           return getImgProps;
         },
       });
-      const _warnonce = __webpack_require__(4516);
-      const _imageblursvg = __webpack_require__(1569);
-      const _imageconfig = __webpack_require__(3384);
+      const _warnonce = __webpack_require__(8719);
+      const _imageblursvg = __webpack_require__(3207);
+      const _imageconfig = __webpack_require__(2950);
       const VALID_LOADING_VALUES =
         /* unused pure expression or super */ null && [
           "lazy",
@@ -830,7 +830,7 @@
       /***/
     },
 
-    /***/ 1569: /***/ (__unused_webpack_module, exports) => {
+    /***/ 3207: /***/ (__unused_webpack_module, exports) => {
       "use strict";
       /**
        * A shared function, used on both client and server, to generate a SVG blur placeholder.
@@ -885,7 +885,7 @@
       /***/
     },
 
-    /***/ 1674: /***/ (
+    /***/ 3455: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -912,10 +912,10 @@
         },
       });
       const _interop_require_default = __webpack_require__(4345);
-      const _getimgprops = __webpack_require__(1332);
-      const _imagecomponent = __webpack_require__(5040);
+      const _getimgprops = __webpack_require__(4429);
+      const _imagecomponent = __webpack_require__(3608);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(5145)
+        __webpack_require__(232)
       );
       function getImageProps(imgProps) {
         const { props } = (0, _getimgprops.getImgProps)(imgProps, {
@@ -947,7 +947,7 @@
       /***/
     },
 
-    /***/ 5145: /***/ (__unused_webpack_module, exports) => {
+    /***/ 232: /***/ (__unused_webpack_module, exports) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -982,7 +982,7 @@
       /***/
     },
 
-    /***/ 8722: /***/ (
+    /***/ 2665: /***/ (
       __unused_webpack_module,
       __webpack_exports__,
       __webpack_require__
@@ -999,8 +999,8 @@
 
       // EXTERNAL MODULE: ./node_modules/.pnpm/react@19.0.0-rc-187dd6a7-20240806/node_modules/react/jsx-runtime.js
       var jsx_runtime = __webpack_require__(8548);
-      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+main-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-187dd6a7-20240806_re_6tugpsz4gl2xsqrwds5bwcbi3y/node_modules/next/image.js
-      var next_image = __webpack_require__(6629);
+      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+diff-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-187dd6a7-20240806_re_wxf3je55uhqnotjiz3wkaonabu/node_modules/next/image.js
+      var next_image = __webpack_require__(2856);
       var image_default = /*#__PURE__*/ __webpack_require__.n(next_image); // CONCATENATED MODULE: ./pages/nextjs.png
       /* harmony default export */ const nextjs = {
         src: "/_next/static/media/nextjs.cae0b805.png",
@@ -1030,12 +1030,12 @@
       /***/
     },
 
-    /***/ 6629: /***/ (
+    /***/ 2856: /***/ (
       module,
       __unused_webpack_exports,
       __webpack_require__
     ) => {
-      module.exports = __webpack_require__(1674);
+      module.exports = __webpack_require__(3455);
 
       /***/
     },
@@ -1045,7 +1045,7 @@
     /******/ var __webpack_exec__ = (moduleId) =>
       __webpack_require__((__webpack_require__.s = moduleId));
     /******/ __webpack_require__.O(0, [2888, 9774, 179], () =>
-      __webpack_exec__(9842)
+      __webpack_exec__(5684)
     );
     /******/ var __webpack_exports__ = __webpack_require__.O();
     /******/ _N_E = __webpack_exports__;
Diff for 8711-HASH.js

Diff too large to display

Diff for main-HASH.js

Diff too large to display

Diff for app-page-exp..ntime.dev.js
failed to diff
Diff for app-page-exp..time.prod.js

Diff too large to display

Diff for app-page-tur..time.prod.js

Diff too large to display

Diff for app-page-tur..time.prod.js

Diff too large to display

Diff for app-page.runtime.dev.js
failed to diff
Diff for app-page.runtime.prod.js

Diff too large to display

Diff for app-route-ex..ntime.dev.js

Diff too large to display

Diff for app-route-ex..time.prod.js

Diff too large to display

Diff for app-route-tu..time.prod.js

Diff too large to display

Diff for app-route-tu..time.prod.js

Diff too large to display

Diff for app-route.runtime.dev.js

Diff too large to display

Diff for app-route.ru..time.prod.js

Diff too large to display

Diff for pages-api-tu..time.prod.js

Diff too large to display

Diff for pages-api.runtime.dev.js

Diff too large to display

Diff for pages-api.ru..time.prod.js

Diff too large to display

Diff for pages-turbo...time.prod.js

Diff too large to display

Diff for pages.runtime.dev.js

Diff too large to display

Diff for pages.runtime.prod.js

Diff too large to display

Diff for server.runtime.prod.js

Diff too large to display

Commit: bef71a0

@devjiwonchoi devjiwonchoi marked this pull request as ready for review August 15, 2024 07:48
@eps1lon
Copy link
Member

eps1lon commented Aug 15, 2024

Do we even support a Next.js app without React installed? This should come with a test.

@ijjk ijjk added the tests label Aug 15, 2024
@devjiwonchoi devjiwonchoi force-pushed the refac/install-types-react-only-if-react-is-installed branch from 1f39cfc to 90181d4 Compare August 15, 2024 19:54
@ijjk
Copy link
Member

ijjk commented Aug 15, 2024

Failing test suites

Commit: bef71a0

TURBOPACK=1 pnpm test-dev test/development/middleware-errors/index.test.ts (turbopack)

  • middleware - development errors > when there is a compilation error after boot > logs the error correctly
Expand output

● middleware - development errors › when there is a compilation error after boot › logs the error correctly

TIMED OUT: success

undefined

Error: expect(received).toContain(expected) // indexOf

Expected substring: "Expected '{', got '}'"
Received string:    "  ▲ Next.js 15.0.0-canary.123 (turbo)
  - Local:        http://localhost:36921·
 ✓ Starting...
Creating turbopack project {
  dir: '/tmp/next-install-a8d852faf60ae470f359ecddbd9a7fdc5fb3f62cac28648c4511ca3933032ceb',
  testMode: true
}
 ✓ Compiled in 105ms
 ✓ Ready in 534ms
 ✓ Compiled in 2ms
 ○ Compiling middleware ...
 ✓ Compiled middleware in 580ms
 ✓ Compiled /_error in 77ms
 GET / 500 in 735ms
"

  718 |
  719 |   if (hardError) {
> 720 |     throw new Error('TIMED OUT: ' + regex + '\n\n' + content + '\n\n' + lastErr)
      |           ^
  721 |   }
  722 |   return false
  723 | }

  at check (lib/next-test-utils.ts:720:11)
  at Object.<anonymous> (development/middleware-errors/index.test.ts:294:7)

Read more about building and testing Next.js in contributing.md.

TURBOPACK=1 pnpm test-start test/production/ci-missing-typescript-deps/index.test.ts (turbopack)

  • ci-missing-typescript-deps > should not throw an error if beta version of @types/react and @types/react-dom is installed
Expand output

● ci-missing-typescript-deps › should not throw an error if beta version of @types/react and @types/react-dom is installed

expect(received).toContain(expected) // indexOf

Expected substring: "We detected TypeScript in your project and created"
Received string:    "⚠ No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache
  ▲ Next.js 15.0.0-canary.123 (turbo)·
   Linting and checking validity of types ...
It looks like you're trying to use TypeScript but do not have the required package(s) installed.·
Please install react and react-dom by running:·
	pnpm install --save-dev react react-dom·
If you are not trying to use TypeScript, please remove the tsconfig.json file from your package root (and any TypeScript files in your pages directory).·
"

  69 |     try {
  70 |       const nextBuild = await next.build()
> 71 |       expect(nextBuild.cliOutput).toContain(
     |                                   ^
  72 |         // matching the part of the success message that isn't colored.
  73 |         `We detected TypeScript in your project and created`
  74 |       )

  at Object.toContain (production/ci-missing-typescript-deps/index.test.ts:71:35)

Read more about building and testing Next.js in contributing.md.

TURBOPACK=1 pnpm test-start test/production/app-dir/app-fetch-build-cache/app-fetch-build-cache.test.ts (turbopack)

  • app fetch build cache > should not use stale data if present
Expand output

● app fetch build cache › should not use stale data if present

next build failed with code/signal 1

   98 |           if (code || signal)
   99 |             reject(
> 100 |               new Error(`next build failed with code/signal ${code || signal}`)
      |               ^
  101 |             )
  102 |           else resolve()
  103 |         })

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:100:15)

Read more about building and testing Next.js in contributing.md.

TURBOPACK=1 pnpm test-start test/production/sharp-basic/sharp-basic.test.ts (turbopack)

  • sharp support with hasNextSupport > should work using cheerio
Expand output

● sharp support with hasNextSupport › should work using cheerio

next build failed with code/signal 1

   98 |           if (code || signal)
   99 |             reject(
> 100 |               new Error(`next build failed with code/signal ${code || signal}`)
      |               ^
  101 |             )
  102 |           else resolve()
  103 |         })

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:100:15)

Read more about building and testing Next.js in contributing.md.

pnpm test-start test/production/app-dir/fetch-cache/fetch-cache.test.ts

  • fetch-cache > should have correct fetchUrl field for fetches and unstable_cache
  • fetch-cache > should retry 3 times when revalidate times out
  • fetch-cache > should not retry for failed fetch-cache GET
Expand output

● fetch-cache › should have correct fetchUrl field for fetches and unstable_cache

next build failed with code/signal 1

   98 |           if (code || signal)
   99 |             reject(
> 100 |               new Error(`next build failed with code/signal ${code || signal}`)
      |               ^
  101 |             )
  102 |           else resolve()
  103 |         })

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:100:15)

● fetch-cache › should retry 3 times when revalidate times out

next build failed with code/signal 1

   98 |           if (code || signal)
   99 |             reject(
> 100 |               new Error(`next build failed with code/signal ${code || signal}`)
      |               ^
  101 |             )
  102 |           else resolve()
  103 |         })

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:100:15)

● fetch-cache › should not retry for failed fetch-cache GET

next build failed with code/signal 1

   98 |           if (code || signal)
   99 |             reject(
> 100 |               new Error(`next build failed with code/signal ${code || signal}`)
      |               ^
  101 |             )
  102 |           else resolve()
  103 |         })

  at ChildProcess.<anonymous> (lib/next-modes/next-start.ts:100:15)

● Test suite failed to run

TypeError: Cannot read properties of undefined (reading 'destroy')

  170 |   })
  171 |   afterAll(async () => {
> 172 |     await next.destroy()
      |                ^
  173 |     if (fetchCacheServer) fetchCacheServer.close()
  174 |     if (nextInstance) await killApp(nextInstance)
  175 |   })

  at Object.destroy (production/app-dir/fetch-cache/fetch-cache.test.ts:172:16)

Read more about building and testing Next.js in contributing.md.

@samcx
Copy link
Member

samcx commented Aug 15, 2024

Curious, should we also consider @types/react-dom? Also curious whether @types/node is needed or not.

Comment on lines 4 to 12
const { next } = nextTestSetup({
files: __dirname,
installCommand: 'pnpm remove react react-dom',
})

it('should work using cheerio without react, react-dom', async () => {
const $ = await next.render$('/')
expect($('p').text()).toBe('hello world')
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this test work? Shouldn't it just do a regular pnpm dev and then check no react or @types/react is in dependencies or devDependencies?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we always install react and react-dom, this manually removes after install.

const finalDependencies = {
react: reactVersion,
'react-dom': reactVersion,
'@types/react': 'latest',
'@types/react-dom': 'latest',
typescript: 'latest',
'@types/node': 'latest',
...this.dependencies,
...this.packageJson?.dependencies,
}

Screenshot 2024-08-17 at 12 15 04 PM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But where in the assertion do I see that?

@devjiwonchoi
Copy link
Member Author

@samcx Will add @types/react-dom on the next stack, and for @types/node, I think we should leave as is since the type is necessary for calling functions like fs.

it('should not have react or react-dom in the package.json', async () => {
const packageJson = await next.readJSON('package.json')
expect(packageJson.dependencies).toHaveProperty('next')
expect(packageJson.dependencies).not.toHaveProperty('react')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't testing Next.js behavior. You're manually removing react and react-dom so obviously it's not in the package.json.

Let's do this by the books and add a commit testing the existing behavior and another commit making the change and updating that test so that we know what we actually changed.

When the project is using TypeScript, we auto-installed `@types/react`
but not `@types/react-dom`, so we install it as well if `react` exists
in the project.
@devjiwonchoi
Copy link
Member Author

Since we have to modify route definitions (.next/types/app/...) as well, I find this inadequate to implement for Next.js where an alternate is simply installing @types/react.

@devjiwonchoi devjiwonchoi deleted the refac/install-types-react-only-if-react-is-installed branch August 21, 2024 17:25
@github-actions github-actions bot added the locked label Sep 5, 2024
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 5, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants