diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 798b17c4dd..80602bde4b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: yarn + # Disable cache for Node 22 due to V8 bug in 22.21.0 + # https://github.com/nodejs/node/issues/56010 + cache: ${{ matrix.node-version != '22' && 'yarn' || '' }} cache-dependency-path: '**/yarn.lock' - name: Print system information run: | @@ -123,7 +125,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: yarn + # Disable cache for Node 22 due to V8 bug in 22.21.0 + # https://github.com/nodejs/node/issues/56010 + cache: ${{ matrix.node-version != '22' && 'yarn' || '' }} cache-dependency-path: '**/yarn.lock' - name: Print system information run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe72b2fc7..23cad6a84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,15 @@ Changes since the last non-beta release. - **Improved RSC Payload Error Handling**: Errors that happen during generation of RSC payload are transferred properly to rails side and logs the error message and stack. [PR #1888](https://github.com/shakacode/react_on_rails/pull/1888) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +#### Changed + +- **Shakapacker 9.0.0 Upgrade**: Upgraded Shakapacker from 8.2.0 to 9.0.0 with Babel transpiler configuration for compatibility. Key changes include: + - Configured `javascript_transpiler: babel` in shakapacker.yml (Shakapacker 9.0 defaults to SWC which has PropTypes handling issues) + - Added precompile hook support via `bin/shakapacker-precompile-hook` for ReScript builds and pack generation + - Configured CSS Modules to use default exports (`namedExport: false`) for backward compatibility with existing `import styles from` syntax + - Fixed webpack configuration to process SCSS rules and CSS loaders in a single pass for better performance + [PR 1904](https://github.com/shakacode/react_on_rails/pull/1904) by [justin808](https://github.com/justin808). + #### Bug Fixes - **Use as Git dependency**: All packages can now be installed as Git dependencies. This is useful for development and testing purposes. See [CONTRIBUTING.md](./CONTRIBUTING.md#git-dependencies) for documentation. [PR #1873](https://github.com/shakacode/react_on_rails/pull/1873) by [alexeyr-ci2](https://github.com/alexeyr-ci2). diff --git a/Gemfile.development_dependencies b/Gemfile.development_dependencies index e11cd21c92..30acfd8ab5 100644 --- a/Gemfile.development_dependencies +++ b/Gemfile.development_dependencies @@ -1,6 +1,6 @@ # frozen_string_literal: true -gem "shakapacker", "8.2.0" +gem "shakapacker", "9.0.0" gem "bootsnap", require: false gem "rails", "~> 7.1" diff --git a/Gemfile.lock b/Gemfile.lock index 5fe05932b3..2d83409248 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -342,7 +342,7 @@ GEM rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.1.0) - shakapacker (8.2.0) + shakapacker (9.0.0) activesupport (>= 5.2) package_json rack-proxy (>= 0.6.1) @@ -440,7 +440,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker (= 8.2.0) + shakapacker (= 9.0.0) spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/knip.ts b/knip.ts index 12801f717a..97f8e0c837 100644 --- a/knip.ts +++ b/knip.ts @@ -10,6 +10,7 @@ const config: KnipConfig = { ignoreBinaries: [ // Has to be installed globally 'yalc', + // Used in package.json scripts (devDependency, so unlisted in production mode) 'nps', // Pro package binaries used in Pro workflows 'playwright', @@ -109,8 +110,9 @@ const config: KnipConfig = { 'bin/.*', ], ignoreDependencies: [ - // Knip thinks it can be a devDependency, but it's supposed to be in dependencies. + // Build-time dependencies not detected by Knip in any mode '@babel/runtime', + 'mini-css-extract-plugin', // There's no ReScript plugin for Knip '@rescript/react', // The Babel plugin fails to detect it @@ -120,17 +122,15 @@ const config: KnipConfig = { 'node-libs-browser', // The below dependencies are not detected by the Webpack plugin // due to the config issue. - 'css-loader', 'expose-loader', 'file-loader', 'imports-loader', - 'mini-css-extract-plugin', 'null-loader', - 'sass', - 'sass-loader', 'sass-resources-loader', 'style-loader', 'url-loader', + // Transitive dependency of shakapacker but listed as direct dependency + 'webpack-merge', ], }, }, diff --git a/spec/dummy/Gemfile.lock b/spec/dummy/Gemfile.lock index 7641720904..018db9efe3 100644 --- a/spec/dummy/Gemfile.lock +++ b/spec/dummy/Gemfile.lock @@ -346,7 +346,7 @@ GEM rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.1.0) - shakapacker (8.2.0) + shakapacker (9.0.0) activesupport (>= 5.2) package_json rack-proxy (>= 0.6.1) @@ -441,7 +441,7 @@ DEPENDENCIES scss_lint sdoc selenium-webdriver (= 4.9.0) - shakapacker (= 8.2.0) + shakapacker (= 9.0.0) spring (~> 4.0) sprockets (~> 4.0) sqlite3 (~> 1.6) diff --git a/spec/dummy/babel.config.js b/spec/dummy/babel.config.js index 6aa1d24be2..b401544fa1 100644 --- a/spec/dummy/babel.config.js +++ b/spec/dummy/babel.config.js @@ -1,4 +1,5 @@ -const defaultConfigFunc = require('shakapacker/package/babel/preset'); +// eslint-disable-next-line import/extensions +const defaultConfigFunc = require('shakapacker/package/babel/preset.js'); module.exports = function createBabelConfig(api) { const resultConfig = defaultConfigFunc(api); diff --git a/spec/dummy/bin/shakapacker-precompile-hook b/spec/dummy/bin/shakapacker-precompile-hook new file mode 100755 index 0000000000..b650b02db5 --- /dev/null +++ b/spec/dummy/bin/shakapacker-precompile-hook @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Shakapacker precompile hook +# This script runs before Shakapacker compilation in both development and production. +# See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md + +require "fileutils" + +# Find Rails root by walking upward looking for config/environment.rb +def find_rails_root + dir = Dir.pwd + loop do + return dir if File.exist?(File.join(dir, "config", "environment.rb")) + + parent = File.dirname(dir) + return nil if parent == dir # Reached filesystem root + + dir = parent + end +end + +# Build ReScript if needed +def build_rescript_if_needed + # Check for both old (bsconfig.json) and new (rescript.json) config files + return unless File.exist?("bsconfig.json") || File.exist?("rescript.json") + + puts "🔧 Building ReScript..." + + # Cross-platform package manager detection + yarn_available = system("yarn", "--version", out: File::NULL, err: File::NULL) + npm_available = system("npm", "--version", out: File::NULL, err: File::NULL) + + success = if yarn_available + system("yarn", "build:rescript") + elsif npm_available + system("npm", "run", "build:rescript") + else + warn "⚠️ Warning: Neither yarn nor npm found. Skipping ReScript build." + return + end + + if success + puts "✅ ReScript build completed successfully" + else + warn "❌ ReScript build failed" + exit 1 + end +end + +# Generate React on Rails packs if needed +# rubocop:disable Metrics/CyclomaticComplexity +def generate_packs_if_needed + # Find Rails root directory + rails_root = find_rails_root + return unless rails_root + + # Check if React on Rails initializer exists + initializer_path = File.join(rails_root, "config", "initializers", "react_on_rails.rb") + return unless File.exist?(initializer_path) + + # Check if auto-pack generation is configured (match actual config assignments, not comments) + config_file = File.read(initializer_path) + # Match uncommented configuration lines only (lines not starting with #) + has_auto_load = config_file =~ /^\s*(?!#).*config\.auto_load_bundle\s*=/ + has_components_subdir = config_file =~ /^\s*(?!#).*config\.components_subdirectory\s*=/ + return unless has_auto_load || has_components_subdir + + puts "📦 Generating React on Rails packs..." + + # Cross-platform bundle availability check + bundle_available = system("bundle", "--version", out: File::NULL, err: File::NULL) + return unless bundle_available + + # Check if rake task exists (use array form for security) + task_list = IO.popen(["bundle", "exec", "rails", "-T"], err: [:child, :out], &:read) + return unless task_list.include?("react_on_rails:generate_packs") + + # Use array form for better cross-platform support + success = system("bundle", "exec", "rails", "react_on_rails:generate_packs") + + if success + puts "✅ Pack generation completed successfully" + else + warn "❌ Pack generation failed" + exit 1 + end +end +# rubocop:enable Metrics/CyclomaticComplexity + +# Main execution +begin + build_rescript_if_needed + generate_packs_if_needed + + exit 0 +rescue StandardError => e + warn "❌ Precompile hook failed: #{e.message}" + warn e.backtrace.join("\n") + exit 1 +end diff --git a/spec/dummy/config/shakapacker.yml b/spec/dummy/config/shakapacker.yml index c58b284594..c9565dbb5f 100644 --- a/spec/dummy/config/shakapacker.yml +++ b/spec/dummy/config/shakapacker.yml @@ -5,6 +5,14 @@ default: &default source_entry_path: packs public_root_path: public + # Use Babel instead of SWC (Shakapacker 9.0 default) for better compatibility + # SWC has issues with PropTypes handling + javascript_transpiler: babel + + # Hook to run before compilation (e.g., for ReScript builds, pack generation) + # See: https://github.com/shakacode/shakapacker/blob/main/docs/precompile_hook.md + precompile_hook: bin/shakapacker-precompile-hook + cache_path: tmp/cache/shakapacker webpack_compile_output: false ensure_consistent_versioning: true diff --git a/spec/dummy/config/webpack/commonWebpackConfig.js b/spec/dummy/config/webpack/commonWebpackConfig.js index 66ed67094c..41f8a5647e 100644 --- a/spec/dummy/config/webpack/commonWebpackConfig.js +++ b/spec/dummy/config/webpack/commonWebpackConfig.js @@ -21,10 +21,37 @@ const sassLoaderConfig = { }, }; -const scssConfigIndex = baseClientWebpackConfig.module.rules.findIndex((config) => - '.scss'.match(config.test), -); -baseClientWebpackConfig.module.rules[scssConfigIndex]?.use.push(sassLoaderConfig); +// Process webpack rules in single pass for efficiency +baseClientWebpackConfig.module.rules.forEach((rule) => { + if (Array.isArray(rule.use)) { + // Add sass-resources-loader to all SCSS rules (both .scss and .module.scss) + if (rule.test && '.scss'.match(rule.test)) { + rule.use.push(sassLoaderConfig); + } + + // Configure CSS Modules to use default exports (Shakapacker 9.0 compatibility) + // Shakapacker 9.0 defaults to namedExport: true, but we use default imports + // To restore backward compatibility with existing code using `import styles from` + rule.use.forEach((loader) => { + if ( + loader && + typeof loader === 'object' && + loader.loader && + typeof loader.loader === 'string' && + loader.loader.includes('css-loader') && + loader.options && + typeof loader.options === 'object' && + loader.options.modules && + typeof loader.options.modules === 'object' + ) { + // eslint-disable-next-line no-param-reassign + loader.options.modules.namedExport = false; + // eslint-disable-next-line no-param-reassign + loader.options.modules.exportLocalsConvention = 'camelCase'; + } + }); + } +}); // add jquery const exposeJQuery = { diff --git a/spec/dummy/package.json b/spec/dummy/package.json index 86550eade3..5cbed3ab13 100644 --- a/spec/dummy/package.json +++ b/spec/dummy/package.json @@ -51,7 +51,7 @@ "sass": "^1.43.4", "sass-loader": "^12.3.0", "sass-resources-loader": "^2.1.0", - "shakapacker": "8.2.0", + "shakapacker": "9.0.0", "style-loader": "^3.3.1", "terser-webpack-plugin": "5.3.1", "url-loader": "^4.0.0", diff --git a/spec/dummy/yarn.lock b/spec/dummy/yarn.lock index 7e26a59417..c2c72df59e 100644 --- a/spec/dummy/yarn.lock +++ b/spec/dummy/yarn.lock @@ -3236,6 +3236,11 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + follow-redirects@^1.0.0: version "1.14.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" @@ -5648,13 +5653,14 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shakapacker@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.2.0.tgz#c7bed87b8be2ae565cfe616f68552be545c77e14" - integrity sha512-Ct7BFqJVnKbxdqCzG+ja7Q6LPt/PlB7sSVBfG5jsAvmVCADM05cuoNwEgYNjFGKbDzHAxUqy5XgoI9Y030+JKQ== +shakapacker@9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/shakapacker/-/shakapacker-9.0.0.tgz#36fd2e81ffa3a01075222526b2b079bfd60a6efc" + integrity sha512-q+8VU3AQhPpCLlZmEmyooELmpa10FPXk631rrg46pLAYO40jnEeyK01BtI0SVNvz/nI+QFz1DwZE8NKVk/PRgw== dependencies: js-yaml "^4.1.0" path-complete-extname "^1.0.0" + webpack-merge "^5.8.0" shallow-clone@^3.0.0: version "3.0.1" @@ -6340,6 +6346,15 @@ webpack-merge@5, webpack-merge@^5.7.3: clone-deep "^4.0.1" wildcard "^2.0.0" +webpack-merge@^5.8.0: + version "5.10.0" + resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"