Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 6 additions & 29 deletions packages/void/src/cli/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
requiresSandboxPlatformImage,
resolveSandboxConfig,
} from '../sandbox/config.ts';
import { mergeAndExpand, parseDotenvFile } from '../shared/dotenv.ts';
import { resolveRemoteMode, stripInternalEnvKeys } from '../shared/env.ts';
import { validateSsrEntry } from '../ssr/entry.ts';
import { getApiUrl, getToken, getTokenSource } from './auth.ts';
Expand Down Expand Up @@ -2303,36 +2304,12 @@ async function assertEnvSchemaValid(
* Load env vars for deploy — only .env and .env.production (not .local files).
* Later files win on conflict.
*/
function loadDeployEnv(root: string): Record<string, string> {
export function loadDeployEnv(root: string): Record<string, string> {
// Same parse + merge + expand path env validation uses, so the shipped
// values match what was validated.
const files = ['.env', '.env.production'];
const env: Record<string, string> = {};
for (const file of files) {
const filePath = join(root, file);
if (!existsSync(filePath)) {
continue;
}
const content = readFileSync(filePath, 'utf-8');
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) {
continue;
}
const key = trimmed.slice(0, eqIndex).trim();
let value = trimmed.slice(eqIndex + 1).trim();
// Strip surrounding quotes
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
env[key] = value;
}
}
const parsed = files.map((file) => parseDotenvFile(join(root, file), { expand: false }));
const env = mergeAndExpand(parsed, { onWarn: (msg) => voidWarn(msg) });
const { sanitized, stripped } = stripInternalEnvKeys(env);
if (stripped.length > 0) {
voidWarn(
Expand Down
45 changes: 45 additions & 0 deletions packages/void/test/unit/deploy-env-expand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'pathe';
import { afterEach, describe, expect, it } from 'vite-plus/test';
import { loadDeployEnv } from '../../src/cli/deploy.ts';

describe('loadDeployEnv', () => {
const roots: Array<string> = [];

afterEach(() => {
for (const root of roots.splice(0, roots.length)) {
rmSync(root, { recursive: true, force: true });
}
});

function makeRoot(files: Record<string, string>): string {
const root = mkdtempSync(join(tmpdir(), 'void-deploy-env-'));
roots.push(root);
for (const [name, content] of Object.entries(files)) {
writeFileSync(join(root, name), content);
}
return root;
}

it('expands ${VAR} references so shipped values match what was validated', () => {
const root = makeRoot({
'.env': 'PLAIN=hello\nAPI_HOST=api.example.com\nAPI_URL=https://${API_HOST}/v1\n',
});

const env = loadDeployEnv(root);

expect(env.PLAIN).toBe('hello');
expect(env.API_HOST).toBe('api.example.com');
expect(env.API_URL).toBe('https://api.example.com/v1');
});

it('expands references across .env and .env.production', () => {
const root = makeRoot({
'.env': 'BASE=example.com\n',
'.env.production': 'URL=https://${BASE}/prod\n',
});

expect(loadDeployEnv(root).URL).toBe('https://example.com/prod');
});
});