Skip to content

[codex] Fix ESM-safe config helper binstubs#1104

Merged
justin808 merged 7 commits into
mainfrom
codex/fix-esm-config-binstubs
May 21, 2026
Merged

[codex] Fix ESM-safe config helper binstubs#1104
justin808 merged 7 commits into
mainfrom
codex/fix-esm-config-binstubs

Conversation

@justin808

@justin808 justin808 commented Apr 30, 2026

Copy link
Copy Markdown
Member

Summary

  • replace the app-installed shakapacker-config and diff-bundler-config JavaScript binstubs with Ruby wrappers so apps using "type": "module" do not parse them as ESM
  • add package-level .cjs entrypoints and npm bin mappings for both helpers
  • update the generated config-exporter binstub, dummy app binstub, and rake fallback path to use the package .cjs entrypoint

Fixes #1101.

Validation

  • bundle exec rspec spec/shakapacker/helper_binstubs_spec.rb
  • bundle exec rubocop lib/install/bin/shakapacker-config lib/install/bin/diff-bundler-config lib/tasks/shakapacker/export_bundler_config.rake spec/shakapacker/helper_binstubs_spec.rb spec/dummy/bin/shakapacker-config
  • yarn eslint package/bin/shakapacker-config.cjs package/bin/diff-bundler-config.cjs package/configExporter/cli.ts
  • ruby -c lib/install/bin/shakapacker-config && ruby -c lib/install/bin/diff-bundler-config && ruby -c spec/dummy/bin/shakapacker-config && ruby -c lib/tasks/shakapacker/export_bundler_config.rake
  • node --check package/bin/shakapacker-config.cjs && node --check package/bin/diff-bundler-config.cjs

Note

Medium Risk
Changes how shakapacker-config/diff-bundler-config are installed and executed (Ruby wrapper -> Node .cjs), which can affect CLI/rake task behavior across environments and Windows/path setups, but is scoped to helper binstubs.

Overview
Fixes ESM apps (those with "type": "module") failing to run bin/shakapacker-config and bin/diff-bundler-config by replacing the gem-installed JS binstub templates with Ruby wrappers that locate node and exec packaged CommonJS scripts under node_modules/shakapacker/package/bin/*.cjs.

Adds the .cjs package entrypoints and npm bin mappings in package.json, updates the config-exporter’s generated binstub template to emit the same Ruby wrapper (with explicit chmod), and adjusts the shakapacker:export_bundler_config rake task to invoke binstubs via RbConfig.ruby for consistent Ruby/shebang behavior. Tests were updated/added to allow intentional binstub divergence and to validate wrapper behavior/error cases.

Reviewed by Cursor Bugbot for commit 21179d8. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed shakapacker-config and diff-bundler-config binstub commands to work properly in apps configured with ESM ("type": "module"). Existing applications should re-run bundle exec rake shakapacker:binstubs to install the updated binstubs.

Review Change Stack

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 744a083f-6208-4276-8ebe-68dfb94a20ab

📥 Commits

Reviewing files that changed from the base of the PR and between 3a6c3a1 and 21179d8.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • lib/install/bin/diff-bundler-config
  • lib/install/bin/shakapacker-config
  • lib/tasks/shakapacker/export_bundler_config.rake
  • package.json
  • package/bin/diff-bundler-config.cjs
  • package/bin/shakapacker-config.cjs
  • package/configExporter/cli.ts
  • spec/dummy/bin/shakapacker-config
  • spec/shakapacker/binstub_sync_spec.rb
  • spec/shakapacker/helper_binstubs_spec.rb

Walkthrough

Binstubs bin/shakapacker-config and bin/diff-bundler-config are converted from JavaScript to Ruby wrappers. The wrappers locate Node, verify availability, set environment defaults, and delegate to centralized .cjs scripts in node_modules/shakapacker/package/bin/, solving the require is not defined error in ESM apps.

Changes

ESM-Compatible Binstubs via Ruby Wrappers

Layer / File(s) Summary
CommonJS package bin scripts
package.json, package/bin/shakapacker-config.cjs, package/bin/diff-bundler-config.cjs
Package exports two new CLI executables: shakapacker-config wraps configExporter.run with promise handling; diff-bundler-config loads pack-config-diff, validates its run() function, normalizes exit codes to 0/1/2, and handles async results.
Ruby wrapper binstubs
lib/install/bin/shakapacker-config, lib/install/bin/diff-bundler-config, spec/dummy/bin/shakapacker-config
Binstubs are now Ruby scripts that locate app root via Gemfile, verify node availability, set RAILS_ENV/NODE_ENV defaults, and exec the corresponding .cjs script from node_modules/shakapacker/package/bin/.
Binstub code generation
package/configExporter/cli.ts
The template for generated binstubs is updated to output Ruby wrappers with embedded helper functions instead of JavaScript shims.
Rake task integration
lib/tasks/shakapacker/export_bundler_config.rake
The shakapacker:export_bundler_config task now invokes binstubs via RbConfig.ruby instead of direct execution, adapting to the new wrapper-based architecture.
Spec synchronization and test coverage
spec/shakapacker/binstub_sync_spec.rb, spec/shakapacker/helper_binstubs_spec.rb
Binstub sync specs introduce INTENTIONALLY_DIVERGENT_BINSTUBS allowlist to permit intentional differences between bin/ and lib/install/bin/ binstubs; new comprehensive tests validate wrapper behavior: correct cwd recording, Node availability checks, Gemfile fallback, missing script errors, and Node executable errors.
Changelog documentation
CHANGELOG.md
Documents the fix and advises existing apps to re-run bundle exec rake shakapacker:binstubs to install updated Ruby wrappers.

Sequence Diagram

sequenceDiagram
    participant User
    participant RubyWrapper as Ruby Wrapper<br/>(bin/shakapacker-config)
    participant Node as Node Binary
    participant CJSScript as .cjs Script<br/>(shakapacker-config.cjs)
    participant ConfigExporter as configExporter
    User->>RubyWrapper: bin/shakapacker-config [args]
    RubyWrapper->>RubyWrapper: locate Gemfile ↦ app_root
    RubyWrapper->>RubyWrapper: verify node --version exists
    RubyWrapper->>RubyWrapper: set RAILS_ENV, NODE_ENV env defaults
    RubyWrapper->>RubyWrapper: resolve node_modules/.../shakapacker-config.cjs
    RubyWrapper->>Node: exec node <script> <args>
    Node->>CJSScript: require and execute
    CJSScript->>ConfigExporter: require('./configExporter').run(args)
    ConfigExporter->>CJSScript: return exitCode (Promise or number)
    CJSScript->>Node: process.exit(exitCode)
    Node->>User: exit with code
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hops from Node to Ruby's shore,
Where binstubs wrap what CommonJS held before,
Now ESM apps can call bin/shakapacker-config,
With nary a require is not defined shock!
The .cjs scripts hide safe in node_modules deep,
While Ruby wrappers guide Node with a leap. 🐰

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-esm-config-binstubs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Code Review

Good approach to fix the ESM compatibility problem. Converting JS binstubs to Ruby wrappers that exec Node against .cjs files inside the shakapacker package directory is clean and sidesteps the ESM parse issue entirely.

Issues

Root detection inconsistency between installed and generated binstubs

lib/install/bin/shakapacker-config and lib/install/bin/diff-bundler-config use a defensive shakapacker_app_root() helper that validates the parent directory by checking for a Gemfile or package.json, with a Dir.pwd fallback. The binstub generated by createBinStub() in cli.ts uses plain APP_ROOT = File.expand_path("..", __dir__) without that validation or fallback — and spec/dummy/bin/shakapacker-config does the same. Both the installed template and the generated output land in an app's bin/ directory and should behave consistently. In an edge-case location (e.g. a monorepo workspace where __dir__ is not directly under the app root), the simpler path could silently resolve to the wrong root.

pack-config-diff resolution change should be documented

The old lib/install/bin/diff-bundler-config used createRequire(require.resolve("shakapacker/package.json")) with an explicit comment: "Resolve pack-config-diff via shakapacker's dependency tree so strict package managers (pnpm, Yarn PnP) can find the transitive dependency." The new package/bin/diff-bundler-config.cjs replaces that with a plain require("pack-config-diff").

This is likely correct — the script now executes from within node_modules/shakapacker/package/bin/, so Node's natural resolution climbs the real path and finds pack-config-diff in shakapacker's virtual store entry. But the PR does not mention the deliberate drop of the old workaround, so a future contributor may re-add createRequire thinking it was accidentally lost. A one-line comment explaining why the plain require is now sufficient would prevent that churn.

Minor

  • Missing error-path test: spec/shakapacker/helper_binstubs_spec.rb only covers the happy path. There is no coverage for the branch where the .cjs file is absent (unless File.file?), which is the main new error-handling code in this PR.
  • Double chmod in createBinStub: writeFileSync is called with mode: 0o755 and then chmodSync is called again. The comment acknowledges this, but the first call already handles it on systems that support it; the second call is redundant on those systems.

Comment thread package/bin/diff-bundler-config.cjs Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Rake fallback exec needs explicit Ruby interpreter
    • The fallback now invokes the gem script with RbConfig.ruby so it does not depend on the gem file execute bit.

Create PR

Or push these changes by commenting:

@cursor push 5adaae4e86
Preview (5adaae4e86)
diff --git a/lib/tasks/shakapacker/export_bundler_config.rake b/lib/tasks/shakapacker/export_bundler_config.rake
--- a/lib/tasks/shakapacker/export_bundler_config.rake
+++ b/lib/tasks/shakapacker/export_bundler_config.rake
@@ -1,3 +1,5 @@
+require "rbconfig"
+
 namespace :shakapacker do
   desc <<~DESC
     Export webpack or rspack configuration for debugging and analysis
@@ -54,7 +56,7 @@
       $stderr.puts ""
 
       Dir.chdir(Rails.root) do
-        exec(gem_bin_path, *ARGV[1..])
+        exec(RbConfig.ruby, gem_bin_path, *ARGV[1..])
       end
     else
       # Pass through command-line arguments after the task name

You can send follow-ups to the cloud agent here.

Comment thread lib/tasks/shakapacker/export_bundler_config.rake Outdated
Comment thread package/configExporter/cli.ts Outdated
Comment thread spec/shakapacker/helper_binstubs_spec.rb
@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overall: The approach is sound — replacing JS binstubs with Ruby wrappers that exec node against .cjs entrypoints neatly sidesteps the ESM parsing problem. The change is well-scoped and the new spec provides good coverage. A few things worth addressing before merging:


Issues

1. Test path mismatch on macOS (potential CI-local divergence)

In spec/shakapacker/helper_binstubs_spec.rb (the "exits with an error" test), expected_script_path is built with File.realpath(app_path), but the binstub constructs script_path using File.expand_path("..", __dir__), which does not resolve symlinks. On macOS, Dir.mktmpdir returns /tmp/… which is a symlink to /private/tmp/…, so these two paths diverge and the include assertion fails locally even though CI (Linux) passes.

Fix: use File.realpath inside the binstubs for error messages, or use File.expand_path in the test assertion, or simply compare only the suffix (e.g., end_with).

2. createBinStub hardcodes "shakapacker-config.cjs"

The createBinStub(binStubPath) function in cli.ts receives a path argument but the generated stub body always references shakapacker-config.cjs by name. It's fine today (only one call site), but it's a latent bug if the function is ever reused for a different command. Deriving the .cjs filename from basename(binStubPath) would make it self-consistent.

3. Silent fallback to Dir.pwd in shakapacker_app_root

When neither Gemfile nor package.json is found relative to the binstub's directory, the helper silently falls back to Dir.pwd. If someone runs a binstub from an unrelated directory, this will silently pick up the wrong node_modules tree and produce a confusing "Could not find …" error. A warn before the fallback would make the failure mode obvious.


Minor / nits

4. spec/dummy/bin/shakapacker-config uses Ruby constants instead of locals

The dummy binstub uses APP_ROOT and SCRIPT_PATH (constants) instead of app_root and script_path (locals) like every other binstub in the PR. Constants in top-level scripts are global to Object — not dangerous here, but inconsistent. Worth aligning.

5. Redundant chmodSync in createBinStub

writeFileSync(binStubPath, stubContent, { mode: 0o755 }) already sets the mode; the chmodSync(binStubPath, 0o755) call immediately after is redundant. The try/catch comment acknowledges this but adds dead weight.


Upgrade path

Existing apps that already have the old JavaScript binstubs installed (e.g., a bin/shakapacker-config with #!/usr/bin/env node) won't be updated automatically. The CHANGELOG entry (or upgrade notes) should instruct users to re-run rake shakapacker:binstubs to get the new Ruby wrappers.

Comment thread spec/shakapacker/helper_binstubs_spec.rb Outdated
Comment thread package/configExporter/cli.ts Outdated
Comment thread spec/dummy/bin/shakapacker-config Outdated
Comment thread lib/install/bin/shakapacker-config
Comment thread spec/shakapacker/helper_binstubs_spec.rb
Comment thread package/bin/diff-bundler-config.cjs Outdated
@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Review: ESM-safe config helper binstubs

The overall approach is well-chosen: converting the app-installed JS binstubs to Ruby wrappers that exec node on .cjs entrypoints neatly sidesteps the ESM parse error without requiring apps to carry any bundler-awareness in their binstubs. The new .cjs package entrypoints and bin mappings in package.json are a clean addition.

Bugs / correctness

exec(bin_path.to_s, ...) in the rake task relies on shebang (#64)

In export_bundler_config.rake, the fallback path (line 57) was correctly updated to exec(RbConfig.ruby, gem_bin_path, ...), but the main path when the binstub is found still calls:

exec(bin_path.to_s, *ARGV[1..])

This relies on the OS honouring the #!/usr/bin/env ruby shebang line, which does not work on Windows. It should be made consistent with the fallback:

exec(RbConfig.ruby, bin_path.to_s, *ARGV[1..])

Design concerns

shakapacker_app_rootpackage.json alone is too broad a marker

Checking File.exist?(File.join(candidate, "package.json")) as a standalone condition could match the wrong directory in monorepos where the Rails app is a subdirectory alongside other JS packages. Rails apps with Shakapacker always have a Gemfile; the package.json check is unnecessary and risks a false positive. Covered in an inline comment on lib/install/bin/shakapacker-config.

require.resolve("shakapacker/package.json") under Yarn PnP

In package/bin/diff-bundler-config.cjs, the createRequire anchor calls require.resolve("shakapacker/package.json"). The package.json subpath is not listed in shakapacker's exports map, so Yarn PnP in strict mode will reject it. Options: add "./package.json": "./package.json" to the exports map, or anchor on the declared main entry instead. Covered in an inline comment on the file.

Test coverage gap

The helper_binstubs_spec.rb always creates a Gemfile marker, so the Dir.pwd fallback inside shakapacker_app_root is never tested. Covered in an inline comment on the spec.

Minor

  • spec/dummy/bin/shakapacker-config diverges from the generated template: it uses top-level constants (APP_ROOT, SCRIPT_PATH) and lacks shakapacker_app_root. The test suite copies lib/install/bin/* directly so this doesn't affect coverage, but the dummy binstub should match what a real app gets so it stays representative. Covered in an inline comment.
  • The shakapacker_app_root method is duplicated verbatim across both install binstubs. These are standalone scripts so sharing a helper isn't possible, but a brief synchronisation note (or a shared generator source of truth) would help prevent drift.

Overall this is a solid fix for a real pain point. The issues above are mostly minor or have straightforward fixes; the Yarn PnP require.resolve concern is the one most worth validating against a PnP app before merging.

@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Code Review

The core approach is sound: converting JS binstubs to Ruby wrappers that exec node against .cjs files sidesteps the ESM parse issue cleanly. Test coverage is good — both the happy path (ARGV forwarding, correct cwd) and the missing-script error path are covered.

Issues

spec/dummy/bin/shakapacker-config is inconsistent with the installed binstubs

The installed lib/install/bin/shakapacker-config and lib/install/bin/diff-bundler-config use the defensive shakapacker_app_root helper with a Dir.pwd fallback. spec/dummy/bin/shakapacker-config uses bare APP_ROOT = File.expand_path("..", __dir__) — no fallback, no validation. Since cli.ts now correctly generates the defensive version, the dummy should match it.

No helpful error when node is not found in PATH

exec "node", script_path, *ARGV raises Errno::ENOENT if Node.js is absent from PATH, producing a raw OS error with no Shakapacker context. A simple pre-check (e.g. warn + exit 1 if system("node --version", out: File::NULL, err: File::NULL) returns false) would give users a much clearer signal.

require.resolve("shakapacker/package.json") inside the shakapacker package needs a "why" comment

In package/bin/diff-bundler-config.cjs, the script resolves "shakapacker/package.json" from within the shakapacker package itself — Node climbs the tree, finds the app's node_modules/shakapacker/package.json, and createRequire on that path correctly scopes pack-config-diff resolution to shakapacker's dependency tree (needed for pnpm/Yarn PnP). The existing comment explains the goal but not why the self-referential resolve is correct when the script already lives inside the package. A one-liner would prevent a well-meaning future refactor from replacing it with a simpler __dirname-based approach that breaks strict package managers.

Minor

  • The double chmod in createBinStub (writeFileSync with mode: 0o755 + chmodSync) is intentional, but the comment could note that mode in writeFileSync is a no-op on Windows — that's the actual reason both calls are kept.
  • shakapacker_app_root is duplicated verbatim in both installed binstubs. Expected for standalone scripts, but a comment in each pointing to the other would remind maintainers to keep them in sync.

Comment thread spec/dummy/bin/shakapacker-config Outdated
Comment thread lib/install/bin/shakapacker-config Outdated
Comment thread package/bin/diff-bundler-config.cjs Outdated
Comment thread package/bin/diff-bundler-config.cjs Outdated
Comment thread package/configExporter/cli.ts
@claude

claude Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Review

Clean fix for the ESM parse issue. Replacing the JS binstubs with Ruby wrappers is the right call — the shebang approach is unambiguous regardless of the app's \"type\" field.

Must-fix

exec inconsistency in export_bundler_config.rake

The fallback path (binstub absent) was correctly updated to exec(RbConfig.ruby, gem_bin_path, ...). But the primary path (binstub found, line 64) still uses exec(bin_path.to_s, *ARGV[1..]), relying on #!/usr/bin/env ruby in the shebang. In rbenv/rvm/mise environments env ruby can resolve to a different Ruby than the one running Rake. Both branches should use RbConfig.ruby:

exec(RbConfig.ruby, bin_path.to_s, *ARGV[1..])

Suggestions

CHANGELOG.md — This is a user-visible bug fix (ESM apps silently broke). Per project guidelines, an entry belongs under ### Fixed in the [Unreleased] section.

Code duplicationshakapacker_app_root and shakapacker_node_binary are duplicated verbatim across three locations: lib/install/bin/shakapacker-config, lib/install/bin/diff-bundler-config, and the template string in cli.ts. The template copy is particularly easy to forget when fixing the helpers later. Even a comment pointing from the template to the canonical source files would help.

Looks good

  • The createRequire(__filename) change in diff-bundler-config.cjs is correct — the .cjs file lives inside node_modules/shakapacker/, so Node resolves from within shakapacker's package boundary, which is exactly what strict package managers (pnpm, Yarn PnP) need. (Left an inline note to preserve this reasoning for future contributors.)
  • Test coverage is solid: happy path, Dir.pwd fallback, missing script, and unavailable Node are all covered.
  • The generated binstub template in cli.ts now matches the installed binstubs — the previous inconsistency (plain APP_ROOT vs. the guarded helper) is resolved.

@justin808

Copy link
Copy Markdown
Member Author

Review feedback addressed — 459668f

I triaged the 14 outstanding inline comments + the 5 general review summaries and pushed 459668f. All review threads are now resolved.

Code changes in this commit

  • Rake task consistency (must-fix)lib/tasks/shakapacker/export_bundler_config.rake: the primary exec path now invokes RbConfig.ruby like the fallback path, so the binstub runs under the same Ruby as Rake regardless of shebang/version-manager resolution and works on Windows.
  • CHANGELOG entry (must-fix) — added a ### Fixed line under [Unreleased] describing the ESM binstub fix and instructing existing apps to re-run bundle exec rake shakapacker:binstubs to pick up the new Ruby wrappers.
  • Warn before Dir.pwd fallbackshakapacker_app_root now emits [Shakapacker] No Gemfile found at …; falling back to the current directory (…) before the fallback, in all four locations (two installed binstubs, spec/dummy/bin/shakapacker-config, and the createBinStub template).
  • chmodSync rationale — replaced the misleading "mode should handle it" comment with an explanation that writeFileSync's mode is umask-filtered (so a restrictive umask like 077 would strip the execute bit) while chmodSync sets the mode unconditionally, with the try/catch guarding Windows/FAT filesystems.
  • Sync notes for duplicated helpers — added # Keep in sync with … headers on the three Ruby copies plus the TypeScript template, so the relationship is explicit when one copy changes.
  • Expanded createRequire(__filename) commentpackage/bin/diff-bundler-config.cjs now spells out why anchoring at this file's location is the right choice for pnpm/Yarn PnP, with a "do not simplify" callout to prevent future regressions.

Items already addressed in earlier commits on this branch

These came up repeatedly across the five review summaries and were resolved before this commit landed: switching from require.resolve("shakapacker/package.json") to createRequire(__filename), dropping the package.json marker in favor of Gemfile-only, deriving the .cjs filename in createBinStub from basename(binStubPath), regex-matching invariant suffixes in the macOS-symlink-sensitive spec, error-branch and Dir.pwd-fallback test coverage, the shakapacker_node_binary PATH check, and aligning spec/dummy/bin/shakapacker-config with the installed template. Each inline thread has an individual reply pointing to the relevant code.

Verification

  • bundle exec rspec spec/shakapacker/helper_binstubs_spec.rb — 8 examples, 0 failures
  • bundle exec rubocop (changed Ruby files) — no offenses
  • yarn lint — clean
  • yarn build:types — clean
  • yarn jest --testPathPattern='configExporter' — 160/160 pass

🤖 Generated with Claude Code

justin808 added a commit that referenced this pull request May 21, 2026
Resolves CHANGELOG.md conflict by keeping both main's #1086 and #1085
Fixed entries alongside this branch's #1104 entry.
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Code Review

Verdict: Looks good with minor suggestions.

The approach is sound — Ruby wrappers sidestep ESM parse errors entirely because Node never sees them, and the .cjs extension guarantees CommonJS semantics for the actual scripts. The RbConfig.ruby change in the rake task is a nice bonus for Windows/version-manager correctness. Test coverage hits the important scenarios: ESM app, missing Gemfile fallback, missing script, and missing Node.

Main concern: 4-way duplication

shakapacker_app_root / shakapacker_node_binary are copied verbatim across four files:

  • lib/install/bin/shakapacker-config
  • lib/install/bin/diff-bundler-config
  • spec/dummy/bin/shakapacker-config
  • The template string in createBinStub in package/configExporter/cli.ts

The "Keep in sync" comments are honest, but any change to the fallback logic (e.g., re-adding the package.json check that was in commit 1 but later dropped) must land in all four places at once. One lower-risk alternative: have createBinStub read the canonical lib/install/bin/shakapacker-config at runtime with fs.readFileSync and write it verbatim (swapping shakapacker-config.cjs → the actual script name via a string replace). That eliminates the copy in cli.ts and keeps the truth in one Ruby file. The two install binstubs would still need to stay in sync with each other, but that's a simpler two-file problem.

Not a blocker, but worth addressing before the helper logic grows.

Minor issues (see inline comments)

  • package/bin/shakapacker-config.cjs: error.message will be undefined if a non-Error value is thrown — diff-bundler-config.cjs uses a safer formatError helper; applying it here would be consistent.
  • Fallback test: the "falls back" case verifies exit 0 but doesn't assert the warning is emitted to stderr. A refactor could silently drop the warning without a test failure.
  • NODE_ENV in diff-bundler-config: the default "development" is inherited by the Node child process. Intentional for config loading, or is it just cargo-culted from shakapacker-config?

Comment thread package/bin/shakapacker-config.cjs
Comment thread spec/shakapacker/helper_binstubs_spec.rb
Comment thread lib/install/bin/diff-bundler-config
@justin808 justin808 force-pushed the codex/fix-esm-config-binstubs branch from e8292dc to 935cf92 Compare May 21, 2026 19:37
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Code review posted as inline comments.

@justin808 justin808 force-pushed the codex/fix-esm-config-binstubs branch from 935cf92 to 21179d8 Compare May 21, 2026 20:27

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: NODE_ENV default ignores RAILS_ENV, diverging from ecosystem convention
    • Updated the binstubs and generated binstub template to derive default NODE_ENV from RAILS_ENV, using development only for development/test and production otherwise.

Create PR

Or push these changes by commenting:

@cursor push 13c16e0b3e

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 21179d8. Configure here.

Comment thread lib/install/bin/shakapacker-config
Comment thread package/configExporter/cli.ts
Comment thread spec/shakapacker/binstub_sync_spec.rb
Comment thread lib/install/bin/shakapacker-config
@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Clean, well-motivated fix overall -- see inline comments for specifics. Summary of findings: (1) The sync comment in cli.ts says 'update all three' but spec/dummy/bin/shakapacker-config is a fourth copy not mentioned -- a future contributor will miss it. (2) The binstub_sync_spec test name 'documents every divergent binstub and only divergent binstubs' overstates what it checks; the 'only' direction is handled by the loop above it, not by this test. (3) Minor: the Node probe via system() forks a subprocess; RbConfig::Utils.find_executable walks PATH without forking. (4) Informational: Ruby exec on Windows spawns a child rather than replacing the process -- worth a comment so it is not 'fixed' later. The RbConfig.ruby change in the rake task and the createRequire(__filename) anchor in the .cjs script are both good calls. Test coverage with real subprocesses is solid. Approve pending the sync-comment fix.

@justin808 justin808 marked this pull request as ready for review May 21, 2026 22:10
@justin808 justin808 merged commit 4962cbe into main May 21, 2026
67 of 68 checks passed
@justin808 justin808 deleted the codex/fix-esm-config-binstubs branch May 21, 2026 22:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21179d8bb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/tasks/shakapacker/export_bundler_config.rake
@greptile-apps

greptile-apps Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the app-installed shakapacker-config and diff-bundler-config JavaScript binstubs with Ruby wrappers that locate Node and exec new .cjs package scripts, fixing ESM parse failures in apps with "type": "module". The rake fallback path is also updated to use RbConfig.ruby for cross-platform correctness.

  • New package/bin/*.cjs files carry the original JS logic; diff-bundler-config.cjs correctly switches the createRequire anchor to __filename since it now lives inside the package tree, which is the right behaviour for pnpm/Yarn PnP strict mode.
  • binstub_sync_spec.rb gains an INTENTIONALLY_DIVERGENT_BINSTUBS allowlist and a guard test; together with the existing per-binstub equality tests the full invariant (document-or-match) is enforced.
  • The Ruby wrappers and the createBinStub template do not rescue SystemCallError from exec, so a race between the node --version probe and the actual exec would surface as a raw Ruby backtrace rather than a user-friendly message.

Confidence Score: 4/5

Safe to merge; the Ruby-wrapper approach correctly solves the ESM parse problem and the new .cjs files preserve the original JS logic faithfully.

The core change is well-structured: Ruby wrappers delegate to CommonJS scripts, the rake task now explicitly passes RbConfig.ruby for portability, and tests cover the ESM success path, missing-script, and no-node error cases. The only gap is that all three Ruby wrapper surfaces leave exec calls unguarded by a rescue, so an unexpected exec failure produces a raw Ruby backtrace rather than a Shakapacker-branded error message.

lib/install/bin/shakapacker-config, lib/install/bin/diff-bundler-config, and the generated template in package/configExporter/cli.ts all share the same unrescued exec pattern and should be updated together.

Important Files Changed

Filename Overview
lib/install/bin/shakapacker-config Replaced JS binstub with Ruby wrapper that locates node and execs package/bin/shakapacker-config.cjs; exec call has no rescue if node disappears between the existence check and exec.
lib/install/bin/diff-bundler-config Replaced JS binstub with Ruby wrapper mirroring shakapacker-config; same exec-without-rescue concern.
package/bin/shakapacker-config.cjs New CommonJS entrypoint extracted from the old JS binstub; run() is typed as Promise so the exit code chain is safe.
package/bin/diff-bundler-config.cjs New CommonJS entrypoint using createRequire(__filename) instead of the old require.resolve anchor; correct because this file lives inside the shakapacker package tree.
package/configExporter/cli.ts createBinStub now emits a Ruby wrapper with packageScript interpolated from basename(binStubPath); only ever called with bin/shakapacker-config so packageScript is always shakapacker-config.cjs.
lib/tasks/shakapacker/export_bundler_config.rake Both exec branches now use RbConfig.ruby as the interpreter instead of relying on shebangs, improving Windows compatibility and version-manager consistency.
spec/shakapacker/helper_binstubs_spec.rb New spec covers ESM-app success path, missing-script error, and no-node error; uses a fake CJS node script and Open3 for real subprocess validation.
spec/shakapacker/binstub_sync_spec.rb Adds INTENTIONALLY_DIVERGENT_BINSTUBS allowlist and a guard test; the new test only checks one direction (documented-but-missing), with the reverse covered by per-binstub equality tests.
spec/dummy/bin/shakapacker-config Updated to match the new Ruby wrapper pattern used by lib/install/bin/shakapacker-config.
package.json Adds npm bin mappings for shakapacker-config and diff-bundler-config pointing to the new .cjs entrypoints.

Sequence Diagram

sequenceDiagram
    participant Rake as rake shakapacker:export_bundler_config
    participant Ruby as Ruby (RbConfig.ruby)
    participant Binstub as bin/shakapacker-config (Ruby wrapper)
    participant Node as node
    participant CJS as package/bin/shakapacker-config.cjs
    participant TS as configExporter run()

    Rake->>Ruby: "exec(RbConfig.ruby, binstub_path, *ARGV[1..])"
    Ruby->>Binstub: runs binstub script
    Binstub->>Binstub: shakapacker_app_root()
    Binstub->>Binstub: shakapacker_node_binary() via system node --version
    Binstub->>Binstub: "verify node_modules/shakapacker/package/bin/*.cjs exists"
    Binstub->>Node: "exec node_bin, script_path, *ARGV"
    Node->>CJS: runs .cjs file
    CJS->>TS: run(process.argv.slice(2))
    TS-->>CJS: Promise exitCode
    CJS-->>Node: process.exit(exitCode)
Loading

Reviews (1): Last reviewed commit: "Allow shakapacker-config binstubs to div..." | Re-trigger Greptile

Comment on lines +43 to +45
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 If exec fails after the node existence check (e.g., the binary is found by system but is then unexecutable due to a race or filesystem error), Ruby raises an unhandled Errno::ENOENT/Errno::EACCES and the user sees a raw backtrace instead of a helpful message. A rescue around the exec block would surface a cleaner error.

Suggested change
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
end
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
rescue SystemCallError => e
warn "[Shakapacker] Failed to exec #{node_bin.inspect}: #{e.message}"
exit 1
end

Comment on lines +43 to +45
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Same unhandled exec failure path as in shakapacker-config — if the binary disappears between the system probe and the exec call the user gets a raw Ruby backtrace.

Suggested change
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
end
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
rescue SystemCallError => e
warn "[Shakapacker] Failed to exec #{node_bin.inspect}: #{e.message}"
exit 1
end

Comment on lines 564 to 567

APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
exec "node", "./node_modules/.bin/shakapacker-config", *ARGV
Dir.chdir(app_root) do
exec node_bin, script_path, *ARGV
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Generated binstub shares the same unhandled-exec gap. The createBinStub template emits the same Dir.chdir { exec … } pattern without a rescue. Any app that installs the binstub via --init will get the same raw-backtrace failure path. If the rescue is added to lib/install/bin/shakapacker-config, the template here should be updated in the same change to keep the three in sync (as the comment at line 526 already requests).

Comment on lines +40 to +50
it "documents every divergent binstub and only divergent binstubs" do
install_basenames = Dir.glob(File.join(gem_root, "lib/install/bin/*")).map { |p| File.basename(p) }
bin_basenames = Dir.glob(File.join(gem_root, "bin/*")).map { |p| File.basename(p) }
actually_shared = install_basenames & bin_basenames

documented_but_missing = INTENTIONALLY_DIVERGENT_BINSTUBS - actually_shared
expect(documented_but_missing).to be_empty,
"INTENTIONALLY_DIVERGENT_BINSTUBS lists #{documented_but_missing.inspect} " \
"but those files are no longer present in both bin/ and lib/install/bin/. " \
"Remove them from the list."
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test name promises more than it checks. The description says "and only divergent binstubs" but the body only verifies documented_but_missing — i.e., that nothing in INTENTIONALLY_DIVERGENT_BINSTUBS has been deleted. The complementary direction (a binstub present in both dirs with diverging content that is not in the list) is caught by the per-binstub equality tests above, not by this test. Consider renaming to "documents every intentionally-divergent binstub" so the scope is accurate.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@claude

claude Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

Code Review: Fix ESM-safe config helper binstubs

Overall: This is a clean, well-reasoned fix. Converting JS binstubs to Ruby wrappers is the right approach — it sidesteps the ESM parsing problem entirely rather than trying to work around it inside JavaScript. Tests are thorough and cover the key scenarios. A few things worth noting:


Code duplication across three files

The shakapacker_app_root and shakapacker_node_binary helper functions are duplicated across:

  • lib/install/bin/shakapacker-config
  • lib/install/bin/diff-bundler-config
  • The template string inside package/configExporter/cli.ts (which generates bin/shakapacker-config)

The "Keep in sync" comments acknowledge this but don't enforce it — there's no spec that compares the two install binstubs against the template output. If the template drifts from the hand-maintained files, the only signal is a careful reader noticing the comment. Given these are 10-line helpers, a snapshot/fixture test that generates the stub from the template and diffs it against the install binstub would catch drift automatically.


Mixed Ruby/JS interpolation in the cli.ts template string

In createBinStub (cli.ts lines 532–533 and 541), the JS template literal contains:

warn "[Shakapacker] No Gemfile found at #{candidate.inspect}; " \
     "falling back to the current directory (#{Dir.pwd.inspect})."

#{...} is Ruby interpolation syntax — JavaScript ignores it (only ${...} is JS interpolation), so it passes through correctly to the generated Ruby file. This is correct but could easily trip up a future maintainer who sees #{} inside a JS template literal and assumes it's a bug. A short comment above the template explaining that #{} is intentional Ruby interpolation that JS preserves verbatim would prevent a well-meaning "fix."


system spawns a subprocess to detect Node on every invocation

shakapacker_node_binary runs system("node", "--version") (with suppressed output) to check whether Node is on PATH. This is clear and portable, but it forks a process on every binstub call. An alternative is searching ENV["PATH"] in-process with File.executable?. That said, this is a one-time startup cost and the current approach is easier to read, so this is a minor note rather than a blocker.


Test coverage

  • The four scenarios (ESM app, no-Gemfile fallback, missing script, unavailable Node) map directly to the new code paths. Good coverage.
  • File.realpath(app_path) in the CWD assertion correctly handles /tmp/private/tmp symlinks on macOS.
  • The updated binstub_sync_spec.rb guard — ensuring INTENTIONALLY_DIVERGENT_BINSTUBS names actually exist in both directories — is a good meta-test.

Minor

  • The CHANGELOG entry is informative and correctly calls out the migration step (bundle exec rake shakapacker:binstubs).
  • Using RbConfig.ruby in the rake task instead of relying on the shebang is the right call for Windows and version-manager consistency.

Comment on lines +526 to +527
# Keep in sync with lib/install/bin/shakapacker-config and
# lib/install/bin/diff-bundler-config; update all three when changing helpers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding a note here that #{} in the lines below is Ruby interpolation syntax — JavaScript template literals only interpolate ${...}, so these pass through verbatim to the generated file. Without a comment, a future reader may assume #{} is a typo or an unused interpolation and "fix" it.

Comment on lines +15 to +19
def shakapacker_node_binary
node_bin = "node"
return node_bin if system(node_bin, "--version", out: File::NULL, err: File::NULL)

warn "[Shakapacker] Could not find Node.js executable #{node_bin.inspect}. " \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system call forks a subprocess on every invocation just to confirm Node is reachable. For a low-cost alternative that stays in-process:

Suggested change
def shakapacker_node_binary
node_bin = "node"
return node_bin if system(node_bin, "--version", out: File::NULL, err: File::NULL)
warn "[Shakapacker] Could not find Node.js executable #{node_bin.inspect}. " \
def shakapacker_node_binary
node_bin = "node"
found = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |dir|
File.executable?(File.join(dir, node_bin))
end
return node_bin if found
warn "[Shakapacker] Could not find Node.js executable #{node_bin.inspect}. " \
"Install Node.js and try again."
exit 1
end

Not a blocker — the system approach is clear and the subprocess cost is tiny at startup — but worth considering if you'd prefer to avoid the fork.

Comment on lines +64 to +86

it "falls back to the current directory when #{command}'s parent has no Gemfile" do
Dir.mktmpdir("shakapacker-binstub-") do |app_path|
FileUtils.mkdir_p(File.join(app_path, "bin"))
install_fake_node_script(app_path, command)

binstub_path = File.join(app_path, "bin", command)
FileUtils.cp(File.join(gem_root, "lib", "install", "bin", command), binstub_path)
FileUtils.chmod(0o755, binstub_path)

output_path = File.join(app_path, "binstub-output.json")
_stdout, stderr, status = Open3.capture3(
{ "SHAKAPACKER_BINSTUB_OUTPUT" => output_path },
binstub_path,
chdir: app_path
)

expect(status).to be_success, stderr
expect(JSON.parse(File.read(output_path))).to eq(
"cwd" => File.realpath(app_path),
"argv" => []
)
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case sets up a directory without a Gemfile, so shakapacker_app_root will warn and fall back to Dir.pwd. The test passes chdir: app_path, so Dir.pwd equals app_path, and the fake node script is installed there. This means the test is really validating "fallback works and the script is still found at cwd" — the description "falls back to the current directory" captures that correctly.

One small thing: it might be worth asserting that stderr includes the fallback warning so that if the fallback logic changes silently, this test catches it.

justin808 added a commit that referenced this pull request May 23, 2026
Address the medium-priority test-coverage follow-ups from the PR #1104
review.

1. Cover spec/dummy/bin/shakapacker-config in the sync spec. The dummy
   binstub used by the test app must stay byte-identical to the install
   template; otherwise a stale wrapper could silently mask install-
   template changes.

2. Add a JS test that invokes createBinStub for both helper binstub
   names and asserts the generated content is byte-identical to the
   checked-in lib/install/bin/* files. Exports createBinStub from
   package/configExporter/cli.ts so the test can reach it. This closes
   the gap where the Ruby sync spec couldn't reach into the JS template.

3. Assert the `[Shakapacker] No Gemfile found at …` warning in
   helper_binstubs_spec.rb so the fallback path can't lose its warning
   silently.

4. Update the "Keep in sync" comment in all four locations to list all
   four copies (the previous wording said "all three" and missed the
   dummy binstub).

5. Rename the "documents every divergent binstub and only divergent
   binstubs" example to "all documented divergent binstubs still exist
   in both directories" so the name matches what the test actually
   verifies — the "only" guarantee comes from the iteration loop, not
   this example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request May 23, 2026
Address the low-priority polish item from the PR #1104 review.

`shakapacker_node_binary` previously verified Node was installed by
running `system("node", "--version")`, which forks a subprocess on
every binstub invocation just to confirm presence. Replace with a
direct ENV["PATH"] walk, falling back to PATHEXT-style extensions on
Windows. Cuts a fork+exec from every `bin/shakapacker-config` and
`bin/diff-bundler-config` call.

Applied identically across the three checked-in helper binstubs
(lib/install/bin/shakapacker-config, lib/install/bin/diff-bundler-config,
spec/dummy/bin/shakapacker-config) and the createBinStub template in
package/configExporter/cli.ts. binstub_sync_spec.rb +
test/configExporter/createBinStub.test.js (from PR #1128) keep the four
copies byte-aligned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request May 24, 2026
## Summary

Address the **low-priority optimization** from the [PR #1104 follow-up
review](#1123).

This is **PR 3 of 3** for #1123 — the smallest of the three, isolated so
it can land or be punted independently.

### Change

`shakapacker_node_binary` in the helper binstubs previously verified
Node was installed by running `system("node", "--version")` with both
streams piped to `/dev/null`. That forks a subprocess on every binstub
invocation — just to learn whether Node exists. Replaced with a direct
walk of `ENV["PATH"]` that also probes `PATHEXT`-style extensions on
Windows (so `node.exe`/`node.cmd` are still discoverable). One fork+exec
saved per `bin/shakapacker-config` and `bin/diff-bundler-config` call.

The return value is unchanged (the literal string `"node"`), so the
subsequent `exec node_bin, script_path, *ARGV` still goes through the
OS's `PATH` resolution exactly as before.

Applied byte-identically across all four copies kept in sync by PR #1128
(`binstub_sync_spec.rb` + `test/configExporter/createBinStub.test.js`):

- `lib/install/bin/shakapacker-config`
- `lib/install/bin/diff-bundler-config`
- `spec/dummy/bin/shakapacker-config`
- the `createBinStub` template in `package/configExporter/cli.ts`

### Stacked on PR #1128

This branch is stacked on top of `jg/1123-binstub-sync-coverage` (PR
#1128). GitHub will auto-update the base to `main` once #1128 merges. If
#1128 is closed instead, this PR can rebase onto `main` with a trivial
change (only the comment header and `NODE_ENV` line differ, and only the
comment-header lines need replaying).

Refs #1123.

## Test plan

- [x] `bundle exec rspec spec/shakapacker/binstub_sync_spec.rb` — 2
examples pass (dummy↔install parity preserved)
- [x] `bundle exec rspec spec/shakapacker/helper_binstubs_spec.rb` — 8
examples pass, including the "node unavailable" path that uses
`PATH=/nonexistent`
- [x] `yarn jest test/configExporter/createBinStub.test.js` — 2 examples
pass (template still matches install templates byte-for-byte)
- [x] `bundle exec rubocop` — no offenses on changed Ruby files
- [x] `yarn eslint` — no errors on changed TS file
- [x] `ruby -c` on all three modified binstubs — syntax OK

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches the helper binstubs used to launch Node-based tooling; any bug
in the new `PATH`/`PATHEXT` scan could prevent commands from running,
especially on Windows.
> 
> **Overview**
> Replaces `shakapacker_node_binary`’s `system("node", "--version")`
check with a no-fork scan of `ENV["PATH"]`, including Windows `PATHEXT`
extension probing.
> 
> Applies the same updated Node lookup logic across all four synced
helper-binstub copies (`lib/install/bin/*`, `spec/dummy/bin/*`, and the
`createBinStub` template in `package/configExporter/cli.ts`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
1889de4. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@justin808 justin808 mentioned this pull request May 25, 2026
5 tasks
justin808 added a commit that referenced this pull request May 27, 2026
Address the medium-priority test-coverage follow-ups from the PR #1104
review.

1. Cover spec/dummy/bin/shakapacker-config in the sync spec. The dummy
   binstub used by the test app must stay byte-identical to the install
   template; otherwise a stale wrapper could silently mask install-
   template changes.

2. Add a JS test that invokes createBinStub for both helper binstub
   names and asserts the generated content is byte-identical to the
   checked-in lib/install/bin/* files. Exports createBinStub from
   package/configExporter/cli.ts so the test can reach it. This closes
   the gap where the Ruby sync spec couldn't reach into the JS template.

3. Assert the `[Shakapacker] No Gemfile found at …` warning in
   helper_binstubs_spec.rb so the fallback path can't lose its warning
   silently.

4. Update the "Keep in sync" comment in all four locations to list all
   four copies (the previous wording said "all three" and missed the
   dummy binstub).

5. Rename the "documents every divergent binstub and only divergent
   binstubs" example to "all documented divergent binstubs still exist
   in both directories" so the name matches what the test actually
   verifies — the "only" guarantee comes from the iteration loop, not
   this example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request May 27, 2026
## Summary

Stamps the v10.1.0 stable release header (was `v10.1.0-rc.2`) and
curates the prerelease entries per `/update-changelog` guidance.

## Curation applied

- **Merged PR #1133 → PR #1096** (Added: supplemental npm packages).
#1133 restructured the supplemental deps from direct → peer in rc.2, so
the original entry's "bundle the managed-build stack as direct
dependencies" description was wrong for stable consumers. Rewrote to
describe the final peer-deps shape (auto-installed on npm 7+) and credit
both PRs. The #1133 standalone Changed entry was removed.
- **Merged PR #1132 → PR #1104** (Fixed: ESM binstubs). #1132 fixed the
`export_bundler_config` dispatch regression that #1104 introduced in
rc.1 when switching from JS to Ruby binstubs; stable users never see the
bug, so the fix is RC-only churn that belongs folded into the original
entry. Also folded in #1132's minor improvements (NODE_ENV from
RAILS_ENV mapping, Node lookup without execution, friendlier non-Error
CLI messages).
- **Merged PR #1106 → PR #1086** (Fixed: local-path version detection).
Both shipped in rc.0 as iterations of the same regex fix; merged into a
single entry covering `../`, `./`, and `file:` patterns.
- **Lightened Migration Notes**: removed "bundled stack" language now
that supplementals declare required peers instead of direct deps.
- **Skipped post-rc.2 commit**: PR #1138 (skill tooling guidance) is
internal — not user-visible.
- **Compare links**: `[Unreleased]` now points from `v10.1.0`; replaced
`[v10.1.0-rc.2]` with `[v10.1.0]: v10.0.0...v10.1.0`.

## Test plan

- [x] `yarn lint` passes
- [x] `yarn prettier --check CHANGELOG.md` passes
- [x] File ends with trailing newline
- [x] No duplicate section headings
- [x] All merged PRs credited

After merge, run `bundle exec rake release` (no args) — the rake task
picks up `v10.1.0` from CHANGELOG.md and auto-creates the GitHub
release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation-only changelog edits with no runtime or dependency
behavior changes.
> 
> **Overview**
> **Curates `CHANGELOG.md` for the v10.1.0 stable release** (replacing
the `v10.1.0-rc.2` header dated May 25, 2026) so release notes match
what stable users actually ship.
> 
> Prerelease churn is folded into fewer, accurate bullets: supplemental
npm packages (**#1096** + **#1133**) now describe **required peer
dependencies** (not direct “bundled” deps), with the standalone
**#1133** *Changed* entry removed; ESM/config binstub fixes (**#1104** +
**#1132**) and local-path version detection (**#1086** + **#1106**) are
single *Fixed* items. **Migration Notes** no longer imply a direct
bundled stack on supplementals.
> 
> Footer compare links: **`[Unreleased]`** from `v10.1.0`,
**`[v10.1.0]`** replaces the rc.2 link (`v10.0.0...v10.1.0`).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2b57d5f. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request May 27, 2026
Address the medium-priority test-coverage follow-ups from the PR #1104
review.

1. Cover spec/dummy/bin/shakapacker-config in the sync spec. The dummy
   binstub used by the test app must stay byte-identical to the install
   template; otherwise a stale wrapper could silently mask install-
   template changes.

2. Add a JS test that invokes createBinStub for both helper binstub
   names and asserts the generated content is byte-identical to the
   checked-in lib/install/bin/* files. Exports createBinStub from
   package/configExporter/cli.ts so the test can reach it. This closes
   the gap where the Ruby sync spec couldn't reach into the JS template.

3. Assert the `[Shakapacker] No Gemfile found at …` warning in
   helper_binstubs_spec.rb so the fallback path can't lose its warning
   silently.

4. Update the "Keep in sync" comment in all four locations to list all
   four copies (the previous wording said "all three" and missed the
   dummy binstub).

5. Rename the "documents every divergent binstub and only divergent
   binstubs" example to "all documented divergent binstubs still exist
   in both directories" so the name matches what the test actually
   verifies — the "only" guarantee comes from the iteration loop, not
   this example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request May 28, 2026
Address the medium-priority test-coverage follow-ups from the PR #1104
review.

1. Cover spec/dummy/bin/shakapacker-config in the sync spec. The dummy
   binstub used by the test app must stay byte-identical to the install
   template; otherwise a stale wrapper could silently mask install-
   template changes.

2. Add a JS test that invokes createBinStub for both helper binstub
   names and asserts the generated content is byte-identical to the
   checked-in lib/install/bin/* files. Exports createBinStub from
   package/configExporter/cli.ts so the test can reach it. This closes
   the gap where the Ruby sync spec couldn't reach into the JS template.

3. Assert the `[Shakapacker] No Gemfile found at …` warning in
   helper_binstubs_spec.rb so the fallback path can't lose its warning
   silently.

4. Update the "Keep in sync" comment in all four locations to list all
   four copies (the previous wording said "all three" and missed the
   dummy binstub).

5. Rename the "documents every divergent binstub and only divergent
   binstubs" example to "all documented divergent binstubs still exist
   in both directories" so the name matches what the test actually
   verifies — the "only" guarantee comes from the iteration loop, not
   this example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
justin808 added a commit that referenced this pull request Jun 30, 2026
#1127)

## Summary

Address the **high-priority bug fixes** from the [PR #1104 follow-up
review](#1123) on
ESM-safe config binstubs.

This is **PR 1 of 3** for #1123 — focused on user-visible regressions.
Test-coverage and polish items will follow in separate PRs to keep
review scope tight.

### Fixes

- **#1 — rake task crash for upgraded apps with a legacy JS binstub.**
`shakapacker:export_bundler_config` previously invoked
`bin/shakapacker-config` via `RbConfig.ruby` unconditionally. Apps
upgraded from older Shakapacker versions still carry a `#!/usr/bin/env
node` JavaScript binstub that this gem did not rewrite — running it
through Ruby would try to parse JS as Ruby and crash. The task now
detects the shebang and dispatches: Ruby binstubs keep the
version-manager protection from #1104; legacy JS binstubs are exec'd
directly so their shebang resolves Node, and the user is nudged to run
`rake shakapacker:binstubs` to upgrade. A new
`Shakapacker::Utils::Misc.js_binstub?` helper makes the detection
unit-testable.

- **#2 — `NODE_ENV` default ignored `RAILS_ENV` in helper binstubs.**
The helper binstubs hard-coded `ENV["NODE_ENV"] ||= "development"`,
which diverged from `Shakapacker.ensure_node_env!`. Concretely,
`RAILS_ENV=production bin/shakapacker-config show-config` with
`NODE_ENV` unset spawned Node with `NODE_ENV=development` and produced
the development bundler config. The default now mirrors the Ruby logic
(`DEV_ENVS = %w[development test]`): only development and test default
to `NODE_ENV=development`; everything else defaults to `production`.
Applied in all four sync'd copies: `lib/install/bin/shakapacker-config`,
`lib/install/bin/diff-bundler-config`,
`spec/dummy/bin/shakapacker-config`, and the `createBinStub` template in
`package/configExporter/cli.ts`.

- **#6 — `error.message` guard.** `package/bin/shakapacker-config.cjs`
and `bin/shakapacker-config` now stringify non-`Error` throws (`error
instanceof Error ? error.message : String(error)`) instead of printing
`undefined`, matching the existing `formatError` pattern in
`diff-bundler-config.cjs`.

- **#7 — `NODE_ENV` default in `diff-bundler-config`.** Resolved by #2's
fix: kept the default (now using the corrected logic) with a comment
explaining why it stays consistent with `shakapacker-config`.

### Follow-up PRs

Items deferred to keep this PR focused:

- **PR 2** — test-coverage gaps: dummy binstub sync coverage (#3),
`createBinStub` template sync spec (#4), fallback-warning assertion
(#5), "Keep in sync" comments updated to list all four locations (#8),
`binstub_sync_spec` test rename (#9).
- **PR 3** — polish: replace `system(node, "--version")` probe with a
no-fork PATH walk (#10).

Refs #1123.

## Test plan

- [x] `bundle exec rspec spec/shakapacker/utils_misc_spec.rb` — new unit
tests for `js_binstub?` (8 examples)
- [x] `bundle exec rspec spec/shakapacker/helper_binstubs_spec.rb` —
existing helper binstub specs still pass (8 examples)
- [x] `bundle exec rspec spec/shakapacker/binstub_sync_spec.rb` — sync
spec still passes
- [x] `bundle exec rspec spec/shakapacker/doctor_spec.rb` — doctor specs
(147 examples) still pass
- [x] `bundle exec rubocop` — no offenses on changed Ruby files
- [x] `yarn eslint` — no errors on changed JS/TS files
- [x] `ruby -c` on all modified Ruby binstubs/rake task — syntax OK

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes how rake tasks and binstubs choose
interpreters and default environment variables, which can affect
build/config export behavior across environments.
> 
> **Overview**
> **Fixes config-export helper regressions for upgraded apps.**
`shakapacker:export_bundler_config` now detects legacy `#!/usr/bin/env
node` `bin/shakapacker-config` shims and execs them directly (while
still running modern Ruby binstubs via `RbConfig.ruby`), avoiding
crashes and prompting users to regenerate binstubs.
> 
> **Aligns environment defaults across helper binstubs.**
`shakapacker-config`/`diff-bundler-config` now default `NODE_ENV` to
`development` only for `RAILS_ENV` in `development|test`, otherwise
`production`, and both Node shims now stringify non-`Error` throws for
clearer stderr. Adds a unit-tested
`Shakapacker::Utils::Misc.js_binstub?` helper and updates the changelog.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
08c3c39. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed a regression when exporting bundler config after upgrades from
older installations.
* Improved detection of legacy JavaScript helper scripts (including
`env`-style shebangs) and now runs the appropriate Node or Ruby
interpreter.
* Enhanced error handling and messages when the JavaScript or Ruby
runtime can’t be executed, including upgrade guidance to refresh helper
scripts.
* **Tests**
* Expanded coverage for helper-script interpreter detection, guidance
output, and failure scenarios (including missing runtimes and varied
shebang formats).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

require is not defined in ES module scope on new config helpers

1 participant