vercel / next.js

The React Framework
https://nextjs.org
MIT License
122.59k stars 26.23k forks source link

feat: `requiredEnv` #67187

Open devjiwonchoi opened 3 days ago

devjiwonchoi commented 3 days ago

Why?

In some cases, when the environment variable was missed out during the build, it does not exit which ends up requiring to re-deploy or re-build the app after adding the environment variables.

What?

To prevent this additional steps, this PR added a next config option requiredEnv where the users can add the keys of the required environment variables. If the required variable was missed out during next build, it will exit with providing the keys of missing variables.

How?

During the build, we check if requiredEnv exists, then we check if we have all those required env been loaded. If not, we exit with the warning message.

Usage

Set the requiredEnv in the next config, that are required to be loaded during build.

// next.config.mjs
const nextConfig = {
  requiredEnv: ['OPENAI_API_KEY']
}

export default nextConfig

If the env variable of OPENAI_API_KEY was not loaded, next will exit during the build with output:

 ⨯ Missing required environment variables: OPENAI_API_KEY
ijjk commented 3 days ago

Stats from current PR

Default Build (Increase detected ⚠️)
General Overall increase ⚠️ | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | buildDuration | 17.3s | 14.7s | N/A | | buildDurationCached | 8.2s | 7.1s | N/A | | nodeModulesSize | 359 MB | 359 MB | ⚠️ +6.1 kB | | nextStartRea..uration (ms) | 393ms | 394ms | N/A |
Client Bundles (main, webpack) | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | 3377.HASH.js gzip | 168 B | 168 B | ✓ | | 6561-HASH.js gzip | 36 kB | 36 kB | N/A | | 7908-HASH.js gzip | 5.06 kB | 5.07 kB | N/A | | 7ba1ee62-HASH.js gzip | 51.7 kB | 51.7 kB | N/A | | framework-HASH.js gzip | 56.7 kB | 56.7 kB | N/A | | main-app-HASH.js gzip | 221 B | 218 B | N/A | | main-HASH.js gzip | 32.2 kB | 32.2 kB | N/A | | webpack-HASH.js gzip | 1.71 kB | 1.71 kB | N/A | | Overall change | 168 B | 168 B | ✓ |
Legacy Client Bundles (polyfills) | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | polyfills-HASH.js gzip | 31 kB | 31 kB | ✓ | | Overall change | 31 kB | 31 kB | ✓ |
Client Pages | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | _app-HASH.js gzip | 192 B | 192 B | ✓ | | _error-HASH.js gzip | 192 B | 191 B | N/A | | amp-HASH.js gzip | 511 B | 511 B | ✓ | | css-HASH.js gzip | 340 B | 342 B | N/A | | dynamic-HASH.js gzip | 2.53 kB | 2.52 kB | N/A | | edge-ssr-HASH.js gzip | 265 B | 266 B | N/A | | head-HASH.js gzip | 361 B | 363 B | N/A | | hooks-HASH.js gzip | 389 B | 389 B | ✓ | | image-HASH.js gzip | 4.27 kB | 4.27 kB | N/A | | index-HASH.js gzip | 268 B | 268 B | ✓ | | link-HASH.js gzip | 2.69 kB | 2.69 kB | N/A | | routerDirect..HASH.js gzip | 327 B | 324 B | N/A | | script-HASH.js gzip | 395 B | 394 B | N/A | | withRouter-HASH.js gzip | 322 B | 321 B | N/A | | 1afbb74e6ecf..834.css gzip | 106 B | 106 B | ✓ | | Overall change | 1.47 kB | 1.47 kB | ✓ |
Client Build Manifests | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | _buildManifest.js gzip | 485 B | 482 B | N/A | | Overall change | 0 B | 0 B | ✓ |
Rendered Page Sizes | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | index.html gzip | 523 B | 522 B | N/A | | link.html gzip | 536 B | 537 B | N/A | | withRouter.html gzip | 520 B | 518 B | N/A | | Overall change | 0 B | 0 B | ✓ |
Edge SSR bundle Size | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | edge-ssr.js gzip | 127 kB | 127 kB | N/A | | page.js gzip | 167 kB | 167 kB | N/A | | Overall change | 0 B | 0 B | ✓ |
Middleware size | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | middleware-b..fest.js gzip | 661 B | 657 B | N/A | | middleware-r..fest.js gzip | 155 B | 154 B | N/A | | middleware.js gzip | 29.5 kB | 29.5 kB | N/A | | edge-runtime..pack.js gzip | 1.03 kB | 1.03 kB | ✓ | | Overall change | 1.03 kB | 1.03 kB | ✓ |
Next Runtimes | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | app-page-exp...dev.js gzip | 184 kB | 184 kB | ✓ | | app-page-exp..prod.js gzip | 112 kB | 112 kB | ✓ | | app-page-tur..prod.js gzip | 123 kB | 123 kB | ✓ | | app-page-tur..prod.js gzip | 119 kB | 119 kB | ✓ | | app-page.run...dev.js gzip | 178 kB | 178 kB | ✓ | | app-page.run..prod.js gzip | 109 kB | 109 kB | ✓ | | app-route-ex...dev.js gzip | 23.4 kB | 23.4 kB | ✓ | | app-route-ex..prod.js gzip | 18.8 kB | 18.8 kB | ✓ | | app-route-tu..prod.js gzip | 18.8 kB | 18.8 kB | ✓ | | app-route-tu..prod.js gzip | 18.6 kB | 18.6 kB | ✓ | | app-route.ru...dev.js gzip | 24.6 kB | 24.6 kB | ✓ | | app-route.ru..prod.js gzip | 18.6 kB | 18.6 kB | ✓ | | pages-api-tu..prod.js gzip | 9.55 kB | 9.55 kB | ✓ | | pages-api.ru...dev.js gzip | 9.82 kB | 9.82 kB | ✓ | | pages-api.ru..prod.js gzip | 9.55 kB | 9.55 kB | ✓ | | pages-turbo...prod.js gzip | 21.6 kB | 21.6 kB | ✓ | | pages.runtim...dev.js gzip | 22.1 kB | 22.1 kB | ✓ | | pages.runtim..prod.js gzip | 21.6 kB | 21.6 kB | ✓ | | server.runti..prod.js gzip | 51.7 kB | 51.7 kB | ✓ | | Overall change | 1.09 MB | 1.09 MB | ✓ |
build cache Overall increase ⚠️ | | vercel/next.js canary | devjiwonchoi/next.js feat/required-env | Change | | - | - | - | - | | 0.pack gzip | 1.67 MB | 1.67 MB | ⚠️ +573 B | | index.pack gzip | 129 kB | 129 kB | N/A | | Overall change | 1.67 MB | 1.67 MB | ⚠️ +573 B |
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 ```diff @@ -1,7 +1,7 @@ (self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([ [8358], { - /***/ 774: /***/ ( + /***/ 3834: /***/ ( __unused_webpack_module, __unused_webpack_exports, __webpack_require__ @@ -9,7 +9,7 @@ (window.__NEXT_P = window.__NEXT_P || []).push([ "/image", function () { - return __webpack_require__(482); + return __webpack_require__(5458); }, ]); if (false) { @@ -18,7 +18,7 @@ /***/ }, - /***/ 9611: /***/ (module, exports, __webpack_require__) => { + /***/ 375: /***/ (module, exports, __webpack_require__) => { "use strict"; /* __next_internal_client_entry_do_not_use__ cjs */ Object.defineProperty(exports, "__esModule", { @@ -40,15 +40,15 @@ __webpack_require__(3537) ); const _head = /*#__PURE__*/ _interop_require_default._( - __webpack_require__(6973) + __webpack_require__(6393) ); - const _getimgprops = __webpack_require__(4823); - const _imageconfig = __webpack_require__(2356); - const _imageconfigcontextsharedruntime = __webpack_require__(8081); - const _warnonce = __webpack_require__(2683); - const _routercontextsharedruntime = __webpack_require__(852); + const _getimgprops = __webpack_require__(5446); + const _imageconfig = __webpack_require__(6510); + const _imageconfigcontextsharedruntime = __webpack_require__(2630); + const _warnonce = __webpack_require__(169); + const _routercontextsharedruntime = __webpack_require__(4584); const _imageloader = /*#__PURE__*/ _interop_require_default._( - __webpack_require__(1634) + __webpack_require__(4947) ); // This is replaced by webpack define plugin const configEnv = { @@ -376,7 +376,7 @@ /***/ }, - /***/ 4823: /***/ ( + /***/ 5446: /***/ ( __unused_webpack_module, exports, __webpack_require__ @@ -392,9 +392,9 @@ return getImgProps; }, }); - const _warnonce = __webpack_require__(2683); - const _imageblursvg = __webpack_require__(4199); - const _imageconfig = __webpack_require__(2356); + const _warnonce = __webpack_require__(169); + const _imageblursvg = __webpack_require__(5411); + const _imageconfig = __webpack_require__(6510); const VALID_LOADING_VALUES = /* unused pure expression or super */ null && [ "lazy", @@ -769,7 +769,7 @@ /***/ }, - /***/ 4199: /***/ (__unused_webpack_module, exports) => { + /***/ 5411: /***/ (__unused_webpack_module, exports) => { "use strict"; /** * A shared function, used on both client and server, to generate a SVG blur placeholder. @@ -824,7 +824,7 @@ /***/ }, - /***/ 1104: /***/ ( + /***/ 2870: /***/ ( __unused_webpack_module, exports, __webpack_require__ @@ -851,10 +851,10 @@ }, }); const _interop_require_default = __webpack_require__(1478); - const _getimgprops = __webpack_require__(4823); - const _imagecomponent = __webpack_require__(9611); + const _getimgprops = __webpack_require__(5446); + const _imagecomponent = __webpack_require__(375); const _imageloader = /*#__PURE__*/ _interop_require_default._( - __webpack_require__(1634) + __webpack_require__(4947) ); function getImageProps(imgProps) { const { props } = (0, _getimgprops.getImgProps)(imgProps, { @@ -886,7 +886,7 @@ /***/ }, - /***/ 1634: /***/ (__unused_webpack_module, exports) => { + /***/ 4947: /***/ (__unused_webpack_module, exports) => { "use strict"; Object.defineProperty(exports, "__esModule", { @@ -921,7 +921,7 @@ /***/ }, - /***/ 482: /***/ ( + /***/ 5458: /***/ ( __unused_webpack_module, __webpack_exports__, __webpack_require__ @@ -938,8 +938,8 @@ // EXTERNAL MODULE: ./node_modules/.pnpm/react@19.0.0-rc-6230622a1a-20240610/node_modules/react/jsx-runtime.js var jsx_runtime = __webpack_require__(898); - // EXTERNAL MODULE: ./node_modules/.pnpm/file+..+main-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-6230622a1a-20240610_react_vrduafyijdumnv5m4yyxi35vzu/node_modules/next/image.js - var next_image = __webpack_require__(4064); + // EXTERNAL MODULE: ./node_modules/.pnpm/file+..+diff-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-6230622a1a-20240610_react_66pum5zqjmu66dbgsbrnmwmucy/node_modules/next/image.js + var next_image = __webpack_require__(3515); 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", @@ -969,12 +969,12 @@ /***/ }, - /***/ 4064: /***/ ( + /***/ 3515: /***/ ( module, __unused_webpack_exports, __webpack_require__ ) => { - module.exports = __webpack_require__(1104); + module.exports = __webpack_require__(2870); /***/ }, @@ -984,7 +984,7 @@ /******/ var __webpack_exec__ = (moduleId) => __webpack_require__((__webpack_require__.s = moduleId)); /******/ __webpack_require__.O(0, [2888, 9774, 179], () => - __webpack_exec__(774) + __webpack_exec__(3834) ); /******/ var __webpack_exports__ = __webpack_require__.O(); /******/ _N_E = __webpack_exports__; ```

Commit: 408b162cee0fb7c54e38d878f438f94c13106587

eric-burel commented 3 days ago

Is it possible to derive this value from a function provided by an unbundled third party library written in TS? And will this requiredEnv chack run client side?

We have this use case in the State of JS monorepo where we have to manage many variables for multiple apps. We currently export a getConfig function from a shared package, that also run a check that all variables are there for the current application. We write our packages in TypeScript and let Next.js bundle them, in order to avoid having 2 builds systems for internal packages that will be bundled anyway. Here is the said shared package (notice our package.json exports map that uses .ts).

It is called during instrumentation and in the root RSC layout so it's called both server-side and client-side.

Maybe a 3rd party package with a more explicit call could be useful, rather than aiming for a Next.js specific function?

devjiwonchoi commented 3 days ago

@eric-burel I'm currently improving the design of requiredEnv, would love to hear more about your use case.

Current requiredEnv check runs on the loaded envs during the build (initial part of the build), which includes all the process.env values at that span.

So if the value is not loaded during initial part of the build time, it won't be known. Are the values retrieved from getConfig won't be known until been called?

Since Next.js by default loads env values from .env, .env.local, etc. these values are the target ATM.

devjiwonchoi commented 3 days ago

I feel like this could be handled if https://github.com/vercel/next.js/pull/63051 is merged, we can import typescript functions like getConfig and just map the values to the config, right?

eric-burel commented 2 days ago

@devjiwonchoi the env variables are still loaded as usual using dotenv files, the getConfig helpers plays a role quite similar to your proposal by checking if the right environment variables are indeed loaded, except that it works across multiple apps. I've also added some logic to be able to feed import.meta.env to the system manually, this is needed for Vite-based frameworks such as Astro (up-to-date code is here). We support optional environment variables as well. The loading of required env variable is synchronous because we get it from a local yaml config, but we could imagine an async system if config is stored into a database.