Skip to content

Commit c707618

Browse files
authored
Merge pull request #213 from pneumaticapp/frontend/configuration/46741__migrate_frontend_to_runtime_environment
46741 frontend [ Configuration ] Migrate Frontend to Runtime Environment Variables
2 parents f98f511 + 3cfe1dc commit c707618

12 files changed

Lines changed: 1094 additions & 57 deletions

File tree

docker-compose.src.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,7 @@ services:
396396
FIREBASE_APP_ID: ${FIREBASE_APP_ID:-}
397397
FIREBASE_MEASUREMENT_ID: ${FIREBASE_MEASUREMENT_ID:-}
398398
RECAPTCHA_SITE_KEY: ${RECAPTCHA_SITE_KEY:-}
399-
command: >
400-
sh -c "
401-
npm run build-client:prod &&
402-
pm2-runtime start pm2.json"
399+
command: pm2-runtime start pm2.json
403400
expose:
404401
- 8000
405402
networks:

docker-compose.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,10 +399,7 @@ services:
399399
FIREBASE_APP_ID: ${FIREBASE_APP_ID:-}
400400
FIREBASE_MEASUREMENT_ID: ${FIREBASE_MEASUREMENT_ID:-}
401401
RECAPTCHA_SITE_KEY: ${RECAPTCHA_SITE_KEY:-}
402-
command: >
403-
sh -c "
404-
npm run build-client:prod &&
405-
pm2-runtime start pm2.json"
402+
command: pm2-runtime start pm2.json
406403
expose:
407404
- 8000
408405
networks:

frontend/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ ADD package.json /pneumatic_frontend/package.json
2121
RUN npm ci --legacy-peer-deps
2222

2323
ADD . /pneumatic_frontend/
24+
25+
# Build webpack bundle at image build time (env-independent since runtime config refactor)
26+
ENV NODE_OPTIONS="--max-old-space-size=3072"
27+
RUN npm run build-client:prod
28+
29+
CMD ["pm2-runtime", "start", "pm2.json"]

frontend/config/common.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@
172172
"mainPage",
173173
"formSubdomain",
174174
"recaptchaSecret",
175-
"firebase"
175+
"firebase",
176+
"featureFlags"
176177
]
177178
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// <reference types="jest" />
2+
3+
/**
4+
* Tests for webpack.config.js behavioral changes:
5+
* 1. No dotenv — .env is not read at build time
6+
* 2. No envKeys — process.env is not forwarded to bundle
7+
* 3. DefinePlugin only sets process.env.NODE_ENV
8+
* 4. No MCS_RUN_ENV in HtmlWebpackPlugin
9+
* 5. No Sentry webpack plugin
10+
*/
11+
12+
jest.mock('mini-css-extract-plugin', () => {
13+
class MiniCssExtractPlugin {
14+
static loader = {};
15+
}
16+
return MiniCssExtractPlugin;
17+
});
18+
19+
jest.mock('fork-ts-checker-webpack-plugin', () => {
20+
return class ForkTsCheckerWebpackPlugin {};
21+
});
22+
23+
jest.mock('html-webpack-plugin', () => {
24+
return class HtmlWebpackPlugin {
25+
public options: Record<string, unknown>;
26+
constructor(options: Record<string, unknown>) {
27+
this.options = options;
28+
}
29+
};
30+
});
31+
32+
interface IWebpackPlugin {
33+
constructor: { name: string };
34+
definitions?: Record<string, string>;
35+
options?: Record<string, unknown> & { authToken?: string; filename?: string };
36+
}
37+
38+
describe('webpack.config.js', () => {
39+
const ORIGINAL_ENV = process.env;
40+
41+
beforeEach(() => {
42+
jest.resetModules();
43+
process.env = { ...ORIGINAL_ENV };
44+
});
45+
46+
afterAll(() => {
47+
process.env = ORIGINAL_ENV;
48+
});
49+
50+
const loadConfig = (envOverrides: Record<string, string> = {}) => {
51+
Object.assign(process.env, envOverrides);
52+
return require('../../webpack.config.js') as { plugins: IWebpackPlugin[]; entry: unknown; mode: string; devtool: string };
53+
};
54+
55+
describe('DefinePlugin configuration', () => {
56+
it('defines process.env.NODE_ENV via DefinePlugin (not full process.env)', () => {
57+
process.env.NODE_ENV = 'production';
58+
const config = loadConfig({ NODE_ENV: 'production' });
59+
60+
const definePlugin = config.plugins.find(
61+
(p: IWebpackPlugin) => p.constructor.name === 'DefinePlugin',
62+
);
63+
64+
expect(definePlugin).toBeDefined();
65+
expect(definePlugin!.definitions).toEqual({
66+
'process.env.NODE_ENV': '"production"',
67+
});
68+
});
69+
70+
it('does not forward arbitrary env vars to the bundle', () => {
71+
const config = loadConfig({
72+
NODE_ENV: 'development',
73+
SECRET_KEY: 'should-not-be-in-bundle',
74+
BACKEND_URL: 'https://example.com',
75+
});
76+
77+
const definePlugin = config.plugins.find(
78+
(p: IWebpackPlugin) => p.constructor.name === 'DefinePlugin',
79+
);
80+
81+
expect(definePlugin!.definitions).not.toHaveProperty('process.env');
82+
const definedKeys = Object.keys(definePlugin!.definitions!);
83+
expect(definedKeys).not.toContain('process.env');
84+
expect(definedKeys).toEqual(['process.env.NODE_ENV']);
85+
});
86+
});
87+
88+
describe('HtmlWebpackPlugin configuration', () => {
89+
it('main template does not have mcsRunEnv property', () => {
90+
const config = loadConfig({ NODE_ENV: 'production' });
91+
92+
const htmlPlugins = config.plugins.filter(
93+
(p: IWebpackPlugin) => p.constructor.name === 'HtmlWebpackPlugin',
94+
);
95+
96+
const mainPlugin = htmlPlugins.find((p: IWebpackPlugin) => p.options?.filename === 'main.ejs');
97+
expect(mainPlugin).toBeDefined();
98+
expect(mainPlugin!.options).not.toHaveProperty('mcsRunEnv');
99+
});
100+
101+
it('forms template does not have mcsRunEnv property', () => {
102+
const config = loadConfig({ NODE_ENV: 'production' });
103+
104+
const htmlPlugins = config.plugins.filter(
105+
(p: IWebpackPlugin) => p.constructor.name === 'HtmlWebpackPlugin',
106+
);
107+
108+
const formsPlugin = htmlPlugins.find((p: IWebpackPlugin) => p.options?.filename === 'forms.ejs');
109+
expect(formsPlugin).toBeDefined();
110+
expect(formsPlugin!.options).not.toHaveProperty('mcsRunEnv');
111+
});
112+
});
113+
114+
describe('Sentry webpack plugin removal', () => {
115+
it('does not include sentryWebpackPlugin even when SENTRY_AUTH_TOKEN is set', () => {
116+
const config = loadConfig({
117+
NODE_ENV: 'production',
118+
SENTRY_AUTH_TOKEN: 'fake-token',
119+
SENTRY_RELEASE: 'v1.0.0',
120+
SENTRY_ORG: 'test-org',
121+
SENTRY_PROJECT: 'test-project',
122+
});
123+
124+
const pluginNames = config.plugins.map((p: IWebpackPlugin) => p.constructor.name);
125+
expect(pluginNames).not.toContain('sentryWebpackPlugin');
126+
127+
const hasSentryPlugin = config.plugins.some(
128+
(p: IWebpackPlugin) =>
129+
p.constructor.name.toLowerCase().includes('sentry') ||
130+
(p.options && p.options.authToken),
131+
);
132+
expect(hasSentryPlugin).toBe(false);
133+
});
134+
});
135+
136+
describe('dotenv removal', () => {
137+
it('does not require dotenv module', () => {
138+
const config = loadConfig({ NODE_ENV: 'production' });
139+
140+
// Config should load successfully without a .env file
141+
expect(config).toBeDefined();
142+
expect(config.entry).toBeDefined();
143+
});
144+
});
145+
146+
describe('mode and devtool', () => {
147+
it('sets mode to production when NODE_ENV is production', () => {
148+
const config = loadConfig({ NODE_ENV: 'production' });
149+
150+
expect(config.mode).toBe('production');
151+
});
152+
153+
it('sets mode to development when NODE_ENV is development', () => {
154+
const config = loadConfig({ NODE_ENV: 'development' });
155+
156+
expect(config.mode).toBe('development');
157+
});
158+
});
159+
});

0 commit comments

Comments
 (0)