@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.
- package/CHANGELOG.md +32 -0
- package/README.md +95 -0
- package/dist/cli/index.mjs +464 -55
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +6 -6
- package/dist/preview/.next/build-manifest.json +5 -5
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +3 -3
- package/dist/preview/.next/required-server-files.json +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/_not-found.html +1 -1
- package/dist/preview/.next/server/app/_not-found.rsc +2 -2
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/index.html +1 -1
- package/dist/preview/.next/server/app/index.rsc +3 -3
- package/dist/preview/.next/server/app/page.js +8 -7
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/chunks/191.js +10 -10
- package/dist/preview/.next/server/chunks/496.js +3 -3
- package/dist/preview/.next/server/chunks/601.js +4 -4
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/pages/404.html +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/pages/_app.js +1 -1
- package/dist/preview/.next/server/pages/_document.js +1 -1
- package/dist/preview/.next/server/pages/_error.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/server/webpack-runtime.js +1 -1
- package/dist/preview/.next/static/chunks/985-f32f025f8cdc74d3.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-781d5c4076d3b10d.js +1 -0
- package/dist/preview/.next/static/chunks/webpack-464ea5083b838c17.js +1 -0
- package/dist/preview/.next/static/css/8b2927e38b2520cf.css +3 -0
- package/dist/preview/.next/trace +14 -13
- package/package.json +17 -6
- package/src/app/page.tsx +11 -2
- package/src/components/variables/context.tsx +30 -18
- package/src/lib/config.ts +1 -18
- package/src/lib/hooks/use-hot-reload.ts +31 -0
- package/src/lib/types.ts +10 -0
- package/src/lib/validate.ts +3 -1
- package/src/lib/variables/index.ts +28 -6
- package/src/utils/get-config-file.ts +2 -0
- package/dist/preview/.next/static/chunks/174-5a80ff32c1746c12.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-5dac690b5d4c8e8b.js +0 -1
- package/dist/preview/.next/static/chunks/webpack-e84142516ca52ef7.js +0 -1
- package/dist/preview/.next/static/css/6842db20c57f3076.css +0 -3
- /package/dist/preview/.next/static/{v9TlUn5liPNhAPJYeB3QH → ti02qYR7TtWD2j2orEzoT}/_buildManifest.js +0 -0
- /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": "
|
|
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": "
|
|
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.
|
|
58
|
+
"next": "^15.3.3",
|
|
51
59
|
"ora": "^8.2.0",
|
|
52
60
|
"react-hook-form": "^7.56.4",
|
|
53
|
-
"
|
|
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) =>
|
|
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(
|
|
126
|
-
|
|
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;
|
package/src/lib/validate.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
|
|
3
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
|
|
124
|
+
const files = getFiles();
|
|
103
125
|
const variablesFromFiles = files[environment];
|
|
104
126
|
|
|
105
127
|
const result: FileValues = {};
|