Skip to content

Commit 6cc8c6f

Browse files
committed
feat: automate editing env files
- adds a script which edits and writes env values into files - added basic input validation for env values
1 parent 7314f49 commit 6cc8c6f

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"lint": "eslint .",
1515
"prettier:format": "prettier './**/*.{js,json,md,sol,ts,yml}' --write && yarn run lint --fix",
1616
"prettier:check": "prettier './**/*.{js,json,md,sol,ts,yml}' --check && yarn run lint",
17-
"testAll": "tests/runAll.sh"
17+
"testAll": "tests/runAll.sh",
18+
"setup-envs": "node scripts/setup-envs.js"
1819
},
1920
"devDependencies": {
2021
"@offchainlabs/eslint-config-typescript": "^0.2.1",

scripts/setup-envs.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* Environment setup script for Arbitrum Tutorials.
5+
* Usage:
6+
* yarn setup-envs
7+
*/
8+
9+
/* eslint-disable no-await-in-loop */
10+
11+
const fs = require('fs');
12+
const path = require('path');
13+
const readline = require('readline');
14+
15+
const VARS = ['PRIVATE_KEY', 'CHAIN_RPC', 'PARENT_CHAIN_RPC', 'L1_RPC'];
16+
17+
function log(msg) {
18+
console.log(msg);
19+
}
20+
function warn(msg) {
21+
console.warn(msg);
22+
}
23+
function error(msg) {
24+
console.error(msg);
25+
}
26+
27+
async function promptForValues() {
28+
const values = {};
29+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
30+
const ask = (q) => new Promise((res) => rl.question(q, (ans) => res(ans.trim())));
31+
for (const v of VARS) {
32+
const optional = v === 'L1_RPC';
33+
const existing = process.env[v] ? ` [default: ${process.env[v]}]` : '';
34+
const prompt = optional ? `${v} (optional)${existing}: ` : `${v}${existing}: `;
35+
const ans = await ask(prompt);
36+
if (ans) {
37+
values[v] = ans;
38+
} else if (process.env[v]) {
39+
values[v] = process.env[v];
40+
} else if (!optional) {
41+
error(`Required value missing: ${v}`);
42+
process.exit(1);
43+
}
44+
}
45+
rl.close();
46+
return values;
47+
}
48+
49+
function replaceOrAppend(contentLines, key, value) {
50+
const prefix = key + '=';
51+
let replaced = false;
52+
for (let i = 0; i < contentLines.length; i++) {
53+
const line = contentLines[i];
54+
if (line.startsWith(prefix)) {
55+
contentLines[i] = `${key}="${value}"`;
56+
replaced = true;
57+
break;
58+
}
59+
}
60+
if (!replaced) contentLines.push(`${key}="${value}"`);
61+
}
62+
63+
function processSampleFile(samplePath, envPath, values) {
64+
const lines = fs.readFileSync(samplePath, 'utf8').split(/\r?\n/);
65+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
66+
for (const v of VARS) {
67+
if (values[v]) replaceOrAppend(lines, v, values[v]);
68+
}
69+
70+
const newContent = lines.join('\n') + '\n';
71+
fs.writeFileSync(envPath, newContent, 'utf8');
72+
if (samplePath !== envPath) {
73+
fs.unlinkSync(samplePath); // remove sample after successful creation
74+
}
75+
}
76+
77+
function processDirectory(dir, values, summary) {
78+
const samplePath = path.join(dir, '.env-sample');
79+
const envPath = path.join(dir, '.env');
80+
const hasSample = fs.existsSync(samplePath);
81+
const hasEnv = fs.existsSync(envPath);
82+
83+
if (!hasSample && !hasEnv) return;
84+
85+
try {
86+
if (hasSample) {
87+
processSampleFile(samplePath, envPath, values);
88+
summary.updated.push(dir);
89+
} else if (hasEnv) {
90+
processSampleFile(envPath, envPath, values);
91+
summary.updated.push(dir);
92+
}
93+
} catch (e) {
94+
summary.errors.push({ dir, error: e.message });
95+
}
96+
}
97+
98+
function validate(values) {
99+
const pk = values.PRIVATE_KEY;
100+
if (!/^0x[a-fA-F0-9]{64}$/.test(pk)) {
101+
throw new Error('PRIVATE_KEY must be 0x + 64 hex characters.');
102+
}
103+
['CHAIN_RPC', 'PARENT_CHAIN_RPC'].forEach((k) => {
104+
if (!/^https?:\/\/\\S+$/i.test(values[k])) {
105+
throw new Error(`${k} must be an http(s) URL.`);
106+
}
107+
});
108+
if (values.L1_RPC && !/^https?:\/\/\\S+$/i.test(values.L1_RPC)) {
109+
throw new Error('L1_RPC must be an http(s) URL if provided.');
110+
}
111+
}
112+
113+
async function main() {
114+
log('Arbitrum Tutorials environment setup starting...');
115+
const values = await promptForValues();
116+
117+
if (!values.PRIVATE_KEY || !values.CHAIN_RPC || !values.PARENT_CHAIN_RPC) {
118+
error('PRIVATE_KEY, CHAIN_RPC, and PARENT_CHAIN_RPC are required.');
119+
process.exit(1);
120+
}
121+
122+
validate(values);
123+
124+
const rootDir = path.resolve(__dirname, '..');
125+
const packagesDir = path.join(rootDir, 'packages');
126+
127+
const summary = { updated: [], errors: [] };
128+
129+
processDirectory(rootDir, values, summary);
130+
131+
if (fs.existsSync(packagesDir)) {
132+
const entries = fs.readdirSync(packagesDir);
133+
for (const entry of entries) {
134+
const fullPath = path.join(packagesDir, entry);
135+
if (fs.statSync(fullPath).isDirectory()) {
136+
processDirectory(fullPath, values, summary);
137+
}
138+
}
139+
}
140+
141+
log('Environment setup complete.');
142+
log(`Updated: ${summary.updated.length}`);
143+
if (summary.errors.length) {
144+
warn('Errors encountered:');
145+
for (const e of summary.errors) warn(` - ${e.dir}: ${e.error}`);
146+
}
147+
148+
log('\nExample: run a tutorial script after env creation:');
149+
log(' cd packages/greeter && npx hardhat run scripts/sendParentMessage.ts');
150+
}
151+
152+
main().catch((e) => {
153+
error(e.stack || e.message);
154+
process.exit(1);
155+
});

0 commit comments

Comments
 (0)