shakapacker 8.4.0 → 9.0.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/.eslintignore +1 -0
- data/.eslintrc.fast.js +40 -0
- data/.eslintrc.js +48 -0
- data/.github/STATUS.md +1 -0
- data/.github/workflows/claude-code-review.yml +54 -0
- data/.github/workflows/claude.yml +50 -0
- data/.github/workflows/dummy.yml +8 -4
- data/.github/workflows/generator.yml +17 -14
- data/.github/workflows/node.yml +23 -1
- data/.github/workflows/ruby.yml +11 -0
- data/.github/workflows/test-bundlers.yml +170 -0
- data/.gitignore +17 -0
- data/.husky/pre-commit +2 -0
- data/.npmignore +56 -0
- data/.prettierignore +3 -0
- data/.rubocop.yml +1 -0
- data/.yalcignore +26 -0
- data/CHANGELOG.md +156 -18
- data/CLAUDE.md +29 -0
- data/CONTRIBUTING.md +138 -20
- data/Gemfile.lock +3 -3
- data/README.md +130 -5
- data/Rakefile +39 -4
- data/TODO.md +50 -0
- data/TODO_v9.md +87 -0
- data/conductor-setup.sh +70 -0
- data/conductor.json +7 -0
- data/docs/cdn_setup.md +379 -0
- data/docs/css-modules-export-mode.md +512 -0
- data/docs/deployment.md +10 -1
- data/docs/optional-peer-dependencies.md +198 -0
- data/docs/peer-dependencies.md +60 -0
- data/docs/rspack.md +190 -0
- data/docs/rspack_migration_guide.md +202 -0
- data/docs/transpiler-migration.md +188 -0
- data/docs/transpiler-performance.md +179 -0
- data/docs/troubleshooting.md +5 -0
- data/docs/typescript-migration.md +378 -0
- data/docs/typescript.md +99 -0
- data/docs/using_esbuild_loader.md +3 -3
- data/docs/using_swc_loader.md +5 -3
- data/docs/v6_upgrade.md +10 -0
- data/docs/v9_upgrade.md +413 -0
- data/lib/install/bin/shakapacker +3 -5
- 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 +12 -2
- data/lib/install/config/webpack/webpack.config.ts +7 -0
- data/lib/install/package.json +38 -0
- data/lib/install/template.rb +194 -44
- data/lib/shakapacker/configuration.rb +141 -0
- data/lib/shakapacker/dev_server_runner.rb +25 -5
- data/lib/shakapacker/doctor.rb +844 -0
- data/lib/shakapacker/manifest.rb +4 -2
- data/lib/shakapacker/rspack_runner.rb +19 -0
- data/lib/shakapacker/runner.rb +144 -4
- data/lib/shakapacker/swc_migrator.rb +376 -0
- data/lib/shakapacker/utils/manager.rb +2 -0
- 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 +2 -1
- data/lib/tasks/shakapacker/doctor.rake +8 -0
- data/lib/tasks/shakapacker/install.rake +12 -2
- data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
- data/lib/tasks/shakapacker.rake +1 -0
- data/package/.npmignore +4 -0
- data/package/babel/preset.ts +56 -0
- data/package/config.ts +175 -0
- data/package/{dev_server.js → dev_server.ts} +8 -5
- data/package/env.ts +92 -0
- data/package/environments/base.ts +138 -0
- data/package/environments/development.ts +90 -0
- data/package/environments/production.ts +80 -0
- data/package/environments/test.ts +53 -0
- data/package/environments/types.ts +90 -0
- data/package/esbuild/index.ts +42 -0
- data/package/index.d.ts +3 -97
- data/package/index.ts +52 -0
- data/package/loaders.d.ts +28 -0
- data/package/optimization/rspack.ts +36 -0
- data/package/optimization/webpack.ts +57 -0
- data/package/plugins/rspack.ts +103 -0
- data/package/plugins/webpack.ts +62 -0
- data/package/rspack/index.ts +64 -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 +40 -0
- data/package/rules/{jscommon.js → jscommon.ts} +4 -4
- data/package/rules/{less.js → less.ts} +4 -4
- data/package/rules/raw.ts +25 -0
- data/package/rules/rspack.ts +176 -0
- data/package/rules/{sass.js → sass.ts} +7 -3
- data/package/rules/{stylus.js → stylus.ts} +4 -8
- data/package/rules/swc.ts +10 -0
- data/package/rules/{index.js → webpack.ts} +1 -1
- data/package/swc/index.ts +54 -0
- data/package/types/README.md +87 -0
- data/package/types/index.ts +60 -0
- data/package/types.ts +108 -0
- data/package/utils/configPath.ts +6 -0
- data/package/utils/debug.ts +49 -0
- data/package/utils/defaultConfigPath.ts +4 -0
- data/package/utils/errorCodes.ts +219 -0
- data/package/utils/errorHelpers.ts +143 -0
- data/package/utils/getStyleRule.ts +64 -0
- data/package/utils/helpers.ts +85 -0
- data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
- data/package/utils/pathValidation.ts +139 -0
- data/package/utils/requireOrError.ts +15 -0
- data/package/utils/snakeToCamelCase.ts +5 -0
- data/package/utils/typeGuards.ts +342 -0
- data/package/utils/validateDependencies.ts +61 -0
- data/package/webpack-types.d.ts +33 -0
- data/package/webpackDevServerConfig.ts +117 -0
- data/package.json +134 -9
- data/scripts/remove-use-strict.js +45 -0
- data/scripts/type-check-no-emit.js +27 -0
- data/test/package/config.test.js +3 -0
- data/test/package/env.test.js +42 -7
- data/test/package/environments/base.test.js +5 -1
- data/test/package/rules/babel.test.js +16 -0
- data/test/package/rules/esbuild.test.js +1 -1
- data/test/package/rules/raw.test.js +40 -7
- data/test/package/rules/swc.test.js +1 -1
- data/test/package/rules/webpack.test.js +35 -0
- data/test/package/staging.test.js +4 -3
- data/test/package/transpiler-defaults.test.js +127 -0
- data/test/peer-dependencies.sh +85 -0
- 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 +142 -0
- data/test/typescript/securityValidation.test.js +182 -0
- data/tools/README.md +124 -0
- data/tools/css-modules-v9-codemod.js +179 -0
- data/tsconfig.eslint.json +16 -0
- data/tsconfig.json +38 -0
- data/yarn.lock +2704 -767
- metadata +111 -41
- data/package/babel/preset.js +0 -48
- data/package/config.js +0 -56
- 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/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
data/lib/shakapacker.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "active_support/tagged_logging"
|
|
|
6
6
|
module Shakapacker
|
|
7
7
|
extend self
|
|
8
8
|
|
|
9
|
-
DEFAULT_ENV = "
|
|
9
|
+
DEFAULT_ENV = "development".freeze
|
|
10
10
|
|
|
11
11
|
def instance=(instance)
|
|
12
12
|
@instance = instance
|
|
@@ -44,6 +44,7 @@ require_relative "shakapacker/manifest"
|
|
|
44
44
|
require_relative "shakapacker/compiler"
|
|
45
45
|
require_relative "shakapacker/commands"
|
|
46
46
|
require_relative "shakapacker/dev_server"
|
|
47
|
+
require_relative "shakapacker/doctor"
|
|
47
48
|
require_relative "shakapacker/deprecation_helper"
|
|
48
49
|
|
|
49
50
|
require_relative "shakapacker/railtie" if defined?(Rails)
|
|
@@ -2,10 +2,20 @@ install_template_path = File.expand_path("../../install/template.rb", __dir__).f
|
|
|
2
2
|
bin_path = ENV["BUNDLE_BIN"] || Rails.root.join("bin")
|
|
3
3
|
|
|
4
4
|
namespace :shakapacker do
|
|
5
|
-
desc "Install Shakapacker in this application"
|
|
6
|
-
task install: [:check_node] do |task|
|
|
5
|
+
desc "Install Shakapacker in this application (use SHAKAPACKER_ASSETS_BUNDLER=rspack for Rspack, --typescript for TypeScript)"
|
|
6
|
+
task :install, [:bundler, :typescript] => [:check_node] do |task, args|
|
|
7
7
|
Shakapacker::Configuration.installing = true
|
|
8
8
|
|
|
9
|
+
if args[:bundler] == "rspack" || ENV["SHAKAPACKER_ASSETS_BUNDLER"] == "rspack"
|
|
10
|
+
ENV["SHAKAPACKER_ASSETS_BUNDLER"] = "rspack"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Set typescript flag if passed as argument
|
|
14
|
+
# Accepts: typescript, true, or any truthy value
|
|
15
|
+
if args[:typescript] && args[:typescript] != "false"
|
|
16
|
+
ENV["SHAKAPACKER_USE_TYPESCRIPT"] = "true"
|
|
17
|
+
end
|
|
18
|
+
|
|
9
19
|
prefix = task.name.split(/#|shakapacker:install/).first
|
|
10
20
|
|
|
11
21
|
if Rails::VERSION::MAJOR >= 5
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "shakapacker/swc_migrator"
|
|
2
|
+
|
|
3
|
+
namespace :shakapacker do
|
|
4
|
+
desc "Migrate from Babel to SWC transpiler"
|
|
5
|
+
task :migrate_to_swc do
|
|
6
|
+
Shakapacker::SwcMigrator.new(Rails.root).migrate_to_swc
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc "Remove Babel packages after migrating to SWC"
|
|
10
|
+
task :clean_babel_packages do
|
|
11
|
+
Shakapacker::SwcMigrator.new(Rails.root).clean_babel_packages
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/tasks/shakapacker.rake
CHANGED
|
@@ -9,6 +9,7 @@ tasks = {
|
|
|
9
9
|
"shakapacker:check_binstubs" => "Verifies that bin/shakapacker is present",
|
|
10
10
|
"shakapacker:binstubs" => "Installs Shakapacker binstubs in this application",
|
|
11
11
|
"shakapacker:verify_install" => "Verifies if Shakapacker is installed",
|
|
12
|
+
"shakapacker:doctor" => "Checks for configuration issues and missing dependencies"
|
|
12
13
|
}.freeze
|
|
13
14
|
|
|
14
15
|
desc "Lists all available tasks in Shakapacker"
|
data/package/.npmignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { moduleExists, packageFullVersion } from "../utils/helpers"
|
|
2
|
+
import type { ConfigAPI, PluginItem } from "@babel/core"
|
|
3
|
+
|
|
4
|
+
const CORE_JS_VERSION_REGEX = /^\d+\.\d+/
|
|
5
|
+
|
|
6
|
+
const coreJsVersion = (): string => {
|
|
7
|
+
try {
|
|
8
|
+
const version = packageFullVersion("core-js").match(CORE_JS_VERSION_REGEX)
|
|
9
|
+
return version?.[0] ?? "3.8"
|
|
10
|
+
} catch (e) {
|
|
11
|
+
const error = e as NodeJS.ErrnoException
|
|
12
|
+
if (error.code !== "MODULE_NOT_FOUND") {
|
|
13
|
+
throw e
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return "3.8"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export = function config(api: ConfigAPI): { presets: PluginItem[]; plugins: PluginItem[] } {
|
|
21
|
+
const validEnv = ["development", "test", "production"]
|
|
22
|
+
const currentEnv = api.env()
|
|
23
|
+
const isDevelopmentEnv = api.env("development")
|
|
24
|
+
const isProductionEnv = api.env("production")
|
|
25
|
+
const isTestEnv = api.env("test")
|
|
26
|
+
|
|
27
|
+
if (!validEnv.includes(currentEnv)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Please specify a valid NODE_ENV or BABEL_ENV environment variable. Valid values are "development", "test", and "production". Instead, received: "${currentEnv}".`
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const presets: PluginItem[] = [
|
|
34
|
+
isTestEnv && ["@babel/preset-env", { targets: { node: "current" } }],
|
|
35
|
+
(isProductionEnv || isDevelopmentEnv) && [
|
|
36
|
+
"@babel/preset-env",
|
|
37
|
+
{
|
|
38
|
+
useBuiltIns: "entry",
|
|
39
|
+
corejs: coreJsVersion(),
|
|
40
|
+
modules: "auto",
|
|
41
|
+
bugfixes: true,
|
|
42
|
+
exclude: ["transform-typeof-symbol"]
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
moduleExists("@babel/preset-typescript") && "@babel/preset-typescript"
|
|
46
|
+
].filter(Boolean) as PluginItem[]
|
|
47
|
+
|
|
48
|
+
const plugins: PluginItem[] = [["@babel/plugin-transform-runtime", { helpers: false }]].filter(
|
|
49
|
+
Boolean
|
|
50
|
+
) as PluginItem[]
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
presets,
|
|
54
|
+
plugins
|
|
55
|
+
}
|
|
56
|
+
}
|
data/package/config.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { resolve } from "path"
|
|
2
|
+
import { load } from "js-yaml"
|
|
3
|
+
import { existsSync, readFileSync } from "fs"
|
|
4
|
+
import { merge } from "webpack-merge"
|
|
5
|
+
const { ensureTrailingSlash } = require("./utils/helpers")
|
|
6
|
+
const { railsEnv } = require("./env")
|
|
7
|
+
const configPath = require("./utils/configPath")
|
|
8
|
+
const defaultConfigPath = require("./utils/defaultConfigPath")
|
|
9
|
+
import { Config, YamlConfig, LegacyConfig } from "./types"
|
|
10
|
+
const { isValidYamlConfig, createConfigValidationError, isPartialConfig, isValidConfig } = require("./utils/typeGuards")
|
|
11
|
+
const { isFileNotFoundError, createFileOperationError } = require("./utils/errorHelpers")
|
|
12
|
+
|
|
13
|
+
const loadAndValidateYaml = (path: string): YamlConfig => {
|
|
14
|
+
const fileContent = readFileSync(path, "utf8")
|
|
15
|
+
const yamlContent = load(fileContent)
|
|
16
|
+
|
|
17
|
+
if (!isValidYamlConfig(yamlContent)) {
|
|
18
|
+
throw createConfigValidationError(path, railsEnv, "Invalid YAML structure")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return yamlContent as YamlConfig
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const getDefaultConfig = (): Partial<Config> => {
|
|
25
|
+
try {
|
|
26
|
+
const defaultConfig = loadAndValidateYaml(defaultConfigPath)
|
|
27
|
+
return defaultConfig[railsEnv] || defaultConfig.production || {}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (isFileNotFoundError(error)) {
|
|
30
|
+
throw createFileOperationError(
|
|
31
|
+
'read',
|
|
32
|
+
defaultConfigPath,
|
|
33
|
+
`Default configuration not found at ${defaultConfigPath}. Please ensure Shakapacker is properly installed. You may need to run 'yarn add shakapacker' or 'npm install shakapacker'.`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaults = getDefaultConfig()
|
|
41
|
+
let config: Config
|
|
42
|
+
|
|
43
|
+
if (existsSync(configPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const appYmlObject = loadAndValidateYaml(configPath)
|
|
46
|
+
|
|
47
|
+
const envAppConfig = appYmlObject[railsEnv]
|
|
48
|
+
|
|
49
|
+
if (!envAppConfig) {
|
|
50
|
+
/* eslint no-console:0 */
|
|
51
|
+
console.warn(
|
|
52
|
+
`[SHAKAPACKER WARNING] Environment '${railsEnv}' not found in ${configPath}\n` +
|
|
53
|
+
`Available environments: ${Object.keys(appYmlObject).join(', ')}\n` +
|
|
54
|
+
`Using 'production' configuration as fallback.\n\n` +
|
|
55
|
+
`To fix this, either:\n` +
|
|
56
|
+
` - Add a '${railsEnv}' section to your shakapacker.yml\n` +
|
|
57
|
+
` - Set RAILS_ENV to one of the available environments\n` +
|
|
58
|
+
` - Copy settings from another environment as a starting point`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Merge returns the merged type
|
|
63
|
+
const mergedConfig = merge(defaults, envAppConfig || {})
|
|
64
|
+
|
|
65
|
+
// Validate merged config before type assertion
|
|
66
|
+
if (!isPartialConfig(mergedConfig)) {
|
|
67
|
+
throw createConfigValidationError(
|
|
68
|
+
configPath,
|
|
69
|
+
railsEnv,
|
|
70
|
+
`Invalid configuration structure in ${configPath}. Please check your shakapacker.yml syntax and ensure all required fields are properly defined.`
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// After merging with defaults, config should be complete
|
|
75
|
+
// Use type assertion only after validation
|
|
76
|
+
config = mergedConfig as Config
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (isFileNotFoundError(error)) {
|
|
79
|
+
// File not found is OK, use defaults
|
|
80
|
+
if (!isPartialConfig(defaults)) {
|
|
81
|
+
throw createConfigValidationError(
|
|
82
|
+
defaultConfigPath,
|
|
83
|
+
railsEnv,
|
|
84
|
+
`Invalid default configuration. This may indicate a corrupted Shakapacker installation. Try reinstalling with 'yarn add shakapacker --force'.`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
// Using defaults only, might be partial
|
|
88
|
+
config = defaults as Config
|
|
89
|
+
} else {
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// No user config, use defaults
|
|
95
|
+
if (!isPartialConfig(defaults)) {
|
|
96
|
+
throw createConfigValidationError(
|
|
97
|
+
defaultConfigPath,
|
|
98
|
+
railsEnv,
|
|
99
|
+
`Invalid default configuration. This may indicate a corrupted Shakapacker installation. Try reinstalling with 'yarn add shakapacker --force'.`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
// Using defaults only, might be partial
|
|
103
|
+
config = defaults as Config
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
config.outputPath = resolve(config.public_root_path, config.public_output_path)
|
|
107
|
+
|
|
108
|
+
// Ensure that the publicPath includes our asset host so dynamic imports
|
|
109
|
+
// (code-splitting chunks and static assets) load from the CDN instead of a relative path.
|
|
110
|
+
const getPublicPath = (): string => {
|
|
111
|
+
const rootUrl = ensureTrailingSlash(process.env.SHAKAPACKER_ASSET_HOST || "/")
|
|
112
|
+
return `${rootUrl}${config.public_output_path}/`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
config.publicPath = getPublicPath()
|
|
116
|
+
config.publicPathWithoutCDN = `/${config.public_output_path}/`
|
|
117
|
+
|
|
118
|
+
if (config.manifest_path) {
|
|
119
|
+
config.manifestPath = resolve(config.manifest_path)
|
|
120
|
+
} else {
|
|
121
|
+
config.manifestPath = resolve(config.outputPath, "manifest.json")
|
|
122
|
+
}
|
|
123
|
+
// Ensure no duplicate hash functions exist in the returned config object
|
|
124
|
+
if (config.integrity?.hash_functions) {
|
|
125
|
+
config.integrity.hash_functions = [...new Set(config.integrity.hash_functions)]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ensure assets_bundler has a default value
|
|
129
|
+
if (!config.assets_bundler) {
|
|
130
|
+
config.assets_bundler = "webpack"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Allow ENV variable to override assets_bundler
|
|
134
|
+
if (process.env.SHAKAPACKER_ASSETS_BUNDLER) {
|
|
135
|
+
config.assets_bundler = process.env.SHAKAPACKER_ASSETS_BUNDLER
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Define clear defaults
|
|
139
|
+
// Keep Babel as default for webpack to maintain backward compatibility
|
|
140
|
+
// Use SWC for rspack as it's a newer bundler where we can set modern defaults
|
|
141
|
+
const DEFAULT_JAVASCRIPT_TRANSPILER =
|
|
142
|
+
config.assets_bundler === "rspack" ? "swc" : "babel"
|
|
143
|
+
|
|
144
|
+
// Backward compatibility: Check for webpack_loader using proper type guard
|
|
145
|
+
function hasWebpackLoader(obj: unknown): obj is Config & { webpack_loader: string } {
|
|
146
|
+
return (
|
|
147
|
+
typeof obj === 'object' &&
|
|
148
|
+
obj !== null &&
|
|
149
|
+
'webpack_loader' in obj &&
|
|
150
|
+
typeof (obj as Record<string, unknown>).webpack_loader === 'string'
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Allow environment variable to override javascript_transpiler
|
|
155
|
+
if (process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER) {
|
|
156
|
+
config.javascript_transpiler = process.env.SHAKAPACKER_JAVASCRIPT_TRANSPILER
|
|
157
|
+
} else if (hasWebpackLoader(config) && !config.javascript_transpiler) {
|
|
158
|
+
console.warn(
|
|
159
|
+
"[SHAKAPACKER DEPRECATION] The 'webpack_loader' configuration option is deprecated.\n" +
|
|
160
|
+
"Please use 'javascript_transpiler' instead as it better reflects its purpose of configuring JavaScript transpilation regardless of the bundler used."
|
|
161
|
+
)
|
|
162
|
+
config.javascript_transpiler = config.webpack_loader
|
|
163
|
+
} else if (!config.javascript_transpiler) {
|
|
164
|
+
config.javascript_transpiler = DEFAULT_JAVASCRIPT_TRANSPILER
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Ensure webpack_loader is always available for backward compatibility
|
|
168
|
+
Object.defineProperty(config, 'webpack_loader', {
|
|
169
|
+
value: config.javascript_transpiler,
|
|
170
|
+
writable: true,
|
|
171
|
+
enumerable: true,
|
|
172
|
+
configurable: true
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
export = config
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
// These are the raw shakapacker dev server config settings from the YML file with ENV overrides applied.
|
|
2
2
|
const { isBoolean } = require("./utils/helpers")
|
|
3
3
|
const config = require("./config")
|
|
4
|
+
import { DevServerConfig } from "./types"
|
|
4
5
|
|
|
5
|
-
const envFetch = (key) => {
|
|
6
|
+
const envFetch = (key: string): string | boolean | undefined => {
|
|
6
7
|
const value = process.env[key]
|
|
8
|
+
if (!value) return undefined
|
|
7
9
|
return isBoolean(value) ? JSON.parse(value) : value
|
|
8
10
|
}
|
|
9
11
|
|
|
10
|
-
const devServerConfig = config.dev_server
|
|
12
|
+
const devServerConfig: DevServerConfig | undefined = config.dev_server
|
|
11
13
|
|
|
12
14
|
if (devServerConfig) {
|
|
13
|
-
const envPrefix =
|
|
15
|
+
const envPrefix = devServerConfig.env_prefix || "SHAKAPACKER_DEV_SERVER"
|
|
14
16
|
|
|
15
17
|
Object.keys(devServerConfig).forEach((key) => {
|
|
16
18
|
const envValue = envFetch(`${envPrefix}_${key.toUpperCase()}`)
|
|
17
19
|
if (envValue !== undefined) {
|
|
18
|
-
|
|
20
|
+
// Use bracket notation to avoid ASI issues
|
|
21
|
+
(devServerConfig as Record<string, unknown>)[key] = envValue
|
|
19
22
|
}
|
|
20
23
|
})
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
export = devServerConfig || {}
|
data/package/env.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { load } from "js-yaml"
|
|
2
|
+
import { readFileSync } from "fs"
|
|
3
|
+
const defaultConfigPath = require("./utils/defaultConfigPath")
|
|
4
|
+
const configPath = require("./utils/configPath")
|
|
5
|
+
const { isFileNotFoundError } = require("./utils/errorHelpers")
|
|
6
|
+
const { sanitizeEnvValue } = require("./utils/pathValidation")
|
|
7
|
+
|
|
8
|
+
const NODE_ENVIRONMENTS = ["development", "production", "test"] as const
|
|
9
|
+
|
|
10
|
+
// Sanitize environment variables to prevent injection
|
|
11
|
+
const initialRailsEnv = sanitizeEnvValue(process.env.RAILS_ENV)
|
|
12
|
+
const rawNodeEnv = sanitizeEnvValue(process.env.NODE_ENV)
|
|
13
|
+
|
|
14
|
+
// Default NODE_ENV based on RAILS_ENV to match bin/shakapacker behavior (see lib/shakapacker/runner.rb:27)
|
|
15
|
+
// - RAILS_ENV=production → DEFAULT="production" (safe for production builds)
|
|
16
|
+
// - RAILS_ENV=development, test, staging, or unset → DEFAULT="development" (good DX for dev server)
|
|
17
|
+
// This ensures the dev server works out of the box without requiring NODE_ENV to be set explicitly
|
|
18
|
+
const DEFAULT = initialRailsEnv === "production" ? "production" : "development"
|
|
19
|
+
|
|
20
|
+
// Validate NODE_ENV strictly
|
|
21
|
+
const nodeEnv =
|
|
22
|
+
rawNodeEnv &&
|
|
23
|
+
NODE_ENVIRONMENTS.includes(rawNodeEnv as (typeof NODE_ENVIRONMENTS)[number])
|
|
24
|
+
? rawNodeEnv
|
|
25
|
+
: DEFAULT
|
|
26
|
+
|
|
27
|
+
// Log warning if NODE_ENV was invalid
|
|
28
|
+
if (
|
|
29
|
+
rawNodeEnv &&
|
|
30
|
+
!NODE_ENVIRONMENTS.includes(rawNodeEnv as (typeof NODE_ENVIRONMENTS)[number])
|
|
31
|
+
) {
|
|
32
|
+
console.warn(
|
|
33
|
+
`[SHAKAPACKER WARNING] Invalid NODE_ENV value: ${rawNodeEnv}. ` +
|
|
34
|
+
`Valid values are: ${NODE_ENVIRONMENTS.join(", ")}. Using default: ${DEFAULT}`
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isProduction = nodeEnv === "production"
|
|
39
|
+
const isDevelopment = nodeEnv === "development"
|
|
40
|
+
|
|
41
|
+
interface ConfigFile {
|
|
42
|
+
[environment: string]: Record<string, unknown>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let config: ConfigFile
|
|
46
|
+
try {
|
|
47
|
+
config = load(readFileSync(configPath, "utf8")) as ConfigFile
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
if (isFileNotFoundError(error)) {
|
|
50
|
+
// File not found, use default configuration
|
|
51
|
+
try {
|
|
52
|
+
config = load(readFileSync(defaultConfigPath, "utf8")) as ConfigFile
|
|
53
|
+
} catch (defaultError) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Failed to load Shakapacker configuration.\n` +
|
|
56
|
+
`Neither user config (${configPath}) nor default config (${defaultConfigPath}) could be loaded.\n\n` +
|
|
57
|
+
`To fix this issue:\n` +
|
|
58
|
+
`1. Create a config/shakapacker.yml file in your project\n` +
|
|
59
|
+
`2. Or set the SHAKAPACKER_CONFIG environment variable to point to your config file\n` +
|
|
60
|
+
`3. Or reinstall Shakapacker to restore the default configuration:\n` +
|
|
61
|
+
` npm install shakapacker --force\n` +
|
|
62
|
+
` yarn add shakapacker --force`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const availableEnvironments = Object.keys(config).join("|")
|
|
71
|
+
const regex = new RegExp(`^(${availableEnvironments})$`, "g")
|
|
72
|
+
|
|
73
|
+
const runningWebpackDevServer = process.env.WEBPACK_SERVE === "true"
|
|
74
|
+
|
|
75
|
+
const validatedRailsEnv =
|
|
76
|
+
initialRailsEnv && initialRailsEnv.match(regex) ? initialRailsEnv : DEFAULT
|
|
77
|
+
|
|
78
|
+
if (initialRailsEnv && validatedRailsEnv !== initialRailsEnv) {
|
|
79
|
+
/* eslint no-console:0 */
|
|
80
|
+
console.warn(
|
|
81
|
+
`[SHAKAPACKER WARNING] Environment '${initialRailsEnv}' not found in the configuration.\n` +
|
|
82
|
+
`Using '${DEFAULT}' configuration as a fallback.`
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export = {
|
|
87
|
+
railsEnv: validatedRailsEnv,
|
|
88
|
+
nodeEnv,
|
|
89
|
+
isProduction,
|
|
90
|
+
isDevelopment,
|
|
91
|
+
runningWebpackDevServer
|
|
92
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* eslint global-require: 0 */
|
|
2
|
+
/* eslint import/no-dynamic-require: 0 */
|
|
3
|
+
|
|
4
|
+
const { basename, dirname, join, relative, resolve } = require("path")
|
|
5
|
+
const { existsSync, readdirSync } = require("fs")
|
|
6
|
+
import { Dirent } from "fs"
|
|
7
|
+
const extname = require("path-complete-extname")
|
|
8
|
+
// @ts-ignore: webpack is an optional peer dependency (using type-only import)
|
|
9
|
+
import type { Configuration, Entry } from "webpack"
|
|
10
|
+
const config = require("../config")
|
|
11
|
+
const { isProduction } = require("../env")
|
|
12
|
+
|
|
13
|
+
const pluginsPath = resolve(
|
|
14
|
+
__dirname,
|
|
15
|
+
"..",
|
|
16
|
+
"plugins",
|
|
17
|
+
`${config.assets_bundler}.js`
|
|
18
|
+
)
|
|
19
|
+
const { getPlugins } = require(pluginsPath)
|
|
20
|
+
const rulesPath = resolve(
|
|
21
|
+
__dirname,
|
|
22
|
+
"..",
|
|
23
|
+
"rules",
|
|
24
|
+
`${config.assets_bundler}.js`
|
|
25
|
+
)
|
|
26
|
+
const rules = require(rulesPath)
|
|
27
|
+
|
|
28
|
+
// Don't use contentHash except for production for performance
|
|
29
|
+
// https://webpack.js.org/guides/build-performance/#avoid-production-specific-tooling
|
|
30
|
+
const hash = isProduction || config.useContentHash ? "-[contenthash]" : ""
|
|
31
|
+
|
|
32
|
+
const getFilesInDirectory = (dir: string, includeNested: boolean): string[] => {
|
|
33
|
+
if (!existsSync(dir)) {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((dirent: Dirent) => {
|
|
38
|
+
const filePath = join(dir, dirent.name)
|
|
39
|
+
|
|
40
|
+
if (dirent.isDirectory() && includeNested) {
|
|
41
|
+
return getFilesInDirectory(filePath, includeNested)
|
|
42
|
+
}
|
|
43
|
+
if (dirent.isFile()) {
|
|
44
|
+
return filePath
|
|
45
|
+
}
|
|
46
|
+
return []
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const getEntryObject = (): Entry => {
|
|
51
|
+
const entries: Entry = {}
|
|
52
|
+
const rootPath = join(config.source_path, config.source_entry_path)
|
|
53
|
+
if (config.source_entry_path === "/" && config.nested_entries) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid Shakapacker configuration detected!\n\n` +
|
|
56
|
+
`You have set source_entry_path to '/' with nested_entries enabled.\n` +
|
|
57
|
+
`This would create webpack entry points for EVERY file in your source directory,\n` +
|
|
58
|
+
`which would severely impact build performance.\n\n` +
|
|
59
|
+
`To fix this issue, either:\n` +
|
|
60
|
+
`1. Set 'nested_entries: false' in your shakapacker.yml\n` +
|
|
61
|
+
`2. Change 'source_entry_path' to a specific subdirectory (e.g., 'packs')\n` +
|
|
62
|
+
`3. Or use both options for better organization of your entry points`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getFilesInDirectory(rootPath, config.nested_entries).forEach((path) => {
|
|
67
|
+
const namespace = relative(join(rootPath), dirname(path))
|
|
68
|
+
const name = join(namespace, basename(path, extname(path)))
|
|
69
|
+
const assetPath: string = resolve(path)
|
|
70
|
+
|
|
71
|
+
// Allows for multiple filetypes per entry (https://webpack.js.org/guides/entry-advanced/)
|
|
72
|
+
// Transforms the config object value to an array with all values under the same name
|
|
73
|
+
const previousPaths = entries[name]
|
|
74
|
+
if (previousPaths) {
|
|
75
|
+
const pathArray = Array.isArray(previousPaths)
|
|
76
|
+
? previousPaths as string[]
|
|
77
|
+
: [previousPaths as string]
|
|
78
|
+
pathArray.push(assetPath)
|
|
79
|
+
entries[name] = pathArray
|
|
80
|
+
} else {
|
|
81
|
+
entries[name] = assetPath
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return entries
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getModulePaths = (): string[] => {
|
|
89
|
+
const result = [resolve(config.source_path)]
|
|
90
|
+
|
|
91
|
+
if (config.additional_paths) {
|
|
92
|
+
config.additional_paths.forEach((path: string) => result.push(resolve(path)))
|
|
93
|
+
}
|
|
94
|
+
result.push("node_modules")
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const baseConfig: Configuration = {
|
|
100
|
+
mode: "production",
|
|
101
|
+
output: {
|
|
102
|
+
filename: `js/[name]${hash}.js`,
|
|
103
|
+
chunkFilename: `js/[name]${hash}.chunk.js`,
|
|
104
|
+
|
|
105
|
+
// https://webpack.js.org/configuration/output/#outputhotupdatechunkfilename
|
|
106
|
+
hotUpdateChunkFilename: "js/[id].[fullhash].hot-update.js",
|
|
107
|
+
path: config.outputPath,
|
|
108
|
+
publicPath: config.publicPath,
|
|
109
|
+
|
|
110
|
+
// This is required for SRI to work.
|
|
111
|
+
crossOriginLoading: config.integrity && config.integrity.enabled
|
|
112
|
+
? (config.integrity.cross_origin as "anonymous" | "use-credentials" | false)
|
|
113
|
+
: false
|
|
114
|
+
},
|
|
115
|
+
entry: getEntryObject(),
|
|
116
|
+
resolve: {
|
|
117
|
+
extensions: [".js", ".jsx", ".mjs", ".ts", ".tsx", ".coffee"],
|
|
118
|
+
modules: getModulePaths()
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
plugins: getPlugins(),
|
|
122
|
+
|
|
123
|
+
resolveLoader: {
|
|
124
|
+
modules: ["node_modules"]
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
optimization: {
|
|
128
|
+
splitChunks: { chunks: "all" },
|
|
129
|
+
runtimeChunk: "single"
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
module: {
|
|
133
|
+
rules
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export = baseConfig
|
|
138
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Development environment configuration for webpack and rspack bundlers
|
|
3
|
+
* @module environments/development
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { merge } = require("webpack-merge")
|
|
7
|
+
const config = require("../config")
|
|
8
|
+
const baseConfig = require("./base")
|
|
9
|
+
const webpackDevServerConfig = require("../webpackDevServerConfig")
|
|
10
|
+
const { runningWebpackDevServer } = require("../env")
|
|
11
|
+
const { moduleExists } = require("../utils/helpers")
|
|
12
|
+
import type {
|
|
13
|
+
WebpackConfigWithDevServer,
|
|
14
|
+
RspackConfigWithDevServer,
|
|
15
|
+
ReactRefreshWebpackPlugin,
|
|
16
|
+
ReactRefreshRspackPlugin
|
|
17
|
+
} from "./types"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base development configuration shared between webpack and rspack
|
|
21
|
+
*/
|
|
22
|
+
const baseDevConfig = {
|
|
23
|
+
mode: "development" as const,
|
|
24
|
+
devtool: "cheap-module-source-map" as const
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate webpack-specific development configuration
|
|
29
|
+
* @returns Webpack configuration with dev server settings
|
|
30
|
+
*/
|
|
31
|
+
const webpackDevConfig = (): WebpackConfigWithDevServer => {
|
|
32
|
+
const webpackConfig: WebpackConfigWithDevServer = {
|
|
33
|
+
...baseDevConfig,
|
|
34
|
+
...(runningWebpackDevServer && { devServer: webpackDevServerConfig() })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const devServerConfig = webpackDevServerConfig()
|
|
38
|
+
if (
|
|
39
|
+
runningWebpackDevServer &&
|
|
40
|
+
devServerConfig.hot &&
|
|
41
|
+
moduleExists("@pmmmwh/react-refresh-webpack-plugin")
|
|
42
|
+
) {
|
|
43
|
+
// eslint-disable-next-line global-require
|
|
44
|
+
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin")
|
|
45
|
+
webpackConfig.plugins = [
|
|
46
|
+
...(webpackConfig.plugins || []),
|
|
47
|
+
new ReactRefreshWebpackPlugin()
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return webpackConfig
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate rspack-specific development configuration
|
|
56
|
+
* @returns Rspack configuration with dev server settings
|
|
57
|
+
*/
|
|
58
|
+
const rspackDevConfig = (): RspackConfigWithDevServer => {
|
|
59
|
+
const devServerConfig = webpackDevServerConfig()
|
|
60
|
+
const rspackConfig: RspackConfigWithDevServer = {
|
|
61
|
+
...baseDevConfig,
|
|
62
|
+
devServer: {
|
|
63
|
+
...devServerConfig,
|
|
64
|
+
devMiddleware: {
|
|
65
|
+
...(devServerConfig.devMiddleware || {}),
|
|
66
|
+
writeToDisk: (filePath: string) => !filePath.includes(".hot-update.")
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
runningWebpackDevServer &&
|
|
73
|
+
devServerConfig.hot &&
|
|
74
|
+
moduleExists("@rspack/plugin-react-refresh")
|
|
75
|
+
) {
|
|
76
|
+
// eslint-disable-next-line global-require
|
|
77
|
+
const ReactRefreshPlugin = require("@rspack/plugin-react-refresh")
|
|
78
|
+
rspackConfig.plugins = [
|
|
79
|
+
...(rspackConfig.plugins || []),
|
|
80
|
+
new ReactRefreshPlugin()
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return rspackConfig
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const bundlerConfig =
|
|
88
|
+
config.assets_bundler === "rspack" ? rspackDevConfig() : webpackDevConfig()
|
|
89
|
+
|
|
90
|
+
module.exports = merge(baseConfig, bundlerConfig)
|