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

feat: requiredEnv #67187

Draft
wants to merge 5 commits into
base: canary
Choose a base branch
from
Draft

Conversation

devjiwonchoi
Copy link
Member

@devjiwonchoi devjiwonchoi commented Jun 25, 2024

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
Copy link
Member

ijjk commented Jun 25, 2024

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.6s N/A
buildDurationCached 8.3s 7s N/A
nodeModulesSize 359 MB 359 MB ⚠️ +7.9 kB
nextStartRea..uration (ms) 422ms 431ms N/A
Client Bundles (main, webpack)
vercel/next.js canary devjiwonchoi/next.js feat/required-env Change
1780.HASH.js gzip 167 B 167 B
5453-HASH.js gzip 37.4 kB 37.4 kB
7514-HASH.js gzip 5.06 kB 5.05 kB N/A
a7a62840-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 219 B 219 B
main-HASH.js gzip 32.3 kB 32.3 kB N/A
webpack-HASH.js gzip 1.71 kB 1.71 kB
Overall change 39.5 kB 39.5 kB
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 193 B 193 B
_error-HASH.js gzip 192 B 192 B
amp-HASH.js gzip 509 B 510 B N/A
css-HASH.js gzip 341 B 341 B
dynamic-HASH.js gzip 2.52 kB 2.52 kB
edge-ssr-HASH.js gzip 264 B 266 B N/A
head-HASH.js gzip 362 B 363 B N/A
hooks-HASH.js gzip 391 B 390 B N/A
image-HASH.js gzip 4.26 kB 4.26 kB
index-HASH.js gzip 268 B 268 B
link-HASH.js gzip 2.69 kB 2.69 kB N/A
routerDirect..HASH.js gzip 326 B 325 B N/A
script-HASH.js gzip 396 B 397 B N/A
withRouter-HASH.js gzip 320 B 321 B N/A
1afbb74e6ecf..834.css gzip 106 B 106 B
Overall change 7.89 kB 7.89 kB
Client Build Manifests
vercel/next.js canary devjiwonchoi/next.js feat/required-env Change
_buildManifest.js gzip 485 B 483 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 521 B 525 B N/A
link.html gzip 538 B 539 B N/A
withRouter.html gzip 519 B 521 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 660 B 660 B
middleware-r..fest.js gzip 156 B 155 B N/A
middleware.js gzip 29.7 kB 29.7 kB N/A
edge-runtime..pack.js gzip 1.03 kB 1.03 kB
Overall change 1.69 kB 1.69 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 108 kB 108 kB
app-route-ex...dev.js gzip 23.3 kB 23.3 kB
app-route-ex..prod.js gzip 18.7 kB 18.7 kB
app-route-tu..prod.js gzip 18.7 kB 18.7 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.6 kB 9.6 kB
pages-api.ru...dev.js gzip 9.87 kB 9.87 kB
pages-api.ru..prod.js gzip 9.59 kB 9.59 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 56.6 kB 56.6 kB
Overall change 1.1 MB 1.1 MB
build cache Overall increase ⚠️
vercel/next.js canary devjiwonchoi/next.js feat/required-env Change
0.pack gzip 1.68 MB 1.68 MB ⚠️ +1.75 kB
index.pack gzip 131 kB 132 kB ⚠️ +971 B
Overall change 1.81 MB 1.81 MB ⚠️ +2.72 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],
   {
-    /***/ 1362: /***/ (
+    /***/ 9618: /***/ (
       __unused_webpack_module,
       __unused_webpack_exports,
       __webpack_require__
@@ -9,7 +9,7 @@
       (window.__NEXT_P = window.__NEXT_P || []).push([
         "/image",
         function () {
-          return __webpack_require__(2160);
+          return __webpack_require__(699);
         },
       ]);
       if (false) {
@@ -18,7 +18,7 @@
       /***/
     },
 
-    /***/ 537: /***/ (module, exports, __webpack_require__) => {
+    /***/ 9451: /***/ (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__(7092)
+        __webpack_require__(6490)
       );
-      const _getimgprops = __webpack_require__(9834);
-      const _imageconfig = __webpack_require__(5676);
-      const _imageconfigcontextsharedruntime = __webpack_require__(387);
-      const _warnonce = __webpack_require__(451);
-      const _routercontextsharedruntime = __webpack_require__(5357);
+      const _getimgprops = __webpack_require__(3646);
+      const _imageconfig = __webpack_require__(535);
+      const _imageconfigcontextsharedruntime = __webpack_require__(4724);
+      const _warnonce = __webpack_require__(6321);
+      const _routercontextsharedruntime = __webpack_require__(1759);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(3945)
+        __webpack_require__(1882)
       );
       // This is replaced by webpack define plugin
       const configEnv = {
@@ -376,7 +376,7 @@
       /***/
     },
 
-    /***/ 9834: /***/ (
+    /***/ 3646: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -392,9 +392,9 @@
           return getImgProps;
         },
       });
-      const _warnonce = __webpack_require__(451);
-      const _imageblursvg = __webpack_require__(3547);
-      const _imageconfig = __webpack_require__(5676);
+      const _warnonce = __webpack_require__(6321);
+      const _imageblursvg = __webpack_require__(8297);
+      const _imageconfig = __webpack_require__(535);
       const VALID_LOADING_VALUES =
         /* unused pure expression or super */ null && [
           "lazy",
@@ -766,7 +766,7 @@
       /***/
     },
 
-    /***/ 3547: /***/ (__unused_webpack_module, exports) => {
+    /***/ 8297: /***/ (__unused_webpack_module, exports) => {
       "use strict";
       /**
        * A shared function, used on both client and server, to generate a SVG blur placeholder.
@@ -821,7 +821,7 @@
       /***/
     },
 
-    /***/ 6850: /***/ (
+    /***/ 973: /***/ (
       __unused_webpack_module,
       exports,
       __webpack_require__
@@ -848,10 +848,10 @@
         },
       });
       const _interop_require_default = __webpack_require__(1478);
-      const _getimgprops = __webpack_require__(9834);
-      const _imagecomponent = __webpack_require__(537);
+      const _getimgprops = __webpack_require__(3646);
+      const _imagecomponent = __webpack_require__(9451);
       const _imageloader = /*#__PURE__*/ _interop_require_default._(
-        __webpack_require__(3945)
+        __webpack_require__(1882)
       );
       function getImageProps(imgProps) {
         const { props } = (0, _getimgprops.getImgProps)(imgProps, {
@@ -883,7 +883,7 @@
       /***/
     },
 
-    /***/ 3945: /***/ (__unused_webpack_module, exports) => {
+    /***/ 1882: /***/ (__unused_webpack_module, exports) => {
       "use strict";
 
       Object.defineProperty(exports, "__esModule", {
@@ -918,7 +918,7 @@
       /***/
     },
 
-    /***/ 2160: /***/ (
+    /***/ 699: /***/ (
       __unused_webpack_module,
       __webpack_exports__,
       __webpack_require__
@@ -935,8 +935,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/next@file+..+main-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-6230622a1a-20240610__zehaskxadtwcczqqbmt6koh6bq/node_modules/next/image.js
-      var next_image = __webpack_require__(6793);
+      // EXTERNAL MODULE: ./node_modules/.pnpm/next@file+..+diff-repo+packages+next+next-packed.tgz_react-dom@19.0.0-rc-6230622a1a-20240610__uulzbengwsfwhwaa2ambxampcy/node_modules/next/image.js
+      var next_image = __webpack_require__(1428);
       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",
@@ -966,12 +966,12 @@
       /***/
     },
 
-    /***/ 6793: /***/ (
+    /***/ 1428: /***/ (
       module,
       __unused_webpack_exports,
       __webpack_require__
     ) => {
-      module.exports = __webpack_require__(6850);
+      module.exports = __webpack_require__(973);
 
       /***/
     },
@@ -981,7 +981,7 @@
     /******/ var __webpack_exec__ = (moduleId) =>
       __webpack_require__((__webpack_require__.s = moduleId));
     /******/ __webpack_require__.O(0, [2888, 9774, 179], () =>
-      __webpack_exec__(1362)
+      __webpack_exec__(9618)
     );
     /******/ var __webpack_exports__ = __webpack_require__.O();
     /******/ _N_E = __webpack_exports__;
Diff for main-HASH.js

Diff too large to display

Commit: 7ab4d60

@devjiwonchoi devjiwonchoi marked this pull request as draft June 25, 2024 17:16
@eric-burel
Copy link
Contributor

eric-burel commented Jun 26, 2024

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
Copy link
Member Author

@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
Copy link
Member Author

I feel like this could be handled if #63051 is merged, we can import typescript functions like getConfig and just map the values to the config, right?

@eric-burel
Copy link
Contributor

eric-burel commented Jun 26, 2024

@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.

@karlhorky
Copy link
Contributor

@devjiwonchoi The idea for this PR sounds great!

If I'm understanding correctly, this would also replace dotenv-safe - maybe this should be added to the docs or release notes, to make it clear to users that they no longer need extra packages like this?

@ijjk
Copy link
Member

ijjk commented Jul 11, 2024

Failing test suites

Commit: 7ab4d60

TURBOPACK=1 pnpm test-start test/production/app-dir/required-env/required-env.test.ts (turbopack)

  • required-env > should only log app routes
Expand output

● required-env › should only log app routes

expect(received).toBe(expected) // Object.is equality

Expected: 1
Received: 0

   6 |     result = await nextBuild(__dirname, [], { stderr: true })
   7 |
>  8 |     expect(result.code).toBe(1)
     |                         ^
   9 |
  10 |     expect(result.stderr).toContain('Missing required environment variables')
  11 |     expect(result.stderr).toContain('MISSING_ENV')

  at Object.toBe (production/app-dir/required-env/required-env.test.ts:8:25)

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

@Netail
Copy link
Contributor

Netail commented Aug 9, 2024

Any update? Would love this feature <3

@eric-burel
Copy link
Contributor

Small up, Astro 5.0 is bringing a similar feature, with a typesafe environment schema, coupled with a virtual module to import the typesafe config file generated out of the environment variables.
As a project maintainer: I'd really like to see something like that but not specific to Next.js or Astro, like adding a schema system to dotenv.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants