shakapacker 8.4.0 → 9.7.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 +4 -4
- data/.claude/commands/address-review.md +206 -0
- data/.claude/commands/update-changelog.md +354 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
- data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
- data/.github/STATUS.md +1 -0
- data/.github/actionlint-matcher.json +17 -0
- data/.github/workflows/claude-code-review.yml +45 -0
- data/.github/workflows/claude.yml +55 -0
- data/.github/workflows/dummy.yml +18 -5
- data/.github/workflows/eslint-validation.yml +46 -0
- data/.github/workflows/generator.yml +38 -22
- data/.github/workflows/node.yml +116 -2
- data/.github/workflows/ruby.yml +57 -15
- data/.github/workflows/test-bundlers.yml +180 -0
- data/.gitignore +27 -0
- data/.husky/pre-commit +2 -0
- data/.npmignore +56 -0
- data/.prettierignore +7 -0
- data/.rubocop.yml +2 -0
- data/.yalcignore +26 -0
- data/CHANGELOG.md +487 -19
- data/CLAUDE.md +63 -0
- data/CONTRIBUTING.md +268 -21
- data/ESLINT_TECHNICAL_DEBT.md +165 -0
- data/README.md +497 -137
- data/Rakefile +44 -4
- data/TODO.md +58 -0
- data/TODO_v9.md +97 -0
- data/bin/conductor-exec +24 -0
- data/bin/shakapacker-config +11 -0
- data/conductor-setup.sh +147 -0
- data/conductor.json +9 -0
- data/docs/api-reference.md +519 -0
- data/docs/cdn_setup.md +384 -0
- data/docs/common-upgrades.md +695 -0
- data/docs/configuration.md +845 -0
- data/docs/css-modules-export-mode.md +566 -0
- data/docs/customizing_babel_config.md +16 -16
- data/docs/deployment.md +78 -7
- data/docs/developing_shakapacker.md +6 -0
- data/docs/early_hints.md +433 -0
- data/docs/early_hints_manual_api.md +454 -0
- data/docs/feature_testing.md +492 -0
- data/docs/node_package_api.md +70 -0
- data/docs/optional-peer-dependencies.md +203 -0
- data/docs/peer-dependencies.md +71 -0
- data/docs/precompile_hook.md +486 -0
- data/docs/preventing_fouc.md +132 -0
- data/docs/react.md +58 -48
- data/docs/releasing.md +288 -0
- data/docs/rspack.md +218 -0
- data/docs/rspack_migration_guide.md +862 -0
- data/docs/sprockets.md +1 -0
- data/docs/style_loader_vs_mini_css.md +12 -12
- data/docs/subresource_integrity.md +13 -7
- data/docs/transpiler-migration.md +212 -0
- data/docs/transpiler-performance.md +200 -0
- data/docs/troubleshooting.md +272 -24
- data/docs/typescript-migration.md +388 -0
- data/docs/typescript.md +103 -0
- data/docs/using_esbuild_loader.md +12 -12
- data/docs/using_swc_loader.md +121 -16
- data/docs/v6_upgrade.md +42 -19
- data/docs/v7_upgrade.md +8 -6
- data/docs/v8_upgrade.md +13 -12
- data/docs/v9_upgrade.md +616 -0
- data/eslint.config.fast.js +254 -0
- data/eslint.config.js +309 -0
- data/jest.config.js +8 -1
- data/knip.ts +61 -0
- data/lib/install/bin/shakapacker +4 -6
- data/lib/install/bin/shakapacker-config +11 -0
- data/lib/install/bin/shakapacker-dev-server +1 -1
- data/lib/install/binstubs.rb +6 -2
- data/lib/install/config/rspack/rspack.config.js +6 -0
- data/lib/install/config/rspack/rspack.config.ts +7 -0
- data/lib/install/config/shakapacker.yml +75 -12
- data/lib/install/config/webpack/webpack.config.ts +7 -0
- data/lib/install/package.json +38 -0
- data/lib/install/template.rb +207 -45
- data/lib/shakapacker/build_config_loader.rb +147 -0
- data/lib/shakapacker/bundler_switcher.rb +415 -0
- data/lib/shakapacker/compiler.rb +87 -0
- data/lib/shakapacker/configuration.rb +475 -6
- data/lib/shakapacker/dev_server.rb +88 -1
- data/lib/shakapacker/dev_server_runner.rb +240 -6
- data/lib/shakapacker/doctor.rb +1191 -0
- data/lib/shakapacker/env.rb +19 -3
- data/lib/shakapacker/helper.rb +411 -14
- data/lib/shakapacker/install/env.rb +33 -0
- data/lib/shakapacker/instance.rb +93 -4
- data/lib/shakapacker/manifest.rb +167 -30
- data/lib/shakapacker/railtie.rb +4 -0
- data/lib/shakapacker/rspack_runner.rb +19 -0
- data/lib/shakapacker/runner.rb +668 -9
- data/lib/shakapacker/swc_migrator.rb +384 -0
- data/lib/shakapacker/utils/manager.rb +2 -0
- data/lib/shakapacker/utils/version_syntax_converter.rb +1 -1
- data/lib/shakapacker/version.rb +1 -1
- data/lib/shakapacker/version_checker.rb +1 -1
- data/lib/shakapacker/webpack_runner.rb +4 -42
- data/lib/shakapacker.rb +159 -1
- data/lib/tasks/shakapacker/binstubs.rake +4 -2
- data/lib/tasks/shakapacker/check_binstubs.rake +2 -2
- data/lib/tasks/shakapacker/doctor.rake +48 -0
- data/lib/tasks/shakapacker/export_bundler_config.rake +68 -0
- data/lib/tasks/shakapacker/install.rake +16 -4
- data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
- data/lib/tasks/shakapacker/switch_bundler.rake +72 -0
- data/lib/tasks/shakapacker.rake +2 -0
- data/package/.npmignore +4 -0
- data/package/babel/preset.ts +59 -0
- data/package/config.ts +189 -0
- data/package/configExporter/buildValidator.ts +906 -0
- data/package/configExporter/cli.ts +1748 -0
- data/package/configExporter/configDocs.ts +102 -0
- data/package/configExporter/configFile.ts +663 -0
- data/package/configExporter/fileWriter.ts +112 -0
- data/package/configExporter/index.ts +15 -0
- data/package/configExporter/types.ts +159 -0
- data/package/configExporter/yamlSerializer.ts +391 -0
- data/package/dev_server.ts +27 -0
- data/package/env.ts +92 -0
- data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +36 -0
- data/package/environments/base.ts +147 -0
- data/package/environments/development.ts +88 -0
- data/package/environments/production.ts +82 -0
- data/package/environments/test.ts +55 -0
- data/package/environments/types.ts +98 -0
- data/package/esbuild/index.ts +40 -0
- data/package/index.d.ts +68 -93
- data/package/index.d.ts.template +72 -0
- data/package/index.ts +104 -0
- data/package/loaders.d.ts +28 -0
- data/package/optimization/rspack.ts +36 -0
- data/package/optimization/webpack.ts +55 -0
- data/package/plugins/envFilter.ts +82 -0
- data/package/plugins/rspack.ts +119 -0
- data/package/plugins/webpack.ts +82 -0
- data/package/rspack/index.ts +91 -0
- data/package/rules/{babel.js → babel.ts} +2 -2
- data/package/rules/{coffee.js → coffee.ts} +1 -1
- data/package/rules/css.ts +3 -0
- data/package/rules/{erb.js → erb.ts} +1 -1
- data/package/rules/esbuild.ts +10 -0
- data/package/rules/file.ts +41 -0
- data/package/rules/{jscommon.js → jscommon.ts} +5 -4
- data/package/rules/{less.js → less.ts} +4 -4
- data/package/rules/raw.ts +28 -0
- data/package/rules/rspack.ts +174 -0
- data/package/rules/sass.ts +21 -0
- data/package/rules/{stylus.js → stylus.ts} +4 -8
- data/package/rules/swc.ts +10 -0
- data/package/rules/{index.js → webpack.ts} +1 -2
- data/package/swc/index.ts +54 -0
- data/package/types/README.md +90 -0
- data/package/types/index.ts +69 -0
- data/package/types.ts +105 -0
- data/package/utils/bundlerUtils.ts +232 -0
- data/package/utils/configPath.ts +6 -0
- data/package/utils/debug.ts +45 -0
- data/package/utils/defaultConfigPath.ts +7 -0
- data/package/utils/ensureManifestExists.ts +17 -0
- data/package/utils/errorCodes.ts +249 -0
- data/package/utils/errorHelpers.ts +152 -0
- data/package/utils/getStyleRule.ts +75 -0
- data/package/utils/helpers.ts +99 -0
- data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
- data/package/utils/pathValidation.ts +207 -0
- data/package/utils/requireOrError.ts +24 -0
- data/package/utils/snakeToCamelCase.ts +5 -0
- data/package/utils/typeGuards.ts +388 -0
- data/package/utils/validateDependencies.ts +61 -0
- data/package/webpack-types.d.ts +33 -0
- data/package/webpackDevServerConfig.ts +130 -0
- data/package.json +157 -18
- data/scripts/remove-use-strict.js +44 -0
- data/scripts/type-check-no-emit.js +27 -0
- data/shakapacker.gemspec +4 -2
- data/sig/shakapacker/commands.rbs +35 -0
- data/sig/shakapacker/compiler.rbs +65 -0
- data/sig/shakapacker/compiler_strategy.rbs +41 -0
- data/sig/shakapacker/configuration.rbs +140 -0
- data/sig/shakapacker/dev_server.rbs +56 -0
- data/sig/shakapacker/env.rbs +25 -0
- data/sig/shakapacker/helper.rbs +98 -0
- data/sig/shakapacker/instance.rbs +46 -0
- data/sig/shakapacker/manifest.rbs +69 -0
- data/sig/shakapacker/version.rbs +4 -0
- data/sig/shakapacker.rbs +66 -0
- data/test/configExporter/buildValidator.test.js +1295 -0
- data/test/configExporter/configFile.test.js +393 -0
- data/test/configExporter/integration.test.js +262 -0
- data/test/helpers.js +1 -1
- data/test/package/bundlerUtils.rspack.test.js +145 -0
- data/test/package/bundlerUtils.test.js +97 -0
- data/test/package/config.test.js +14 -0
- data/test/package/configExporter/cli.test.js +440 -0
- data/test/package/configExporter/types.test.js +163 -0
- data/test/package/configExporter.test.js +491 -0
- data/test/package/env.test.js +42 -7
- data/test/package/environments/base.test.js +14 -4
- data/test/package/helpers.test.js +2 -2
- data/test/package/plugins/envFiltering.test.js +453 -0
- data/test/package/plugins/webpackSubresourceIntegrity.test.js +89 -0
- data/test/package/rspack/index.test.js +293 -0
- data/test/package/rspack/optimization.test.js +86 -0
- data/test/package/rspack/plugins.test.js +185 -0
- data/test/package/rspack/rules.test.js +229 -0
- data/test/package/rules/babel.test.js +65 -38
- data/test/package/rules/esbuild.test.js +13 -4
- data/test/package/rules/file.test.js +7 -1
- data/test/package/rules/raw.test.js +40 -7
- data/test/package/rules/sass-version-parsing.test.js +71 -0
- data/test/package/rules/sass.test.js +11 -6
- data/test/package/rules/sass1.test.js +8 -5
- data/test/package/rules/sass16.test.js +24 -0
- data/test/package/rules/swc.test.js +50 -39
- data/test/package/rules/webpack.test.js +35 -0
- data/test/package/staging.test.js +4 -3
- data/test/package/transpiler-defaults.test.js +169 -0
- data/test/package/utils/ensureManifestExists.test.js +51 -0
- data/test/package/yamlSerializer.test.js +204 -0
- data/test/peer-dependencies.sh +85 -0
- data/test/resolver.js +34 -3
- data/test/scripts/remove-use-strict.test.js +125 -0
- data/test/typescript/build.test.js +118 -0
- data/test/typescript/environments.test.js +107 -0
- data/test/typescript/pathValidation.test.js +186 -0
- data/test/typescript/requireOrError.test.js +49 -0
- data/test/typescript/securityValidation.test.js +182 -0
- data/tools/README.md +134 -0
- data/tools/css-modules-v9-codemod.js +179 -0
- data/tsconfig.eslint.json +9 -0
- data/tsconfig.json +38 -0
- data/yarn.lock +3202 -1097
- metadata +212 -44
- data/.eslintignore +0 -4
- data/.eslintrc.js +0 -36
- data/Gemfile.lock +0 -251
- data/package/babel/preset.js +0 -48
- data/package/config.js +0 -56
- data/package/dev_server.js +0 -23
- data/package/env.js +0 -48
- data/package/environments/base.js +0 -171
- data/package/environments/development.js +0 -13
- data/package/environments/production.js +0 -88
- data/package/environments/test.js +0 -3
- data/package/esbuild/index.js +0 -40
- data/package/index.js +0 -40
- data/package/rules/css.js +0 -3
- data/package/rules/esbuild.js +0 -10
- data/package/rules/file.js +0 -29
- data/package/rules/raw.js +0 -5
- data/package/rules/sass.js +0 -18
- data/package/rules/swc.js +0 -10
- data/package/swc/index.js +0 -50
- data/package/utils/configPath.js +0 -4
- data/package/utils/defaultConfigPath.js +0 -2
- data/package/utils/getStyleRule.js +0 -40
- data/package/utils/helpers.js +0 -62
- data/package/utils/snakeToCamelCase.js +0 -5
- data/package/webpackDevServerConfig.js +0 -71
- data/test/package/rules/index.test.js +0 -16
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module Shakapacker
|
|
4
|
+
class BuildConfigLoader
|
|
5
|
+
attr_reader :config_file_path
|
|
6
|
+
|
|
7
|
+
def initialize(config_file_path = nil)
|
|
8
|
+
@config_file_path = config_file_path || File.join(Dir.pwd, "config", "shakapacker-builds.yml")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def exists?
|
|
12
|
+
File.exist?(@config_file_path)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def load_build(build_name)
|
|
16
|
+
unless exists?
|
|
17
|
+
raise ArgumentError, "Config file not found: #{@config_file_path}\n" \
|
|
18
|
+
"Run 'bin/shakapacker --init' to generate a sample config file."
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
config = load_config
|
|
22
|
+
fetch_build_or_raise(config, build_name)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def resolve_build_config(build_name, default_bundler: "webpack")
|
|
26
|
+
config = load_config
|
|
27
|
+
build = fetch_build_or_raise(config, build_name)
|
|
28
|
+
|
|
29
|
+
# Resolve bundler with precedence: build.bundler > config.default_bundler > default_bundler
|
|
30
|
+
bundler = build["bundler"] || config["default_bundler"] || default_bundler
|
|
31
|
+
|
|
32
|
+
# Get environment variables
|
|
33
|
+
environment = build["environment"] || {}
|
|
34
|
+
|
|
35
|
+
# Get config file path if specified
|
|
36
|
+
config_file = build["config"]
|
|
37
|
+
if config_file
|
|
38
|
+
# Expand ${BUNDLER} variable
|
|
39
|
+
config_file = config_file.gsub("${BUNDLER}", bundler)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Get bundler_env for --env flags
|
|
43
|
+
bundler_env = build["bundler_env"] || {}
|
|
44
|
+
|
|
45
|
+
# Get outputs
|
|
46
|
+
outputs = build["outputs"] || []
|
|
47
|
+
|
|
48
|
+
# Validate outputs
|
|
49
|
+
if outputs.empty?
|
|
50
|
+
raise ArgumentError, "Build '#{build_name}' has empty outputs array. " \
|
|
51
|
+
"Please specify at least one output type (client, server, or all)."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
name: build_name,
|
|
56
|
+
description: build["description"],
|
|
57
|
+
bundler: bundler,
|
|
58
|
+
dev_server: build["dev_server"],
|
|
59
|
+
environment: environment,
|
|
60
|
+
bundler_env: bundler_env,
|
|
61
|
+
outputs: outputs,
|
|
62
|
+
config_file: config_file
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def uses_dev_server?(build_config)
|
|
67
|
+
# Check explicit dev_server flag first (preferred)
|
|
68
|
+
# Only return early if the value is explicitly set (not nil)
|
|
69
|
+
return build_config[:dev_server] unless build_config[:dev_server].nil?
|
|
70
|
+
|
|
71
|
+
# Fallback: check environment variables for backward compatibility
|
|
72
|
+
env = build_config[:environment]
|
|
73
|
+
return false unless env
|
|
74
|
+
|
|
75
|
+
# Handle both string "true" and boolean true from YAML
|
|
76
|
+
%w[WEBPACK_SERVE HMR].any? do |key|
|
|
77
|
+
value = env[key]
|
|
78
|
+
value.to_s.strip.casecmp("true").zero?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def list_builds
|
|
83
|
+
config = load_config
|
|
84
|
+
builds = config["builds"]
|
|
85
|
+
|
|
86
|
+
puts "\nAvailable builds in #{@config_file_path}:\n\n"
|
|
87
|
+
|
|
88
|
+
builds.each do |name, build|
|
|
89
|
+
bundler = build["bundler"] || config["default_bundler"] || "webpack (default)"
|
|
90
|
+
outputs = build["outputs"] ? build["outputs"].join(", ") : "missing (invalid)"
|
|
91
|
+
|
|
92
|
+
puts " #{name}"
|
|
93
|
+
puts " Description: #{build["description"]}" if build["description"]
|
|
94
|
+
puts " Bundler: #{bundler}"
|
|
95
|
+
puts " Outputs: #{outputs}"
|
|
96
|
+
puts ""
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def fetch_build_or_raise(config, build_name)
|
|
103
|
+
build = config["builds"][build_name]
|
|
104
|
+
unless build
|
|
105
|
+
available = config["builds"].keys.join(", ")
|
|
106
|
+
raise ArgumentError, "Build '#{build_name}' not found in config file.\n" \
|
|
107
|
+
"Available builds: #{available}\n" \
|
|
108
|
+
"Use 'bin/shakapacker --list-builds' to see all available builds."
|
|
109
|
+
end
|
|
110
|
+
build
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Load YAML config file safely with Ruby version compatibility
|
|
114
|
+
# Ruby 3.1+ supports safe_load_file with aliases, older versions need safe_load
|
|
115
|
+
def load_config
|
|
116
|
+
begin
|
|
117
|
+
config = if YAML.respond_to?(:safe_load_file)
|
|
118
|
+
# Ruby 3.1+: Use safe_load_file with aliases enabled
|
|
119
|
+
YAML.safe_load_file(@config_file_path, aliases: true)
|
|
120
|
+
else
|
|
121
|
+
# Ruby 2.7-3.0: Use safe_load with aliases enabled
|
|
122
|
+
YAML.safe_load(
|
|
123
|
+
File.read(@config_file_path),
|
|
124
|
+
permitted_classes: [],
|
|
125
|
+
permitted_symbols: [],
|
|
126
|
+
aliases: true
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
rescue ArgumentError
|
|
130
|
+
# Fallback for older Psych versions without aliases support
|
|
131
|
+
config = YAML.safe_load(
|
|
132
|
+
File.read(@config_file_path),
|
|
133
|
+
permitted_classes: [],
|
|
134
|
+
permitted_symbols: []
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
unless config["builds"]&.is_a?(Hash)
|
|
139
|
+
raise ArgumentError, "Config file must contain a 'builds' object"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
config
|
|
143
|
+
rescue Psych::SyntaxError => e
|
|
144
|
+
raise ArgumentError, "Invalid YAML in config file: #{e.message}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Shakapacker
|
|
7
|
+
# Provides functionality to switch between webpack and rspack bundlers
|
|
8
|
+
class BundlerSwitcher
|
|
9
|
+
SHAKAPACKER_CONFIG = "config/shakapacker.yml"
|
|
10
|
+
CUSTOM_DEPS_CONFIG = ".shakapacker-switch-bundler-dependencies.yml"
|
|
11
|
+
|
|
12
|
+
# Regex pattern to detect assets_bundler key in config (only matches uncommented lines)
|
|
13
|
+
ASSETS_BUNDLER_PATTERN = /^[ \t]*assets_bundler:/
|
|
14
|
+
|
|
15
|
+
# Shared dependencies used by both webpack and rspack
|
|
16
|
+
# These should not be removed when switching bundlers
|
|
17
|
+
SHARED_DEPS = {
|
|
18
|
+
dev: %w[],
|
|
19
|
+
prod: %w[webpack-merge]
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Default dependencies for each bundler (package names only, no versions)
|
|
23
|
+
# Note: Excludes independent/optional dependencies like @swc/core, swc-loader (user-configured
|
|
24
|
+
# transpilers)
|
|
25
|
+
DEFAULT_RSPACK_DEPS = {
|
|
26
|
+
dev: %w[@rspack/cli @rspack/plugin-react-refresh],
|
|
27
|
+
prod: %w[@rspack/core rspack-manifest-plugin]
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
DEFAULT_WEBPACK_DEPS = {
|
|
31
|
+
dev: %w[webpack webpack-cli webpack-dev-server @pmmmwh/react-refresh-webpack-plugin],
|
|
32
|
+
prod: %w[webpack-assets-manifest]
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
attr_reader :root_path
|
|
36
|
+
|
|
37
|
+
def initialize(root_path = nil)
|
|
38
|
+
@root_path = root_path || (defined?(Rails) ? Rails.root : Pathname.new(Dir.pwd))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def current_bundler
|
|
42
|
+
config = load_yaml_config(config_path)
|
|
43
|
+
config.dig("default", "assets_bundler") || "webpack"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def switch_to(bundler, install_deps: false, no_uninstall: false)
|
|
47
|
+
unless %w[webpack rspack].include?(bundler)
|
|
48
|
+
raise ArgumentError, "Invalid bundler: #{bundler}. Must be 'webpack' or 'rspack'"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
current = current_bundler
|
|
52
|
+
config_content = File.read(config_path)
|
|
53
|
+
has_assets_bundler = config_content =~ ASSETS_BUNDLER_PATTERN
|
|
54
|
+
|
|
55
|
+
# Early exit if already using the target bundler
|
|
56
|
+
# For webpack: if current is webpack, we're done (key optional due to default)
|
|
57
|
+
# For rspack: requires explicit key to be present
|
|
58
|
+
already_configured = if bundler == "webpack"
|
|
59
|
+
current == bundler
|
|
60
|
+
else
|
|
61
|
+
current == bundler && has_assets_bundler
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if already_configured && !install_deps
|
|
65
|
+
puts "✅ Already using #{bundler}"
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
if already_configured && install_deps
|
|
70
|
+
puts "✅ Already using #{bundler} - reinstalling dependencies as requested"
|
|
71
|
+
manage_dependencies(bundler, install_deps, switching: false, no_uninstall: no_uninstall)
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
successfully_updated = update_config(bundler, config_content, has_assets_bundler)
|
|
76
|
+
|
|
77
|
+
# Verify the update was successful (only if update reported success)
|
|
78
|
+
verify_config_update(bundler) if successfully_updated
|
|
79
|
+
|
|
80
|
+
puts "✅ Switched from #{current} to #{bundler}"
|
|
81
|
+
puts ""
|
|
82
|
+
puts "📝 Configuration updated in #{SHAKAPACKER_CONFIG}"
|
|
83
|
+
|
|
84
|
+
manage_dependencies(bundler, install_deps, no_uninstall: no_uninstall)
|
|
85
|
+
|
|
86
|
+
puts ""
|
|
87
|
+
puts "🎯 Next steps:"
|
|
88
|
+
puts " 1. Restart your dev server: bin/dev"
|
|
89
|
+
puts " 2. Verify build works: bin/shakapacker"
|
|
90
|
+
puts ""
|
|
91
|
+
puts "💡 Tip: Both webpack and rspack can coexist in package.json during migration"
|
|
92
|
+
puts " Use --install-deps to automatically manage dependencies, or manage manually"
|
|
93
|
+
puts " Use --no-uninstall to skip removing old bundler packages (faster switching)"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def init_config
|
|
97
|
+
if File.exist?(custom_config_path)
|
|
98
|
+
puts "⚠️ #{CUSTOM_DEPS_CONFIG} already exists"
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
config = {
|
|
103
|
+
"rspack" => {
|
|
104
|
+
"devDependencies" => DEFAULT_RSPACK_DEPS[:dev],
|
|
105
|
+
"dependencies" => DEFAULT_RSPACK_DEPS[:prod]
|
|
106
|
+
},
|
|
107
|
+
"webpack" => {
|
|
108
|
+
"devDependencies" => DEFAULT_WEBPACK_DEPS[:dev],
|
|
109
|
+
"dependencies" => DEFAULT_WEBPACK_DEPS[:prod]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
File.write(custom_config_path, YAML.dump(config))
|
|
114
|
+
puts "✅ Created #{CUSTOM_DEPS_CONFIG}"
|
|
115
|
+
puts ""
|
|
116
|
+
puts "You can now customize the dependencies for each bundler in this file."
|
|
117
|
+
puts "The script will automatically use these custom dependencies when switching bundlers."
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def show_usage
|
|
121
|
+
current = current_bundler
|
|
122
|
+
puts "Current bundler: #{current}"
|
|
123
|
+
puts ""
|
|
124
|
+
puts "Usage:"
|
|
125
|
+
puts " rake shakapacker:switch_bundler [webpack|rspack] -- [OPTIONS]"
|
|
126
|
+
puts ""
|
|
127
|
+
puts "Options:"
|
|
128
|
+
puts " --install-deps Automatically install/uninstall dependencies"
|
|
129
|
+
puts " --no-uninstall Skip uninstalling old bundler packages"
|
|
130
|
+
puts " --init-config Create #{CUSTOM_DEPS_CONFIG} with default dependencies"
|
|
131
|
+
puts " --help, -h Show this help message"
|
|
132
|
+
puts ""
|
|
133
|
+
puts "Examples:"
|
|
134
|
+
puts " rake shakapacker:switch_bundler rspack -- --install-deps"
|
|
135
|
+
puts " rake shakapacker:switch_bundler webpack -- --install-deps --no-uninstall"
|
|
136
|
+
puts " rake shakapacker:switch_bundler -- --init-config"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def config_path
|
|
142
|
+
root_path.join(SHAKAPACKER_CONFIG)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def custom_config_path
|
|
146
|
+
root_path.join(CUSTOM_DEPS_CONFIG)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def load_dependencies
|
|
150
|
+
if File.exist?(custom_config_path)
|
|
151
|
+
puts "📝 Using custom dependencies from #{CUSTOM_DEPS_CONFIG}"
|
|
152
|
+
begin
|
|
153
|
+
custom = load_yaml_config(custom_config_path)
|
|
154
|
+
rescue Psych::SyntaxError => e
|
|
155
|
+
puts "❌ Error parsing #{CUSTOM_DEPS_CONFIG}: #{e.message}"
|
|
156
|
+
puts " Please fix the YAML syntax or delete the file to use defaults"
|
|
157
|
+
raise
|
|
158
|
+
end
|
|
159
|
+
rspack_deps = {
|
|
160
|
+
dev: (custom.dig("rspack", "devDependencies") || DEFAULT_RSPACK_DEPS[:dev]) + SHARED_DEPS[:dev],
|
|
161
|
+
prod: (custom.dig("rspack", "dependencies") || DEFAULT_RSPACK_DEPS[:prod]) + SHARED_DEPS[:prod]
|
|
162
|
+
}
|
|
163
|
+
webpack_deps = {
|
|
164
|
+
dev: (custom.dig("webpack", "devDependencies") || DEFAULT_WEBPACK_DEPS[:dev]) + SHARED_DEPS[:dev],
|
|
165
|
+
prod: (custom.dig("webpack", "dependencies") || DEFAULT_WEBPACK_DEPS[:prod]) + SHARED_DEPS[:prod]
|
|
166
|
+
}
|
|
167
|
+
[rspack_deps, webpack_deps]
|
|
168
|
+
else
|
|
169
|
+
rspack_with_shared = {
|
|
170
|
+
dev: DEFAULT_RSPACK_DEPS[:dev] + SHARED_DEPS[:dev],
|
|
171
|
+
prod: DEFAULT_RSPACK_DEPS[:prod] + SHARED_DEPS[:prod]
|
|
172
|
+
}
|
|
173
|
+
webpack_with_shared = {
|
|
174
|
+
dev: DEFAULT_WEBPACK_DEPS[:dev] + SHARED_DEPS[:dev],
|
|
175
|
+
prod: DEFAULT_WEBPACK_DEPS[:prod] + SHARED_DEPS[:prod]
|
|
176
|
+
}
|
|
177
|
+
[rspack_with_shared, webpack_with_shared]
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def update_config(bundler, content, has_assets_bundler)
|
|
182
|
+
# Check if assets_bundler key exists (only uncommented lines)
|
|
183
|
+
unless has_assets_bundler
|
|
184
|
+
# Track whether we successfully added the key
|
|
185
|
+
added = false
|
|
186
|
+
|
|
187
|
+
# Add assets_bundler after javascript_transpiler if it exists (excluding commented lines)
|
|
188
|
+
if (match = content.match(/^[ \t]*(?![ \t]*#)javascript_transpiler:.*$/))
|
|
189
|
+
indent = match[0][/^[ \t]*/]
|
|
190
|
+
content.sub!(/^([ \t]*(?![ \t]*#)javascript_transpiler:.*$)/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
|
|
191
|
+
added = true
|
|
192
|
+
# Otherwise, add it after source_path if it exists (excluding commented lines)
|
|
193
|
+
elsif (match = content.match(/^[ \t]*(?![ \t]*#)source_path:.*$/))
|
|
194
|
+
indent = match[0][/^[ \t]*/]
|
|
195
|
+
content.sub!(/^([ \t]*(?![ \t]*#)source_path:.*$)/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
|
|
196
|
+
added = true
|
|
197
|
+
# Add it after default: &default if it exists
|
|
198
|
+
elsif content.match?(/^default:[ \t]*&default[ \t]*$/)
|
|
199
|
+
# Use default 2-space indentation for this case
|
|
200
|
+
content.sub!(/^(default:[ \t]*&default[ \t]*)$/, "\\1\n#{assets_bundler_entry(bundler, ' ')}")
|
|
201
|
+
added = true
|
|
202
|
+
# Fallback: add after "default:" with proper indentation detection (handles blank lines)
|
|
203
|
+
elsif (match = content.match(/^default:\s*\n\s*([ \t]+)/m))
|
|
204
|
+
# Extract indentation from first indented line after "default:"
|
|
205
|
+
indent = match[1]
|
|
206
|
+
content.sub!(/^(default:\s*)$/, "\\1\n#{assets_bundler_entry(bundler, indent)}")
|
|
207
|
+
added = true
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
unless added
|
|
211
|
+
puts "⚠️ Warning: Could not find appropriate location for assets_bundler in config"
|
|
212
|
+
puts " Please add 'assets_bundler: #{bundler}' to the default section manually"
|
|
213
|
+
end
|
|
214
|
+
else
|
|
215
|
+
# Replace existing assets_bundler value (handles spaces, tabs, and various quote styles)
|
|
216
|
+
# Only matches uncommented lines
|
|
217
|
+
content.gsub!(/^([ \t]*)(?![ \t]*#)(assets_bundler:[ \t]*['"]?)(webpack|rspack)(['"]?)/, "\\1\\2#{bundler}\\4")
|
|
218
|
+
added = true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Update javascript_transpiler recommendation for rspack
|
|
222
|
+
# Only update if not already set to swc and only on uncommented lines
|
|
223
|
+
if bundler == "rspack" && content !~ /^[ \t]*(?![ \t]*#)javascript_transpiler:[ \t]*['"]?swc['"]?/
|
|
224
|
+
content.gsub!(/^([ \t]*(?![ \t]*#)javascript_transpiler:[ \t]*['"]?)(\w+)(['"]?)/, '\1swc\3')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
File.write(config_path, content)
|
|
228
|
+
added
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Verify that the config was updated successfully
|
|
232
|
+
def verify_config_update(bundler)
|
|
233
|
+
config = load_yaml_config(config_path)
|
|
234
|
+
actual_bundler = config.dig("default", "assets_bundler")
|
|
235
|
+
|
|
236
|
+
if actual_bundler != bundler
|
|
237
|
+
raise "Config update verification failed: expected assets_bundler to be '#{bundler}', but got '#{actual_bundler}'"
|
|
238
|
+
end
|
|
239
|
+
rescue Psych::SyntaxError => e
|
|
240
|
+
raise "Config update generated invalid YAML: #{e.message}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Generate the assets_bundler YAML entry with proper indentation
|
|
244
|
+
# @param bundler [String] The bundler name ('webpack' or 'rspack')
|
|
245
|
+
# @param indent [String] The indentation string to use (e.g., ' ' or '\t')
|
|
246
|
+
# @return [String] The formatted YAML entry
|
|
247
|
+
def assets_bundler_entry(bundler, indent)
|
|
248
|
+
"\n#{indent}# Select assets bundler to use\n#{indent}# Available options: 'webpack' (default) or 'rspack'\n#{indent}assets_bundler: \"#{bundler}\""
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def manage_dependencies(bundler, install_deps, switching: true, no_uninstall: false)
|
|
252
|
+
rspack_deps, webpack_deps = load_dependencies
|
|
253
|
+
deps_to_install = bundler == "rspack" ? rspack_deps : webpack_deps
|
|
254
|
+
old_bundler_deps = bundler == "rspack" ? webpack_deps : rspack_deps
|
|
255
|
+
|
|
256
|
+
# Remove shared dependencies from removal list
|
|
257
|
+
deps_to_remove = {
|
|
258
|
+
dev: old_bundler_deps[:dev] - SHARED_DEPS[:dev],
|
|
259
|
+
prod: old_bundler_deps[:prod] - SHARED_DEPS[:prod]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if install_deps
|
|
263
|
+
puts ""
|
|
264
|
+
puts "📦 Managing dependencies..."
|
|
265
|
+
puts ""
|
|
266
|
+
|
|
267
|
+
# Show what will be removed (only when switching and not no_uninstall)
|
|
268
|
+
if switching && !no_uninstall && (!deps_to_remove[:dev].empty? || !deps_to_remove[:prod].empty?)
|
|
269
|
+
puts " 🗑️ Removing:"
|
|
270
|
+
deps_to_remove[:dev].each { |dep| puts " - #{dep} (dev)" }
|
|
271
|
+
deps_to_remove[:prod].each { |dep| puts " - #{dep} (prod)" }
|
|
272
|
+
puts ""
|
|
273
|
+
elsif switching && no_uninstall
|
|
274
|
+
puts " ⏭️ Skipping uninstall (--no-uninstall)"
|
|
275
|
+
puts ""
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Show what will be installed
|
|
279
|
+
if !deps_to_install[:dev].empty? || !deps_to_install[:prod].empty?
|
|
280
|
+
puts " 📦 Installing:"
|
|
281
|
+
deps_to_install[:dev].each { |dep| puts " - #{dep} (dev)" }
|
|
282
|
+
deps_to_install[:prod].each { |dep| puts " - #{dep} (prod)" }
|
|
283
|
+
puts ""
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Remove old bundler dependencies (only when switching and not no_uninstall)
|
|
287
|
+
if switching && !no_uninstall
|
|
288
|
+
remove_dependencies(deps_to_remove)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Install new bundler dependencies
|
|
292
|
+
install_dependencies(deps_to_install)
|
|
293
|
+
|
|
294
|
+
puts " ✅ Dependencies updated"
|
|
295
|
+
else
|
|
296
|
+
print_manual_dependency_instructions(bundler, deps_to_install, deps_to_remove)
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def remove_dependencies(deps)
|
|
301
|
+
package_json = get_package_json
|
|
302
|
+
|
|
303
|
+
# Combine dev and prod dependencies into a single list for removal
|
|
304
|
+
# Package managers remove packages from both dependencies and devDependencies sections if present
|
|
305
|
+
all_deps = deps[:dev] + deps[:prod]
|
|
306
|
+
|
|
307
|
+
unless all_deps.empty?
|
|
308
|
+
unless package_json.manager.remove(all_deps)
|
|
309
|
+
puts " ⚠️ Warning: Failed to uninstall some dependencies"
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def install_dependencies(deps)
|
|
315
|
+
package_json = get_package_json
|
|
316
|
+
|
|
317
|
+
unless deps[:dev].empty?
|
|
318
|
+
unless package_json.manager.add(deps[:dev], type: :dev)
|
|
319
|
+
puts "❌ Failed to install dev dependencies"
|
|
320
|
+
raise "Failed to install dev dependencies"
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
unless deps[:prod].empty?
|
|
325
|
+
unless package_json.manager.add(deps[:prod], type: :production)
|
|
326
|
+
puts "❌ Failed to install prod dependencies"
|
|
327
|
+
raise "Failed to install prod dependencies"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Run a full install to ensure optional dependencies (like native bindings) are properly resolved
|
|
332
|
+
# This is especially important for packages like @rspack/core that use platform-specific native modules
|
|
333
|
+
unless package_json.manager.install
|
|
334
|
+
puts "❌ Failed to run full install to resolve optional dependencies"
|
|
335
|
+
raise "Failed to run full install"
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def get_package_json
|
|
340
|
+
require "package_json"
|
|
341
|
+
PackageJson.read(root_path)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def print_manual_dependency_instructions(bundler, deps_to_install, deps_to_remove)
|
|
345
|
+
puts ""
|
|
346
|
+
puts "⚠️ Dependencies not automatically installed (use --install-deps to auto-install)"
|
|
347
|
+
puts ""
|
|
348
|
+
|
|
349
|
+
package_manager = detect_package_manager
|
|
350
|
+
target_name = bundler == "rspack" ? "rspack" : "webpack"
|
|
351
|
+
old_name = bundler == "rspack" ? "webpack" : "rspack"
|
|
352
|
+
|
|
353
|
+
puts "📦 To install #{target_name} dependencies, run:"
|
|
354
|
+
print_install_commands(package_manager, deps_to_install)
|
|
355
|
+
puts ""
|
|
356
|
+
puts "🗑️ To remove #{old_name} dependencies, run:"
|
|
357
|
+
print_uninstall_commands(package_manager, deps_to_remove)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def detect_package_manager
|
|
361
|
+
get_package_json.manager.binary
|
|
362
|
+
rescue StandardError
|
|
363
|
+
"npm" # Fallback to npm if detection fails
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def print_install_commands(package_manager, deps)
|
|
367
|
+
case package_manager
|
|
368
|
+
when "yarn"
|
|
369
|
+
puts " yarn add --dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
370
|
+
puts " yarn add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
371
|
+
when "pnpm"
|
|
372
|
+
puts " pnpm add -D #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
373
|
+
puts " pnpm add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
374
|
+
when "bun"
|
|
375
|
+
puts " bun add --dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
376
|
+
puts " bun add #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
377
|
+
else # npm
|
|
378
|
+
puts " npm install --save-dev #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
379
|
+
puts " npm install --save #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def print_uninstall_commands(package_manager, deps)
|
|
384
|
+
case package_manager
|
|
385
|
+
when "yarn"
|
|
386
|
+
puts " yarn remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
387
|
+
puts " yarn remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
388
|
+
when "pnpm"
|
|
389
|
+
puts " pnpm remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
390
|
+
puts " pnpm remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
391
|
+
when "bun"
|
|
392
|
+
puts " bun remove #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
393
|
+
puts " bun remove #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
394
|
+
else # npm
|
|
395
|
+
puts " npm uninstall #{deps[:dev].join(' ')}" unless deps[:dev].empty?
|
|
396
|
+
puts " npm uninstall #{deps[:prod].join(' ')}" unless deps[:prod].empty?
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Load YAML config file with Ruby version compatibility
|
|
401
|
+
# Ruby 3.1+ supports aliases: keyword, older versions need YAML.safe_load
|
|
402
|
+
def load_yaml_config(path)
|
|
403
|
+
if YAML.respond_to?(:unsafe_load_file)
|
|
404
|
+
# Ruby 3.1+: Use unsafe_load_file to support aliases/anchors
|
|
405
|
+
YAML.unsafe_load_file(path)
|
|
406
|
+
else
|
|
407
|
+
# Ruby 2.7-3.0: Use safe_load with aliases enabled
|
|
408
|
+
YAML.safe_load(File.read(path), permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
409
|
+
end
|
|
410
|
+
rescue ArgumentError
|
|
411
|
+
# Ruby 2.7 doesn't support aliases keyword - fall back to YAML.load
|
|
412
|
+
YAML.load(File.read(path)) # rubocop:disable Security/YAMLLoad
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
data/lib/shakapacker/compiler.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "open3"
|
|
2
2
|
require "fileutils"
|
|
3
|
+
require "shellwords"
|
|
3
4
|
|
|
4
5
|
require_relative "compiler_strategy"
|
|
5
6
|
|
|
@@ -26,6 +27,7 @@ class Shakapacker::Compiler
|
|
|
26
27
|
true
|
|
27
28
|
else
|
|
28
29
|
acquire_ipc_lock do
|
|
30
|
+
run_precompile_hook if should_run_precompile_hook?
|
|
29
31
|
run_webpack.tap do |success|
|
|
30
32
|
after_compile_hook
|
|
31
33
|
end
|
|
@@ -78,6 +80,91 @@ class Shakapacker::Compiler
|
|
|
78
80
|
/ruby/.match?(first_line) ? RbConfig.ruby : ""
|
|
79
81
|
end
|
|
80
82
|
|
|
83
|
+
def should_run_precompile_hook?
|
|
84
|
+
return false unless config.precompile_hook
|
|
85
|
+
return false if ENV["SHAKAPACKER_SKIP_PRECOMPILE_HOOK"] == "true"
|
|
86
|
+
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def run_precompile_hook
|
|
91
|
+
hook_command = config.precompile_hook
|
|
92
|
+
hook_spec = validate_precompile_hook(hook_command)
|
|
93
|
+
|
|
94
|
+
logger.info "Running precompile hook: #{hook_command}"
|
|
95
|
+
|
|
96
|
+
runtime_env = webpack_env.merge(hook_spec[:env])
|
|
97
|
+
stdout, stderr, status = Open3.capture3(
|
|
98
|
+
runtime_env,
|
|
99
|
+
hook_spec[:executable],
|
|
100
|
+
*hook_spec[:args],
|
|
101
|
+
chdir: File.expand_path(config.root_path)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if status.success?
|
|
105
|
+
logger.info "Precompile hook completed successfully"
|
|
106
|
+
logger.info stdout unless stdout.empty?
|
|
107
|
+
logger.warn stderr unless stderr.empty?
|
|
108
|
+
else
|
|
109
|
+
non_empty_streams = [stdout, stderr].delete_if(&:empty?)
|
|
110
|
+
logger.error "\nPRECOMPILE HOOK FAILED:\nEXIT STATUS: #{status.exitstatus}\nCOMMAND: #{hook_command}\nOUTPUTS:\n#{non_empty_streams.join("\n\n")}"
|
|
111
|
+
logger.error "\nTo fix this:"
|
|
112
|
+
logger.error " 1. Check that the hook script exists and is executable"
|
|
113
|
+
logger.error " 2. Test the hook command manually: #{hook_command}"
|
|
114
|
+
logger.error " 3. Review the error output above for details"
|
|
115
|
+
logger.error " 4. You can disable the hook temporarily by commenting out 'precompile_hook' in shakapacker.yml"
|
|
116
|
+
raise "Precompile hook '#{hook_command}' failed with exit status #{status.exitstatus}"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def validate_precompile_hook(hook_command)
|
|
121
|
+
hook_tokens = begin
|
|
122
|
+
Shellwords.shellsplit(hook_command)
|
|
123
|
+
rescue ArgumentError => e
|
|
124
|
+
raise "Shakapacker configuration error: Invalid precompile_hook command syntax: #{e.message}. Check for unmatched quotes in: #{hook_command}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
env_assignments = {}
|
|
128
|
+
while hook_tokens.first&.match?(/\A[A-Za-z_][A-Za-z0-9_]*=/)
|
|
129
|
+
key, value = hook_tokens.shift.split("=", 2)
|
|
130
|
+
env_assignments[key] = value
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
executable = hook_tokens.shift
|
|
134
|
+
if executable.nil? || executable.empty?
|
|
135
|
+
raise "Shakapacker configuration error: precompile_hook must include an executable command. Got: #{hook_command}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
executable_path = config.root_path.join(executable)
|
|
139
|
+
|
|
140
|
+
# Security: Resolve symlinks and verify the hook points to a file within the project
|
|
141
|
+
# This prevents symlink bypass attacks and path traversal attacks
|
|
142
|
+
begin
|
|
143
|
+
resolved_path = executable_path.realpath
|
|
144
|
+
resolved_root = config.root_path.realpath
|
|
145
|
+
rescue Errno::ENOENT
|
|
146
|
+
# If file doesn't exist, use cleanpath for basic validation
|
|
147
|
+
resolved_path = executable_path.cleanpath
|
|
148
|
+
resolved_root = config.root_path.cleanpath
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Verify path is within project root with proper separator check
|
|
152
|
+
# Using File::SEPARATOR prevents partial path matches (e.g., /project vs /project-evil)
|
|
153
|
+
unless resolved_path.to_s.start_with?(resolved_root.to_s + File::SEPARATOR)
|
|
154
|
+
raise "Security Error: precompile_hook must reference a script within the project root. " \
|
|
155
|
+
"Got: #{hook_command} (resolved to: #{resolved_path})"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Warn if the executable doesn't exist within the project
|
|
159
|
+
unless File.exist?(executable_path)
|
|
160
|
+
logger.warn "⚠️ Warning: precompile_hook executable not found: #{executable_path}"
|
|
161
|
+
logger.warn " The hook command is configured but the script does not exist within the project root."
|
|
162
|
+
logger.warn " Please ensure the script exists or remove 'precompile_hook' from your shakapacker.yml configuration."
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
{ env: env_assignments, executable: executable, args: hook_tokens }
|
|
166
|
+
end
|
|
167
|
+
|
|
81
168
|
def run_webpack
|
|
82
169
|
logger.info "Compiling..."
|
|
83
170
|
|