Skip to content

Commit 094fb24

Browse files
committed
fix(nextjs): resolve bundle size limit violation and implicitly share NODE_ENV in flat layout
1 parent 76b99f8 commit 094fb24

7 files changed

Lines changed: 45 additions & 103 deletions

File tree

.changeset/flat-layout-mode.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@ export const env = arkenv({
1414
DATABASE_URL: "string",
1515
NEXT_PUBLIC_API_URL: "string",
1616
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
17-
}, {
18-
shared: ["NODE_ENV"]
1917
});
2018
```
2119

22-
- Filter client-safe keys starting with `NEXT_PUBLIC_` and shared keys specified in `options.shared`.
20+
- Filter client-safe keys starting with `NEXT_PUBLIC_` and shared keys specified in `options.shared`. `NODE_ENV` is implicitly shared.
2321
- Exclude server-only keys from autocomplete on the client using TypeScript `Pick`.
24-
- Wrap the client-side returned variables in a Proxy that throws an error at runtime when a server-only variable is accessed on the client.
22+
- Wrap the returned variables in a Proxy that throws an error at runtime when a server-only variable is accessed on the client.
2523
- Update CLI scaffolding to generate the Flat layout by default.

packages/cli/src/features/scaffold/env-template.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe("env-template", () => {
5050
expect(template).toContain("DATABASE_URL:");
5151
expect(template).toContain("NEXT_PUBLIC_API_URL:");
5252
expect(template).toContain("NODE_ENV:");
53-
expect(template).toContain('shared: ["NODE_ENV"]');
53+
expect(template).not.toContain("shared:");
5454
expect(template).not.toContain("runtimeEnv:");
5555
});
5656

packages/cli/src/features/scaffold/templates/nextjs-template.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,9 @@ export function buildNextjsTemplate(
9797
if (envKeys && envKeys.length > 0) {
9898
for (const key of envKeys) {
9999
if (key === "NODE_ENV") {
100-
sharedKeyNames.push(key);
100+
// NODE_ENV is implicitly shared, no need to list it in options.shared
101101
}
102102
}
103-
} else {
104-
sharedKeyNames.push("NODE_ENV");
105103
}
106104

107105
const optionParts: string[] = [];

packages/nextjs/src/config.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,11 @@ describe("codegen process", () => {
211211

212212
// Check warning header
213213
expect(generatedContent).not.toContain("// @ts-nocheck");
214-
expect(generatedContent).toContain("This file is auto-generated by ArkEnv");
214+
expect(generatedContent).toContain("Generated by ArkEnv. DO NOT EDIT DIRECTLY.");
215215

216216
// Check exports and wrapper types
217217
expect(generatedContent).toContain("export function createEnv<");
218-
expect(generatedContent).toContain("const arkenv = createEnv;");
219-
expect(generatedContent).toContain("export default arkenv;");
218+
expect(generatedContent).toContain("export default createEnv;");
220219

221220
// Check that relative path import was resolved correctly (relative from __temp_tests__/env.gen.ts to __temp_tests__/env.ts is ./env)
222221
// Wait, path.relative(__temp_tests__, __temp_tests__/env.ts) is "env.ts", which normalizes to "./env"
@@ -382,8 +381,7 @@ describe("withArkEnv wrapper", () => {
382381

383382
const generatedContent = fs.readFileSync(genPath, "utf-8");
384383
expect(generatedContent).toContain("export function createEnv<");
385-
expect(generatedContent).toContain("const arkenv = createEnv;");
386-
expect(generatedContent).toContain("export default arkenv;");
384+
expect(generatedContent).toContain("export default createEnv;");
387385
expect(generatedContent).toContain(
388386
'NEXT_PUBLIC_API_URL: typeof window !== "undefined" ? (globalThis as any).__arkenv_env__?.NEXT_PUBLIC_API_URL ?? process.env.NEXT_PUBLIC_API_URL : process.env.NEXT_PUBLIC_API_URL,',
389387
);

packages/nextjs/src/config.ts

Lines changed: 36 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -375,21 +375,15 @@ export function extractKeys(content: string): {
375375
if (args.optionsArg) {
376376
const sharedMatch = args.optionsArg.match(/shared\s*:\s*\[([\s\S]*?)\]/);
377377
if (sharedMatch) {
378-
const arrayContent = sharedMatch[1];
379-
const stringRegex =
380-
/(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|`([^`\\]*(?:\\.[^`\\]*)*)`)/g;
381-
const matches = arrayContent.matchAll(stringRegex);
378+
const matches = sharedMatch[1].matchAll(/['"`](.*?)['"`]/g);
382379
for (const match of matches) {
383-
const val = match[1] || match[2] || match[3];
384-
if (val) {
385-
optionSharedKeys.push(val);
386-
}
380+
optionSharedKeys.push(match[1]);
387381
}
388382
}
389383
}
390384

391385
for (const key of topKeys) {
392-
if (optionSharedKeys.includes(key)) {
386+
if (optionSharedKeys.includes(key) || key === "NODE_ENV") {
393387
sharedKeys.push(key);
394388
} else if (key.startsWith("NEXT_PUBLIC_")) {
395389
clientKeys.push(key);
@@ -401,35 +395,45 @@ export function extractKeys(content: string): {
401395
}
402396

403397
/**
404-
* Generate the TypeScript factory code for the tailored createEnv helper.
405-
*
406-
* @param clientKeys The client environment variable keys
407-
* @param sharedKeys The shared environment variable keys
408-
* @returns The generated TypeScript source code string
398+
* Generate the triple-tab indented runtime environment variables mapping.
409399
*/
410-
function generateFactoryCode(
400+
function generateRuntimeEnvLines(
411401
clientKeys: string[],
412402
sharedKeys: string[],
413403
): string {
414404
const allKeys = Array.from(new Set([...clientKeys, ...sharedKeys]));
415-
const runtimeEnvLines = allKeys
405+
return allKeys
416406
.map(
417407
(key) =>
418408
`\t\t\t${key}: typeof window !== "undefined" ? (globalThis as any).__arkenv_env__?.${key} ?? process.env.${key} : process.env.${key},`,
419409
)
420410
.join("\n");
411+
}
421412

422-
return `/* eslint-disable */
423-
// prettier-ignore
413+
const GENERATED_HEADER = `/* eslint-disable */
424414
// biome-ignore format: auto-generated
415+
// Generated by ArkEnv. DO NOT EDIT DIRECTLY.
416+
`;
417+
418+
const GENERATED_FOOTER = `
419+
export default createEnv;
420+
`;
421+
425422
/**
426-
* @file env.gen.ts
427-
* @note This file is auto-generated by ArkEnv. DO NOT EDIT DIRECTLY.
428-
* @see https://arkenv.js.org
423+
* Generate the TypeScript factory code for the tailored createEnv helper.
424+
*
425+
* @param clientKeys The client environment variable keys
426+
* @param sharedKeys The shared environment variable keys
427+
* @returns The generated TypeScript source code string
429428
*/
429+
function generateFactoryCode(
430+
clientKeys: string[],
431+
sharedKeys: string[],
432+
): string {
433+
const runtimeEnvLines = generateRuntimeEnvLines(clientKeys, sharedKeys);
430434

435+
return `${GENERATED_HEADER}
431436
import { createEnv as coreCreateEnv } from "@arkenv/nextjs";
432-
import type { Infer } from "@arkenv/nextjs";
433437
434438
export { type } from "@arkenv/nextjs";
435439
@@ -443,18 +447,15 @@ export function createEnv<
443447
[K in keyof TClient]: K extends \`NEXT_PUBLIC_\${string}\` ? unknown : never;
444448
};
445449
shared?: TShared;
446-
}): Readonly<Infer<TServer & TClient & TShared>> {
450+
}) {
447451
return coreCreateEnv({
448452
...options,
449453
runtimeEnv: {
450454
${runtimeEnvLines}
451455
},
452456
} as any) as any;
453457
}
454-
455-
const arkenv = createEnv;
456-
export default arkenv;
457-
`;
458+
${GENERATED_FOOTER}`;
458459
}
459460

460461
/**
@@ -464,25 +465,10 @@ function generateFlatFactoryCode(
464465
clientKeys: string[],
465466
sharedKeys: string[],
466467
): string {
467-
const allKeys = Array.from(new Set([...clientKeys, ...sharedKeys]));
468-
const runtimeEnvLines = allKeys
469-
.map(
470-
(key) =>
471-
`\t\t\t${key}: typeof window !== "undefined" ? (globalThis as any).__arkenv_env__?.${key} ?? process.env.${key} : process.env.${key},`,
472-
)
473-
.join("\n");
474-
475-
return `/* eslint-disable */
476-
// prettier-ignore
477-
// biome-ignore format: auto-generated
478-
/**
479-
* @file env.gen.ts
480-
* @note This file is auto-generated by ArkEnv. DO NOT EDIT DIRECTLY.
481-
* @see https://arkenv.js.org
482-
*/
468+
const runtimeEnvLines = generateRuntimeEnvLines(clientKeys, sharedKeys);
483469

470+
return `${GENERATED_HEADER}
484471
import { createEnv as coreCreateEnv } from "@arkenv/nextjs";
485-
import type { Infer } from "@arkenv/nextjs";
486472
487473
export { type } from "@arkenv/nextjs";
488474
@@ -495,36 +481,15 @@ export function createEnv<
495481
shared?: readonly (keyof TSchema)[];
496482
extends?: [...TExtends];
497483
},
498-
): Readonly<Infer<TSchema>> {
499-
const parsedEnv = coreCreateEnv(schema as any, {
484+
) {
485+
return coreCreateEnv(schema as any, {
500486
...options,
501487
runtimeEnv: {
502488
${runtimeEnvLines}
503489
},
504490
} as any) as any;
505-
506-
return new Proxy(parsedEnv, {
507-
get(target, prop, receiver) {
508-
if (typeof prop === "string") {
509-
const isSchemaKey = prop in schema;
510-
const isServer = typeof window === "undefined";
511-
const isClientVar = prop.startsWith("NEXT_PUBLIC_");
512-
const isSharedVar = options?.shared?.includes(prop);
513-
514-
if (isSchemaKey && !isServer && !isClientVar && !isSharedVar) {
515-
throw new Error(
516-
\`Accessing server-side environment variable '\${prop}' on the client is not allowed.\`
517-
);
518-
}
519-
}
520-
return Reflect.get(target, prop, receiver);
521-
}
522-
}) as any;
523491
}
524-
525-
const arkenv = createEnv;
526-
export default arkenv;
527-
`;
492+
${GENERATED_FOOTER}`;
528493
}
529494

530495
/**
@@ -541,23 +506,9 @@ function generateClientFactoryCode(
541506
clientKeys: string[],
542507
sharedKeys: string[],
543508
): string {
544-
const allKeys = Array.from(new Set([...clientKeys, ...sharedKeys]));
545-
const runtimeEnvLines = allKeys
546-
.map(
547-
(key) =>
548-
`\t\t\t${key}: typeof window !== "undefined" ? (globalThis as any).__arkenv_env__?.${key} ?? process.env.${key} : process.env.${key},`,
549-
)
550-
.join("\n");
551-
552-
return `/* eslint-disable */
553-
// prettier-ignore
554-
// biome-ignore format: auto-generated
555-
/**
556-
* @file env.gen.ts
557-
* @note This file is auto-generated by ArkEnv. DO NOT EDIT DIRECTLY.
558-
* @see https://arkenv.js.org
559-
*/
509+
const runtimeEnvLines = generateRuntimeEnvLines(clientKeys, sharedKeys);
560510

511+
return `${GENERATED_HEADER}
561512
import { createEnv as coreCreateEnv } from "@arkenv/nextjs/client";
562513
563514
export { type } from "@arkenv/nextjs/client";
@@ -580,8 +531,5 @@ ${runtimeEnvLines}
580531
},
581532
} as any);
582533
}
583-
584-
const arkenv = createEnv;
585-
export default arkenv;
586-
`;
534+
${GENERATED_FOOTER}`;
587535
}

packages/nextjs/src/create-env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export function createEnvInternal(
9898
} else {
9999
const sharedKeys = options.shared || [];
100100
for (const key of Object.keys(flatSchema)) {
101-
if (sharedKeys.includes(key)) {
101+
if (sharedKeys.includes(key) || key === "NODE_ENV") {
102102
shared[key] = flatSchema[key];
103103
} else if (key.startsWith("NEXT_PUBLIC_")) {
104104
client[key] = flatSchema[key];

packages/nextjs/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function createEnv<
2424
distill.Out<at.infer<TSchema, $>>,
2525
Extract<
2626
keyof distill.Out<at.infer<TSchema, $>>,
27-
(keyof TSchema & `NEXT_PUBLIC_${string}`) | TShared
27+
(keyof TSchema & `NEXT_PUBLIC_${string}`) | TShared | (keyof TSchema & "NODE_ENV")
2828
>
2929
> &
3030
MergeExtends<TExtends>

0 commit comments

Comments
 (0)