shakapacker 9.4.0 → 9.6.0.beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a0519c935e0e3d98204aeaeec6cc35b57fa13be1fe8a6f62e5e6cd5124d09c7
4
- data.tar.gz: 7f6fe4134fd9b17de2dcd26b5c9e80f1f41d2019dfa0ed337092c1d23a9aa077
3
+ metadata.gz: 16a87e8e339c085afc34f5f70af6fee7aa534590949f02faa1863d9c5e7f9f7d
4
+ data.tar.gz: 19d1f859e00dc29bfe4c593ba1b4a1e5b64c6347fc1b56a2cbabcc8c8020205b
5
5
  SHA512:
6
- metadata.gz: 8789ef5d5cf102254f27c0833b3a7bd8239b5b97d2dfa97ac79fb01827931397dc71dd35f82971a61354a1751f6dafd80d91a322e51b6390f83132f17a786626
7
- data.tar.gz: 3a6cff63ba261b9cec55cdd683e7cfa96a24e52b753639643bf2f6874ed49c0f50a990a771a85c4265e9e6394a8fc7e8b4e3440ed5d62982f89b98cddadf9450
6
+ metadata.gz: f4286284b7cbbec5324ce2ad26c6d6892cfcecae96e31c7324ff0a533f332301b4008e7083592b3b4b639be131758ab6fa5bc81ed46ac0903f2370f20a05fae0
7
+ data.tar.gz: e4e6bfdd5837dabfd9f5d031e5a1d905ca88b6c1d50b1f3d440fb3f3c6a23d0abbec1d8839a769536af52d67eed5657ec602786c824dd57156f1678026ec300e
@@ -2,7 +2,7 @@ name: Claude Code Review
2
2
 
3
3
  on:
4
4
  pull_request:
5
- types: [opened, synchronize]
5
+ types: [opened, synchronize, ready_for_review, reopened]
6
6
  # Optional: Only run on specific file changes
7
7
  # paths:
8
8
  # - "src/**/*.ts"
@@ -36,18 +36,9 @@ jobs:
36
36
  uses: anthropics/claude-code-action@v1
37
37
  with:
38
38
  claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
39
- prompt: |
40
- Please review this pull request and provide feedback on:
41
- - Code quality and best practices
42
- - Potential bugs or issues
43
- - Performance considerations
44
- - Security concerns
45
- - Test coverage
46
-
47
- Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
48
-
49
- Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
50
-
39
+ plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
40
+ plugins: 'code-review@claude-code-plugins'
41
+ prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
51
42
  # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
52
- # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options
53
- claude_args: '--model claude-sonnet-4-5-20250929 --allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
43
+ # or https://code.claude.com/docs/en/cli-reference for available options
44
+
@@ -45,5 +45,6 @@ jobs:
45
45
 
46
46
  # Optional: Add claude_args to customize behavior and configuration
47
47
  # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
48
- # or https://docs.claude.com/en/docs/claude-code/sdk#command-line for available options
49
- # claude_args: '--model claude-opus-4-1-20250805 --allowed-tools Bash(gh pr:*)'
48
+ # or https://code.claude.com/docs/en/cli-reference for available options
49
+ # claude_args: '--allowed-tools Bash(gh pr:*)'
50
+
data/.prettierignore CHANGED
@@ -1,3 +1,7 @@
1
1
  # Template files that should maintain their original quote style
2
2
  lib/install/config/**/*.ts
3
3
  lib/install/config/**/*.js
4
+
5
+ # Temporarily ignore workflow files that cannot be modified in PRs due to claude-review validation
6
+ .github/workflows/claude.yml
7
+ .github/workflows/claude-code-review.yml
data/.rubocop.yml CHANGED
@@ -10,6 +10,7 @@ AllCops:
10
10
  - "node_modules/**/*"
11
11
  - "_actions/**/*"
12
12
  - "spec/dummy-rspack/**/*"
13
+ - "bin/conductor-exec" # Shell script, not Ruby
13
14
 
14
15
  # Prefer &&/|| over and/or.
15
16
  Style/AndOr:
data/CHANGELOG.md CHANGED
@@ -11,9 +11,80 @@
11
11
 
12
12
  Changes since the last non-beta release.
13
13
 
14
+ ### Changed
15
+
16
+ - **BREAKING: sass-loader now defaults to modern Sass API**. [PR #879](https://github.com/shakacode/shakapacker/pull/879) by [justin808](https://github.com/justin808). The sass-loader configuration now uses `api: "modern"` instead of the deprecated legacy API. This improves compatibility with plugins like sass-resources-loader that require the modern API. If you experience issues after upgrading, you can revert to the legacy API by customizing your webpack config:
17
+
18
+ ```javascript
19
+ // config/webpack/webpack.config.js
20
+ const { generateWebpackConfig } = require("shakapacker")
21
+ const config = generateWebpackConfig()
22
+
23
+ // Find and modify sass-loader options
24
+ config.module.rules.forEach((rule) => {
25
+ if (rule.use) {
26
+ rule.use.forEach((loader) => {
27
+ if (loader.loader?.includes("sass-loader")) {
28
+ loader.options.api = "legacy"
29
+ }
30
+ })
31
+ }
32
+ })
33
+
34
+ module.exports = config
35
+ ```
36
+
37
+ ### Fixed
38
+
39
+ - **Fixed orphaned webpack/rspack processes when foreman receives SIGTERM**. [PR #888](https://github.com/shakacode/shakapacker/pull/888) by [jordan-brough](https://github.com/jordan-brough). When running under foreman, sending SIGTERM to foreman (e.g. `kill <pid>`) would kill the Ruby shakapacker process but leave the webpack/rspack child process running as an orphan. DevServerRunner now uses `exec` to replace the Ruby process entirely, and Runner uses `spawn` with SIGTERM forwarding to ensure the child process is properly terminated.
40
+ - **Fixed installer writing wrong shakapacker version in package.json**. [PR #899](https://github.com/shakacode/shakapacker/pull/899) by [justin808](https://github.com/justin808). The `shakapacker:install` generator now keeps the `package.json` dependency value in sync with the exact version or path that was requested, instead of relying on the post-install value which could differ.
41
+ - **Fixed NODE_ENV=test causing DefinePlugin warnings**. [PR #870](https://github.com/shakacode/shakapacker/pull/870) by [justin808](https://github.com/justin808). When RAILS_ENV=test, Shakapacker now sets NODE_ENV=development instead of NODE_ENV=test. This prevents webpack/rspack DefinePlugin conflicts since these bundlers only recognize "development" and "production" as valid NODE_ENV values.
42
+
43
+ ### Documentation
44
+
45
+ - **Added CDN limitation warnings for Early Hints feature**. [PR #878](https://github.com/shakacode/shakapacker/pull/878) by [justin808](https://github.com/justin808). The early hints documentation now prominently notes that most CDNs (Cloudflare, AWS CloudFront, AWS ALB) strip HTTP 103 responses before they reach end users. Debug mode also includes CDN warnings in HTML comments.
46
+
47
+ ## [v9.5.0] - January 7, 2026
48
+
49
+ ### Security
50
+
51
+ - **CRITICAL: Fixed environment variable leak via EnvironmentPlugin**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). The default webpack and rspack plugins were passing the entire `process.env` to `EnvironmentPlugin`, which exposed ALL build environment variables (including secrets like `DATABASE_URL`, `AWS_SECRET_ACCESS_KEY`, `RAILS_MASTER_KEY`, etc.) to client-side JavaScript bundles when code referenced `process.env.VARIABLE_NAME`. **Note**: This issue is especially critical with webpack 5.103+ due to a [serialization change](https://github.com/webpack/webpack/commit/eecdeeb746b2f996ed4ab74365dd72c95070196b) that can embed all environment variables into bundles when `import.meta.env` is accessed conditionally. This vulnerability was inherited from webpacker v1.0.0 (January 2017) and has been present in all versions of webpacker and shakapacker. **Action required**: After upgrading, rotate any secrets that may have been exposed in production JavaScript bundles.
52
+
53
+ ### Added
54
+
55
+ - **Added `SHAKAPACKER_PUBLIC_*` prefix convention for client-side environment variables**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). Any environment variable prefixed with `SHAKAPACKER_PUBLIC_` is automatically exposed to client-side JavaScript. This follows the same convention used by Next.js (`NEXT_PUBLIC_*`) and Vite (`VITE_*`), making it explicit which variables are intended for client-side use.
56
+
57
+ ```bash
58
+ # These are automatically available in your JavaScript
59
+ export SHAKAPACKER_PUBLIC_API_URL=https://api.example.com
60
+ export SHAKAPACKER_PUBLIC_ANALYTICS_ID=UA-12345
61
+ ```
62
+
63
+ - **Added `SHAKAPACKER_ENV_VARS` environment variable as escape hatch for extending allowed client-side env vars**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). Set `SHAKAPACKER_ENV_VARS=VAR1,VAR2,VAR3` to expose additional environment variables to client-side JavaScript beyond the default allowlist (`NODE_ENV`, `RAILS_ENV`, `WEBPACK_SERVE`). Only add non-sensitive variables that are safe to embed in public JavaScript bundles.
64
+
65
+ ### Changed
66
+
67
+ - **BREAKING: EnvironmentPlugin now uses allowlist instead of exposing all env vars**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). Only `NODE_ENV`, `RAILS_ENV`, `WEBPACK_SERVE`, and any `SHAKAPACKER_PUBLIC_*` variables are exposed by default. If your client-side code relies on other environment variables, either rename them with the `SHAKAPACKER_PUBLIC_` prefix (recommended), add them via `SHAKAPACKER_ENV_VARS`, or customize your webpack/rspack config. This is a security fix - the previous behavior was dangerous.
68
+
69
+ **Migration examples:**
70
+
71
+ ```bash
72
+ # Option 1 (recommended): Use the SHAKAPACKER_PUBLIC_ prefix
73
+ export SHAKAPACKER_PUBLIC_API_BASE_URL=https://api.example.com
74
+
75
+ # Option 2: Use SHAKAPACKER_ENV_VARS for existing variable names
76
+ SHAKAPACKER_ENV_VARS=API_BASE_URL bundle exec rails assets:precompile
77
+ ```
78
+
79
+ ### Fixed
80
+
81
+ - **Fixed gemspec to exclude Gemfile.lock from published gem**. [PR #856](https://github.com/shakacode/shakapacker/pull/856) by [adrien-k](https://github.com/adrien-k). The gemspec's file pattern now correctly excludes `Gemfile.lock`, preventing vulnerability alerts during Docker image scans caused by outdated pinned versions in the lock file.
82
+
83
+ ## [v9.4.0] - November 22, 2025
84
+
14
85
  ### Added
15
86
 
16
- - **Added `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable to skip precompile hook**. [PR #XXX](https://github.com/shakacode/shakapacker/pull/XXX) by [justin808](https://github.com/justin808). Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to skip the precompile hook during compilation. This is useful when using process managers like Foreman or Overmind to run the hook once before starting multiple webpack processes, preventing duplicate hook execution. **Migration tip:** If you have a custom `bin/dev` script that starts multiple webpack processes, you can now run the precompile hook once in the script and set this environment variable to prevent each webpack process from running the hook again. See the [precompile hook documentation](./docs/precompile_hook.md#skipping-the-hook) for implementation examples.
87
+ - **Added `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable to skip precompile hook**. [PR #850](https://github.com/shakacode/shakapacker/pull/850) by [justin808](https://github.com/justin808). Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to skip the precompile hook during compilation. This is useful when using process managers like Foreman or Overmind to run the hook once before starting multiple webpack processes, preventing duplicate hook execution. **Migration tip:** If you have a custom `bin/dev` script that starts multiple webpack processes, you can now run the precompile hook once in the script and set this environment variable to prevent each webpack process from running the hook again. See the [precompile hook documentation](./docs/precompile_hook.md#skipping-the-hook) for implementation examples.
17
88
 
18
89
  ## [v9.3.4-beta.0] - November 17, 2025
19
90
 
@@ -763,7 +834,9 @@ Note: [Rubygem is 6.3.0.pre.rc.1](https://rubygems.org/gems/shakapacker/versions
763
834
 
764
835
  See [CHANGELOG.md in rails/webpacker (up to v5.4.3)](https://github.com/rails/webpacker/blob/master/CHANGELOG.md)
765
836
 
766
- [Unreleased]: https://github.com/shakacode/shakapacker/compare/v9.3.4-beta.0...main
837
+ [Unreleased]: https://github.com/shakacode/shakapacker/compare/v9.5.0...main
838
+ [v9.5.0]: https://github.com/shakacode/shakapacker/compare/v9.4.0...v9.5.0
839
+ [v9.4.0]: https://github.com/shakacode/shakapacker/compare/v9.3.4...v9.4.0
767
840
  [v9.3.4-beta.0]: https://github.com/shakacode/shakapacker/compare/v9.3.3...v9.3.4-beta.0
768
841
  [v9.3.3]: https://github.com/shakacode/shakapacker/compare/v9.3.2...v9.3.3
769
842
  [v9.3.2]: https://github.com/shakacode/shakapacker/compare/v9.3.1...v9.3.2
data/CLAUDE.md CHANGED
@@ -12,6 +12,10 @@
12
12
  - Run corresponding RSpec tests when changing source files
13
13
  - For example, when changing `lib/shakapacker/foo.rb`, run `spec/shakapacker/foo_spec.rb`
14
14
  - Run the full test suite with `bundle exec rspec` before pushing
15
+ - **Use explicit RSpec spy assertions** - prefer `have_received`/`not_to have_received` over indirect counter patterns
16
+ - Good: `expect(Open3).to have_received(:capture3).with(anything, hook_command, anything)`
17
+ - Good: `expect(Open3).not_to have_received(:capture3).with(anything, hook_command, anything)`
18
+ - Avoid: `call_count += 1` followed by `expect(call_count).to eq(1)`
15
19
 
16
20
  ## Code Style
17
21
 
@@ -41,3 +45,11 @@
41
45
  - This gem supports both webpack and rspack configurations
42
46
  - Test changes with both bundlers when modifying core functionality
43
47
  - Be aware of the dual package.json/Gemfile dependency management
48
+
49
+ ## Conductor Environment
50
+
51
+ - **Version manager support**: The setup script detects mise, asdf, or direct PATH tools (rbenv/nvm/nodenv)
52
+ - **bin/conductor-exec**: Use this wrapper for commands when tool versions aren't detected correctly in Conductor's non-interactive shell
53
+ - Example: `bin/conductor-exec bundle exec rubocop`
54
+ - The wrapper uses `mise exec` if mise is available, otherwise falls back to direct execution
55
+ - **conductor.json scripts** already use this wrapper, so you typically don't need to use it manually
@@ -0,0 +1,24 @@
1
+ #!/bin/zsh
2
+ # Wrapper script to run commands in Conductor with proper tool versions
3
+ # Usage: bin/conductor-exec <command> [args...]
4
+ #
5
+ # This is needed because Conductor (and other non-interactive shells) don't
6
+ # source .zshrc, so version manager PATH configuration isn't active by default.
7
+ #
8
+ # - If mise is available: uses `mise exec` for correct tool versions
9
+ # - Otherwise: falls back to direct execution (for asdf/rbenv/nvm users)
10
+ #
11
+ # Examples:
12
+ # bin/conductor-exec ruby --version # Uses correct Ruby version
13
+ # bin/conductor-exec bundle exec rubocop # Correct Ruby for linting
14
+ # bin/conductor-exec git commit -m "msg" # Pre-commit hooks work correctly
15
+ # bin/conductor-exec yarn install # Uses correct Node version
16
+ #
17
+ # See: https://github.com/shakacode/react_on_rails-demos/issues/105
18
+
19
+ if command -v mise &> /dev/null; then
20
+ exec mise exec -- "$@"
21
+ else
22
+ # Fall back to direct execution for non-mise users
23
+ exec "$@"
24
+ fi
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Minimal shim - all logic is in the TypeScript module
4
+ const { run } = require("shakapacker/configExporter")
5
+
6
+ run(process.argv.slice(2))
7
+ .then((exitCode) => process.exit(exitCode))
8
+ .catch((error) => {
9
+ console.error(error.message)
10
+ process.exit(1)
11
+ })
data/conductor-setup.sh CHANGED
@@ -1,43 +1,124 @@
1
- #!/usr/bin/env bash
1
+ #!/bin/zsh
2
2
  set -euo pipefail
3
3
 
4
4
  echo "🔧 Setting up Shakapacker workspace..."
5
5
 
6
- # Set up Ruby version if asdf is available
7
- if command -v asdf &> /dev/null; then
8
- echo "📝 Using asdf Ruby version management..."
9
- # Ensure we have the right Ruby version file
10
- echo "ruby 3.3.4" > .tool-versions
11
- # Use asdf exec to run commands with the right Ruby version
12
- BUNDLE_CMD="asdf exec bundle"
6
+ # Detect and initialize version manager
7
+ # Supports: mise, asdf, or direct PATH (rbenv/nvm/nodenv already in PATH)
8
+ VERSION_MANAGER="none"
9
+
10
+ echo "📋 Detecting version manager..."
11
+
12
+ if command -v mise &> /dev/null; then
13
+ VERSION_MANAGER="mise"
14
+ echo "✅ Found mise"
15
+ # Trust mise config for current directory only
16
+ mise trust 2>/dev/null || true
17
+ elif [[ -f ~/.asdf/asdf.sh ]]; then
18
+ VERSION_MANAGER="asdf"
19
+ source ~/.asdf/asdf.sh
20
+ echo "✅ Found asdf (from ~/.asdf/asdf.sh)"
21
+ elif command -v asdf &> /dev/null; then
22
+ VERSION_MANAGER="asdf"
23
+ # For homebrew-installed asdf
24
+ if [[ -f /opt/homebrew/opt/asdf/libexec/asdf.sh ]]; then
25
+ source /opt/homebrew/opt/asdf/libexec/asdf.sh
26
+ fi
27
+ echo "✅ Found asdf"
13
28
  else
14
- BUNDLE_CMD="bundle"
29
+ echo "ℹ️ No version manager detected, using system PATH"
30
+ echo " (Assuming rbenv/nvm/nodenv or system tools are already configured)"
31
+ fi
32
+
33
+ # Ensure version config exists for asdf/mise users
34
+ if [[ "$VERSION_MANAGER" != "none" ]] && [[ ! -f .tool-versions ]] && [[ ! -f .mise.toml ]]; then
35
+ echo "📝 Creating .tool-versions from project version files..."
36
+
37
+ # Read Ruby version from .ruby-version or use default
38
+ if [[ -f .ruby-version ]]; then
39
+ RUBY_VER=$(cat .ruby-version | tr -d '[:space:]')
40
+ else
41
+ RUBY_VER="3.3.4" # Default: recent stable Ruby
42
+ fi
43
+
44
+ # Read Node version from .node-version or use default
45
+ if [[ -f .node-version ]]; then
46
+ NODE_VER=$(cat .node-version | tr -d '[:space:]')
47
+ else
48
+ NODE_VER="20.18.0" # Default: LTS Node
49
+ fi
50
+
51
+ cat > .tool-versions << EOF
52
+ ruby $RUBY_VER
53
+ nodejs $NODE_VER
54
+ EOF
55
+ echo " Using Ruby $RUBY_VER, Node $NODE_VER"
15
56
  fi
16
57
 
17
- # Check for required tools
18
- if ! $BUNDLE_CMD --version &> /dev/null; then
19
- echo " Error: Ruby bundler is not installed"
20
- echo "Please install bundler first: gem install bundler"
58
+ # Install tools via mise (after .tool-versions exists)
59
+ if [[ "$VERSION_MANAGER" == "mise" ]]; then
60
+ echo "📦 Installing tools via mise..."
61
+ mise install
62
+ fi
63
+
64
+ # Helper function to run commands with the detected version manager
65
+ run_cmd() {
66
+ if [[ "$VERSION_MANAGER" == "mise" ]] && [[ -x "bin/conductor-exec" ]]; then
67
+ bin/conductor-exec "$@"
68
+ else
69
+ "$@"
70
+ fi
71
+ }
72
+
73
+ # Check required tools
74
+ echo "📋 Checking required tools..."
75
+ run_cmd ruby --version >/dev/null 2>&1 || { echo "❌ Error: Ruby is not installed or not in PATH."; exit 1; }
76
+ run_cmd node --version >/dev/null 2>&1 || { echo "❌ Error: Node.js is not installed or not in PATH."; exit 1; }
77
+
78
+ # Check Ruby version
79
+ RUBY_VERSION=$(run_cmd ruby -v | awk '{print $2}')
80
+ MIN_RUBY_VERSION="2.7.0"
81
+ if [[ $(echo -e "$MIN_RUBY_VERSION\n$RUBY_VERSION" | sort -V | head -n1) != "$MIN_RUBY_VERSION" ]]; then
82
+ echo "❌ Error: Ruby version $RUBY_VERSION is too old. Shakapacker requires Ruby >= 2.7.0"
83
+ echo " Please upgrade Ruby using your version manager or system package manager."
21
84
  exit 1
22
85
  fi
86
+ echo "✅ Ruby version: $RUBY_VERSION"
23
87
 
24
- if ! command -v yarn &> /dev/null; then
25
- echo "❌ Error: Yarn is not installed"
26
- echo "Please install yarn first"
88
+ # Check Node version
89
+ NODE_VERSION=$(run_cmd node -v | cut -d'v' -f2)
90
+ MIN_NODE_VERSION="14.0.0"
91
+ if [[ $(echo -e "$MIN_NODE_VERSION\n$NODE_VERSION" | sort -V | head -n1) != "$MIN_NODE_VERSION" ]]; then
92
+ echo "❌ Error: Node.js version v$NODE_VERSION is too old. Shakapacker requires Node.js >= 14.0.0"
93
+ echo " Please upgrade Node.js using your version manager or system package manager."
27
94
  exit 1
28
95
  fi
96
+ echo "✅ Node.js version: v$NODE_VERSION"
97
+
98
+ # Copy any environment files from root if they exist
99
+ if [ -n "${CONDUCTOR_ROOT_PATH:-}" ]; then
100
+ if [ -f "$CONDUCTOR_ROOT_PATH/.env" ]; then
101
+ echo "📝 Copying .env file..."
102
+ cp "$CONDUCTOR_ROOT_PATH/.env" .env
103
+ fi
104
+
105
+ if [ -f "$CONDUCTOR_ROOT_PATH/.env.local" ]; then
106
+ echo "📝 Copying .env.local file..."
107
+ cp "$CONDUCTOR_ROOT_PATH/.env.local" .env.local
108
+ fi
109
+ fi
29
110
 
30
111
  # Install Ruby dependencies
31
- echo "📦 Installing Ruby dependencies..."
32
- $BUNDLE_CMD install
112
+ echo "💎 Installing Ruby dependencies..."
113
+ run_cmd bundle install
33
114
 
34
115
  # Install JavaScript dependencies
35
116
  echo "📦 Installing JavaScript dependencies..."
36
- yarn install --frozen-lockfile
117
+ run_cmd yarn install --frozen-lockfile
37
118
 
38
119
  # Set up Husky git hooks
39
120
  echo "🪝 Setting up Husky git hooks..."
40
- npx husky
121
+ run_cmd npx husky
41
122
  if [ ! -f .husky/pre-commit ]; then
42
123
  echo "Creating pre-commit hook..."
43
124
  cat > .husky/pre-commit << 'EOF'
@@ -47,24 +128,20 @@ EOF
47
128
  chmod +x .husky/pre-commit
48
129
  fi
49
130
 
50
- # Copy environment files if they exist in root
51
- if [ -n "${CONDUCTOR_ROOT_PATH:-}" ]; then
52
- if [ -f "$CONDUCTOR_ROOT_PATH/.env" ]; then
53
- echo "📋 Copying .env file from root..."
54
- cp "$CONDUCTOR_ROOT_PATH/.env" .env
55
- fi
56
-
57
- if [ -f "$CONDUCTOR_ROOT_PATH/.env.local" ]; then
58
- echo "📋 Copying .env.local file from root..."
59
- cp "$CONDUCTOR_ROOT_PATH/.env.local" .env.local
60
- fi
61
- fi
131
+ # Verify linting tools are available
132
+ echo "✅ Verifying linting tools..."
133
+ run_cmd bundle exec rubocop --version
62
134
 
63
- echo " Workspace setup complete!"
135
+ echo " Workspace setup complete!"
64
136
  echo ""
65
- echo "Available commands:"
66
- echo " - Run tests: bundle exec rspec"
67
- echo " - Run specific test suites: bundle exec rake run_spec:gem"
68
- echo " - Run JavaScript tests: yarn test"
69
- echo " - Lint JavaScript: yarn lint"
70
- echo " - Lint Ruby: bundle exec rubocop"
137
+ echo "📚 Key commands:"
138
+ echo " bundle exec rspec - Run Ruby tests"
139
+ echo " bundle exec rake run_spec:gem - Run gem-specific tests"
140
+ echo " • yarn test - Run JavaScript tests"
141
+ echo " • yarn lint - Run JavaScript linting"
142
+ echo " • bundle exec rubocop - Run Ruby linting (required before commits)"
143
+ echo ""
144
+ if [[ "$VERSION_MANAGER" == "mise" ]]; then
145
+ echo "💡 Tip: Use 'bin/conductor-exec <command>' if tool versions aren't detected correctly."
146
+ fi
147
+ echo "⚠️ Remember: Always run 'bundle exec rubocop' before committing!"
data/conductor.json CHANGED
@@ -1,7 +1,9 @@
1
1
  {
2
2
  "scripts": {
3
3
  "setup": "./conductor-setup.sh",
4
- "run": "bundle exec rspec",
4
+ "run": "bin/conductor-exec bundle exec rspec",
5
+ "test": "bin/conductor-exec bundle exec rspec",
6
+ "lint": "bin/conductor-exec bundle exec rubocop && bin/conductor-exec yarn lint",
5
7
  "archive": ""
6
8
  }
7
9
  }
@@ -617,14 +617,57 @@ Shakapacker validates configuration at runtime and provides helpful error messag
617
617
 
618
618
  Some options can be overridden via environment variables:
619
619
 
620
- | Variable | Description | Example |
621
- | ---------------------------- | ------------------------ | ------------------------- |
622
- | `SHAKAPACKER_CONFIG` | Path to shakapacker.yml | `config/webpack.yml` |
623
- | `SHAKAPACKER_ASSETS_BUNDLER` | Override assets bundler | `rspack` |
624
- | `SHAKAPACKER_PRECOMPILE` | Override precompile flag | `false` |
625
- | `SHAKAPACKER_ASSET_HOST` | Override asset host | `https://cdn.example.com` |
626
- | `NODE_ENV` | Node environment | `production` |
627
- | `RAILS_ENV` | Rails environment | `staging` |
620
+ | Variable | Description | Example |
621
+ | ---------------------------- | -------------------------------------------------- | ---------------------------- |
622
+ | `SHAKAPACKER_CONFIG` | Path to shakapacker.yml | `config/webpack.yml` |
623
+ | `SHAKAPACKER_ASSETS_BUNDLER` | Override assets bundler | `rspack` |
624
+ | `SHAKAPACKER_PRECOMPILE` | Override precompile flag | `false` |
625
+ | `SHAKAPACKER_ASSET_HOST` | Override asset host | `https://cdn.example.com` |
626
+ | `SHAKAPACKER_PUBLIC_*` | Auto-exposed to client-side JS (prefix convention) | `SHAKAPACKER_PUBLIC_API_URL` |
627
+ | `SHAKAPACKER_ENV_VARS` | Additional env vars to expose to client-side JS | `API_URL,FEATURE_FLAGS` |
628
+ | `NODE_ENV` | Node environment | `production` |
629
+ | `RAILS_ENV` | Rails environment | `staging` |
630
+
631
+ ### Exposing Environment Variables to Client-Side JavaScript
632
+
633
+ By default, only `NODE_ENV`, `RAILS_ENV`, and `WEBPACK_SERVE` are exposed to client-side JavaScript via webpack/rspack's `EnvironmentPlugin`. This is a security measure to prevent accidentally leaking secrets like `DATABASE_URL`, `API_SECRET_KEY`, etc. into your JavaScript bundles.
634
+
635
+ #### SHAKAPACKER*PUBLIC*\* Prefix (Recommended)
636
+
637
+ Any environment variable prefixed with `SHAKAPACKER_PUBLIC_` is automatically exposed to client-side code. This follows the same convention used by Next.js (`NEXT_PUBLIC_*`) and Vite (`VITE_*`):
638
+
639
+ ```bash
640
+ # These are automatically available in your JavaScript
641
+ export SHAKAPACKER_PUBLIC_API_URL=https://api.example.com
642
+ export SHAKAPACKER_PUBLIC_ANALYTICS_ID=UA-12345
643
+ export SHAKAPACKER_PUBLIC_FEATURE_FLAGS=dark_mode,beta_ui
644
+ ```
645
+
646
+ ```javascript
647
+ // Access in your JavaScript code
648
+ console.log(process.env.SHAKAPACKER_PUBLIC_API_URL)
649
+ console.log(process.env.SHAKAPACKER_PUBLIC_ANALYTICS_ID)
650
+ ```
651
+
652
+ The prefix makes it explicit which variables are intended for client-side use, preventing accidental exposure of secrets.
653
+
654
+ #### SHAKAPACKER_ENV_VARS (Legacy/Escape Hatch)
655
+
656
+ For variables without the `SHAKAPACKER_PUBLIC_` prefix, you can use `SHAKAPACKER_ENV_VARS` to expose them:
657
+
658
+ ```bash
659
+ # Expose additional variables during build
660
+ SHAKAPACKER_ENV_VARS=API_BASE_URL,FEATURE_FLAGS bundle exec rake assets:precompile
661
+
662
+ # In development
663
+ SHAKAPACKER_ENV_VARS=API_BASE_URL bin/shakapacker-dev-server
664
+ ```
665
+
666
+ ```javascript
667
+ // These are available after adding to SHAKAPACKER_ENV_VARS
668
+ console.log(process.env.API_BASE_URL)
669
+ console.log(process.env.FEATURE_FLAGS)
670
+ ```
628
671
 
629
672
  ## Best Practices
630
673
 
data/docs/early_hints.md CHANGED
@@ -4,6 +4,8 @@ This guide shows you how to use HTTP 103 Early Hints with Shakapacker to optimiz
4
4
 
5
5
  > **📚 Related Documentation:** For advanced manual control using `send_pack_early_hints` in controllers before expensive work, see [early_hints_manual_api.md](early_hints_manual_api.md).
6
6
 
7
+ > ⚠️ **CDN Limitation**: Most CDNs (Cloudflare, AWS CloudFront, AWS ALB, etc.) strip HTTP 103 Early Hints before they reach end users. While Shakapacker sends early hints correctly, and your application server (Puma/Thruster) forwards them properly, **CDNs typically strip the 103 response**. End users will only receive the 200 response with Link headers (which arrive too late to provide early hints benefits). This is an industry-wide limitation—even major sites like GitHub, Google, and Shopify don't serve 103 in production through CDNs. For full early hints delivery, you need either direct connections without a CDN, or specific CDN configuration (e.g., Cloudflare's Early Hints feature on paid plans, which works differently by caching Link headers). See [Reverse proxy stripping 103 responses](#reverse-proxy-stripping-103-responses) for configuration details.
8
+
7
9
  ## What are Early Hints?
8
10
 
9
11
  HTTP 103 Early Hints is emitted **after** Rails has finished rendering but **before** the final response is sent, allowing browsers to begin fetching resources (JS, CSS) prior to receiving the full HTML response. This may significantly improve page load performance or cause an equally significant regression, depending on the page's content.
@@ -336,8 +338,12 @@ See the [Manual API Guide](early_hints_manual_api.md) for detailed examples and
336
338
  - **Modern browsers:**
337
339
  - Chrome/Edge/Firefox 103+ ✅
338
340
  - Safari 16.4+ ✅
341
+ - **Infrastructure that preserves 103 responses:**
342
+ - Direct connections (no CDN) ✅
343
+ - Cloudflare with Early Hints enabled (paid plans, works via Link header caching) ✅
344
+ - Most CDNs/load balancers ❌ (strip 103 responses - see [CDN Limitation](#troubleshooting) above)
339
345
 
340
- If requirements not met, feature gracefully degrades with no errors.
346
+ If requirements not met, feature gracefully degrades with no errors. The Link headers will still be present in the 200 response, which may provide some browser prefetching benefits even when 103 is stripped.
341
347
 
342
348
  ## Quick Reference
343
349
 
@@ -377,8 +383,9 @@ Debug mode adds HTML comments to your page showing:
377
383
  - What pack names were processed
378
384
  - What Link headers were sent
379
385
  - HTTP/2 support status
386
+ - CDN warning reminder (since most CDNs strip 103 responses)
380
387
 
381
- View page source and look for `<!-- Shakapacker Early Hints Debug -->` comments.
388
+ View page source and look for `<!-- Shakapacker Early Hints -->` comments.
382
389
 
383
390
  **Early hints not appearing:**
384
391
 
@@ -23,6 +23,52 @@ The precompile hook is especially useful when you need to run commands like:
23
23
  - **Development**: Runs before `bin/shakapacker --watch` or dev server starts
24
24
  - **Production**: Runs before `bundle exec rake assets:precompile`
25
25
 
26
+ ## Choosing an Approach
27
+
28
+ Use `precompile_hook` when your setup should always run preparatory commands right
29
+ before Shakapacker compiles. For React on Rails projects, this is often the
30
+ simplest default.
31
+
32
+ For projects with more custom startup needs (for example, additional build steps
33
+ or strict process ordering in `bin/dev`), you can run those commands explicitly
34
+ before launching long-running processes instead of using `precompile_hook`.
35
+
36
+ ### Comparison
37
+
38
+ | Aspect | `precompile_hook` | Explicit setup in `bin/dev` |
39
+ |--------|-------------------|-------------------------------|
40
+ | Best for | Default/consistent pre-build tasks | Custom multi-step dev boot flows |
41
+ | Runs when | Immediately before compilation starts | Wherever you place it in startup script |
42
+ | Production integration | Automatic via `assets:precompile` | Requires explicit production wiring |
43
+ | Process manager complexity | Lower | Higher (you own orchestration) |
44
+ | Debugging | Centralized hook command | Fully explicit command-by-command flow |
45
+
46
+ ### `shakapacker_precompile` Interaction
47
+
48
+ `shakapacker_precompile` controls whether Shakapacker compilation is included in
49
+ `assets:precompile`, while `precompile_hook` controls whether a preparatory command
50
+ runs before compilation.
51
+
52
+ ```yaml
53
+ # Option A: Default behavior
54
+ shakapacker_precompile: true
55
+ precompile_hook: "bin/shakapacker-precompile-hook"
56
+
57
+ # Option B: You manage compilation elsewhere
58
+ shakapacker_precompile: false
59
+ precompile_hook: "bin/shakapacker-precompile-hook"
60
+
61
+ # Option C: Fully explicit startup flow (no hook)
62
+ shakapacker_precompile: false
63
+ # precompile_hook: not set
64
+ ```
65
+
66
+ To temporarily skip only the hook, set:
67
+
68
+ ```bash
69
+ SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true
70
+ ```
71
+
26
72
  ## Configuration
27
73
 
28
74
  Add the `precompile_hook` option to your `config/shakapacker.yml`:
@@ -300,7 +346,7 @@ This is useful when:
300
346
  - Running the hook manually and then compiling multiple times
301
347
  - Debugging compilation issues without the hook
302
348
 
303
- **Note:** The examples below show how to implement this in your custom `bin/dev` script. If you're using React on Rails, the generated `bin/dev` script already implements this pattern automatically - it runs the precompile hook once before launching processes, then sets `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution.
349
+ **Note:** The examples below show how to implement this in your custom `bin/dev` script. If you're using React on Rails v13.1.0+, the generated `bin/dev` script already implements this pattern automatically - **no action needed**. It runs the precompile hook once before launching processes, then sets `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution.
304
350
 
305
351
  **Recommended: Use Procfile env prefix**
306
352
 
@@ -141,6 +141,9 @@ if (setup_path = Rails.root.join("bin/setup")).exist?
141
141
  end
142
142
 
143
143
  Dir.chdir(Rails.root) do
144
+ npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION)
145
+ shakapacker_dependency_value = nil
146
+
144
147
  # In CI, use the pre-packed tarball if available
145
148
  if ENV["SHAKAPACKER_NPM_PACKAGE"]
146
149
  package_path = ENV["SHAKAPACKER_NPM_PACKAGE"]
@@ -167,6 +170,7 @@ Dir.chdir(Rails.root) do
167
170
  say "📦 Installing shakapacker from local package: #{absolute_path}", :cyan
168
171
  begin
169
172
  @package_json.manager.add!([absolute_path], type: :production)
173
+ shakapacker_dependency_value = absolute_path
170
174
  rescue PackageJson::Error
171
175
  say "Shakapacker installation failed 😭 See above for details.", :red
172
176
  exit 1
@@ -174,9 +178,9 @@ Dir.chdir(Rails.root) do
174
178
  else
175
179
  say "⚠️ SHAKAPACKER_NPM_PACKAGE set but file not found: #{absolute_path}", :yellow
176
180
  say "Falling back to npm registry...", :yellow
177
- npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION)
178
181
  begin
179
182
  @package_json.manager.add!(["shakapacker@#{npm_version}"], type: :production)
183
+ shakapacker_dependency_value = npm_version
180
184
  rescue PackageJson::Error
181
185
  say "Shakapacker installation failed 😭 See above for details.", :red
182
186
  exit 1
@@ -187,10 +191,10 @@ Dir.chdir(Rails.root) do
187
191
  exit 1
188
192
  end
189
193
  else
190
- npm_version = Shakapacker::Utils::VersionSyntaxConverter.new.rubygem_to_npm(Shakapacker::VERSION)
191
194
  say "Installing shakapacker@#{npm_version}"
192
195
  begin
193
196
  @package_json.manager.add!(["shakapacker@#{npm_version}"], type: :production)
197
+ shakapacker_dependency_value = npm_version
194
198
  rescue PackageJson::Error
195
199
  say "Shakapacker installation failed 😭 See above for details.", :red
196
200
  exit 1
@@ -201,8 +205,8 @@ Dir.chdir(Rails.root) do
201
205
  if pj["dependencies"] && pj["dependencies"]["shakapacker"]
202
206
  {
203
207
  "dependencies" => pj["dependencies"].merge({
204
- # TODO: workaround for test suite - long-run need to actually account for diff pkg manager behaviour
205
- "shakapacker" => pj["dependencies"]["shakapacker"].delete_prefix("^")
208
+ # Keep package.json aligned with the exact source/version this installer requested.
209
+ "shakapacker" => shakapacker_dependency_value || pj["dependencies"]["shakapacker"].delete_prefix("^")
206
210
  })
207
211
  }
208
212
  else