next.js
next.js copied to clipboard
feat: `requiredEnv`
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
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/[email protected]/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
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?
@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.
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?
@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.
@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?
Failing test suites
Commit: 7ab4d605fb6419897b87fb7a3bc2cbcc212294bc
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.
Any update? Would love this feature <3
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.
@devjiwonchoi is this feature no longer planned for inclusion in Next.js?
Will come up with a better usage, possibly pairing up with typedEnv. :)
@devjiwonchoi nice! Maybe you can add a link to the PR conversation here once you have an issue or PR open? Would love to follow along and try it out when it's available.