Skip to content

Commit cf8f530

Browse files
authored
Fix NoMethodError when environment is missing from shakapacker.yml (#669)
Why Deploying to staging/custom environments crashed with nil error. Summary Added configuration fallback (production) and fixed NODE_ENV assignment for custom environments like staging. Key improvements - Configuration falls back to production for undefined environments - NODE_ENV auto-set to production for staging and custom environments - Added logging when fallback occurs for easier debugging Impact Existing: No change for standard dev/test/production setups New: Staging and custom environments work without explicit config Risks None. Graceful fallback maintains backward compatibility.
1 parent b678c95 commit cf8f530

10 files changed

Lines changed: 320 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@
1111

1212
Changes since the last non-beta release.
1313

14-
_No unreleased changes._
14+
### Fixed
15+
16+
- Fixed NoMethodError when custom environment (e.g., staging) is not defined in shakapacker.yml. [PR #669](https://github.com/shakacode/shakapacker/pull/669) by [justin808](https://github.com/justin808).
17+
- When deploying to environments like Heroku staging with `RAILS_ENV=staging`, shakapacker would crash with `undefined method 'deep_symbolize_keys' for nil:NilClass`
18+
- **Configuration fallback:** Now properly falls back to production environment configuration (appropriate for staging)
19+
- **NODE_ENV handling:** `bin/shakapacker` now automatically sets `NODE_ENV=production` for custom environments (staging, etc.)
20+
- Previously: `RAILS_ENV=staging` would set `NODE_ENV=development`, breaking webpack optimizations
21+
- Now: `RAILS_ENV` in `[development, test]` uses that value for `NODE_ENV`, everything else uses `production`
22+
- Logs informational message when falling back to help with debugging
23+
- This ensures shakapacker works with any Rails environment even if not explicitly defined in shakapacker.yml
24+
- Fixes [#663](https://github.com/shakacode/shakapacker/issues/663)
1525

1626
## [v9.1.0] - October 8, 2025
1727

docs/deployment.md

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
Shakapacker hooks up a new `shakapacker:compile` task to `assets:precompile`, which gets run whenever you run `assets:precompile`.
44
If you are not using Sprockets `shakapacker:compile` is automatically aliased to `assets:precompile`.
55

6-
```
7-
86
## Heroku
97

108
In order for your Shakapacker app to run on Heroku, you'll need to do a bit of configuration before hand.
@@ -19,13 +17,59 @@ git push heroku master
1917

2018
We're essentially doing the following here:
2119

22-
* Creating an app on Heroku
23-
* Creating a Postgres database for the app (this is assuming that you're using Heroku Postgres for your app)
24-
* Adding the Heroku NodeJS and Ruby buildpacks for your app. This allows the `npm` or `yarn` executables to properly function when compiling your app - as well as Ruby.
25-
* Pushing your code to Heroku and kicking off the deployment
20+
- Creating an app on Heroku
21+
- Creating a Postgres database for the app (this is assuming that you're using Heroku Postgres for your app)
22+
- Adding the Heroku NodeJS and Ruby buildpacks for your app. This allows the `npm` or `yarn` executables to properly function when compiling your app - as well as Ruby.
23+
- Pushing your code to Heroku and kicking off the deployment
2624

2725
Your production build process is responsible for installing your JavaScript dependencies before `rake assets:precompile`. For example, if you are on Heroku, the `heroku/nodejs` buildpack must run **prior** to the `heroku/ruby` buildpack for precompilation to run successfully.
2826

27+
### Custom Rails Environments (e.g., staging)
28+
29+
**Key distinction:**
30+
31+
- **RAILS_ENV** is used to look up configuration in `config/shakapacker.yml`
32+
- **NODE_ENV** is used by your `webpack.config.js` (or `rspack.config.js`) for build optimizations
33+
34+
**Good news:** As of this version, `bin/shakapacker` automatically sets `NODE_ENV=production` for custom environments like staging:
35+
36+
```bash
37+
# NODE_ENV automatically set to 'production' for staging
38+
RAILS_ENV=staging bin/shakapacker
39+
40+
# Also works with rake task
41+
RAILS_ENV=staging bundle exec rails assets:precompile
42+
```
43+
44+
**How it works:**
45+
46+
- `RAILS_ENV=development``NODE_ENV=development`
47+
- `RAILS_ENV=test``NODE_ENV=test`
48+
- `RAILS_ENV=production``NODE_ENV=production`
49+
- Any other custom env → `NODE_ENV=production`
50+
51+
**Configuration fallback:**
52+
53+
You don't need to add custom environments to your `shakapacker.yml`. Shakapacker automatically falls back to production-like defaults:
54+
55+
1. First, it looks for the environment you're deploying to (e.g., `staging`)
56+
2. If not found, it falls back to `production` configuration
57+
58+
This means staging environments automatically use production settings (compile: false, cache_manifest: true, etc.).
59+
60+
**Optional: Staging-specific configuration**
61+
62+
If you want different settings for staging, explicitly add a `staging` section:
63+
64+
```yaml
65+
staging:
66+
<<: *default
67+
compile: false
68+
cache_manifest: true
69+
# Staging-specific overrides (e.g., different output path)
70+
public_output_path: packs-staging
71+
```
72+
2973
## Nginx
3074
3175
Shakapacker doesn't serve anything in production. You’re expected to configure your web server to serve files in public/ directly.
@@ -117,10 +161,10 @@ namespace :deploy do
117161
desc "Run rake js install"
118162
task :js_install do
119163
require "package_json"
120-
164+
121165
# this will use the package manager specified via `packageManager`, or otherwise fallback to `npm`
122166
native_js_install_command = PackageJson.read.manager.native_install_command(frozen: true).join(" ")
123-
167+
124168
on roles(:web) do
125169
within release_path do
126170
execute("cd #{release_path} && #{native_js_install_command}")

docs/troubleshooting.md

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
```
2424

2525
## Incorrect peer dependencies
26+
2627
Shakapacker uses peer dependencies to make it easier to manage what versions are being used for your app, which is especially
2728
useful for patching security vulnerabilities. However, not all package managers will actually enforce these versions - notably,
2829
Yarn will omit a warning rather than erroring if you forget to update a peer dependency:
@@ -32,6 +33,7 @@ warning " > shakapacker@6.1.1" has incorrect peer dependency "compression-webpac
3233
```
3334

3435
This omission resulted in an error in the browser:
36+
3537
```
3638
Failed to load resource: net::ERR_CONTENT_DECODING_FAILED
3739
```
@@ -40,6 +42,40 @@ The error was caused by an old version of the peer dependency `webpack-compressi
4042

4143
So, be sure to investigate warnings from `yarn install`!
4244

45+
## NoMethodError: undefined method 'deep_symbolize_keys' for nil:NilClass
46+
47+
If you see this error during deployment (especially on Heroku with a staging environment):
48+
49+
```
50+
NoMethodError: undefined method 'deep_symbolize_keys' for nil:NilClass
51+
from shakapacker/configuration.rb:XXX:in 'load'
52+
```
53+
54+
This happens when deploying to a custom Rails environment (like `staging`) that isn't explicitly defined in your `config/shakapacker.yml` file.
55+
56+
**Solution:** This was fixed in Shakapacker v9.1.1+. Upgrade to the latest version:
57+
58+
```ruby
59+
# Gemfile
60+
gem 'shakapacker', '~> 9.1'
61+
```
62+
63+
After upgrading, Shakapacker will automatically fall back to sensible defaults when your environment isn't defined:
64+
65+
1. First tries your environment (e.g., `staging`)
66+
2. Falls back to `production` configuration
67+
68+
**Alternative:** If you can't upgrade immediately, explicitly add your environment to `config/shakapacker.yml`:
69+
70+
```yaml
71+
staging:
72+
<<: *default
73+
compile: false
74+
cache_manifest: true
75+
```
76+
77+
See the [deployment guide](./deployment.md#custom-rails-environments-eg-staging) for more details.
78+
4379
## ENOENT: no such file or directory - node-sass
4480
4581
If you get the error `ENOENT: no such file or directory - node-sass` on deploy with
@@ -54,7 +90,7 @@ thing, like Heroku.
5490

5591
However, if you get this on local development, or not during a deploy then you
5692
may need to rebuild `node-sass`. It's a bit of a weird error; basically, it
57-
can't find the `node-sass` binary. An easy solution is to create a postinstall
93+
can't find the `node-sass` binary. An easy solution is to create a postinstall
5894
hook to ensure `node-sass` is rebuilt whenever new modules are installed.
5995

6096
In `package.json`:
@@ -67,19 +103,18 @@ In `package.json`:
67103

68104
## Can't find hello_react.js in manifest.json
69105

70-
* If you get this error `Can't find hello_react.js in manifest.json`
71-
when loading a view in the browser it's because webpack is still compiling packs.
72-
Shakapacker uses a `manifest.json` file to keep track of packs in all environments,
73-
however since this file is generated after packs are compiled by webpack. So,
74-
if you load a view in browser whilst webpack is compiling you will get this error.
75-
Therefore, make sure webpack
76-
(i.e `./bin/shakapacker-dev-server`) is running and has
77-
completed the compilation successfully before loading a view.
78-
106+
- If you get this error `Can't find hello_react.js in manifest.json`
107+
when loading a view in the browser it's because webpack is still compiling packs.
108+
Shakapacker uses a `manifest.json` file to keep track of packs in all environments,
109+
however since this file is generated after packs are compiled by webpack. So,
110+
if you load a view in browser whilst webpack is compiling you will get this error.
111+
Therefore, make sure webpack
112+
(i.e `./bin/shakapacker-dev-server`) is running and has
113+
completed the compilation successfully before loading a view.
79114

80115
## throw er; // Unhandled 'error' event
81116

82-
* If you get this error while trying to use Elm, try rebuilding Elm. You can do
117+
- If you get this error while trying to use Elm, try rebuilding Elm. You can do
83118
so with a postinstall hook in your `package.json`:
84119

85120
```json
@@ -90,9 +125,9 @@ completed the compilation successfully before loading a view.
90125

91126
## webpack or webpack-dev-server not found
92127

93-
* This could happen if `shakapacker:install` step is skipped. Please run `bundle exec rails shakapacker:install` to fix the issue.
128+
- This could happen if `shakapacker:install` step is skipped. Please run `bundle exec rails shakapacker:install` to fix the issue.
94129

95-
* If you encounter the above error on heroku after upgrading from Rails 4.x to 5.1.x, then the problem might be related to missing `yarn` binstub. Please run following commands to update/add binstubs:
130+
- If you encounter the above error on heroku after upgrading from Rails 4.x to 5.1.x, then the problem might be related to missing `yarn` binstub. Please run following commands to update/add binstubs:
96131

97132
```bash
98133
bundle config --delete bin
@@ -137,6 +172,7 @@ chmod +x $HOME/your_rails_app/node_modules/.bin/elm-make
137172
```
138173

139174
## Rake assets:precompile fails. ExecJS::RuntimeError
175+
140176
This error occurs because you are trying to minify by `terser` a pack that's already been minified by Shakapacker. To avoid this conflict and prevent appearing of `ExecJS::RuntimeError` error, you will need to disable uglifier from Rails config:
141177

142178
```ruby
@@ -152,10 +188,11 @@ Rails.application.config.assets.js_compressor = Uglifier.new(harmony: true)
152188
### Angular: WARNING in ./node_modules/@angular/core/esm5/core.js, Critical dependency: the request of a dependency is an expression
153189

154190
To silent these warnings, please update `config/webpack/webpack.config.js`:
191+
155192
```js
156-
const webpack = require('webpack')
157-
const { resolve } = require('path')
158-
const { generateWebpackConfig } = require('shakapacker')
193+
const webpack = require("webpack")
194+
const { resolve } = require("path")
195+
const { generateWebpackConfig } = require("shakapacker")
159196
160197
module.exports = generateWebpackConfig({
161198
plugins: [
@@ -192,6 +229,7 @@ Thus ProvidePlugin manages build-time dependencies to global symbols whereas the
192229
**You don't need to assign dependencies on `window`.**
193230

194231
For instance, with [jQuery](https://jquery.com/):
232+
195233
```diff
196234
// app/javascript/entrypoints/application.js
197235
@@ -200,19 +238,20 @@ For instance, with [jQuery](https://jquery.com/):
200238
```
201239

202240
Instead do:
241+
203242
```js
204243
// config/webpack/webpack.config.js
205244
206-
const webpack = require('webpack')
207-
const { generateWebpackConfig } = require('shakapacker')
245+
const webpack = require("webpack")
246+
const { generateWebpackConfig } = require("shakapacker")
208247
209248
module.exports = generateWebpackConfig({
210249
plugins: [
211250
new webpack.ProvidePlugin({
212-
$: 'jquery',
213-
jQuery: 'jquery',
251+
$: "jquery",
252+
jQuery: "jquery"
214253
})
215-
],
254+
]
216255
})
217256
```
218257

@@ -225,7 +264,7 @@ application is using your staging `config.asset_host` host when using
225264

226265
This can be fixed by setting the environment variable `SHAKAPACKER_ASSET_HOST` to
227266
an empty string where your assets are compiled. On Heroku this is done under
228-
*Settings* -> *Config Vars*.
267+
_Settings_ -> _Config Vars_.
229268

230269
This way shakapacker won't hard-code the CDN host into the manifest file used by
231270
`javascript_pack_tag`, but instead fetch the CDN host at runtime, resolving the
@@ -243,6 +282,7 @@ In order to generate the storage path, we rely on the filename that's [provided
243282
This usually works out of the box. There's a potential problem however, if you use the [context setting](https://webpack.js.org/configuration/entry-context/#context) in your webpack config. By default this is set to current Node working directory/project root.
244283

245284
If you were to override it like:
285+
246286
```
247287
{
248288
context: path.resolve(__dirname, '../../app/javascript')
@@ -252,12 +292,14 @@ If you were to override it like:
252292
Then the filename available in the rule generator will be relative to that directory.
253293

254294
This means for example:
295+
255296
- a static asset from `node_modules` folder could end up being referenced with path of `../../node_modules/some_module/static_file.jpg` rather than simply `node_modules/some_module/static_file.jpg`.
256297
- a static asset in one of the `additional_paths`, example `app/assets/images/image.jpg`, would end up being referenced with path of `../assets/images/image.jpg`.
257298

258299
Those paths are later passed to [output path generation in the rule](https://github.com/shakacode/shakapacker/blob/e52b335dbabfb934fe7d3076a8322b97d5ef3470/package/rules/file.js#L25-L26), where we would end up with a path like `static/../../node_modules/some_module/static_file.jpg`, resulting in the file being output in a location two directories above the desired path.
259300

260301
You can avoid this by:
302+
261303
- not using overridden `context` in your webpack config, if there's no good reason for it.
262304
- using custom Webpack config to modify the static file rule, following a similar process as outlined in the [Webpack Configuration](https://github.com/shakacode/shakapacker/blob/main/README.md#webpack-configuration) section of the readme.
263305

lib/shakapacker.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ module Shakapacker
77
extend self
88

99
DEFAULT_ENV = "development".freeze
10+
# Environments that use their RAILS_ENV value for NODE_ENV
11+
# All other environments (production, staging, etc.) use "production" for webpack optimizations
12+
DEV_TEST_ENVS = %w[development test].freeze
1013

1114
def instance=(instance)
1215
@instance = instance
@@ -24,6 +27,13 @@ def with_node_env(env)
2427
ENV["NODE_ENV"] = original
2528
end
2629

30+
# Set NODE_ENV based on RAILS_ENV if not already set
31+
# - development/test environments use their RAILS_ENV value
32+
# - all other environments (production, staging, etc.) use "production" for webpack optimizations
33+
def ensure_node_env!
34+
ENV["NODE_ENV"] ||= DEV_TEST_ENVS.include?(ENV["RAILS_ENV"]) ? ENV["RAILS_ENV"] : "production"
35+
end
36+
2737
def ensure_log_goes_to_stdout
2838
old_logger = Shakapacker.logger
2939
Shakapacker.logger = Logger.new(STDOUT)

lib/shakapacker/configuration.rb

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,21 @@ def load
255255
rescue ArgumentError
256256
YAML.load_file(config_path.to_s)
257257
end
258-
symbolized_config = config[env].deep_symbolize_keys
258+
259+
# Try to find environment-specific configuration with fallback
260+
# Fallback order: requested env → production
261+
if config[env]
262+
env_config = config[env]
263+
elsif config["production"]
264+
log_fallback(env, "production")
265+
env_config = config["production"]
266+
else
267+
# No suitable configuration found - rely on bundled defaults
268+
log_fallback(env, "none (will use bundled defaults)")
269+
env_config = nil
270+
end
271+
272+
symbolized_config = env_config&.deep_symbolize_keys || {}
259273

260274
return symbolized_config
261275
rescue Errno::ENOENT => e
@@ -280,7 +294,10 @@ def defaults
280294
rescue ArgumentError
281295
YAML.load_file(path)
282296
end
283-
HashWithIndifferentAccess.new(config[env] || config[Shakapacker::DEFAULT_ENV])
297+
# Load defaults from bundled shakapacker.yml (always has all environments)
298+
# Note: This differs from load() which reads user's config and may be missing environments
299+
# Fallback to production ensures staging and other custom envs get production-like defaults
300+
HashWithIndifferentAccess.new(config[env] || config["production"])
284301
end
285302
end
286303

@@ -289,4 +306,13 @@ def relative_path(path)
289306

290307
path
291308
end
309+
310+
def log_fallback(requested_env, fallback_env)
311+
return unless Shakapacker.logger
312+
313+
Shakapacker.logger.info(
314+
"Shakapacker environment '#{requested_env}' not found in #{config_path}, " \
315+
"falling back to '#{fallback_env}'"
316+
)
317+
end
292318
end

lib/shakapacker/rspack_runner.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module Shakapacker
66
class RspackRunner < Shakapacker::Runner
77
def self.run(argv)
88
$stdout.sync = true
9-
ENV["NODE_ENV"] ||= (ENV["RAILS_ENV"] == "production") ? "production" : "development"
9+
Shakapacker.ensure_node_env!
1010
new(argv).run
1111
end
1212

lib/shakapacker/runner.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Runner
2424
].freeze
2525
def self.run(argv)
2626
$stdout.sync = true
27-
ENV["NODE_ENV"] ||= (ENV["RAILS_ENV"] == "production") ? "production" : "development"
27+
Shakapacker.ensure_node_env!
2828

2929
# Create a single runner instance to avoid loading configuration twice.
3030
# We extend it with the appropriate build command based on the bundler type.

0 commit comments

Comments
 (0)