From 68907651b6a04dcc459cd21fda2edb20f018ea0f Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:49:19 -0400 Subject: [PATCH] feat: env vars should be readonly (#111) Co-authored-by: Julius Marminge --- .changeset/nervous-houses-cover.md | 5 ++ .gitignore | 1 + packages/core/index.ts | 20 ++++++-- packages/core/test/smoke.test.ts | 79 +++++++++++++++++++++--------- packages/nextjs/test/smoke.test.ts | 32 +++++++----- 5 files changed, 98 insertions(+), 39 deletions(-) create mode 100644 .changeset/nervous-houses-cover.md diff --git a/.changeset/nervous-houses-cover.md b/.changeset/nervous-houses-cover.md new file mode 100644 index 00000000..d718e854 --- /dev/null +++ b/.changeset/nervous-houses-cover.md @@ -0,0 +1,5 @@ +--- +"@t3-oss/env-core": patch +--- + +fix: mark type as readonly diff --git a/.gitignore b/.gitignore index c1f6471c..630aa476 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ dist .vercel .DS_Store +env.d.ts next-env.d.ts **/.vscode diff --git a/packages/core/index.ts b/packages/core/index.ts index f910f828..f3d66dfb 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -149,10 +149,12 @@ export function createEnv< TShared extends Record = NonNullable >( opts: EnvOptions -): Simplify< - z.infer> & - z.infer> & - z.infer> +): Readonly< + Simplify< + z.infer> & + z.infer> & + z.infer> + > > { const runtimeEnv = opts.runtimeEnvStrict ?? opts.runtimeEnv ?? process.env; @@ -209,6 +211,16 @@ export function createEnv< } return target[prop as keyof typeof target]; }, + // Maybe reconsider this in the future: + // https://github.com/t3-oss/t3-env/pull/111#issuecomment-1682931526 + // set(_target, prop) { + // // Readonly - this is the error message you get from assigning to a frozen object + // throw new Error( + // typeof prop === "string" + // ? `Cannot assign to read only property ${prop} of object #` + // : `Cannot assign to read only property of object #` + // ); + // }, }); // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any diff --git a/packages/core/test/smoke.test.ts b/packages/core/test/smoke.test.ts index d4ac5f51..e7297bdc 100644 --- a/packages/core/test/smoke.test.ts +++ b/packages/core/test/smoke.test.ts @@ -106,10 +106,12 @@ describe("return type is correctly inferred", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - BAR: string; - FOO_BAR: string; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + BAR: string; + FOO_BAR: string; + }> + >(); expect(env).toMatchObject({ BAR: "bar", @@ -128,10 +130,12 @@ describe("return type is correctly inferred", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - BAR: number; - FOO_BAR: string; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + BAR: number; + FOO_BAR: string; + }> + >(); expect(env).toMatchObject({ BAR: 123, @@ -149,9 +153,11 @@ describe("return type is correctly inferred", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - BAR: string; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + BAR: string; + }> + >(); expect(env).toMatchObject({ BAR: "bar", @@ -173,10 +179,12 @@ test("can pass number and booleans", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - PORT: number; - IS_DEV: boolean; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + PORT: number; + IS_DEV: boolean; + }> + >(); expect(env).toMatchObject({ PORT: 123, @@ -280,7 +288,7 @@ describe("client/server only mode", () => { runtimeEnv: { FOO_BAR: "foo" }, }); - expectTypeOf(env).toEqualTypeOf<{ FOO_BAR: string }>(); + expectTypeOf(env).toEqualTypeOf>(); expect(env).toMatchObject({ FOO_BAR: "foo" }); }); @@ -292,7 +300,7 @@ describe("client/server only mode", () => { runtimeEnv: { BAR: "bar" }, }); - expectTypeOf(env).toEqualTypeOf<{ BAR: string }>(); + expectTypeOf(env).toEqualTypeOf>(); expect(env).toMatchObject({ BAR: "bar" }); }); @@ -336,11 +344,13 @@ describe("shared can be accessed on both server and client", () => { runtimeEnv: process.env, }); - expectTypeOf(env).toEqualTypeOf<{ - NODE_ENV: "development" | "production" | "test"; - BAR: string; - FOO_BAR: string; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + NODE_ENV: "development" | "production" | "test"; + BAR: string; + FOO_BAR: string; + }> + >(); test("server", () => { const { window } = globalThis; @@ -369,3 +379,28 @@ describe("shared can be accessed on both server and client", () => { globalThis.window = window; }); }); + +test("envs are readonly", () => { + const env = createEnv({ + server: { BAR: z.string() }, + runtimeEnv: { BAR: "bar" }, + }); + + /** + * We currently don't enforce readonly during runtime: + * https://github.com/t3-oss/t3-env/pull/111#issuecomment-1682931526 + */ + + // expect(() => { + // // @ts-expect-error - envs are readonly + // env.BAR = "foo"; + // }).toThrowErrorMatchingInlineSnapshot( + // '"Cannot assign to read only property BAR of object #"' + // ); + + // expect(env).toMatchObject({ BAR: "bar" }); + + // @ts-expect-error - envs are readonly + env.BAR = "foo"; + expect(env).toMatchObject({ BAR: "foo" }); +}); diff --git a/packages/nextjs/test/smoke.test.ts b/packages/nextjs/test/smoke.test.ts index bc1f1cda..e834aa92 100644 --- a/packages/nextjs/test/smoke.test.ts +++ b/packages/nextjs/test/smoke.test.ts @@ -114,11 +114,13 @@ test("new experimental runtime option only requires client vars", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - BAR: string; - NEXT_PUBLIC_BAR: string; - NODE_ENV: "development" | "production"; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + BAR: string; + NEXT_PUBLIC_BAR: string; + NODE_ENV: "development" | "production"; + }> + >(); expect(env).toMatchObject({ BAR: "bar", @@ -138,10 +140,12 @@ describe("return type is correctly inferred", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - BAR: string; - NEXT_PUBLIC_BAR: string; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + BAR: string; + NEXT_PUBLIC_BAR: string; + }> + >(); expect(env).toMatchObject({ BAR: "bar", @@ -159,10 +163,12 @@ describe("return type is correctly inferred", () => { }, }); - expectTypeOf(env).toEqualTypeOf<{ - BAR: number; - NEXT_PUBLIC_BAR: string; - }>(); + expectTypeOf(env).toEqualTypeOf< + Readonly<{ + BAR: number; + NEXT_PUBLIC_BAR: string; + }> + >(); expect(env).toMatchObject({ BAR: 123,