@vineforecast/next-public-env 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # Next.js Public Runtime Environment
2
+
3
+ `next-public-env` is a lightweight utility that dynamically injects environment
4
+ variables into your Next.js application at *runtime* instead of just at build time.
5
+
6
+ ## The Problem
7
+
8
+ Next.js's standard approach bakes environment variables into your application
9
+ during the build process through `NEXT_PUBLIC_` variables. This means you need a
10
+ separate build for each environment; one for development, another for staging,
11
+ and yet another for production. This violates the "build once, deploy many"
12
+ principle and creates unnecessary complexity in your deployment pipeline.
13
+
14
+ ## Features
15
+
16
+ - **Type Safety & Validation:** Integrates with Zod for schema validation, type
17
+ coercion, and full TypeScript support.
18
+ - **Error-Resilient:** Environment variables remain accessible even when pages
19
+ throw unhandled errors.
20
+ - **Universal API:** Use the same `getPublicEnv()` function in both Server and
21
+ Client Components.
22
+ - **Lightweight:** Adds only ~275 bytes to your client bundle.
23
+
24
+ ## Installation
25
+
26
+ Install it via your preferred package manager:
27
+
28
+ ```bash
29
+ yarn add next-public-env
30
+ ```
31
+ ```bash
32
+ pnpm add next-public-env
33
+ ```
34
+ ```bash
35
+ npm install next-public-env
36
+ ```
37
+
38
+ ## Getting Started
39
+
40
+ ### 1. Define Your Environment Config
41
+
42
+ Create a file to configure your public environment variables (e.g.,
43
+ `public-env.ts`).
44
+
45
+ **Basic (Type-Safe):**
46
+ ```ts
47
+ // public-env.ts
48
+ import { createPublicEnv } from 'next-public-env';
49
+
50
+ export const { getPublicEnv, PublicEnv } = createPublicEnv({
51
+ NODE_ENV: process.env.NODE_ENV,
52
+ API_URL: process.env.API_URL,
53
+ MAINTENANCE_MODE: process.env.MAINTENANCE_MODE === 'true',
54
+ });
55
+ ```
56
+
57
+ **With Zod Validation (Recommended):**
58
+ ```ts
59
+ // public-env.ts
60
+ import { createPublicEnv } from 'next-public-env';
61
+
62
+ export const { getPublicEnv, PublicEnv } = createPublicEnv(
63
+ {
64
+ NODE_ENV: process.env.NODE_ENV,
65
+ API_URL: process.env.API_URL,
66
+ MAINTENANCE_MODE: process.env.MAINTENANCE_MODE,
67
+ PORT: process.env.PORT,
68
+ },
69
+ {
70
+ schema: (z) => ({
71
+ NODE_ENV: z.enum(['development', 'production', 'test']),
72
+ API_URL: z.string().url(),
73
+ MAINTENANCE_MODE: z.enum(['on', 'off']).default('off'),
74
+ PORT: z.coerce.number().default(3000), // Converts string to number
75
+ }),
76
+ }
77
+ );
78
+ ```
79
+
80
+ ### 2. Add to Root Layout (App Router)
81
+
82
+ Place `<PublicEnv />` in your root layout to make variables available
83
+ client-side:
84
+
85
+ ```tsx
86
+ // app/layout.tsx
87
+ import { PublicEnv } from './public-env';
88
+
89
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
90
+ return (
91
+ <html lang="en">
92
+ <body>
93
+ <PublicEnv />
94
+ {children}
95
+ </body>
96
+ </html>
97
+ );
98
+ }
99
+ ```
100
+
101
+ ### 2b. Add to _document (Pages Router)
102
+
103
+ If you are using the Pages Router, render `<PublicEnvScript />` in your custom
104
+ `_document.tsx` to inject the runtime variables:
105
+
106
+ ```tsx
107
+ // pages/_document.tsx
108
+ import Document, { Html, Head, Main, NextScript } from 'next/document';
109
+ import { PublicEnvScript } from '../public-env';
110
+
111
+ export default class MyDocument extends Document {
112
+ render() {
113
+ return (
114
+ <Html lang="en">
115
+ <Head />
116
+ <body>
117
+ <PublicEnvScript />
118
+ <Main />
119
+ <NextScript />
120
+ </body>
121
+ </Html>
122
+ );
123
+ }
124
+ }
125
+ ```
126
+
127
+ ### 3. Use Anywhere
128
+
129
+ Access your environment variables with full type safety:
130
+
131
+ ```tsx
132
+ // Server Component
133
+ import { getPublicEnv } from './public-env';
134
+
135
+ export default function ServerPage() {
136
+ const env = getPublicEnv();
137
+ return <div>API URL: {env.API_URL}</div>;
138
+ }
139
+
140
+ // Client Component
141
+ 'use client';
142
+ import { getPublicEnv } from './public-env';
143
+
144
+ export function ClientComponent() {
145
+ const env = getPublicEnv();
146
+ return <div>API URL: {env.API_URL}</div>;
147
+ }
148
+ ```
149
+
150
+ ## Rendering Behavior
151
+
152
+ ### Default: Dynamic Rendering
153
+
154
+ When you use `getPublicEnv()` in a Server Component, that route automatically
155
+ switches to **dynamic rendering**. This ensures your environment variables are
156
+ always read fresh from the server at request time, rather than being cached at
157
+ build time.
158
+
159
+ ### Advanced: Manual Rendering Control
160
+
161
+ For specific use cases, you can override this behavior using the
162
+ `dynamicRendering` option. Set it to `'manual'` to disable the automatic
163
+ `noStore()` call and take full control of your routes' rendering behavior.
164
+
165
+
166
+
167
+ ## Cache Components Support
168
+
169
+ Next.js Cache Components (enabled via `cacheComponents: true` in
170
+ `next.config.js`) prerender routes into a static HTML shell by default. This
171
+ means that environment variables are read at build time, which defeats the
172
+ purpose of `next-public-env`.
173
+
174
+ To ensure your environment variables are read at runtime, you must opt-out of
175
+ the static shell by making your component dynamic. You can do this by using any
176
+ [runtime API](https://nextjs.org/docs/app/getting-started/cache-components#runtime-data)(like
177
+ `headers()`, `cookies()`, etc.) or by using the `getPublicEnvAsync()` function
178
+ provided by this library which automatically calls `await connection()` to
179
+ opt-out of the static shell.
180
+
181
+ ### Using `getPublicEnvAsync()`
182
+
183
+ `getPublicEnvAsync()` is a helper function that automatically calls `await
184
+ connection()` to opt-out of the static shell and returns your environment
185
+ variables.
186
+
187
+ > **Important:** Components using `getPublicEnvAsync()` (or any runtime API)
188
+ > should be wrapped in a `<Suspense>` boundary to allow the rest of the page to be
189
+ > prerendered.
190
+
191
+ ```tsx
192
+ import { Suspense } from 'react';
193
+ import { getPublicEnvAsync } from './public-env';
194
+
195
+ async function EnvComponent() {
196
+ const env = await getPublicEnvAsync();
197
+ return <div>API URL: {env.API_URL}</div>;
198
+ }
199
+
200
+ export default function Page() {
201
+ return (
202
+ <div>
203
+ <h1>My Page</h1>
204
+ <Suspense fallback={<div>Loading env...</div>}>
205
+ <EnvComponent />
206
+ </Suspense>
207
+ </div>
208
+ );
209
+ }
210
+ ```
211
+
212
+ ## API Reference
213
+
214
+ ### `createPublicEnv(publicEnv, options)`
215
+
216
+ This is the main function used to configure the library.
217
+
218
+ #### **Parameters**
219
+
220
+ | Parameter | Type | Required? | Description |
221
+ | :---------- | :------- | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | `publicEnv` | `object` | Yes | An object that explicitly defines the variables and values to be made available. This acts as an allowlist, ensuring no other `process.env` variables are exposed. |
223
+ | `options` | `object` | No | An optional object for advanced configuration like schema validation and rendering behavior. |
224
+
225
+ #### **Options Object Properties**
226
+
227
+ | Property | Type | Description |
228
+ | :-------------------- | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
229
+ | `schema` | `(z) => ZodObject` | An optional function that receives the Zod library (`z`) as an argument and returns a Zod schema object. The keys in the schema must match the keys in your `publicEnv` object. Used for validation, type coercion, and setting defaults. |
230
+ | `validateAtBuildStep` | `boolean` | If `true`, the library validates your `publicEnv` object against the schema during `next build`. Useful for failing builds early in CI/CD. **Default: `false`**. |
231
+ | `dynamicRendering` | `'auto'` \| `'manual'` | Controls the dynamic rendering behavior. `'auto'` (default) automatically opts-out of static rendering by calling `noStore()`. `'manual'` requires you to manage rendering behavior yourself. **Warning:** Using `manual` incorrectly can lead to undefined variables. **Default: `'auto'`**. |
232
+
233
+ #### **Returns**
234
+
235
+ The function returns an object containing the `getPublicEnv` function and the
236
+ `PublicEnv` component.
237
+
238
+ | Property | Type | Description |
239
+ | :------------ | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- |
240
+ | `getPublicEnv`| `() => EnvObject` | A function that returns your public environment variables. The return type is inferred from your `publicEnv` object and Zod schema. |
241
+ | `getPublicEnvAsync`| `() => Promise<EnvObject>` | An async function that returns your public environment variables and opts-out of static rendering by calling `await connection()`. |
242
+ | `PublicEnv` | `React.Component` | A React component that must be rendered in your root layout to inject the environment variables for client-side access. |
243
+ | `PublicEnvScript` | `React.Component` | A React component that must be rendered in `pages/_document.tsx` to inject the environment variables for client-side access. |
@@ -0,0 +1,12 @@
1
+ declare function createPublicEnv(): {
2
+ getPublicEnv(): Record<string, any>;
3
+ getPublicEnvAsync(): Promise<never>;
4
+ PublicEnv(): null;
5
+ };
6
+ declare global {
7
+ interface Window {
8
+ __NEXT_PUBLIC_ENV?: Record<string, any>;
9
+ }
10
+ }
11
+
12
+ export { createPublicEnv };
@@ -0,0 +1 @@
1
+ function n(){return{getPublicEnv(){return"__NEXT_PUBLIC_ENV"in window||console.error('"__NEXT_PUBLIC_ENV" was not found on Window. Did you forget to render <PublicEnv /> in your root Layout or <PublicEnvScript /> in your _document?'),window.__NEXT_PUBLIC_ENV||{}},async getPublicEnvAsync(){throw new Error("getPublicEnvAsync is not supported on the client. Use getPublicEnv instead.")},PublicEnv(){return null}}}export{n as createPublicEnv};
@@ -0,0 +1 @@
1
+ var n='function i(n){window.__NEXT_PUBLIC_ENV||Object.defineProperty(window,"__NEXT_PUBLIC_ENV",{value:Object.freeze(n),enumerable:!0})}';function t(e){return`(${n})(${e});`}export{t as a};
@@ -0,0 +1,6 @@
1
+ declare const FlushConfig: React.FC<{
2
+ config: string;
3
+ nonce?: string;
4
+ }>;
5
+
6
+ export { FlushConfig };
@@ -0,0 +1 @@
1
+ "use client";import{a as e}from"../chunk-CPE4ZGRB.js";import{useServerInsertedHTML as s}from"next/navigation";import{useRef as c}from"react";import{jsx as u}from"react/jsx-runtime";var f=({config:r,nonce:n})=>{let t=c(!1);return s(()=>{if(t.current)return null;t.current=!0;let o=e(r);return u("script",{dangerouslySetInnerHTML:{__html:o},type:"text/javascript",nonce:n})}),null};export{f as FlushConfig};
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod/v4';
2
+ import * as z4 from 'zod/v4/core';
3
+
4
+ declare global {
5
+ interface Window {
6
+ __NEXT_PUBLIC_ENV?: Record<string, any>;
7
+ }
8
+ }
9
+ type SchemaShapeFactory<Shape extends z4.$ZodShape = z4.$ZodShape> = (zod: typeof z) => Shape;
10
+ type Options<Shape extends z4.$ZodShape> = {
11
+ /**
12
+ * A factory function that receives zod and returns an object shape.
13
+ * The shape will be wrapped with z.object() internally.
14
+ *
15
+ * @example
16
+ * schema: (z) => ({
17
+ * NODE_ENV: z.enum(['development', 'production']),
18
+ * API_URL: z.string().url()
19
+ * })
20
+ */
21
+ schema?: SchemaShapeFactory<Shape>;
22
+ /**
23
+ * Next.js statically generates 404 and other static pages at build time.
24
+ *
25
+ * By default, next-public-env validation runs only at runtime (next start)
26
+ * when the process starts since some environment variables may not be
27
+ * available at build time.
28
+ *
29
+ * To also run validation at build time (next build), set this to `true`.
30
+ * @default false
31
+ */
32
+ validateAtBuildStep?: boolean;
33
+ /**
34
+ * Controls how `getPublicEnv` opts-out of Next.js' static rendering.
35
+ *
36
+ * - `auto`: (Default) Automatically calls `noStore()` from `next/cache`
37
+ * wherever `getPublicEnv()` is used. This ensures the route is
38
+ * dynamically rendered, which is crucial for accessing environment
39
+ * variables that are only available at runtime.
40
+ *
41
+ * - `manual`: Disables the automatic `noStore()` call. Use this if you
42
+ * want to manually control the rendering behavior of your routes.
43
+ *
44
+ * @warning When set to `manual`, you are responsible for ensuring that
45
+ * routes using `getPublicEnv()` are not statically built, as this could
46
+ * lead to runtime environment variables being undefined.
47
+ *
48
+ * @default 'auto'
49
+ */
50
+ dynamicRendering?: 'auto' | 'manual';
51
+ };
52
+ type PublicEnvType = (props: {
53
+ nonce?: string;
54
+ }) => Promise<React.ReactElement>;
55
+ type PublicEnvScriptType = (props: {
56
+ nonce?: string;
57
+ }) => React.ReactElement;
58
+ declare function createPublicEnv<Shape extends z4.$ZodShape, T extends Record<keyof Shape, any>>(values: T, options: Options<Shape> & {
59
+ schema: SchemaShapeFactory<Shape>;
60
+ }): {
61
+ getPublicEnv: () => z4.infer<z4.$ZodObject<Shape>>;
62
+ getPublicEnvAsync: () => Promise<z4.infer<z4.$ZodObject<Shape>>>;
63
+ PublicEnv: PublicEnvType;
64
+ PublicEnvScript: PublicEnvScriptType;
65
+ };
66
+ declare function createPublicEnv<T extends Record<string, any>>(values: T, options?: Omit<Options<never>, 'schema'>): {
67
+ getPublicEnv: () => T;
68
+ getPublicEnvAsync: () => Promise<T>;
69
+ PublicEnv: PublicEnvType;
70
+ PublicEnvScript: PublicEnvScriptType;
71
+ };
72
+
73
+ export { createPublicEnv };
@@ -0,0 +1,3 @@
1
+ import{a as S}from"../chunk-CPE4ZGRB.js";import{z as m}from"zod/v4";import*as h from"zod/v4/core";import{FlushConfig as T}from"./FlushConfig";import{jsx as g}from"react/jsx-runtime";var f=({config:e,nonce:r})=>{let t=S(e);return g("script",{dangerouslySetInnerHTML:{__html:t},type:"text/javascript",nonce:r})};import{PHASE_PRODUCTION_BUILD as O}from"next/constants";import{Suspense as _}from"react";import*as d from"next/server";import{unstable_noStore as P}from"next/cache";async function i(e){if(P(),e)return d.connection()}import{jsx as o}from"react/jsx-runtime";function C(e){return`\u274C Invalid environment variables found:
2
+ ${e.issues.map(t=>`- Invalid variable [${t.path.join(".")||"configuration"}]: ${t.message}`).join(`
3
+ `)}`}function E(){if(typeof window<"u")throw new Error("This wasn't supposed to happen. This file should only be resolved on the server.")}function H(e,r){let t=r?.schema,s=process.env.NEXT_PHASE===O,c=(()=>{if(!t||s&&!r?.validateAtBuildStep)return e;let n=t(m),b=m.object(n),a=h.safeParse(b,e);if(!a.success){let y=C(a.error);throw new Error(y)}return a.data})(),p=JSON.stringify(c),v=r?.dynamicRendering??"auto",u=process.env.__NEXT_CACHE_COMPONENTS==="true"||process.env.__NEXT_CACHE_COMPONENTS===!0,l=async({nonce:n})=>(v==="auto"&&await i(u),o(T,{config:p,nonce:n}));return{getPublicEnv(){return E(),i(),c},async getPublicEnvAsync(){return E(),await i(!0),c},async PublicEnv({nonce:n}){return u?o(_,{fallback:null,children:o(l,{nonce:n})}):o(l,{nonce:n})},PublicEnvScript({nonce:n}){return o(f,{config:p,nonce:n})}}}export{H as createPublicEnv};
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@vineforecast/next-public-env",
3
+ "description": "Manage type-safe runtime environment variables in Next.js for both the server and the client.",
4
+ "version": "1.2.0",
5
+ "author": "alizeait",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "types": "./dist/server/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "node": "./dist/server/index.js",
12
+ "browser": "./dist/browser/index.js",
13
+ "types": "./dist/server/index.d.ts",
14
+ "default": "./dist/server/index.js"
15
+ }
16
+ },
17
+ "typesVersions": {
18
+ "*": {
19
+ "*": [
20
+ "./dist/server/index.d.ts"
21
+ ]
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "test:e2e": "playwright test",
32
+ "check-types": "tsc --noEmit"
33
+ },
34
+ "peerDependencies": {
35
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
36
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
37
+ "zod": "^3.25.0 || ^4.0.0"
38
+ },
39
+ "dependencies": {
40
+ "zod": "^3.25.0 || ^4.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@playwright/test": "^1.56.1",
44
+ "@testing-library/dom": "^10.4.1",
45
+ "@testing-library/react": "^16.3.0",
46
+ "@types/node": "^22.9.0",
47
+ "@types/react": "^19.2.2",
48
+ "jsdom": "^27.0.1",
49
+ "next": "^16.0.3",
50
+ "oxlint": "^1.24.0",
51
+ "prettier": "^3.6.2",
52
+ "react": "^19.2.0",
53
+ "react-dom": "^19.2.0",
54
+ "tsup": "^8.5.0",
55
+ "typescript": "^5.9.3",
56
+ "vitest": "^4.0.1",
57
+ "zod": "^3.25.0 || ^4.0.0"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/alizeait/next-public-env.git"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/alizeait/next-public-env/issues"
65
+ },
66
+ "homepage": "https://github.com/alizeait/next-public-env#readme",
67
+ "keywords": [
68
+ "nextjs",
69
+ "react",
70
+ "zod"
71
+ ],
72
+ "engines": {
73
+ "node": ">=20.0.0"
74
+ }
75
+ }