Skip to content

Commit e81d252

Browse files
author
Guy Bedford
authored
feat: support --env option for the runtime build (#1090)
1 parent ee409bd commit e81d252

File tree

11 files changed

+358
-89
lines changed

11 files changed

+358
-89
lines changed

integration-tests/cli/env.test.js

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import test from 'brittle';
2+
import { EnvParser } from '../../src/env.js';
3+
4+
test('EnvParser should parse single key-value pair', function (t) {
5+
const parser = new EnvParser();
6+
parser.parse('NODE_ENV=production');
7+
8+
t.alike(parser.getEnv(), {
9+
NODE_ENV: 'production',
10+
});
11+
});
12+
13+
test('EnvParser should parse multiple comma-separated values', function (t) {
14+
const parser = new EnvParser();
15+
parser.parse('NODE_ENV=production,DEBUG=true,PORT=3000');
16+
17+
t.alike(parser.getEnv(), {
18+
NODE_ENV: 'production',
19+
DEBUG: 'true',
20+
PORT: '3000',
21+
});
22+
});
23+
24+
test('EnvParser should merge multiple parse calls', function (t) {
25+
const parser = new EnvParser();
26+
parser.parse('NODE_ENV=production');
27+
parser.parse('DEBUG=true');
28+
29+
t.alike(parser.getEnv(), {
30+
NODE_ENV: 'production',
31+
DEBUG: 'true',
32+
});
33+
});
34+
35+
test('EnvParser should inherit existing environment variables', function (t) {
36+
const parser = new EnvParser();
37+
38+
// Set up some test environment variables
39+
process.env.TEST_VAR1 = 'value1';
40+
process.env.TEST_VAR2 = 'value2';
41+
42+
parser.parse('TEST_VAR1,TEST_VAR2');
43+
44+
t.alike(parser.getEnv(), {
45+
TEST_VAR1: 'value1',
46+
TEST_VAR2: 'value2',
47+
});
48+
49+
// Cleanup
50+
delete process.env.TEST_VAR1;
51+
delete process.env.TEST_VAR2;
52+
});
53+
54+
test('EnvParser should handle mixed inheritance and setting', function (t) {
55+
const parser = new EnvParser();
56+
57+
process.env.TEST_VAR = 'inherited';
58+
59+
parser.parse('TEST_VAR,NEW_VAR=set');
60+
61+
t.alike(parser.getEnv(), {
62+
TEST_VAR: 'inherited',
63+
NEW_VAR: 'set',
64+
});
65+
66+
// Cleanup
67+
delete process.env.TEST_VAR;
68+
});
69+
70+
test('EnvParser should handle values with spaces', function (t) {
71+
const parser = new EnvParser();
72+
parser.parse('MESSAGE=Hello World');
73+
74+
t.alike(parser.getEnv(), {
75+
MESSAGE: 'Hello World',
76+
});
77+
});
78+
79+
test('EnvParser should handle values with equals signs', function (t) {
80+
const parser = new EnvParser();
81+
parser.parse('DATABASE_URL=postgres://user:pass@localhost:5432/db');
82+
83+
t.alike(parser.getEnv(), {
84+
DATABASE_URL: 'postgres://user:pass@localhost:5432/db',
85+
});
86+
});
87+
88+
test('EnvParser should handle empty values', function (t) {
89+
const parser = new EnvParser();
90+
parser.parse('EMPTY=');
91+
92+
t.alike(parser.getEnv(), {
93+
EMPTY: '',
94+
});
95+
});
96+
97+
test('EnvParser should handle whitespace', function (t) {
98+
const parser = new EnvParser();
99+
parser.parse(' KEY = value with spaces ');
100+
101+
t.alike(parser.getEnv(), {
102+
KEY: ' value with spaces', // Leading whitespace preserved, trailing removed
103+
});
104+
105+
// Test multiple values with whitespace
106+
parser.parse(' KEY2 = value2 , KEY3 = value3 ');
107+
108+
t.alike(parser.getEnv(), {
109+
KEY: ' value with spaces',
110+
KEY2: ' value2',
111+
KEY3: ' value3',
112+
});
113+
});
114+
115+
test('EnvParser should merge and override values', function (t) {
116+
const parser = new EnvParser();
117+
parser.parse('KEY=first');
118+
parser.parse('KEY=second');
119+
120+
t.alike(parser.getEnv(), {
121+
KEY: 'second',
122+
});
123+
});
124+
125+
test('EnvParser should throw on missing equal sign', function (t) {
126+
const parser = new EnvParser();
127+
128+
t.exception(
129+
() => parser.parse('INVALID_FORMAT'),
130+
'Invalid environment variable format: INVALID_FORMAT\nMust be in format KEY=VALUE',
131+
);
132+
});
133+
134+
test('EnvParser should throw on empty key', function (t) {
135+
const parser = new EnvParser();
136+
137+
t.exception(
138+
() => parser.parse('=value'),
139+
'Invalid environment variable format: =value\nMust be in format KEY=VALUE',
140+
);
141+
});
142+
143+
test('EnvParser should handle empty constructor', function (t) {
144+
const parser = new EnvParser();
145+
146+
t.alike(parser.getEnv(), {});
147+
});
148+
149+
test('EnvParser should handle multiple commas and whitespace', function (t) {
150+
const parser = new EnvParser();
151+
parser.parse('KEY1=value1, KEY2=value2,,,KEY3=value3');
152+
153+
t.alike(parser.getEnv(), {
154+
KEY1: 'value1',
155+
KEY2: 'value2',
156+
KEY3: 'value3',
157+
});
158+
});
159+
160+
test('EnvParser should handle values containing escaped characters', function (t) {
161+
const parser = new EnvParser();
162+
163+
// This is how Node.js argv will receive it after shell processing
164+
parser.parse('A=VERBATIM CONTENTS\\, GO HERE'); // Users will type: --env 'A=VERBATIM CONTENTS\, GO HERE'
165+
166+
t.alike(parser.getEnv(), {
167+
A: 'VERBATIM CONTENTS, GO HERE', // Comma should be unescaped in final value
168+
});
169+
});

integration-tests/cli/help.test.js

Lines changed: 0 additions & 72 deletions
This file was deleted.

integration-tests/js-compute/fixtures/app/fastly.toml.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ name = "js-test-app"
99
service_id = ""
1010

1111
[scripts]
12-
build = "node ../../../../js-compute-runtime-cli.js --enable-experimental-high-resolution-time-methods src/index.js"
12+
build = "node ../../../../js-compute-runtime-cli.js --env LOCAL_TEST,TEST=\"foo\" --enable-experimental-high-resolution-time-methods src/index.js"
1313

1414
[local_server]
1515

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
/* eslint-env serviceworker */
22
import { env } from 'fastly:env';
33
import { routes, isRunningLocally } from './routes.js';
4-
import { assert } from './assertions.js';
4+
import { strictEqual } from './assertions.js';
5+
6+
// hostname didn't exist at initialization, so can still be captured at runtime
7+
const wizerHostname = env('FASTLY_HOSTNAME');
8+
const wizerLocal = env('LOCAL_TEST');
59

610
routes.set('/env', () => {
11+
strictEqual(wizerHostname, undefined);
12+
713
if (isRunningLocally()) {
8-
assert(
14+
strictEqual(
915
env('FASTLY_HOSTNAME'),
1016
'localhost',
1117
`env("FASTLY_HOSTNAME") === "localhost"`,
1218
);
19+
} else {
20+
strictEqual(env('FASTLY_HOSTNAME'), undefined);
1321
}
22+
23+
strictEqual(wizerLocal, 'local val');
24+
25+
// at runtime these remain captured from Wizer time, even if we didn't call env
26+
strictEqual(env('LOCAL_TEST'), 'local val');
27+
strictEqual(env('TEST'), 'foo');
1428
});

integration-tests/js-compute/test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { copyFile, readFile, writeFile } from 'node:fs/promises';
1111
import core from '@actions/core';
1212
import TOML from '@iarna/toml';
1313

14+
// test environment variable handling
15+
process.env.LOCAL_TEST = 'local val';
16+
1417
async function killPortProcess(port) {
1518
zx.verbose = false;
1619
const pids = (await zx`lsof -ti:${port}`).stdout;

js-compute-runtime-cli.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
output,
1717
version,
1818
help,
19+
env,
1920
} = await parseInputs(process.argv.slice(2));
2021

2122
if (version) {
@@ -41,6 +42,7 @@ if (version) {
4142
aotCache,
4243
moduleMode,
4344
bundle,
45+
env,
4446
);
4547
await addSdkMetadataField(output, enableAOT);
4648
}

runtime/fastly/builtins/fastly.cpp

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ using fastly::fetch::RequestOrResponse;
2222
using fastly::fetch::Response;
2323
using fastly::logger::Logger;
2424

25+
extern char **environ;
26+
2527
namespace {
2628

2729
bool DEBUG_LOGGING_ENABLED = false;
2830

2931
api::Engine *ENGINE;
3032

33+
// Global storage for Wizer-time environment
34+
std::unordered_map<std::string, std::string> initialized_env;
35+
3136
static void oom_callback(JSContext *cx, void *data) {
3237
fprintf(stderr, "Critical Error: out of memory\n");
3338
fflush(stderr);
@@ -319,15 +324,42 @@ bool Env::env_get(JSContext *cx, unsigned argc, JS::Value *vp) {
319324
if (!args.requireAtLeast(cx, "fastly.env.get", 1))
320325
return false;
321326

322-
auto var_name_chars = core::encode(cx, args[0]);
323-
if (!var_name_chars) {
327+
JS::RootedString str(cx, JS::ToString(cx, args[0]));
328+
if (!str) {
324329
return false;
325330
}
326-
JS::RootedString env_var(cx, JS_NewStringCopyZ(cx, getenv(var_name_chars.begin())));
327-
if (!env_var)
331+
332+
JS::UniqueChars ptr = JS_EncodeStringToUTF8(cx, str);
333+
if (!ptr) {
328334
return false;
335+
}
336+
337+
// This shouldn't fail, since the encode operation ensured `str` is linear.
338+
JSLinearString *linear = JS_EnsureLinearString(cx, str);
339+
uint32_t len = JS::GetDeflatedUTF8StringLength(linear);
340+
341+
std::string key_str(ptr.get(), len);
342+
343+
// First check initialized environment
344+
if (auto it = initialized_env.find(key_str); it != initialized_env.end()) {
345+
JS::RootedString env_var(cx, JS_NewStringCopyN(cx, it->second.data(), it->second.size()));
346+
if (!env_var)
347+
return false;
348+
args.rval().setString(env_var);
349+
return true;
350+
}
329351

330-
args.rval().setString(env_var);
352+
// Fallback to getenv with caching
353+
if (const char *value = std::getenv(key_str.c_str())) {
354+
auto [it, _] = initialized_env.emplace(key_str, value);
355+
JS::RootedString env_var(cx, JS_NewStringCopyN(cx, it->second.data(), it->second.size()));
356+
if (!env_var)
357+
return false;
358+
args.rval().setString(env_var);
359+
return true;
360+
}
361+
362+
args.rval().setUndefined();
331363
return true;
332364
}
333365

@@ -475,6 +507,19 @@ bool install(api::Engine *engine) {
475507
}
476508

477509
// fastly:env
510+
// first, store the initialized environment vars from Wizer
511+
initialized_env.clear();
512+
513+
for (char **env = environ; *env; env++) {
514+
const char *entry = *env;
515+
const char *eq = entry;
516+
while (*eq && *eq != '=')
517+
eq++;
518+
519+
if (*eq == '=') {
520+
initialized_env.emplace(std::string(entry, eq - entry), std::string(eq + 1));
521+
}
522+
}
478523
RootedValue env_get(engine->cx());
479524
if (!JS_GetProperty(engine->cx(), Fastly::env, "get", &env_get)) {
480525
return false;

0 commit comments

Comments
 (0)