Skip to content

Commit 9425c01

Browse files
authored
Fix NODE_ENV defaulting to production breaking dev server (#631) (#632)
Why Dev server failed to load correct config when NODE_ENV unset. Summary NODE_ENV now intelligently defaults based on RAILS_ENV instead of always defaulting to "production". Matches bin/shakapacker behavior. Key improvements - Dev server works out of box without requiring NODE_ENV to be set - RAILS_ENV=production defaults to NODE_ENV=production (safe) - All other RAILS_ENV values default to NODE_ENV=development (DX) Impact - Existing: Fixes port and 404 errors in development - New: No migration needed; can remove manual NODE_ENV workarounds Risks - Tests relying on implicit production default needed updates (fixed)
1 parent 55b05bc commit 9425c01

10 files changed

Lines changed: 263 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 98 additions & 16 deletions
Large diffs are not rendered by default.

docs/v9_upgrade.md

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,38 @@ This guide outlines new features, breaking changes, and migration steps for upgr
99
Shakapacker v9 includes TypeScript definitions for better IDE support and type safety.
1010

1111
- **No breaking changes** - JavaScript configs continue to work
12-
- **Optional** - Use TypeScript only if you want it
12+
- **Optional** - Use TypeScript only if you want it
1313
- **Type safety** - Catch configuration errors at compile-time
1414
- **IDE support** - Full autocomplete for all options
1515

1616
See the [TypeScript Documentation](./typescript.md) for usage examples.
1717

18+
### NODE_ENV Default Behavior Fixed
19+
20+
**What changed:** NODE_ENV now intelligently defaults based on RAILS_ENV instead of always defaulting to "production".
21+
22+
**New behavior:**
23+
24+
- When `RAILS_ENV=production``NODE_ENV` defaults to `"production"`
25+
- When `RAILS_ENV=development` or unset → `NODE_ENV` defaults to `"development"`
26+
- When `RAILS_ENV` is any other value (test, staging, etc.) → `NODE_ENV` defaults to `"development"`
27+
28+
**Benefits:**
29+
30+
- **Dev server "just works"** - No need to explicitly set NODE_ENV when running the development server
31+
- **Correct configuration loaded** - Development server now properly loads the development configuration from shakapacker.yml
32+
- **Fixes port issues** - Dev server uses the configured port (e.g., 3035) instead of defaulting to 8080
33+
- **Fixes 404 errors** - Assets load correctly without requiring manual NODE_ENV configuration
34+
35+
**No action required** - This change improves the default behavior and requires no migration.
36+
37+
**If you previously worked around this bug**, you can now remove these workarounds:
38+
39+
- Remove `NODE_ENV=development` from your `.env`, `.env.development`, or `.env.local` files
40+
- Remove `NODE_ENV=development` from your `docker-compose.yml` or Dockerfile
41+
- Remove custom scripts that set NODE_ENV before running the dev server
42+
- Remove `NODE_ENV=development` from your `bin/dev` or Procfile.dev
43+
1844
## Breaking Changes
1945

2046
### 1. CSS Modules Configuration Changed to Named Exports
@@ -25,25 +51,27 @@ See the [TypeScript Documentation](./typescript.md) for usage examples.
2551
2652
**Quick Reference: Configuration Options**
2753

28-
| Configuration | namedExport | exportLocalsConvention | CSS: `.my-button` | Export Available | Works With |
29-
|---------------|-------------|------------------------|-------------------|------------------|------------|
30-
| **v9 Default** | `true` | `'camelCaseOnly'` | `.my-button` | `myButton` only | ✅ Named exports |
31-
| **Alternative** | `true` | `'dashesOnly'` | `.my-button` | `'my-button'` only | ✅ Named exports |
32-
| **v8 Style** | `false` | `'camelCase'` | `.my-button` | Both `myButton` AND `'my-button'` | ✅ Default export |
33-
| **❌ Invalid** | `true` | `'camelCase'` | - | - | ❌ Build Error |
54+
| Configuration | namedExport | exportLocalsConvention | CSS: `.my-button` | Export Available | Works With |
55+
| --------------- | ----------- | ---------------------- | ----------------- | --------------------------------- | ----------------- |
56+
| **v9 Default** | `true` | `'camelCaseOnly'` | `.my-button` | `myButton` only | ✅ Named exports |
57+
| **Alternative** | `true` | `'dashesOnly'` | `.my-button` | `'my-button'` only | ✅ Named exports |
58+
| **v8 Style** | `false` | `'camelCase'` | `.my-button` | Both `myButton` AND `'my-button'` | ✅ Default export |
59+
| **❌ Invalid** | `true` | `'camelCase'` | - | - | ❌ Build Error |
3460

3561
**JavaScript Projects:**
62+
3663
```js
3764
// Before (v8)
38-
import styles from './Component.module.css';
39-
<button className={styles.button} />
65+
import styles from "./Component.module.css"
66+
;<button className={styles.button} />
4067

4168
// After (v9)
42-
import { button } from './Component.module.css';
43-
<button className={button} />
69+
import { button } from "./Component.module.css"
70+
;<button className={button} />
4471
```
4572

4673
**TypeScript Projects:**
74+
4775
```typescript
4876
// Before (v8)
4977
import styles from './Component.module.css';
@@ -57,6 +85,7 @@ import * as styles from './Component.module.css';
5785
**Migration Options:**
5886

5987
1. **Update your code** (Recommended):
88+
6089
- JavaScript: Change to named imports (`import { className }`)
6190
- TypeScript: Change to namespace imports (`import * as styles`)
6291
- Kebab-case class names are automatically converted to camelCase
@@ -66,6 +95,7 @@ import * as styles from './Component.module.css';
6695
- This gives you time to migrate gradually
6796

6897
**Benefits of the change:**
98+
6999
- Eliminates webpack/TypeScript warnings
70100
- Better tree-shaking of unused CSS classes
71101
- More explicit about which classes are used
@@ -76,15 +106,17 @@ import * as styles from './Component.module.css';
76106
**What changed:** The configuration option has been renamed to better reflect its purpose.
77107

78108
**Before (v8):**
109+
79110
```yml
80111
# config/shakapacker.yml
81-
webpack_loader: 'babel'
112+
webpack_loader: "babel"
82113
```
83114
84115
**After (v9):**
116+
85117
```yml
86118
# config/shakapacker.yml
87-
javascript_transpiler: 'babel'
119+
javascript_transpiler: "babel"
88120
```
89121
90122
**Note:** The old `webpack_loader` option is deprecated but still supported with a warning.
@@ -96,38 +128,48 @@ javascript_transpiler: 'babel'
96128
**Why:** SWC is 20x faster than Babel while maintaining compatibility with most JavaScript and TypeScript code.
97129

98130
**Impact on existing projects:**
131+
99132
- Your project will continue using Babel if you already have babel packages in package.json
100133
- To switch to SWC for better performance, see migration options below
101134

102135
**Impact on new projects:**
136+
103137
- New installations will use SWC by default
104138
- Babel dependencies won't be installed unless explicitly configured
105139

106140
### Migration Options
107141

108142
#### Option 1 (Recommended): Switch to SWC
143+
109144
```yml
110145
# config/shakapacker.yml
111-
javascript_transpiler: 'swc'
146+
javascript_transpiler: "swc"
112147
```
148+
113149
Then install SWC:
150+
114151
```bash
115152
npm install @swc/core swc-loader
116153
```
117154

118155
#### Option 2: Keep using Babel
156+
119157
```yml
120158
# config/shakapacker.yml
121-
javascript_transpiler: 'babel'
159+
javascript_transpiler: "babel"
122160
```
161+
123162
No other changes needed - your existing babel packages will continue to work.
124163

125164
#### Option 3: Use esbuild
165+
126166
```yml
127167
# config/shakapacker.yml
128-
javascript_transpiler: 'esbuild'
168+
javascript_transpiler: "esbuild"
129169
```
170+
130171
Then install esbuild:
172+
131173
```bash
132174
npm install esbuild esbuild-loader
133175
```
@@ -138,24 +180,27 @@ npm install esbuild esbuild-loader
138180

139181
```yml
140182
# config/shakapacker.yml
141-
assets_bundler: 'rspack' # or 'webpack' (default)
183+
assets_bundler: "rspack" # or 'webpack' (default)
142184
```
143185

144186
### 5. All Peer Dependencies Now Optional
145187

146188
**What changed:** All peer dependencies are now marked as optional via `peerDependenciesMeta`.
147189

148190
**Benefits:**
191+
149192
- **No installation warnings** - You won't see peer dependency warnings for packages you don't use
150193
- **Install only what you need** - Using webpack? Don't install rspack. Using SWC? Don't install Babel.
151194
- **Clear version constraints** - When you do install a package, version compatibility is still enforced
152195

153196
**What this means for you:**
197+
154198
- **Existing projects:** No changes needed. Your existing dependencies will continue to work.
155199
- **New projects:** The installer only adds the packages you actually need based on your configuration.
156200
- **Package manager behavior:** npm, yarn, and pnpm will no longer warn about missing peer dependencies.
157201

158202
**Example:** If you're using SWC with webpack, you only need:
203+
159204
```json
160205
{
161206
"dependencies": {
@@ -168,6 +213,7 @@ assets_bundler: 'rspack' # or 'webpack' (default)
168213
}
169214
}
170215
```
216+
171217
You won't get warnings about missing Babel, Rspack, or esbuild packages.
172218

173219
## Migration Steps
@@ -186,22 +232,22 @@ yarn upgrade shakapacker@^9.0.0
186232

187233
```js
188234
// Find imports like this:
189-
import styles from './styles.module.css';
235+
import styles from "./styles.module.css"
190236
191237
// Replace with named imports:
192-
import { className1, className2 } from './styles.module.css';
238+
import { className1, className2 } from "./styles.module.css"
193239
```
194240

195241
#### Update TypeScript definitions:
196242

197243
```typescript
198244
// Update your CSS module type definitions
199-
declare module '*.module.css' {
245+
declare module "*.module.css" {
200246
// With namedExport: true, css-loader generates individual named exports
201247
// TypeScript can't know the exact names at compile time, so we declare
202248
// a module with any number of string exports
203-
const classes: { readonly [key: string]: string };
204-
export = classes;
249+
const classes: { readonly [key: string]: string }
250+
export = classes
205251
// Note: This allows 'import * as styles' but not 'import styles from'
206252
// because css-loader with namedExport: true doesn't generate a default export
207253
}
@@ -213,13 +259,15 @@ v9 automatically converts kebab-case to camelCase with `exportLocalsConvention:
213259
214260
```css
215261
/* styles.module.css */
216-
.my-button { }
217-
.primary-color { }
262+
.my-button {
263+
}
264+
.primary-color {
265+
}
218266
```
219267

220268
```js
221269
// v9 default - camelCase conversion
222-
import { myButton, primaryColor } from './styles.module.css';
270+
import { myButton, primaryColor } from "./styles.module.css"
223271
```
224272

225273
**Alternative: Keep kebab-case names with 'dashesOnly'**
@@ -256,7 +304,7 @@ If you have `webpack_loader` in your configuration:
256304
# webpack_loader: 'babel'
257305

258306
# NEW:
259-
javascript_transpiler: 'babel'
307+
javascript_transpiler: "babel"
260308
```
261309
262310
### Step 5: Run Tests
@@ -291,6 +339,7 @@ If you see warnings about CSS module exports, ensure you've updated all imports
291339
### Build Error: exportLocalsConvention Incompatible with namedExport
292340

293341
If you see this error:
342+
294343
```
295344
"exportLocalsConvention" with "camelCase" value is incompatible with "namedExport: true" option
296345
```
@@ -312,30 +361,35 @@ If you want to use `'camelCase'` (which exports both original and camelCase vers
312361
If you experience unexpected peer dependency warnings after upgrading to v9, you may need to clear your package manager's cache and reinstall dependencies. This ensures the new optional peer dependency configuration takes effect properly.
313362

314363
**For npm:**
364+
315365
```bash
316366
rm -rf node_modules package-lock.json
317367
npm install
318368
```
319369

320370
**For Yarn:**
371+
321372
```bash
322373
rm -rf node_modules yarn.lock
323374
yarn install
324375
```
325376

326377
**For pnpm:**
378+
327379
```bash
328380
rm -rf node_modules pnpm-lock.yaml
329381
pnpm install
330382
```
331383

332384
**For Bun:**
385+
333386
```bash
334387
rm -rf node_modules bun.lockb
335388
bun install
336389
```
337390

338391
**When is this necessary?**
392+
339393
- If you see peer dependency warnings for packages you don't use (e.g., warnings about Babel when using SWC)
340394
- If your package manager cached the old dependency resolution from v8
341395
- After switching transpilers or bundlers (e.g., from Babel to SWC, or webpack to rspack)

lib/shakapacker.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
module Shakapacker
77
extend self
88

9-
DEFAULT_ENV = "production".freeze
9+
DEFAULT_ENV = "development".freeze
1010

1111
def instance=(instance)
1212
@instance = instance

package/env.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,32 @@ const { isFileNotFoundError } = require("./utils/errorHelpers")
66
const { sanitizeEnvValue } = require("./utils/pathValidation")
77

88
const NODE_ENVIRONMENTS = ["development", "production", "test"] as const
9-
const DEFAULT = "production"
109

1110
// Sanitize environment variables to prevent injection
1211
const initialRailsEnv = sanitizeEnvValue(process.env.RAILS_ENV)
1312
const rawNodeEnv = sanitizeEnvValue(process.env.NODE_ENV)
1413

14+
// Default NODE_ENV based on RAILS_ENV to match bin/shakapacker behavior (see lib/shakapacker/runner.rb:27)
15+
// - RAILS_ENV=production → DEFAULT="production" (safe for production builds)
16+
// - RAILS_ENV=development, test, staging, or unset → DEFAULT="development" (good DX for dev server)
17+
// This ensures the dev server works out of the box without requiring NODE_ENV to be set explicitly
18+
const DEFAULT = initialRailsEnv === "production" ? "production" : "development"
19+
1520
// Validate NODE_ENV strictly
1621
const nodeEnv =
17-
rawNodeEnv && NODE_ENVIRONMENTS.includes(rawNodeEnv as typeof NODE_ENVIRONMENTS[number]) ? rawNodeEnv : DEFAULT
22+
rawNodeEnv &&
23+
NODE_ENVIRONMENTS.includes(rawNodeEnv as (typeof NODE_ENVIRONMENTS)[number])
24+
? rawNodeEnv
25+
: DEFAULT
1826

1927
// Log warning if NODE_ENV was invalid
20-
if (rawNodeEnv && !NODE_ENVIRONMENTS.includes(rawNodeEnv as typeof NODE_ENVIRONMENTS[number])) {
28+
if (
29+
rawNodeEnv &&
30+
!NODE_ENVIRONMENTS.includes(rawNodeEnv as (typeof NODE_ENVIRONMENTS)[number])
31+
) {
2132
console.warn(
2233
`[SHAKAPACKER WARNING] Invalid NODE_ENV value: ${rawNodeEnv}. ` +
23-
`Valid values are: ${NODE_ENVIRONMENTS.join(', ')}. Using default: ${DEFAULT}`
34+
`Valid values are: ${NODE_ENVIRONMENTS.join(", ")}. Using default: ${DEFAULT}`
2435
)
2536
}
2637

@@ -42,13 +53,13 @@ try {
4253
} catch (defaultError) {
4354
throw new Error(
4455
`Failed to load Shakapacker configuration.\n` +
45-
`Neither user config (${configPath}) nor default config (${defaultConfigPath}) could be loaded.\n\n` +
46-
`To fix this issue:\n` +
47-
`1. Create a config/shakapacker.yml file in your project\n` +
48-
`2. Or set the SHAKAPACKER_CONFIG environment variable to point to your config file\n` +
49-
`3. Or reinstall Shakapacker to restore the default configuration:\n` +
50-
` npm install shakapacker --force\n` +
51-
` yarn add shakapacker --force`
56+
`Neither user config (${configPath}) nor default config (${defaultConfigPath}) could be loaded.\n\n` +
57+
`To fix this issue:\n` +
58+
`1. Create a config/shakapacker.yml file in your project\n` +
59+
`2. Or set the SHAKAPACKER_CONFIG environment variable to point to your config file\n` +
60+
`3. Or reinstall Shakapacker to restore the default configuration:\n` +
61+
` npm install shakapacker --force\n` +
62+
` yarn add shakapacker --force`
5263
)
5364
}
5465
} else {
@@ -61,13 +72,14 @@ const regex = new RegExp(`^(${availableEnvironments})$`, "g")
6172

6273
const runningWebpackDevServer = process.env.WEBPACK_SERVE === "true"
6374

64-
const validatedRailsEnv = initialRailsEnv && initialRailsEnv.match(regex) ? initialRailsEnv : DEFAULT
75+
const validatedRailsEnv =
76+
initialRailsEnv && initialRailsEnv.match(regex) ? initialRailsEnv : DEFAULT
6577

6678
if (initialRailsEnv && validatedRailsEnv !== initialRailsEnv) {
6779
/* eslint no-console:0 */
6880
console.warn(
6981
`[SHAKAPACKER WARNING] Environment '${initialRailsEnv}' not found in the configuration.\n` +
70-
`Using '${DEFAULT}' configuration as a fallback.`
82+
`Using '${DEFAULT}' configuration as a fallback.`
7183
)
7284
}
7385

spec/shakapacker/configuration_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,8 @@
429429
)
430430
end
431431

432-
it "#cache_manifest? fall back to 'production' config from bundled file" do
433-
expect(config.cache_manifest?).to be true
432+
it "#cache_manifest? fall back to 'development' config from bundled file" do
433+
expect(config.cache_manifest?).to be false
434434
end
435435

436436
it "#shakapacker_precompile? use 'staging' config from custom file" do

0 commit comments

Comments
 (0)