anylang-dev 0.1.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/LICENSE +21 -0
- package/README.md +231 -0
- package/bin/anylang.js +8 -0
- package/package.json +41 -0
- package/src/cli.js +107 -0
- package/src/config.js +51 -0
- package/src/env.js +32 -0
- package/src/extract.js +348 -0
- package/src/pipeline.js +214 -0
- package/src/providers.js +320 -0
- package/src/runtime.d.ts +12 -0
- package/src/runtime.js +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# anylang-dev
|
|
2
|
+
|
|
3
|
+
`anylang-dev` is a small bring-your-own-key website translation CLI. It scans your source code for explicit translation calls and writes JSON locale files.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const title = $tr("home.title", "This would get translated");
|
|
7
|
+
const untouched = "This stays as it is";
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
It works in JSX and TSX when the text is wrapped in a JavaScript expression:
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
export function Hero() {
|
|
14
|
+
const $tr = useAnyLang(language);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<section>
|
|
18
|
+
<h1>{$tr("home.title", "Welcome back")}</h1>
|
|
19
|
+
<button aria-label={$tr("actions.saveChanges", "Save changes")}>
|
|
20
|
+
{$tr("actions.save", "Save")}
|
|
21
|
+
</button>
|
|
22
|
+
<p>This plain JSX text stays as it is.</p>
|
|
23
|
+
</section>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
By default, `anylang` scans `.js`, `.jsx`, `.ts`, `.tsx`, `.vue`, and `.html` files under `src`.
|
|
29
|
+
|
|
30
|
+
## Language selector
|
|
31
|
+
|
|
32
|
+
`anylang` does not require a built-in selector. Build any selector UI you want and pass the selected locale to `setLanguage`.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
const [language, setSelectedLanguage] = useState<LanguageCode>("en");
|
|
36
|
+
const $tr = useAnyLang(language);
|
|
37
|
+
|
|
38
|
+
function handleLanguageChange(nextLanguage: LanguageCode) {
|
|
39
|
+
setLanguage(nextLanguage);
|
|
40
|
+
setSelectedLanguage(nextLanguage);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<select
|
|
45
|
+
value={language}
|
|
46
|
+
onChange={(event) => handleLanguageChange(event.target.value as LanguageCode)}
|
|
47
|
+
>
|
|
48
|
+
<option value="en">English</option>
|
|
49
|
+
<option value="hi">हिन्दी</option>
|
|
50
|
+
<option value="ja">日本語</option>
|
|
51
|
+
</select>
|
|
52
|
+
);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `$tr("key", "source text")` anywhere in the same render tree:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<h1>{$tr("hero.title", "Translate your website with anylang")}</h1>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm link
|
|
65
|
+
anylang init
|
|
66
|
+
anylang scan
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`anylang scan` creates locale files without calling a translation provider. To translate for real with Gemini, add your own API key to `.env` in the project where you run `anylang`:
|
|
70
|
+
|
|
71
|
+
```env
|
|
72
|
+
GEMINI_API_KEY=your-provider-key
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Then run:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
anylang translate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Config
|
|
82
|
+
|
|
83
|
+
`anylang init` creates:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"sourceLocale": "en",
|
|
88
|
+
"targetLocales": ["hi"],
|
|
89
|
+
"include": ["src/**/*.{js,jsx,ts,tsx,vue,html}"],
|
|
90
|
+
"exclude": ["node_modules", ".git", "dist", "build", ".next"],
|
|
91
|
+
"outDir": "locales",
|
|
92
|
+
"runtime": {
|
|
93
|
+
"output": "src/anylang.generated.ts",
|
|
94
|
+
"importFrom": "anylang-dev/runtime"
|
|
95
|
+
},
|
|
96
|
+
"functionName": "$tr",
|
|
97
|
+
"provider": {
|
|
98
|
+
"name": "gemini",
|
|
99
|
+
"model": "gemini-2.5-flash"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The provider is intentionally BYOK. `anylang` does not include a platform key, proxy requests, track usage, or store billing data. It automatically loads `.env` from the current project before calling the provider.
|
|
105
|
+
|
|
106
|
+
## Providers
|
|
107
|
+
|
|
108
|
+
Choose a provider by setting `provider.name`. Each provider reads its standard API key from `.env`.
|
|
109
|
+
|
|
110
|
+
| Provider | `provider.name` | `.env` key |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| Gemini | `gemini` | `GEMINI_API_KEY` |
|
|
113
|
+
| OpenAI | `openai` | `OPENAI_API_KEY` |
|
|
114
|
+
| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` |
|
|
115
|
+
| Cohere | `cohere` | `COHERE_API_KEY` |
|
|
116
|
+
| Mistral | `mistral` | `MISTRAL_API_KEY` |
|
|
117
|
+
| DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` |
|
|
118
|
+
| Groq | `groq` | `GROQ_API_KEY` |
|
|
119
|
+
| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` |
|
|
120
|
+
| Perplexity | `perplexity` | `PERPLEXITY_API_KEY` |
|
|
121
|
+
| xAI | `xai` | `XAI_API_KEY` |
|
|
122
|
+
| Together AI | `together` | `TOGETHER_API_KEY` |
|
|
123
|
+
| Fireworks AI | `fireworks` | `FIREWORKS_API_KEY` |
|
|
124
|
+
| Custom OpenAI-compatible | `openai-compatible` | `ANYLANG_API_KEY` |
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"provider": {
|
|
131
|
+
"name": "anthropic",
|
|
132
|
+
"model": "claude-3-5-haiku-latest"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For custom OpenAI-compatible gateways, provide `baseUrl` and `model`:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"provider": {
|
|
142
|
+
"name": "openai-compatible",
|
|
143
|
+
"baseUrl": "https://your-gateway.example.com/v1",
|
|
144
|
+
"model": "your-model"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Output
|
|
150
|
+
|
|
151
|
+
Scanning or translating creates:
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
locales/
|
|
155
|
+
en.json
|
|
156
|
+
hi.json
|
|
157
|
+
anylang.lock.json
|
|
158
|
+
src/
|
|
159
|
+
anylang.generated.ts
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The lock file stores SHA-256 fingerprints so unchanged strings are skipped on later runs.
|
|
163
|
+
|
|
164
|
+
## Workflow
|
|
165
|
+
|
|
166
|
+
1. Wrap source text in your app:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<h1>{$tr("hero.title", "Translate your website with anylang")}</h1>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
2. Scan the project:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
anylang scan
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
This writes keyed source entries to `locales/en.json` and creates placeholder entries in each target locale.
|
|
179
|
+
It also generates `src/anylang.generated.ts`, which imports all locale JSON files and exports runtime helpers.
|
|
180
|
+
|
|
181
|
+
3. Translate with Gemini:
|
|
182
|
+
|
|
183
|
+
```env
|
|
184
|
+
GEMINI_API_KEY=your-gemini-api-key
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
anylang translate
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This scans again, sends missing or changed target entries to Gemini, and writes the translated text into files like `locales/hi.json`.
|
|
192
|
+
|
|
193
|
+
Source locale output:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"hero.title": {
|
|
198
|
+
"text": "Translate your website with anylang",
|
|
199
|
+
"variables": []
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Target locale output:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"hero.title": {
|
|
209
|
+
"source": "Translate your website with anylang",
|
|
210
|
+
"text": "anylang से अपनी वेबसाइट का अनुवाद करें",
|
|
211
|
+
"variables": []
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
On later runs, `anylang translate` compares `targetEntry.source` against the current source text. If they differ, it retranslates that key. If they match, it keeps the existing translation and skips the AI call.
|
|
217
|
+
|
|
218
|
+
## Runtime
|
|
219
|
+
|
|
220
|
+
Import the generated runtime file in your app:
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import {
|
|
224
|
+
languages,
|
|
225
|
+
setLanguage,
|
|
226
|
+
useAnyLang,
|
|
227
|
+
type LanguageCode
|
|
228
|
+
} from "./anylang.generated";
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
You do not manually import `en.json`, `hi.json`, `ja.json`, etc. The generated file does that for you based on `sourceLocale` and `targetLocales`.
|
package/bin/anylang.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anylang-dev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Bring-your-own-key website translation JSON generator.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"src",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"bin": {
|
|
13
|
+
"anylang": "bin/anylang.js"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/runtime.js",
|
|
17
|
+
"./runtime": {
|
|
18
|
+
"types": "./src/runtime.d.ts",
|
|
19
|
+
"default": "./src/runtime.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"i18n",
|
|
30
|
+
"translation",
|
|
31
|
+
"localization",
|
|
32
|
+
"gemini",
|
|
33
|
+
"react",
|
|
34
|
+
"tsx"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+ssh://git@github.com/akshaywritescode/anylang-dev.git"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DEFAULT_CONFIG, loadConfig } from "./config.js";
|
|
4
|
+
import { extractProjectStrings } from "./extract.js";
|
|
5
|
+
import { runPipeline } from "./pipeline.js";
|
|
6
|
+
|
|
7
|
+
const help = `anylang
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
init Create anylang.config.json
|
|
11
|
+
extract Print discovered $tr(...) strings
|
|
12
|
+
scan Scan source files and prepare locale JSON files
|
|
13
|
+
translate Scan source files, translate missing entries, and update JSON files
|
|
14
|
+
run Alias for translate
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
--config <path> Config path (default: anylang.config.json)
|
|
18
|
+
--dry-run Do not call a translation provider
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
export async function runCli(argv) {
|
|
22
|
+
const { command, options } = parseArgs(argv);
|
|
23
|
+
|
|
24
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
25
|
+
console.log(help);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (command === "init") {
|
|
30
|
+
await initConfig(options.config);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (command === "extract") {
|
|
35
|
+
const config = await loadConfig(options.config);
|
|
36
|
+
const result = await extractProjectStrings(config);
|
|
37
|
+
for (const item of result.items) {
|
|
38
|
+
console.log(`${item.value}\t${path.relative(process.cwd(), item.file)}:${item.line}:${item.column}`);
|
|
39
|
+
}
|
|
40
|
+
console.log(`Found ${result.items.length} translatable string${result.items.length === 1 ? "" : "s"}.`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (command === "scan") {
|
|
45
|
+
const config = await loadConfig(options.config);
|
|
46
|
+
const summary = await runPipeline(config, { dryRun: true });
|
|
47
|
+
console.log(`Scanned ${summary.sourceCount} source string${summary.sourceCount === 1 ? "" : "s"}.`);
|
|
48
|
+
console.log(`Updated ${summary.localeCount} locale file${summary.localeCount === 1 ? "" : "s"} in ${summary.outDir}.`);
|
|
49
|
+
console.log("No provider calls were made.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (command === "run" || command === "translate") {
|
|
54
|
+
const config = await loadConfig(options.config);
|
|
55
|
+
const summary = await runPipeline(config, { dryRun: options.dryRun });
|
|
56
|
+
console.log(`Extracted ${summary.sourceCount} source string${summary.sourceCount === 1 ? "" : "s"}.`);
|
|
57
|
+
console.log(`Updated ${summary.localeCount} locale file${summary.localeCount === 1 ? "" : "s"} in ${summary.outDir}.`);
|
|
58
|
+
if (summary.translatedCount > 0) {
|
|
59
|
+
console.log(`Translated ${summary.translatedCount} new/changed entr${summary.translatedCount === 1 ? "y" : "ies"}.`);
|
|
60
|
+
}
|
|
61
|
+
if (summary.skippedTranslationCount > 0) {
|
|
62
|
+
console.log(`Skipped ${summary.skippedTranslationCount} entr${summary.skippedTranslationCount === 1 ? "y" : "ies"} without provider calls.`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`Unknown command: ${command}\n\n${help}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseArgs(argv) {
|
|
71
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
|
|
72
|
+
return { command: argv[0], options: { config: "anylang.config.json", dryRun: false } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const options = {
|
|
76
|
+
config: "anylang.config.json",
|
|
77
|
+
dryRun: false
|
|
78
|
+
};
|
|
79
|
+
let command;
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
82
|
+
const arg = argv[index];
|
|
83
|
+
if (!command && !arg.startsWith("--")) {
|
|
84
|
+
command = arg;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (arg === "--config") {
|
|
88
|
+
options.config = argv[index + 1];
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg === "--dry-run") {
|
|
93
|
+
options.dryRun = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { command, options };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function initConfig(configPath = "anylang.config.json") {
|
|
103
|
+
const resolved = path.resolve(configPath);
|
|
104
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
105
|
+
await writeFile(resolved, `${JSON.stringify(DEFAULT_CONFIG, null, 2)}\n`, { flag: "wx" });
|
|
106
|
+
console.log(`Created ${path.relative(process.cwd(), resolved)}`);
|
|
107
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_CONFIG = {
|
|
5
|
+
sourceLocale: "en",
|
|
6
|
+
targetLocales: ["hi"],
|
|
7
|
+
include: ["src/**/*.{js,jsx,ts,tsx,vue,html}"],
|
|
8
|
+
exclude: ["node_modules", ".git", "dist", "build", ".next"],
|
|
9
|
+
outDir: "locales",
|
|
10
|
+
runtime: {
|
|
11
|
+
output: "src/anylang.generated.ts",
|
|
12
|
+
importFrom: "anylang-dev/runtime"
|
|
13
|
+
},
|
|
14
|
+
functionName: "$tr",
|
|
15
|
+
provider: {
|
|
16
|
+
name: "gemini",
|
|
17
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
|
18
|
+
model: "gemini-2.5-flash"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function loadConfig(configPath = "anylang.config.json") {
|
|
23
|
+
const resolved = path.resolve(configPath);
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = await readFile(resolved, "utf8");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error && error.code === "ENOENT") {
|
|
29
|
+
throw new Error(`Missing config file: ${configPath}. Run "anylang init" first.`);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
const config = {
|
|
36
|
+
...DEFAULT_CONFIG,
|
|
37
|
+
...parsed,
|
|
38
|
+
provider: {
|
|
39
|
+
...DEFAULT_CONFIG.provider,
|
|
40
|
+
...(parsed.provider || {})
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (!config.sourceLocale) throw new Error("Config must include sourceLocale.");
|
|
45
|
+
if (!Array.isArray(config.targetLocales)) throw new Error("Config targetLocales must be an array.");
|
|
46
|
+
if (!Array.isArray(config.include)) throw new Error("Config include must be an array.");
|
|
47
|
+
if (!config.outDir) throw new Error("Config must include outDir.");
|
|
48
|
+
if (!config.functionName) throw new Error("Config must include functionName.");
|
|
49
|
+
|
|
50
|
+
return config;
|
|
51
|
+
}
|
package/src/env.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function loadDotEnv(cwd = process.cwd()) {
|
|
5
|
+
const envPath = path.join(cwd, ".env");
|
|
6
|
+
if (!existsSync(envPath)) return;
|
|
7
|
+
|
|
8
|
+
const contents = readFileSync(envPath, "utf8");
|
|
9
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
10
|
+
const trimmed = line.trim();
|
|
11
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
12
|
+
|
|
13
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
14
|
+
if (separatorIndex === -1) continue;
|
|
15
|
+
|
|
16
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
17
|
+
const rawValue = trimmed.slice(separatorIndex + 1).trim();
|
|
18
|
+
if (!key || process.env[key] !== undefined) continue;
|
|
19
|
+
|
|
20
|
+
process.env[key] = unquote(rawValue);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unquote(value) {
|
|
25
|
+
if (
|
|
26
|
+
(value.startsWith("\"") && value.endsWith("\"")) ||
|
|
27
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
28
|
+
) {
|
|
29
|
+
return value.slice(1, -1);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|