@envin/cli 0.0.1 → 1.1.1

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +95 -0
  3. package/dist/cli/index.mjs +464 -55
  4. package/dist/preview/.next/BUILD_ID +1 -1
  5. package/dist/preview/.next/app-build-manifest.json +6 -6
  6. package/dist/preview/.next/build-manifest.json +5 -5
  7. package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
  8. package/dist/preview/.next/next-server.js.nft.json +1 -1
  9. package/dist/preview/.next/prerender-manifest.json +3 -3
  10. package/dist/preview/.next/required-server-files.json +1 -1
  11. package/dist/preview/.next/server/app/_not-found/page.js +1 -1
  12. package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  13. package/dist/preview/.next/server/app/_not-found.html +1 -1
  14. package/dist/preview/.next/server/app/_not-found.rsc +2 -2
  15. package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
  16. package/dist/preview/.next/server/app/index.html +1 -1
  17. package/dist/preview/.next/server/app/index.rsc +3 -3
  18. package/dist/preview/.next/server/app/page.js +8 -7
  19. package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
  20. package/dist/preview/.next/server/chunks/191.js +10 -10
  21. package/dist/preview/.next/server/chunks/496.js +3 -3
  22. package/dist/preview/.next/server/chunks/601.js +4 -4
  23. package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
  24. package/dist/preview/.next/server/pages/404.html +1 -1
  25. package/dist/preview/.next/server/pages/500.html +1 -1
  26. package/dist/preview/.next/server/pages/_app.js +1 -1
  27. package/dist/preview/.next/server/pages/_document.js +1 -1
  28. package/dist/preview/.next/server/pages/_error.js +1 -1
  29. package/dist/preview/.next/server/server-reference-manifest.js +1 -1
  30. package/dist/preview/.next/server/server-reference-manifest.json +1 -1
  31. package/dist/preview/.next/server/webpack-runtime.js +1 -1
  32. package/dist/preview/.next/static/chunks/985-f32f025f8cdc74d3.js +1 -0
  33. package/dist/preview/.next/static/chunks/app/page-781d5c4076d3b10d.js +1 -0
  34. package/dist/preview/.next/static/chunks/webpack-464ea5083b838c17.js +1 -0
  35. package/dist/preview/.next/static/css/8b2927e38b2520cf.css +3 -0
  36. package/dist/preview/.next/trace +14 -13
  37. package/package.json +17 -6
  38. package/src/app/page.tsx +11 -2
  39. package/src/components/variables/context.tsx +30 -18
  40. package/src/lib/config.ts +1 -18
  41. package/src/lib/hooks/use-hot-reload.ts +31 -0
  42. package/src/lib/types.ts +10 -0
  43. package/src/lib/validate.ts +3 -1
  44. package/src/lib/variables/index.ts +28 -6
  45. package/src/utils/get-config-file.ts +2 -0
  46. package/dist/preview/.next/static/chunks/174-5a80ff32c1746c12.js +0 -1
  47. package/dist/preview/.next/static/chunks/app/page-5dac690b5d4c8e8b.js +0 -1
  48. package/dist/preview/.next/static/chunks/webpack-e84142516ca52ef7.js +0 -1
  49. package/dist/preview/.next/static/css/6842db20c57f3076.css +0 -3
  50. /package/dist/preview/.next/static/{v9TlUn5liPNhAPJYeB3QH → ti02qYR7TtWD2j2orEzoT}/_buildManifest.js +0 -0
  51. /package/dist/preview/.next/static/{v9TlUn5liPNhAPJYeB3QH → ti02qYR7TtWD2j2orEzoT}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@envin/cli",
3
- "version": "0.0.1",
3
+ "version": "1.1.1",
4
+ "description": "Type-safe env validation with live previews",
4
5
  "keywords": [
5
6
  "turbostarter",
6
7
  "environment variables",
@@ -9,6 +10,10 @@
9
10
  "arktype",
10
11
  "valibot"
11
12
  ],
13
+ "homepage": "https://envin.turbostarter.dev",
14
+ "bugs": {
15
+ "url": "https://github.com/turbostarter/envin/issues"
16
+ },
12
17
  "repository": {
13
18
  "type": "git",
14
19
  "url": "git+https://github.com/turbostarter/envin.git",
@@ -26,7 +31,8 @@
26
31
  "dev": "tsup-node --watch",
27
32
  "dev:preview": "cd ../../apps/example && tsx ../../packages/cli/src/cli/index.ts dev",
28
33
  "start": "node dist/cli/index.mjs",
29
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "tsc --noEmit",
35
+ "prepack": "bun ../../scripts/replace-workspace-protocol.ts && bun ../../scripts/populate-readme.ts"
30
36
  },
31
37
  "dependencies": {
32
38
  "@babel/parser": "^7.27.0",
@@ -41,16 +47,21 @@
41
47
  "@radix-ui/react-tooltip": "^1.2.4",
42
48
  "@svgr/webpack": "^8.1.0",
43
49
  "chalk": "^5.4.1",
50
+ "chokidar": "^4.0.3",
44
51
  "commander": "^13.1.0",
52
+ "debounce": "^2.2.0",
45
53
  "dotenv": "^16.5.0",
46
- "envin": "workspace:*",
54
+ "envin": "1.1.1",
47
55
  "esbuild": "^0.25.0",
48
56
  "log-symbols": "^7.0.0",
49
57
  "mime-types": "^3.0.1",
50
- "next": "^15.3.1",
58
+ "next": "^15.3.3",
51
59
  "ora": "^8.2.0",
52
60
  "react-hook-form": "^7.56.4",
53
- "zod": "^3.25.17"
61
+ "socket.io": "^4.8.1",
62
+ "socket.io-client": "^4.8.1",
63
+ "tsconfig-paths": "^4.2.0",
64
+ "zod": "^3.25.56"
54
65
  },
55
66
  "devDependencies": {
56
67
  "@babel/core": "7.26.10",
@@ -81,4 +92,4 @@
81
92
  "tw-animate-css": "^1.2.9",
82
93
  "typescript": "5.8.3"
83
94
  }
84
- }
95
+ }
package/src/app/page.tsx CHANGED
@@ -1,9 +1,18 @@
1
+ import path from "node:path";
1
2
  import { Ban } from "lucide-react";
2
3
  import { Envin } from "@/components/envin";
3
- import { config } from "@/lib/config";
4
4
  import { getVariables } from "@/lib/variables";
5
+ import { getConfigFile } from "@/utils/get-config-file";
6
+ import { envDirectoryAbsolutePath } from "./env";
7
+
8
+ export const envConfigFilePath = path.join(
9
+ envDirectoryAbsolutePath ?? "",
10
+ "env.config.ts",
11
+ );
5
12
 
6
13
  export default async function Home() {
14
+ const { config } = await getConfigFile(envConfigFilePath);
15
+
7
16
  if (!config) {
8
17
  return (
9
18
  <div className="flex grow h-full border-destructive border-dashed border-2 rounded-lg flex-col items-center justify-center">
@@ -18,7 +27,7 @@ export default async function Home() {
18
27
  );
19
28
  }
20
29
 
21
- const variables = await getVariables();
30
+ const variables = await getVariables(config);
22
31
 
23
32
  return <Envin variables={variables} />;
24
33
  }
@@ -1,14 +1,17 @@
1
1
  "use client";
2
2
 
3
+ import { useRouter } from "next/navigation";
3
4
  import {
4
5
  createContext,
5
6
  useCallback,
6
7
  useContext,
7
8
  useEffect,
9
+ useRef,
8
10
  useState,
9
11
  } from "react";
10
12
  import { type UseFormReturn, useForm } from "react-hook-form";
11
13
  import { useFilters } from "@/components/filters/context";
14
+ import { useHotreload } from "@/lib/hooks/use-hot-reload";
12
15
  import type { StandardSchemaV1 } from "@/lib/standard";
13
16
  import { type FileValues, Status, type Variables } from "@/lib/types";
14
17
  import { validate } from "@/lib/validate";
@@ -37,8 +40,10 @@ export const VariablesProvider = ({
37
40
  children: React.ReactNode;
38
41
  variables: Variables;
39
42
  }) => {
43
+ const router = useRouter();
40
44
  const { query, status, environment } = useFilters();
41
45
  const [fileValues, setFileValues] = useState<FileValues>({});
46
+ const isResettingRef = useRef(false);
42
47
  const form = useForm({
43
48
  defaultValues: Object.fromEntries(
44
49
  Object.entries(variables).map(([key, value]) => [
@@ -50,19 +55,6 @@ export const VariablesProvider = ({
50
55
  const [issues, setIssues] = useState<VariablesContextType["issues"]>([]);
51
56
  const [filteredKeys, setFilteredKeys] = useState(Object.keys(variables));
52
57
 
53
- useEffect(() => {
54
- const { touchedFields } = form.formState;
55
- const newValues = Object.fromEntries(
56
- Object.entries(variables).map(([key, value]) => {
57
- if (touchedFields[key]) {
58
- return [key, form.getValues(key)];
59
- }
60
- return [key, fileValues[key]?.value ?? value.default ?? ""];
61
- }),
62
- );
63
- form.reset(newValues, { keepDirtyValues: true });
64
- }, [fileValues, variables, form]);
65
-
66
58
  const onValidate = useCallback(async (data: Record<string, unknown>) => {
67
59
  const result = await validate(data);
68
60
  setIssues(result.issues ?? []);
@@ -73,11 +65,13 @@ export const VariablesProvider = ({
73
65
  formState: {
74
66
  values: true,
75
67
  },
76
- callback: (data) => onValidate(data.values),
68
+ callback: (data) => {
69
+ if (!isResettingRef.current) {
70
+ onValidate(data.values);
71
+ }
72
+ },
77
73
  });
78
74
 
79
- onValidate(form.getValues());
80
-
81
75
  return () => unsubscribe();
82
76
  }, [form, onValidate]);
83
77
 
@@ -115,6 +109,10 @@ export const VariablesProvider = ({
115
109
  [variables],
116
110
  );
117
111
 
112
+ useHotreload(() => {
113
+ router.refresh();
114
+ });
115
+
118
116
  useEffect(() => {
119
117
  setFilteredKeys(
120
118
  filterByStatus(filterByQuery(Object.keys(variables), query), status),
@@ -122,8 +120,22 @@ export const VariablesProvider = ({
122
120
  }, [filterByStatus, filterByQuery, query, status, variables]);
123
121
 
124
122
  useEffect(() => {
125
- getFileValues(environment).then(setFileValues);
126
- }, [environment]);
123
+ getFileValues(environment).then((newFileValues) => {
124
+ setFileValues(newFileValues);
125
+ const newValues = Object.fromEntries(
126
+ Object.entries(variables).map(([key, value]) => [
127
+ key,
128
+ newFileValues[key]?.value ?? value.default ?? "",
129
+ ]),
130
+ );
131
+
132
+ isResettingRef.current = true;
133
+ form.reset(newValues, { keepDirtyValues: true });
134
+ isResettingRef.current = false;
135
+
136
+ onValidate(newValues);
137
+ });
138
+ }, [environment, variables, form, onValidate]);
127
139
 
128
140
  return (
129
141
  <VariablesContext.Provider
package/src/lib/config.ts CHANGED
@@ -1,11 +1,9 @@
1
- import { readFileSync } from "node:fs";
2
1
  import path from "node:path";
3
- import { parse } from "dotenv";
4
2
  import { envDirectoryAbsolutePath } from "@/app/env";
5
3
  import { Environment } from "@/lib/types";
6
4
  import { getConfigFile } from "@/utils/get-config-file";
7
5
 
8
- const envConfigFilePath = path.join(
6
+ export const envConfigFilePath = path.join(
9
7
  envDirectoryAbsolutePath ?? "",
10
8
  "env.config.ts",
11
9
  );
@@ -20,18 +18,3 @@ export const FILES = {
20
18
  ".env.production.local",
21
19
  ],
22
20
  } as const;
23
-
24
- export const files = Object.fromEntries(
25
- Object.entries(FILES).map(([environment, files]) => [
26
- environment,
27
- files.map((file) => {
28
- try {
29
- return parse(
30
- readFileSync(path.join(envDirectoryAbsolutePath ?? "", file), "utf8"),
31
- );
32
- } catch {
33
- return {};
34
- }
35
- }),
36
- ]),
37
- );
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import { io, type Socket } from "socket.io-client";
5
+ import type { HotReloadChange } from "@/cli/utils/hot-reload/types";
6
+
7
+ /**
8
+ * Hook that detects any "reload" event sent from the CLI's web socket
9
+ * and calls the received parameter callback
10
+ */
11
+ export const useHotreload = (
12
+ onShouldReload: (changes: HotReloadChange[]) => void,
13
+ ) => {
14
+ const socketRef = useRef<Socket | null>(null);
15
+
16
+ useEffect(() => {
17
+ if (!socketRef.current) {
18
+ socketRef.current = io();
19
+ }
20
+ const socket = socketRef.current;
21
+
22
+ socket.on("reload", (changes: HotReloadChange[]) => {
23
+ console.debug("Reloading...");
24
+ void onShouldReload(changes);
25
+ });
26
+
27
+ return () => {
28
+ socket.off();
29
+ };
30
+ }, [onShouldReload]);
31
+ };
package/src/lib/types.ts CHANGED
@@ -32,6 +32,16 @@ export type Environment = (typeof Environment)[keyof typeof Environment];
32
32
 
33
33
  export const DEFAULT_PRESET = "root";
34
34
 
35
+ export const FILES = {
36
+ [Environment.DEVELOPMENT]: [".env", ".env.development", ".env.local"],
37
+ [Environment.PRODUCTION]: [
38
+ ".env",
39
+ ".env.production",
40
+ ".env.local",
41
+ ".env.production.local",
42
+ ],
43
+ } as const;
44
+
35
45
  export interface Variable {
36
46
  preset: string;
37
47
  group: VariableGroup;
@@ -1,7 +1,8 @@
1
1
  "use server";
2
2
 
3
- import { config } from "@/lib/config";
3
+ import { envConfigFilePath } from "@/app/page";
4
4
  import { parseWithDictionary, type StandardSchemaV1 } from "@/lib/standard";
5
+ import { getConfigFile } from "@/utils/get-config-file";
5
6
 
6
7
  const toIssue = (issue: StandardSchemaV1.Issue) => {
7
8
  return {
@@ -11,6 +12,7 @@ const toIssue = (issue: StandardSchemaV1.Issue) => {
11
12
  };
12
13
 
13
14
  export const validate = async (variables: Record<string, unknown>) => {
15
+ const { config } = await getConfigFile(envConfigFilePath);
14
16
  if (!config?.env._schema) {
15
17
  return {
16
18
  issues: [],
@@ -1,10 +1,15 @@
1
1
  "use server";
2
2
 
3
+ import { readFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { parse } from "dotenv";
3
6
  import type { TPreset } from "envin/types";
4
- import { config, FILES, files } from "@/lib/config";
7
+ import { envDirectoryAbsolutePath } from "@/app/env";
5
8
  import {
9
+ type Config,
6
10
  DEFAULT_PRESET,
7
11
  Environment,
12
+ FILES,
8
13
  type FileValues,
9
14
  type Variable,
10
15
  VariableGroup,
@@ -12,7 +17,7 @@ import {
12
17
  import { getDefault } from "./default";
13
18
  import { getDescription } from "./description";
14
19
 
15
- export const getVariables = async () => {
20
+ export const getVariables = async (config: Config) => {
16
21
  if (!config) {
17
22
  return {};
18
23
  }
@@ -93,13 +98,30 @@ const getVariable = (key: string, preset: TPreset): Variable | null => {
93
98
  };
94
99
  };
95
100
 
101
+ const getFiles = () => {
102
+ return Object.fromEntries(
103
+ Object.entries(FILES).map(([environment, files]) => [
104
+ environment,
105
+ files.map((file) => {
106
+ try {
107
+ return parse(
108
+ readFileSync(
109
+ path.join(envDirectoryAbsolutePath ?? "", file),
110
+ "utf8",
111
+ ),
112
+ );
113
+ } catch {
114
+ return {};
115
+ }
116
+ }),
117
+ ]),
118
+ );
119
+ };
120
+
96
121
  export const getFileValues = async (
97
122
  environment: Environment = Environment.DEVELOPMENT,
98
123
  ): Promise<FileValues> => {
99
- if (!config) {
100
- return {};
101
- }
102
-
124
+ const files = getFiles();
103
125
  const variablesFromFiles = files[environment];
104
126
 
105
127
  const result: FileValues = {};
@@ -1,3 +1,5 @@
1
+ "use server";
2
+
1
3
  import path from "node:path";
2
4
  import { type BuildFailure, build, type OutputFile } from "esbuild";
3
5
  import { z } from "zod";