Skip to content

Commit 1bde289

Browse files
matthewvolkchanceaclark
authored andcommitted
fix(cli): symlink .env.local to .dev.vars before preview (#2944)
Wrangler reads secrets from .dev.vars, not .env.local. Create a symlink from .bigcommerce/.dev.vars to .env.local before running opennextjs-cloudflare preview so the local server has access to environment variables.
1 parent 77e809a commit 1bde289

2 files changed

Lines changed: 68 additions & 1 deletion

File tree

packages/catalyst/src/cli/commands/start.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { Command } from 'commander';
22
import { execa } from 'execa';
3+
import { existsSync, lstatSync, symlinkSync } from 'node:fs';
34
import { afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest';
45

56
import { consola } from '../lib/logger';
67
import { program } from '../program';
78

89
import { start } from './start';
910

11+
vi.mock('node:fs', () => ({
12+
existsSync: vi.fn(() => false),
13+
lstatSync: vi.fn(),
14+
symlinkSync: vi.fn(),
15+
}));
16+
1017
vi.mock('execa', () => ({
1118
execa: vi.fn(() => Promise.resolve({})),
1219
__esModule: true,
@@ -44,3 +51,38 @@ test('calls execa with OpenNext production optimized server', async () => {
4451
}),
4552
);
4653
});
54+
55+
test('creates symlink when .env.local exists but .dev.vars does not', async () => {
56+
vi.mocked(existsSync).mockImplementation((p) => {
57+
if (String(p).endsWith('.env.local')) return true;
58+
59+
return false;
60+
});
61+
62+
await program.parseAsync(['node', 'catalyst', 'start']);
63+
64+
expect(symlinkSync).toHaveBeenCalledWith(
65+
expect.stringContaining('.env.local'),
66+
expect.stringContaining('.dev.vars'),
67+
);
68+
});
69+
70+
test('warns when .dev.vars exists and is not a symlink', async () => {
71+
vi.mocked(existsSync).mockReturnValue(true);
72+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- partial mock
73+
vi.mocked(lstatSync).mockReturnValue({
74+
isSymbolicLink: () => false,
75+
} as ReturnType<typeof lstatSync>);
76+
77+
await program.parseAsync(['node', 'catalyst', 'start']);
78+
79+
expect(symlinkSync).not.toHaveBeenCalled();
80+
});
81+
82+
test('warns when .env.local does not exist', async () => {
83+
vi.mocked(existsSync).mockReturnValue(false);
84+
85+
await program.parseAsync(['node', 'catalyst', 'start']);
86+
87+
expect(symlinkSync).not.toHaveBeenCalled();
88+
});

packages/catalyst/src/cli/commands/start.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
11
import { Command } from 'commander';
22
import { execa } from 'execa';
3-
import { join } from 'node:path';
3+
import { existsSync, lstatSync, symlinkSync } from 'node:fs';
4+
import { join, relative } from 'node:path';
5+
6+
import { consola } from '../lib/logger';
47

58
export const start = new Command('start')
69
.description(
710
'Start a local preview of your Catalyst storefront using the OpenNext Cloudflare adapter.',
811
)
912
.action(async () => {
13+
const envLocal = join(process.cwd(), '.env.local');
14+
const devVars = join(process.cwd(), '.bigcommerce', '.dev.vars');
15+
16+
if (existsSync(envLocal)) {
17+
if (!existsSync(devVars)) {
18+
const target = relative(join(process.cwd(), '.bigcommerce'), envLocal);
19+
20+
symlinkSync(target, devVars);
21+
consola.success('Symlinked .bigcommerce/.dev.vars → .env.local');
22+
} else if (!lstatSync(devVars).isSymbolicLink()) {
23+
consola.warn(
24+
'.bigcommerce/.dev.vars already exists and is not a symlink. ' +
25+
'Wrangler will use it instead of .env.local.',
26+
);
27+
}
28+
} else {
29+
consola.warn(
30+
'No .env.local file found. The preview server may fail without environment variables.\n' +
31+
'Create a .env.local file in your project root with the required variables.',
32+
);
33+
}
34+
1035
await execa(
1136
'pnpm',
1237
[

0 commit comments

Comments
 (0)