Class: Shakapacker::SwcMigrator

Inherits:
Object
  • Object
show all
Defined in:
lib/shakapacker/swc_migrator.rb

Constant Summary collapse

BABEL_PACKAGES =

Babel packages safe to remove when migrating to SWC Note: @babel/core and @babel/eslint-parser are excluded as they may be needed for ESLint

[
  "@babel/plugin-proposal-class-properties",
  "@babel/plugin-proposal-object-rest-spread",
  "@babel/plugin-syntax-dynamic-import",
  "@babel/plugin-transform-destructuring",
  "@babel/plugin-transform-regenerator",
  "@babel/plugin-transform-runtime",
  "@babel/preset-env",
  "@babel/preset-react",
  "@babel/preset-typescript",
  "@babel/runtime",
  "babel-loader",
  "babel-plugin-macros",
  "babel-plugin-transform-react-remove-prop-types"
].freeze
ESLINT_BABEL_PACKAGES =

Babel packages that may be needed for ESLint - only remove if user explicitly confirms

[
  "@babel/core",
  "@babel/eslint-parser"
].freeze
SWC_PACKAGES =
{
  "@swc/core" => "^1.7.39",
  "swc-loader" => "^0.2.6"
}.freeze
ESLINT_CONFIG_FILES =
%w[
  .eslintrc
  .eslintrc.js
  .eslintrc.cjs
  .eslintrc.yaml
  .eslintrc.yml
  .eslintrc.json
].freeze
SWC_CONFIG_WEBPACK_HEADER =
<<~JS.freeze
  // config/swc.config.js
  // This file is merged with Shakapacker's default SWC configuration
  // See: https://swc.rs/docs/configuration/compilation
JS
SWC_CONFIG_RSPACK_HEADER =

Rspack never reads config/swc.config.js: package/rules/rspack.ts sets its builtin:swc-loader options inline, so the file is generated with a header that says so.

The example keeps test: 'match' and overrides the JS and TS rules separately on purpose. Matching on use.loader alone (a single override with no test) looks tidier but is wrong: webpack-merge cannot pair such an override with a specific rule, so it appends builtin:swc-loader onto EVERY rule's use chain, including the CSS and Sass rules. Verified at webpack-merge 5.10.0 and 6.0.1.

<<~JS.freeze
  // config/swc.config.js
  // NOTE: This file is NOT read when assets_bundler is 'rspack'.
  // Shakapacker's Rspack config sets its 'builtin:swc-loader' options inline, so nothing
  // here reaches your build. To customize SWC for Rspack, apply webpack-merge's
  // mergeWithRules to the OUTPUT of generateRspackConfig(). Passing an override into
  // generateRspackConfig() cannot replace the built-in rule: it merges with plain merge,
  // which concatenates module.rules and leaves the built-in rule in place alongside yours.
  //
  // Use options: 'merge', not 'replace'. 'merge' deep-merges into the built-in options so
  // parser.jsx (JS), parser.syntax/tsx (TS), and transform.react.runtime survive.
  // 'replace' swaps the whole options object and silently drops them, breaking JSX and
  // TypeScript compilation.
  //
  // Keep test: 'match' in the merge spec. Matching on use.loader alone appends
  // builtin:swc-loader to every rule's use chain, CSS and Sass included.
  //
  //   const { mergeWithRules } = require('shakapacker');
  //   const { generateRspackConfig } = require('shakapacker/rspack');
  //
  //   const swcOptions = { jsc: { keepClassNames: true } }; // your SWC options
  //
  //   module.exports = mergeWithRules({
  //     module: { rules: { test: 'match', use: { loader: 'match', options: 'merge' } } },
  //   })(generateRspackConfig(), {
  //     module: {
  //       rules: [
  //         // Shakapacker registers separate builtin:swc-loader rules for JS and TS.
  //         // Override both: a JS-only override never reaches .ts/.tsx files.
  //         {
  //           test: /\\.(js|jsx|mjs)$/,
  //           use: [{ loader: 'builtin:swc-loader', options: swcOptions }],
  //         },
  //         {
  //           test: /\\.(ts|tsx)$/,
  //           use: [{ loader: 'builtin:swc-loader', options: swcOptions }],
  //         },
  //       ],
  //     },
  //   });
  //
  // See: https://github.com/shakacode/shakapacker/blob/main/docs/rspack.md
JS
SWC_CONFIG_BODY =
<<~JS.freeze
  const { env } = require('shakapacker');

  module.exports = {
    options: {
      jsc: {
        // CRITICAL for Stimulus compatibility: Prevents SWC from mangling class names
        // which breaks Stimulus's class-based controller discovery mechanism
        keepClassNames: true,
        transform: {
          react: {
            runtime: 'automatic',
            refresh: env.isDevelopment && env.runningWebpackDevServer,
          },
        },
      },
    },
  };
JS
DEFAULT_SWC_CONFIG =

Kept for backward compatibility: same value as before, and still the webpack variant.

"#{SWC_CONFIG_WEBPACK_HEADER}\n#{SWC_CONFIG_BODY}".freeze
RSPACK_SWC_CONFIG =
"#{SWC_CONFIG_RSPACK_HEADER}\n#{SWC_CONFIG_BODY}".freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root_path, logger: nil) ⇒ SwcMigrator

Returns a new instance of SwcMigrator.



132
133
134
135
# File 'lib/shakapacker/swc_migrator.rb', line 132

def initialize(root_path, logger: nil)
  @root_path = Pathname.new(root_path)
  @logger = logger || Logger.new($stdout)
end

Instance Attribute Details

#loggerObject (readonly)

Returns the value of attribute logger.



9
10
11
# File 'lib/shakapacker/swc_migrator.rb', line 9

def logger
  @logger
end

#root_pathObject (readonly)

Returns the value of attribute root_path.



9
10
11
# File 'lib/shakapacker/swc_migrator.rb', line 9

def root_path
  @root_path
end

Instance Method Details

#clean_babel_packages(run_installer: true) ⇒ Object



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/shakapacker/swc_migrator.rb', line 175

def clean_babel_packages(run_installer: true)
  logger.info "๐Ÿงน Removing Babel packages..."

  package_json_path = root_path.join("package.json")
  unless package_json_path.exist?
    logger.error "โŒ No package.json found"
    return { removed_packages: [], config_files_deleted: [], preserved_packages: [] }
  end

  # Check if ESLint uses Babel parser
  preserved_for_eslint = []
  if eslint_uses_babel?
    logger.info "\nโš ๏ธ  ESLint configuration detected that uses Babel parser"
    logger.info "   Preserving @babel/core and @babel/eslint-parser for ESLint compatibility"
    logger.info "   To switch ESLint parser:"
    logger.info "   1. For TypeScript: use @typescript-eslint/parser"
    logger.info "   2. For JavaScript: use espree (ESLint's default parser)"
    preserved_for_eslint = ESLINT_BABEL_PACKAGES
  end

  removed_packages = remove_babel_from_package_json(package_json_path, preserve: preserved_for_eslint)
  deleted_files = delete_babel_config_files

  if removed_packages.any?
    logger.info "โœ… Babel packages removed successfully!"
    run_package_manager_install if run_installer
  else
    logger.info "โ„น๏ธ  No Babel packages found to remove"
  end

  { removed_packages: removed_packages, config_files_deleted: deleted_files, preserved_packages: preserved_for_eslint }
end

#find_babel_packagesObject



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/shakapacker/swc_migrator.rb', line 208

def find_babel_packages
  package_json_path = root_path.join("package.json")
  return [] unless package_json_path.exist?

  begin
    package_json = JSON.parse(File.read(package_json_path))
    dependencies = package_json["dependencies"] || {}
    dev_dependencies = package_json["devDependencies"] || {}
    all_deps = dependencies.merge(dev_dependencies)

    # Find all babel packages (including ESLint-related ones for display)
    all_babel_packages = BABEL_PACKAGES + ESLINT_BABEL_PACKAGES
    found_packages = all_babel_packages.select { |pkg| all_deps.key?(pkg) }
    found_packages
  rescue JSON::ParserError => e
    logger.error "Failed to parse package.json: #{e.message}"
    []
  end
end

#migrate_to_swc(run_installer: true) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/shakapacker/swc_migrator.rb', line 137

def migrate_to_swc(run_installer: true)
  logger.info "๐Ÿ”„ Starting migration from Babel to SWC..."

  results = {
    config_updated: update_shakapacker_config,
    packages_installed: install_swc_packages,
    swc_config_created: create_swc_config,
    babel_packages_found: find_babel_packages
  }

  logger.info "๐ŸŽ‰ Migration to SWC complete!"
  logger.info "   Note: SWC is approximately 20x faster than Babel for transpilation."
  logger.info "   Please test your application thoroughly after migration."
  logger.info "\n๐Ÿ“ Configuration Info:"
  log_swc_config_guidance

  # Show cleanup recommendations if babel packages found
  if results[:babel_packages_found].any?
    logger.info "\n๐Ÿงน Cleanup Recommendations:"
    logger.info "   Found the following Babel packages in your package.json:"
    results[:babel_packages_found].each do |package|
      logger.info "   - #{package}"
    end
    logger.info "\n   To remove them, run:"
    logger.info "   bundle exec rake shakapacker:clean_babel_packages"
  end

  # Suggest running doctor to verify configuration
  logger.info "\n๐Ÿฉบ Run 'bundle exec rake shakapacker:doctor' to verify your configuration"

  # Run package manager install if packages were added
  if run_installer && results[:packages_installed].any?
    run_package_manager_install
  end

  results
end