shakapacker 9.4.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a0519c935e0e3d98204aeaeec6cc35b57fa13be1fe8a6f62e5e6cd5124d09c7
4
- data.tar.gz: 7f6fe4134fd9b17de2dcd26b5c9e80f1f41d2019dfa0ed337092c1d23a9aa077
3
+ metadata.gz: 3b214b9c8a64b8d7a39fa119818f9cd85e872abf2278ca9659685ebfe73d3324
4
+ data.tar.gz: 06a7f954d5909f19df8cf13188b398e0b3ce2d5b4b17c6d74eca65e8573e6a30
5
5
  SHA512:
6
- metadata.gz: 8789ef5d5cf102254f27c0833b3a7bd8239b5b97d2dfa97ac79fb01827931397dc71dd35f82971a61354a1751f6dafd80d91a322e51b6390f83132f17a786626
7
- data.tar.gz: 3a6cff63ba261b9cec55cdd683e7cfa96a24e52b753639643bf2f6874ed49c0f50a990a771a85c4265e9e6394a8fc7e8b4e3440ed5d62982f89b98cddadf9450
6
+ metadata.gz: f728d9d77e03e1c8113edefdd426fd96b900eb64ce7c8e6470cd2e78f45e2f326645d58c31a95dc383809cfebef6af157cd06c145c9204d9975368212e842cb1
7
+ data.tar.gz: 7feac095bbf2182d7e87c21fd8f739cbf774ae1bdb8ec55dbe297e76b154f00b81cbabb3ee6f838dda9fd42b67fe718a749b4778924cf23c8f65b896e6a1b7da
data/CHANGELOG.md CHANGED
@@ -11,9 +11,49 @@
11
11
 
12
12
  Changes since the last non-beta release.
13
13
 
14
+ _None yet._
15
+
16
+ ## [v9.5.0] - January 7, 2026
17
+
18
+ ### Security
19
+
20
+ - **CRITICAL: Fixed environment variable leak via EnvironmentPlugin**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). The default webpack and rspack plugins were passing the entire `process.env` to `EnvironmentPlugin`, which exposed ALL build environment variables (including secrets like `DATABASE_URL`, `AWS_SECRET_ACCESS_KEY`, `RAILS_MASTER_KEY`, etc.) to client-side JavaScript bundles when code referenced `process.env.VARIABLE_NAME`. **Note**: This issue is especially critical with webpack 5.103+ due to a [serialization change](https://github.com/webpack/webpack/commit/eecdeeb746b2f996ed4ab74365dd72c95070196b) that can embed all environment variables into bundles when `import.meta.env` is accessed conditionally. This vulnerability was inherited from webpacker v1.0.0 (January 2017) and has been present in all versions of webpacker and shakapacker. **Action required**: After upgrading, rotate any secrets that may have been exposed in production JavaScript bundles.
21
+
14
22
  ### Added
15
23
 
16
- - **Added `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable to skip precompile hook**. [PR #XXX](https://github.com/shakacode/shakapacker/pull/XXX) by [justin808](https://github.com/justin808). Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to skip the precompile hook during compilation. This is useful when using process managers like Foreman or Overmind to run the hook once before starting multiple webpack processes, preventing duplicate hook execution. **Migration tip:** If you have a custom `bin/dev` script that starts multiple webpack processes, you can now run the precompile hook once in the script and set this environment variable to prevent each webpack process from running the hook again. See the [precompile hook documentation](./docs/precompile_hook.md#skipping-the-hook) for implementation examples.
24
+ - **Added `SHAKAPACKER_PUBLIC_*` prefix convention for client-side environment variables**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). Any environment variable prefixed with `SHAKAPACKER_PUBLIC_` is automatically exposed to client-side JavaScript. This follows the same convention used by Next.js (`NEXT_PUBLIC_*`) and Vite (`VITE_*`), making it explicit which variables are intended for client-side use.
25
+
26
+ ```bash
27
+ # These are automatically available in your JavaScript
28
+ export SHAKAPACKER_PUBLIC_API_URL=https://api.example.com
29
+ export SHAKAPACKER_PUBLIC_ANALYTICS_ID=UA-12345
30
+ ```
31
+
32
+ - **Added `SHAKAPACKER_ENV_VARS` environment variable as escape hatch for extending allowed client-side env vars**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). Set `SHAKAPACKER_ENV_VARS=VAR1,VAR2,VAR3` to expose additional environment variables to client-side JavaScript beyond the default allowlist (`NODE_ENV`, `RAILS_ENV`, `WEBPACK_SERVE`). Only add non-sensitive variables that are safe to embed in public JavaScript bundles.
33
+
34
+ ### Changed
35
+
36
+ - **BREAKING: EnvironmentPlugin now uses allowlist instead of exposing all env vars**. [PR #857](https://github.com/shakacode/shakapacker/pull/857) by [justin808](https://github.com/justin808). Only `NODE_ENV`, `RAILS_ENV`, `WEBPACK_SERVE`, and any `SHAKAPACKER_PUBLIC_*` variables are exposed by default. If your client-side code relies on other environment variables, either rename them with the `SHAKAPACKER_PUBLIC_` prefix (recommended), add them via `SHAKAPACKER_ENV_VARS`, or customize your webpack/rspack config. This is a security fix - the previous behavior was dangerous.
37
+
38
+ **Migration examples:**
39
+
40
+ ```bash
41
+ # Option 1 (recommended): Use the SHAKAPACKER_PUBLIC_ prefix
42
+ export SHAKAPACKER_PUBLIC_API_BASE_URL=https://api.example.com
43
+
44
+ # Option 2: Use SHAKAPACKER_ENV_VARS for existing variable names
45
+ SHAKAPACKER_ENV_VARS=API_BASE_URL bundle exec rails assets:precompile
46
+ ```
47
+
48
+ ### Fixed
49
+
50
+ - **Fixed gemspec to exclude Gemfile.lock from published gem**. [PR #856](https://github.com/shakacode/shakapacker/pull/856) by [adrien-k](https://github.com/adrien-k). The gemspec's file pattern now correctly excludes `Gemfile.lock`, preventing vulnerability alerts during Docker image scans caused by outdated pinned versions in the lock file.
51
+
52
+ ## [v9.4.0] - November 22, 2025
53
+
54
+ ### Added
55
+
56
+ - **Added `SHAKAPACKER_SKIP_PRECOMPILE_HOOK` environment variable to skip precompile hook**. [PR #850](https://github.com/shakacode/shakapacker/pull/850) by [justin808](https://github.com/justin808). Set `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to skip the precompile hook during compilation. This is useful when using process managers like Foreman or Overmind to run the hook once before starting multiple webpack processes, preventing duplicate hook execution. **Migration tip:** If you have a custom `bin/dev` script that starts multiple webpack processes, you can now run the precompile hook once in the script and set this environment variable to prevent each webpack process from running the hook again. See the [precompile hook documentation](./docs/precompile_hook.md#skipping-the-hook) for implementation examples.
17
57
 
18
58
  ## [v9.3.4-beta.0] - November 17, 2025
19
59
 
@@ -216,6 +256,7 @@ See the [v9 Upgrade Guide](https://github.com/shakacode/shakapacker/blob/main/do
216
256
  ### ⚠️ Breaking Changes
217
257
 
218
258
  1. **SWC is now the default JavaScript transpiler instead of Babel** ([PR 603](https://github.com/shakacode/shakapacker/pull/603) by [justin808](https://github.com/justin808))
259
+
219
260
  - Babel dependencies are no longer included as peer dependencies
220
261
  - Improves compilation speed by 20x
221
262
  - **Migration for existing projects:**
@@ -232,6 +273,7 @@ See the [v9 Upgrade Guide](https://github.com/shakacode/shakapacker/blob/main/do
232
273
  ```
233
274
 
234
275
  2. **CSS Modules now use named exports by default** ([PR 599](https://github.com/shakacode/shakapacker/pull/599))
276
+
235
277
  - **JavaScript:** Use named imports: `import { className } from './styles.module.css'`
236
278
  - **TypeScript:** Use namespace imports: `import * as styles from './styles.module.css'`
237
279
  - To keep the old behavior with default imports, see [CSS Modules Export Mode documentation](./docs/css-modules-export-mode.md) for configuration instructions
@@ -497,6 +539,7 @@ See the [v8 Upgrade Guide](https://github.com/shakacode/shakapacker/blob/main/do
497
539
 
498
540
  - Set `source_entry_path` to `packs` and `nested_entries` to `true` in`shakapacker.yml` [PR 284](https://github.com/shakacode/shakapacker/pull/284) by [ahangarha](https://github.com/ahangarha).
499
541
  - Dev server configuration is modified to follow [webpack recommended configurations](https://webpack.js.org/configuration/dev-server/) for dev server. [PR276](https://github.com/shakacode/shakapacker/pull/276) by [ahangarha](https://github.com/ahangarha):
542
+
500
543
  - Deprecated `https` entry is removed from the default configuration file, allowing to set `server` or `https` as per the project requirements. For more detail, check webpack documentation. The `https` entry can be effective only if there is no `server` entry in the config file.
501
544
  - `allowed_hosts` is now set to `auto` instead of `all` by default.
502
545
 
@@ -763,7 +806,9 @@ Note: [Rubygem is 6.3.0.pre.rc.1](https://rubygems.org/gems/shakapacker/versions
763
806
 
764
807
  See [CHANGELOG.md in rails/webpacker (up to v5.4.3)](https://github.com/rails/webpacker/blob/master/CHANGELOG.md)
765
808
 
766
- [Unreleased]: https://github.com/shakacode/shakapacker/compare/v9.3.4-beta.0...main
809
+ [Unreleased]: https://github.com/shakacode/shakapacker/compare/v9.5.0...main
810
+ [v9.5.0]: https://github.com/shakacode/shakapacker/compare/v9.4.0...v9.5.0
811
+ [v9.4.0]: https://github.com/shakacode/shakapacker/compare/v9.3.4...v9.4.0
767
812
  [v9.3.4-beta.0]: https://github.com/shakacode/shakapacker/compare/v9.3.3...v9.3.4-beta.0
768
813
  [v9.3.3]: https://github.com/shakacode/shakapacker/compare/v9.3.2...v9.3.3
769
814
  [v9.3.2]: https://github.com/shakacode/shakapacker/compare/v9.3.1...v9.3.2
data/CLAUDE.md CHANGED
@@ -12,6 +12,10 @@
12
12
  - Run corresponding RSpec tests when changing source files
13
13
  - For example, when changing `lib/shakapacker/foo.rb`, run `spec/shakapacker/foo_spec.rb`
14
14
  - Run the full test suite with `bundle exec rspec` before pushing
15
+ - **Use explicit RSpec spy assertions** - prefer `have_received`/`not_to have_received` over indirect counter patterns
16
+ - Good: `expect(Open3).to have_received(:capture3).with(anything, hook_command, anything)`
17
+ - Good: `expect(Open3).not_to have_received(:capture3).with(anything, hook_command, anything)`
18
+ - Avoid: `call_count += 1` followed by `expect(call_count).to eq(1)`
15
19
 
16
20
  ## Code Style
17
21
 
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Minimal shim - all logic is in the TypeScript module
4
+ const { run } = require("shakapacker/configExporter")
5
+
6
+ run(process.argv.slice(2))
7
+ .then((exitCode) => process.exit(exitCode))
8
+ .catch((error) => {
9
+ console.error(error.message)
10
+ process.exit(1)
11
+ })
@@ -617,14 +617,57 @@ Shakapacker validates configuration at runtime and provides helpful error messag
617
617
 
618
618
  Some options can be overridden via environment variables:
619
619
 
620
- | Variable | Description | Example |
621
- | ---------------------------- | ------------------------ | ------------------------- |
622
- | `SHAKAPACKER_CONFIG` | Path to shakapacker.yml | `config/webpack.yml` |
623
- | `SHAKAPACKER_ASSETS_BUNDLER` | Override assets bundler | `rspack` |
624
- | `SHAKAPACKER_PRECOMPILE` | Override precompile flag | `false` |
625
- | `SHAKAPACKER_ASSET_HOST` | Override asset host | `https://cdn.example.com` |
626
- | `NODE_ENV` | Node environment | `production` |
627
- | `RAILS_ENV` | Rails environment | `staging` |
620
+ | Variable | Description | Example |
621
+ | ---------------------------- | -------------------------------------------------- | ---------------------------- |
622
+ | `SHAKAPACKER_CONFIG` | Path to shakapacker.yml | `config/webpack.yml` |
623
+ | `SHAKAPACKER_ASSETS_BUNDLER` | Override assets bundler | `rspack` |
624
+ | `SHAKAPACKER_PRECOMPILE` | Override precompile flag | `false` |
625
+ | `SHAKAPACKER_ASSET_HOST` | Override asset host | `https://cdn.example.com` |
626
+ | `SHAKAPACKER_PUBLIC_*` | Auto-exposed to client-side JS (prefix convention) | `SHAKAPACKER_PUBLIC_API_URL` |
627
+ | `SHAKAPACKER_ENV_VARS` | Additional env vars to expose to client-side JS | `API_URL,FEATURE_FLAGS` |
628
+ | `NODE_ENV` | Node environment | `production` |
629
+ | `RAILS_ENV` | Rails environment | `staging` |
630
+
631
+ ### Exposing Environment Variables to Client-Side JavaScript
632
+
633
+ By default, only `NODE_ENV`, `RAILS_ENV`, and `WEBPACK_SERVE` are exposed to client-side JavaScript via webpack/rspack's `EnvironmentPlugin`. This is a security measure to prevent accidentally leaking secrets like `DATABASE_URL`, `API_SECRET_KEY`, etc. into your JavaScript bundles.
634
+
635
+ #### SHAKAPACKER*PUBLIC*\* Prefix (Recommended)
636
+
637
+ Any environment variable prefixed with `SHAKAPACKER_PUBLIC_` is automatically exposed to client-side code. This follows the same convention used by Next.js (`NEXT_PUBLIC_*`) and Vite (`VITE_*`):
638
+
639
+ ```bash
640
+ # These are automatically available in your JavaScript
641
+ export SHAKAPACKER_PUBLIC_API_URL=https://api.example.com
642
+ export SHAKAPACKER_PUBLIC_ANALYTICS_ID=UA-12345
643
+ export SHAKAPACKER_PUBLIC_FEATURE_FLAGS=dark_mode,beta_ui
644
+ ```
645
+
646
+ ```javascript
647
+ // Access in your JavaScript code
648
+ console.log(process.env.SHAKAPACKER_PUBLIC_API_URL)
649
+ console.log(process.env.SHAKAPACKER_PUBLIC_ANALYTICS_ID)
650
+ ```
651
+
652
+ The prefix makes it explicit which variables are intended for client-side use, preventing accidental exposure of secrets.
653
+
654
+ #### SHAKAPACKER_ENV_VARS (Legacy/Escape Hatch)
655
+
656
+ For variables without the `SHAKAPACKER_PUBLIC_` prefix, you can use `SHAKAPACKER_ENV_VARS` to expose them:
657
+
658
+ ```bash
659
+ # Expose additional variables during build
660
+ SHAKAPACKER_ENV_VARS=API_BASE_URL,FEATURE_FLAGS bundle exec rake assets:precompile
661
+
662
+ # In development
663
+ SHAKAPACKER_ENV_VARS=API_BASE_URL bin/shakapacker-dev-server
664
+ ```
665
+
666
+ ```javascript
667
+ // These are available after adding to SHAKAPACKER_ENV_VARS
668
+ console.log(process.env.API_BASE_URL)
669
+ console.log(process.env.FEATURE_FLAGS)
670
+ ```
628
671
 
629
672
  ## Best Practices
630
673
 
@@ -300,7 +300,7 @@ This is useful when:
300
300
  - Running the hook manually and then compiling multiple times
301
301
  - Debugging compilation issues without the hook
302
302
 
303
- **Note:** The examples below show how to implement this in your custom `bin/dev` script. If you're using React on Rails, the generated `bin/dev` script already implements this pattern automatically - it runs the precompile hook once before launching processes, then sets `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution.
303
+ **Note:** The examples below show how to implement this in your custom `bin/dev` script. If you're using React on Rails v13.1.0+, the generated `bin/dev` script already implements this pattern automatically - **no action needed**. It runs the precompile hook once before launching processes, then sets `SHAKAPACKER_SKIP_PRECOMPILE_HOOK=true` to prevent duplicate execution.
304
304
 
305
305
  **Recommended: Use Procfile env prefix**
306
306
 
@@ -1,4 +1,4 @@
1
1
  module Shakapacker
2
2
  # Change the version in package.json too, please!
3
- VERSION = "9.4.0".freeze
3
+ VERSION = "9.5.0".freeze
4
4
  end
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared environment variable filtering logic for webpack and rspack plugins.
3
+ *
4
+ * SECURITY: This module ensures only allowlisted environment variables are
5
+ * exposed to client-side JavaScript bundles, preventing accidental leakage
6
+ * of secrets like DATABASE_URL, API keys, etc.
7
+ */
8
+
9
+ /**
10
+ * Allowlist of environment variables that are safe to expose to client-side JavaScript.
11
+ *
12
+ * SECURITY: Never add sensitive variables like DATABASE_URL, API keys, or secrets.
13
+ * These values are embedded directly into the JavaScript bundle and are publicly visible.
14
+ *
15
+ * Users can extend this list via:
16
+ * 1. SHAKAPACKER_PUBLIC_* prefix (auto-exposed, similar to Next.js/Vite conventions)
17
+ * 2. SHAKAPACKER_ENV_VARS environment variable (comma-separated list)
18
+ * 3. Customizing their webpack/rspack config
19
+ */
20
+ export const DEFAULT_ALLOWED_ENV_VARS = [
21
+ "NODE_ENV",
22
+ "RAILS_ENV",
23
+ "WEBPACK_SERVE"
24
+ ] as const
25
+
26
+ /**
27
+ * Prefix for environment variables that are automatically exposed to client-side code.
28
+ * Similar to Next.js's NEXT_PUBLIC_ and Vite's VITE_ prefixes.
29
+ *
30
+ * Example: SHAKAPACKER_PUBLIC_API_URL will be available as process.env.SHAKAPACKER_PUBLIC_API_URL
31
+ */
32
+ export const PUBLIC_ENV_PREFIX = "SHAKAPACKER_PUBLIC_"
33
+
34
+ /**
35
+ * Gets the list of environment variables to expose to client-side code.
36
+ * Combines:
37
+ * 1. Default allowed vars (NODE_ENV, RAILS_ENV, WEBPACK_SERVE)
38
+ * 2. Any vars with SHAKAPACKER_PUBLIC_ prefix (auto-exposed)
39
+ * 3. Any user-specified vars from SHAKAPACKER_ENV_VARS
40
+ */
41
+ export const getAllowedEnvVars = (): string[] => {
42
+ const allowed: string[] = [...DEFAULT_ALLOWED_ENV_VARS]
43
+
44
+ // Auto-expose any SHAKAPACKER_PUBLIC_* variables (similar to Next.js/Vite convention)
45
+ Object.keys(process.env).forEach((key) => {
46
+ if (key.startsWith(PUBLIC_ENV_PREFIX)) {
47
+ allowed.push(key)
48
+ }
49
+ })
50
+
51
+ // Allow users to specify additional env vars via SHAKAPACKER_ENV_VARS
52
+ const userVars = process.env.SHAKAPACKER_ENV_VARS
53
+ if (userVars) {
54
+ const additionalVars = userVars
55
+ .split(",")
56
+ .map((v) => v.trim())
57
+ .filter(Boolean)
58
+
59
+ allowed.push(...additionalVars)
60
+ }
61
+
62
+ // Remove duplicates (can occur if same var is in multiple sources)
63
+ return [...new Set(allowed)]
64
+ }
65
+
66
+ /**
67
+ * Builds a filtered environment object containing only allowed variables.
68
+ * Returns an object with variable names as keys and their values.
69
+ * Uses null as default for missing variables (webpack/rspack treat null as optional).
70
+ */
71
+ export const getFilteredEnv = (): Record<string, string | null> => {
72
+ const allowedVars = getAllowedEnvVars()
73
+ const filtered: Record<string, string | null> = {}
74
+
75
+ for (const varName of allowedVars) {
76
+ // Use null as default for missing vars - webpack/rspack treat null as optional
77
+ // (undefined would cause them to throw if the var is used but not set)
78
+ filtered[varName] = process.env[varName] ?? null
79
+ }
80
+
81
+ return filtered
82
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Config } from "../types"
2
+ import { getFilteredEnv } from "./envFilter"
2
3
 
3
4
  const { requireOrError } = require("../utils/requireOrError")
4
5
 
@@ -29,7 +30,9 @@ interface Manifest {
29
30
 
30
31
  const getPlugins = (): unknown[] => {
31
32
  const plugins = [
32
- new rspack.EnvironmentPlugin(process.env),
33
+ // SECURITY: Only expose allowlisted environment variables to prevent secrets leaking
34
+ // into client-side bundles. See envFilter.ts for the allowlist configuration.
35
+ new rspack.EnvironmentPlugin(getFilteredEnv()),
33
36
  new RspackManifestPlugin({
34
37
  fileName: config.manifestPath.split("/").pop(), // Get just the filename
35
38
  publicPath: config.publicPathWithoutCDN,
@@ -1,4 +1,5 @@
1
1
  import type { Config } from "../types"
2
+ import { getFilteredEnv } from "./envFilter"
2
3
 
3
4
  const { requireOrError } = require("../utils/requireOrError")
4
5
  // TODO: Change to `const { WebpackAssetsManifest }` when dropping 'webpack-assets-manifest < 6.0.0' (Node >=20.10.0) support
@@ -15,7 +16,9 @@ const getPlugins = (): unknown[] => {
15
16
  ? WebpackAssetsManifest.WebpackAssetsManifest
16
17
  : WebpackAssetsManifest
17
18
  const plugins = [
18
- new webpack.EnvironmentPlugin(process.env),
19
+ // SECURITY: Only expose allowlisted environment variables to prevent secrets leaking
20
+ // into client-side bundles. See envFilter.ts for the allowlist configuration.
21
+ new webpack.EnvironmentPlugin(getFilteredEnv()),
19
22
  new WebpackAssetsManifestConstructor({
20
23
  merge: true,
21
24
  entrypoints: true,
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shakapacker",
3
- "version": "9.4.0",
3
+ "version": "9.5.0",
4
4
  "description": "Use webpack to manage app-like JavaScript modules in Rails",
5
5
  "homepage": "https://github.com/shakacode/shakapacker",
6
6
  "bugs": {
data/shakapacker.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
29
29
  s.add_development_dependency "rubocop-performance"
30
30
 
31
31
  s.files = `git ls-files -z`.split("\x0").reject { |f|
32
- f.match(%r{^(test|spec|features|tmp|node_modules|packages|coverage|Gemfile.lock|rakelib)/})
32
+ f.match(%r{^(test|spec|features|tmp|node_modules|packages|coverage|Gemfile.lock|rakelib)($|/)})
33
33
  } + Dir.glob("sig/**/*.rbs")
34
34
 
35
35
  s.test_files = `git ls-files -- test/*`.split("\n")
@@ -52,6 +52,7 @@ class Shakapacker::Compiler
52
52
 
53
53
  def bin_shakapacker_path: () -> Pathname
54
54
 
55
+ # Returns true if precompile hook should run, false if disabled or skipped via ENV
55
56
  def should_run_precompile_hook?: () -> bool
56
57
 
57
58
  def run_precompile_hook: () -> void
@@ -0,0 +1,453 @@
1
+ /**
2
+ * Security tests for environment variable filtering in EnvironmentPlugin.
3
+ *
4
+ * These tests verify that only allowlisted environment variables are exposed
5
+ * to client-side JavaScript bundles, preventing accidental leakage of secrets.
6
+ *
7
+ * CVE: Environment variables leak via EnvironmentPlugin(process.env)
8
+ * See: https://github.com/shakacode/shakapacker/security/advisories
9
+ */
10
+
11
+ const fs = require("fs")
12
+ const path = require("path")
13
+
14
+ const pluginsDir = path.resolve(__dirname, "../../../package/plugins")
15
+
16
+ describe("environment variable filtering security", () => {
17
+ const originalEnv = { ...process.env }
18
+
19
+ beforeEach(() => {
20
+ // Set up test environment with sensitive variables
21
+ process.env.NODE_ENV = "production"
22
+ process.env.RAILS_ENV = "production"
23
+ process.env.WEBPACK_SERVE = "false"
24
+
25
+ // Simulate sensitive build environment variables
26
+ process.env.DATABASE_URL = "postgres://user:password@host/db"
27
+ process.env.AWS_ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE"
28
+ process.env.AWS_SECRET_ACCESS_KEY =
29
+ "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
30
+ process.env.RAILS_MASTER_KEY = "abc123secretmasterkey456"
31
+ process.env.STRIPE_SECRET_KEY = "sk_live_secretkey123"
32
+ process.env.SESSION_SECRET = "supersecrettoken"
33
+
34
+ // Clear any cached modules
35
+ jest.resetModules()
36
+ })
37
+
38
+ afterEach(() => {
39
+ // Restore original environment
40
+ Object.keys(process.env).forEach((key) => {
41
+ if (!(key in originalEnv)) {
42
+ delete process.env[key]
43
+ }
44
+ })
45
+ Object.assign(process.env, originalEnv)
46
+ delete process.env.SHAKAPACKER_ENV_VARS
47
+ })
48
+
49
+ describe("shared envFilter module", () => {
50
+ it("exists and exports the filtering functions", () => {
51
+ const envFilterSource = fs.readFileSync(
52
+ path.join(pluginsDir, "envFilter.ts"),
53
+ "utf8"
54
+ )
55
+
56
+ // Verify exports
57
+ expect(envFilterSource).toContain("export const DEFAULT_ALLOWED_ENV_VARS")
58
+ expect(envFilterSource).toContain("export const getAllowedEnvVars")
59
+ expect(envFilterSource).toContain("export const getFilteredEnv")
60
+ })
61
+
62
+ it("has the default allowlist with only safe variables", () => {
63
+ const envFilterSource = fs.readFileSync(
64
+ path.join(pluginsDir, "envFilter.ts"),
65
+ "utf8"
66
+ )
67
+
68
+ // Extract the DEFAULT_ALLOWED_ENV_VARS array from source
69
+ const allowlistMatch = envFilterSource.match(
70
+ /DEFAULT_ALLOWED_ENV_VARS\s*=\s*\[([\s\S]*?)\]\s*as const/
71
+ )
72
+ expect(allowlistMatch).toBeTruthy()
73
+
74
+ const allowlistContent = allowlistMatch[1]
75
+
76
+ // These patterns should NEVER appear in the allowlist
77
+ const sensitivePatterns = [
78
+ "DATABASE",
79
+ "SECRET",
80
+ "PASSWORD",
81
+ "CREDENTIAL",
82
+ "AWS_",
83
+ "STRIPE",
84
+ "MASTER"
85
+ ]
86
+
87
+ sensitivePatterns.forEach((pattern) => {
88
+ expect(allowlistContent.toUpperCase()).not.toContain(pattern)
89
+ })
90
+
91
+ // Verify expected safe vars are present
92
+ expect(allowlistContent).toContain("NODE_ENV")
93
+ expect(allowlistContent).toContain("RAILS_ENV")
94
+ expect(allowlistContent).toContain("WEBPACK_SERVE")
95
+ })
96
+
97
+ it("includes SHAKAPACKER_ENV_VARS extension support", () => {
98
+ const envFilterSource = fs.readFileSync(
99
+ path.join(pluginsDir, "envFilter.ts"),
100
+ "utf8"
101
+ )
102
+
103
+ expect(envFilterSource).toContain("SHAKAPACKER_ENV_VARS")
104
+ expect(envFilterSource).toContain('split(",")')
105
+ })
106
+
107
+ it("exports PUBLIC_ENV_PREFIX constant", () => {
108
+ const envFilterSource = fs.readFileSync(
109
+ path.join(pluginsDir, "envFilter.ts"),
110
+ "utf8"
111
+ )
112
+
113
+ expect(envFilterSource).toContain("export const PUBLIC_ENV_PREFIX")
114
+ expect(envFilterSource).toContain('SHAKAPACKER_PUBLIC_"')
115
+ })
116
+
117
+ it("auto-exposes SHAKAPACKER_PUBLIC_* variables", () => {
118
+ const envFilterSource = fs.readFileSync(
119
+ path.join(pluginsDir, "envFilter.ts"),
120
+ "utf8"
121
+ )
122
+
123
+ // Verify the prefix check is present
124
+ expect(envFilterSource).toContain("startsWith(PUBLIC_ENV_PREFIX)")
125
+ expect(envFilterSource).toContain("Object.keys(process.env)")
126
+ })
127
+
128
+ it("uses SHAKAPACKER_PUBLIC_ prefix (with trailing underscore) to prevent system var exposure", () => {
129
+ const envFilterSource = fs.readFileSync(
130
+ path.join(pluginsDir, "envFilter.ts"),
131
+ "utf8"
132
+ )
133
+
134
+ // SECURITY: The prefix MUST include the trailing underscore to ensure
135
+ // Shakapacker system variables like SHAKAPACKER_CONFIG, SHAKAPACKER_ENV_VARS,
136
+ // SHAKAPACKER_PRECOMPILE, etc. are NOT accidentally exposed.
137
+ // Only SHAKAPACKER_PUBLIC_* variables should be auto-exposed.
138
+ const prefixMatch = envFilterSource.match(
139
+ /PUBLIC_ENV_PREFIX\s*=\s*["']([^"']+)["']/
140
+ )
141
+ expect(prefixMatch).toBeTruthy()
142
+ expect(prefixMatch[1]).toBe("SHAKAPACKER_PUBLIC_")
143
+
144
+ // Verify the trailing underscore is present - this is critical for security
145
+ expect(prefixMatch[1]).toMatch(/_$/)
146
+ })
147
+
148
+ it("handles whitespace and empty values in CSV", () => {
149
+ const envFilterSource = fs.readFileSync(
150
+ path.join(pluginsDir, "envFilter.ts"),
151
+ "utf8"
152
+ )
153
+
154
+ // Verify trim() is called on each value
155
+ expect(envFilterSource).toMatch(/\.map\(\s*\(?v\)?\s*=>\s*v\.trim\(\)/)
156
+ // Verify filter(Boolean) is called to remove empty strings
157
+ expect(envFilterSource).toMatch(/\.filter\(Boolean\)/)
158
+ })
159
+ })
160
+
161
+ describe("webpack plugin", () => {
162
+ it("imports from shared envFilter module", () => {
163
+ const webpackPluginSource = fs.readFileSync(
164
+ path.join(pluginsDir, "webpack.ts"),
165
+ "utf8"
166
+ )
167
+
168
+ expect(webpackPluginSource).toContain(
169
+ 'import { getFilteredEnv } from "./envFilter"'
170
+ )
171
+ })
172
+
173
+ it("uses getFilteredEnv() not process.env", () => {
174
+ const webpackPluginSource = fs.readFileSync(
175
+ path.join(pluginsDir, "webpack.ts"),
176
+ "utf8"
177
+ )
178
+
179
+ // SECURITY: Verify the dangerous pattern is NOT present
180
+ expect(webpackPluginSource).not.toMatch(
181
+ /new webpack\.EnvironmentPlugin\(process\.env\)/
182
+ )
183
+
184
+ // Verify the safe pattern IS present
185
+ expect(webpackPluginSource).toMatch(/getFilteredEnv\(\)/)
186
+ })
187
+
188
+ it("does not duplicate the filtering logic", () => {
189
+ const webpackPluginSource = fs.readFileSync(
190
+ path.join(pluginsDir, "webpack.ts"),
191
+ "utf8"
192
+ )
193
+
194
+ // Should NOT have its own copy of these
195
+ expect(webpackPluginSource).not.toContain("DEFAULT_ALLOWED_ENV_VARS")
196
+ expect(webpackPluginSource).not.toContain("PUBLIC_ENV_PREFIX")
197
+ expect(webpackPluginSource).not.toContain("getAllowedEnvVars")
198
+ })
199
+ })
200
+
201
+ describe("rspack plugin", () => {
202
+ it("imports from shared envFilter module", () => {
203
+ const rspackPluginSource = fs.readFileSync(
204
+ path.join(pluginsDir, "rspack.ts"),
205
+ "utf8"
206
+ )
207
+
208
+ expect(rspackPluginSource).toContain(
209
+ 'import { getFilteredEnv } from "./envFilter"'
210
+ )
211
+ })
212
+
213
+ it("uses getFilteredEnv() not process.env", () => {
214
+ const rspackPluginSource = fs.readFileSync(
215
+ path.join(pluginsDir, "rspack.ts"),
216
+ "utf8"
217
+ )
218
+
219
+ // SECURITY: Verify the dangerous pattern is NOT present
220
+ expect(rspackPluginSource).not.toMatch(
221
+ /new rspack\.EnvironmentPlugin\(process\.env\)/
222
+ )
223
+
224
+ // Verify the safe pattern IS present
225
+ expect(rspackPluginSource).toMatch(/getFilteredEnv\(\)/)
226
+ })
227
+
228
+ it("does not duplicate the filtering logic", () => {
229
+ const rspackPluginSource = fs.readFileSync(
230
+ path.join(pluginsDir, "rspack.ts"),
231
+ "utf8"
232
+ )
233
+
234
+ // Should NOT have its own copy of these
235
+ expect(rspackPluginSource).not.toContain("DEFAULT_ALLOWED_ENV_VARS")
236
+ expect(rspackPluginSource).not.toContain("PUBLIC_ENV_PREFIX")
237
+ expect(rspackPluginSource).not.toContain("getAllowedEnvVars")
238
+ })
239
+ })
240
+
241
+ describe("consistency", () => {
242
+ it("both plugins use the same shared module", () => {
243
+ const webpackPluginSource = fs.readFileSync(
244
+ path.join(pluginsDir, "webpack.ts"),
245
+ "utf8"
246
+ )
247
+ const rspackPluginSource = fs.readFileSync(
248
+ path.join(pluginsDir, "rspack.ts"),
249
+ "utf8"
250
+ )
251
+
252
+ // Both should import from the same source
253
+ const webpackImport = webpackPluginSource.match(
254
+ /import\s*{[^}]*getFilteredEnv[^}]*}\s*from\s*["']([^"']+)["']/
255
+ )
256
+ const rspackImport = rspackPluginSource.match(
257
+ /import\s*{[^}]*getFilteredEnv[^}]*}\s*from\s*["']([^"']+)["']/
258
+ )
259
+
260
+ expect(webpackImport).toBeTruthy()
261
+ expect(rspackImport).toBeTruthy()
262
+ expect(webpackImport[1]).toBe(rspackImport[1])
263
+ })
264
+ })
265
+
266
+ /**
267
+ * Runtime behavioral tests that actually call the filtering functions.
268
+ * These complement the static source analysis tests above.
269
+ */
270
+ describe("runtime behavior", () => {
271
+ // Helper to get fresh module instance (avoiding caching issues)
272
+ const getEnvFilter = () => {
273
+ jest.resetModules()
274
+ return require("../../../package/plugins/envFilter")
275
+ }
276
+
277
+ describe("getAllowedEnvVars", () => {
278
+ it("returns default allowed vars when SHAKAPACKER_ENV_VARS is unset", () => {
279
+ delete process.env.SHAKAPACKER_ENV_VARS
280
+ // Remove any SHAKAPACKER_PUBLIC_* vars from test setup
281
+ const publicVars = Object.keys(process.env).filter((key) =>
282
+ key.startsWith("SHAKAPACKER_PUBLIC_")
283
+ )
284
+ publicVars.forEach((key) => {
285
+ delete process.env[key]
286
+ })
287
+
288
+ const { getAllowedEnvVars } = getEnvFilter()
289
+ const allowed = getAllowedEnvVars()
290
+
291
+ expect(allowed).toContain("NODE_ENV")
292
+ expect(allowed).toContain("RAILS_ENV")
293
+ expect(allowed).toContain("WEBPACK_SERVE")
294
+ expect(allowed).toHaveLength(3)
295
+ })
296
+
297
+ it("includes SHAKAPACKER_PUBLIC_* variables when present", () => {
298
+ delete process.env.SHAKAPACKER_ENV_VARS
299
+ process.env.SHAKAPACKER_PUBLIC_API_URL = "https://api.example.com"
300
+ process.env.SHAKAPACKER_PUBLIC_ANALYTICS_ID = "UA-12345"
301
+
302
+ const { getAllowedEnvVars } = getEnvFilter()
303
+ const allowed = getAllowedEnvVars()
304
+
305
+ expect(allowed).toContain("NODE_ENV")
306
+ expect(allowed).toContain("RAILS_ENV")
307
+ expect(allowed).toContain("WEBPACK_SERVE")
308
+ expect(allowed).toContain("SHAKAPACKER_PUBLIC_API_URL")
309
+ expect(allowed).toContain("SHAKAPACKER_PUBLIC_ANALYTICS_ID")
310
+
311
+ // Cleanup
312
+ delete process.env.SHAKAPACKER_PUBLIC_API_URL
313
+ delete process.env.SHAKAPACKER_PUBLIC_ANALYTICS_ID
314
+ })
315
+
316
+ it("does NOT include SHAKAPACKER_* system variables (without PUBLIC_)", () => {
317
+ process.env.SHAKAPACKER_CONFIG = "/custom/path"
318
+ process.env.SHAKAPACKER_PRECOMPILE = "true"
319
+ delete process.env.SHAKAPACKER_ENV_VARS
320
+
321
+ const { getAllowedEnvVars } = getEnvFilter()
322
+ const allowed = getAllowedEnvVars()
323
+
324
+ expect(allowed).not.toContain("SHAKAPACKER_CONFIG")
325
+ expect(allowed).not.toContain("SHAKAPACKER_PRECOMPILE")
326
+
327
+ // Cleanup
328
+ delete process.env.SHAKAPACKER_CONFIG
329
+ delete process.env.SHAKAPACKER_PRECOMPILE
330
+ })
331
+
332
+ it("parses SHAKAPACKER_ENV_VARS CSV and includes those variables", () => {
333
+ process.env.SHAKAPACKER_ENV_VARS = "CUSTOM_VAR1,CUSTOM_VAR2,ANOTHER_VAR"
334
+
335
+ const { getAllowedEnvVars } = getEnvFilter()
336
+ const allowed = getAllowedEnvVars()
337
+
338
+ expect(allowed).toContain("CUSTOM_VAR1")
339
+ expect(allowed).toContain("CUSTOM_VAR2")
340
+ expect(allowed).toContain("ANOTHER_VAR")
341
+ })
342
+
343
+ it("handles whitespace in SHAKAPACKER_ENV_VARS CSV", () => {
344
+ process.env.SHAKAPACKER_ENV_VARS = " VAR1 , VAR2 , VAR3 "
345
+
346
+ const { getAllowedEnvVars } = getEnvFilter()
347
+ const allowed = getAllowedEnvVars()
348
+
349
+ expect(allowed).toContain("VAR1")
350
+ expect(allowed).toContain("VAR2")
351
+ expect(allowed).toContain("VAR3")
352
+ })
353
+
354
+ it("ignores empty entries in SHAKAPACKER_ENV_VARS CSV", () => {
355
+ process.env.SHAKAPACKER_ENV_VARS = "VAR1,,VAR2,,,VAR3,"
356
+
357
+ const { getAllowedEnvVars } = getEnvFilter()
358
+ const allowed = getAllowedEnvVars()
359
+
360
+ expect(allowed).toContain("VAR1")
361
+ expect(allowed).toContain("VAR2")
362
+ expect(allowed).toContain("VAR3")
363
+ // Should not contain empty strings
364
+ expect(allowed.filter((v) => v === "")).toHaveLength(0)
365
+ })
366
+
367
+ it("deduplicates variables from multiple sources", () => {
368
+ process.env.SHAKAPACKER_ENV_VARS = "NODE_ENV,CUSTOM_VAR"
369
+
370
+ const { getAllowedEnvVars } = getEnvFilter()
371
+ const allowed = getAllowedEnvVars()
372
+
373
+ // NODE_ENV should only appear once (from defaults, not duplicated from CSV)
374
+ const nodeEnvCount = allowed.filter((v) => v === "NODE_ENV").length
375
+ expect(nodeEnvCount).toBe(1)
376
+ })
377
+ })
378
+
379
+ describe("getFilteredEnv", () => {
380
+ it("exposes allowed variables with their values", () => {
381
+ delete process.env.SHAKAPACKER_ENV_VARS
382
+ process.env.NODE_ENV = "production"
383
+ process.env.RAILS_ENV = "staging"
384
+
385
+ const { getFilteredEnv } = getEnvFilter()
386
+ const filtered = getFilteredEnv()
387
+
388
+ expect(filtered).toHaveProperty("NODE_ENV", "production")
389
+ expect(filtered).toHaveProperty("RAILS_ENV", "staging")
390
+ })
391
+
392
+ it("omits sensitive variables that are not in allowlist", () => {
393
+ delete process.env.SHAKAPACKER_ENV_VARS
394
+ // Sensitive vars are set in beforeEach
395
+
396
+ const { getFilteredEnv } = getEnvFilter()
397
+ const filtered = getFilteredEnv()
398
+
399
+ // SECURITY: These must NOT be present
400
+ expect(filtered).not.toHaveProperty("DATABASE_URL")
401
+ expect(filtered).not.toHaveProperty("AWS_ACCESS_KEY_ID")
402
+ expect(filtered).not.toHaveProperty("AWS_SECRET_ACCESS_KEY")
403
+ expect(filtered).not.toHaveProperty("RAILS_MASTER_KEY")
404
+ expect(filtered).not.toHaveProperty("STRIPE_SECRET_KEY")
405
+ expect(filtered).not.toHaveProperty("SESSION_SECRET")
406
+ })
407
+
408
+ it("exposes SHAKAPACKER_PUBLIC_* variables with their values", () => {
409
+ delete process.env.SHAKAPACKER_ENV_VARS
410
+ process.env.SHAKAPACKER_PUBLIC_API_URL = "https://api.example.com"
411
+
412
+ const { getFilteredEnv } = getEnvFilter()
413
+ const filtered = getFilteredEnv()
414
+
415
+ expect(filtered).toHaveProperty(
416
+ "SHAKAPACKER_PUBLIC_API_URL",
417
+ "https://api.example.com"
418
+ )
419
+
420
+ // Cleanup
421
+ delete process.env.SHAKAPACKER_PUBLIC_API_URL
422
+ })
423
+
424
+ it("uses null for missing variables from SHAKAPACKER_ENV_VARS", () => {
425
+ process.env.SHAKAPACKER_ENV_VARS = "MISSING_VAR,ANOTHER_MISSING"
426
+ delete process.env.MISSING_VAR
427
+ delete process.env.ANOTHER_MISSING
428
+
429
+ const { getFilteredEnv } = getEnvFilter()
430
+ const filtered = getFilteredEnv()
431
+
432
+ expect(filtered).toHaveProperty("MISSING_VAR", null)
433
+ expect(filtered).toHaveProperty("ANOTHER_MISSING", null)
434
+ })
435
+
436
+ it("includes variables from SHAKAPACKER_ENV_VARS with their values", () => {
437
+ process.env.SHAKAPACKER_ENV_VARS = "CUSTOM_API_URL"
438
+ process.env.CUSTOM_API_URL = "https://custom.example.com"
439
+
440
+ const { getFilteredEnv } = getEnvFilter()
441
+ const filtered = getFilteredEnv()
442
+
443
+ expect(filtered).toHaveProperty(
444
+ "CUSTOM_API_URL",
445
+ "https://custom.example.com"
446
+ )
447
+
448
+ // Cleanup
449
+ delete process.env.CUSTOM_API_URL
450
+ })
451
+ })
452
+ })
453
+ })
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shakapacker
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.4.0
4
+ version: 9.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -174,7 +174,6 @@ files:
174
174
  - ESLINT_TECHNICAL_DEBT.md
175
175
  - Gemfile
176
176
  - Gemfile.development_dependencies
177
- - Gemfile.lock
178
177
  - MIT-LICENSE
179
178
  - README.md
180
179
  - Rakefile
@@ -182,6 +181,7 @@ files:
182
181
  - TODO_v9.md
183
182
  - __mocks__/nonexistent/package.json
184
183
  - __mocks__/sass-loader/package.json
184
+ - bin/shakapacker-config
185
185
  - conductor-setup.sh
186
186
  - conductor.json
187
187
  - config/README.md
@@ -314,6 +314,7 @@ files:
314
314
  - package/loaders.d.ts
315
315
  - package/optimization/rspack.ts
316
316
  - package/optimization/webpack.ts
317
+ - package/plugins/envFilter.ts
317
318
  - package/plugins/rspack.ts
318
319
  - package/plugins/webpack.ts
319
320
  - package/rspack/index.ts
@@ -381,6 +382,7 @@ files:
381
382
  - test/package/environments/production.test.js
382
383
  - test/package/helpers.test.js
383
384
  - test/package/index.test.js
385
+ - test/package/plugins/envFiltering.test.js
384
386
  - test/package/production.test.js
385
387
  - test/package/rules/babel.test.js
386
388
  - test/package/rules/esbuild.test.js
@@ -413,7 +415,7 @@ homepage: https://github.com/shakacode/shakapacker
413
415
  licenses:
414
416
  - MIT
415
417
  metadata:
416
- source_code_uri: https://github.com/shakacode/shakapacker/tree/v9.4.0
418
+ source_code_uri: https://github.com/shakacode/shakapacker/tree/v9.5.0
417
419
  rdoc_options: []
418
420
  require_paths:
419
421
  - lib
@@ -448,6 +450,7 @@ test_files:
448
450
  - test/package/environments/production.test.js
449
451
  - test/package/helpers.test.js
450
452
  - test/package/index.test.js
453
+ - test/package/plugins/envFiltering.test.js
451
454
  - test/package/production.test.js
452
455
  - test/package/rules/babel.test.js
453
456
  - test/package/rules/esbuild.test.js
data/Gemfile.lock DELETED
@@ -1,254 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- shakapacker (9.4.0)
5
- activesupport (>= 5.2)
6
- package_json
7
- rack-proxy (>= 0.6.1)
8
- railties (>= 5.2)
9
- semantic_range (>= 2.3.0)
10
-
11
- GEM
12
- remote: https://rubygems.org/
13
- specs:
14
- actioncable (7.2.1)
15
- actionpack (= 7.2.1)
16
- activesupport (= 7.2.1)
17
- nio4r (~> 2.0)
18
- websocket-driver (>= 0.6.1)
19
- zeitwerk (~> 2.6)
20
- actionmailbox (7.2.1)
21
- actionpack (= 7.2.1)
22
- activejob (= 7.2.1)
23
- activerecord (= 7.2.1)
24
- activestorage (= 7.2.1)
25
- activesupport (= 7.2.1)
26
- mail (>= 2.8.0)
27
- actionmailer (7.2.1)
28
- actionpack (= 7.2.1)
29
- actionview (= 7.2.1)
30
- activejob (= 7.2.1)
31
- activesupport (= 7.2.1)
32
- mail (>= 2.8.0)
33
- rails-dom-testing (~> 2.2)
34
- actionpack (7.2.1)
35
- actionview (= 7.2.1)
36
- activesupport (= 7.2.1)
37
- nokogiri (>= 1.8.5)
38
- racc
39
- rack (>= 2.2.4, < 3.2)
40
- rack-session (>= 1.0.1)
41
- rack-test (>= 0.6.3)
42
- rails-dom-testing (~> 2.2)
43
- rails-html-sanitizer (~> 1.6)
44
- useragent (~> 0.16)
45
- actiontext (7.2.1)
46
- actionpack (= 7.2.1)
47
- activerecord (= 7.2.1)
48
- activestorage (= 7.2.1)
49
- activesupport (= 7.2.1)
50
- globalid (>= 0.6.0)
51
- nokogiri (>= 1.8.5)
52
- actionview (7.2.1)
53
- activesupport (= 7.2.1)
54
- builder (~> 3.1)
55
- erubi (~> 1.11)
56
- rails-dom-testing (~> 2.2)
57
- rails-html-sanitizer (~> 1.6)
58
- activejob (7.2.1)
59
- activesupport (= 7.2.1)
60
- globalid (>= 0.3.6)
61
- activemodel (7.2.1)
62
- activesupport (= 7.2.1)
63
- activerecord (7.2.1)
64
- activemodel (= 7.2.1)
65
- activesupport (= 7.2.1)
66
- timeout (>= 0.4.0)
67
- activestorage (7.2.1)
68
- actionpack (= 7.2.1)
69
- activejob (= 7.2.1)
70
- activerecord (= 7.2.1)
71
- activesupport (= 7.2.1)
72
- marcel (~> 1.0)
73
- activesupport (7.2.1)
74
- base64
75
- bigdecimal
76
- concurrent-ruby (~> 1.0, >= 1.3.1)
77
- connection_pool (>= 2.2.5)
78
- drb
79
- i18n (>= 1.6, < 2)
80
- logger (>= 1.4.2)
81
- minitest (>= 5.1)
82
- securerandom (>= 0.3)
83
- tzinfo (~> 2.0, >= 2.0.5)
84
- ast (2.4.2)
85
- base64 (0.2.0)
86
- bigdecimal (3.1.8)
87
- builder (3.3.0)
88
- byebug (11.1.3)
89
- concurrent-ruby (1.3.4)
90
- connection_pool (2.4.1)
91
- crass (1.0.6)
92
- date (3.3.4)
93
- diff-lcs (1.5.1)
94
- drb (2.2.1)
95
- erubi (1.13.0)
96
- globalid (1.2.1)
97
- activesupport (>= 6.1)
98
- i18n (1.14.5)
99
- concurrent-ruby (~> 1.0)
100
- io-console (0.7.2)
101
- irb (1.14.0)
102
- rdoc (>= 4.0.0)
103
- reline (>= 0.4.2)
104
- json (2.7.2)
105
- language_server-protocol (3.17.0.3)
106
- logger (1.6.0)
107
- loofah (2.22.0)
108
- crass (~> 1.0.2)
109
- nokogiri (>= 1.12.0)
110
- mail (2.8.1)
111
- mini_mime (>= 0.1.1)
112
- net-imap
113
- net-pop
114
- net-smtp
115
- marcel (1.0.4)
116
- mini_mime (1.1.5)
117
- mini_portile2 (2.8.7)
118
- minitest (5.25.1)
119
- net-imap (0.4.15)
120
- date
121
- net-protocol
122
- net-pop (0.1.2)
123
- net-protocol
124
- net-protocol (0.2.2)
125
- timeout
126
- net-smtp (0.5.0)
127
- net-protocol
128
- nio4r (2.7.3)
129
- nokogiri (1.16.7)
130
- mini_portile2 (~> 2.8.2)
131
- racc (~> 1.4)
132
- package_json (0.1.0)
133
- parallel (1.26.3)
134
- parser (3.3.4.2)
135
- ast (~> 2.4.1)
136
- racc
137
- psych (5.1.2)
138
- stringio
139
- racc (1.8.1)
140
- rack (3.1.7)
141
- rack-proxy (0.7.7)
142
- rack
143
- rack-session (2.0.0)
144
- rack (>= 3.0.0)
145
- rack-test (2.1.0)
146
- rack (>= 1.3)
147
- rackup (2.1.0)
148
- rack (>= 3)
149
- webrick (~> 1.8)
150
- rails (7.2.1)
151
- actioncable (= 7.2.1)
152
- actionmailbox (= 7.2.1)
153
- actionmailer (= 7.2.1)
154
- actionpack (= 7.2.1)
155
- actiontext (= 7.2.1)
156
- actionview (= 7.2.1)
157
- activejob (= 7.2.1)
158
- activemodel (= 7.2.1)
159
- activerecord (= 7.2.1)
160
- activestorage (= 7.2.1)
161
- activesupport (= 7.2.1)
162
- bundler (>= 1.15.0)
163
- railties (= 7.2.1)
164
- rails-dom-testing (2.2.0)
165
- activesupport (>= 5.0.0)
166
- minitest
167
- nokogiri (>= 1.6)
168
- rails-html-sanitizer (1.6.0)
169
- loofah (~> 2.21)
170
- nokogiri (~> 1.14)
171
- railties (7.2.1)
172
- actionpack (= 7.2.1)
173
- activesupport (= 7.2.1)
174
- irb (~> 1.13)
175
- rackup (>= 1.0.0)
176
- rake (>= 12.2)
177
- thor (~> 1.0, >= 1.2.2)
178
- zeitwerk (~> 2.6)
179
- rainbow (3.1.1)
180
- rake (13.2.1)
181
- rbs (3.9.5)
182
- logger
183
- rdoc (6.7.0)
184
- psych (>= 4.0.0)
185
- regexp_parser (2.9.2)
186
- reline (0.5.9)
187
- io-console (~> 0.5)
188
- rspec-core (3.13.0)
189
- rspec-support (~> 3.13.0)
190
- rspec-expectations (3.13.2)
191
- diff-lcs (>= 1.2.0, < 2.0)
192
- rspec-support (~> 3.13.0)
193
- rspec-mocks (3.13.1)
194
- diff-lcs (>= 1.2.0, < 2.0)
195
- rspec-support (~> 3.13.0)
196
- rspec-rails (6.0.4)
197
- actionpack (>= 6.1)
198
- activesupport (>= 6.1)
199
- railties (>= 6.1)
200
- rspec-core (~> 3.12)
201
- rspec-expectations (~> 3.12)
202
- rspec-mocks (~> 3.12)
203
- rspec-support (~> 3.12)
204
- rspec-support (3.13.1)
205
- rubocop (1.66.0)
206
- json (~> 2.3)
207
- language_server-protocol (>= 3.17.0)
208
- parallel (~> 1.10)
209
- parser (>= 3.3.0.2)
210
- rainbow (>= 2.2.2, < 4.0)
211
- regexp_parser (>= 2.4, < 3.0)
212
- rubocop-ast (>= 1.32.1, < 2.0)
213
- ruby-progressbar (~> 1.7)
214
- unicode-display_width (>= 2.4.0, < 3.0)
215
- rubocop-ast (1.32.1)
216
- parser (>= 3.3.1.0)
217
- rubocop-performance (1.21.1)
218
- rubocop (>= 1.48.1, < 2.0)
219
- rubocop-ast (>= 1.31.1, < 2.0)
220
- ruby-progressbar (1.13.0)
221
- securerandom (0.3.1)
222
- semantic_range (3.1.0)
223
- stringio (3.1.1)
224
- thor (1.3.2)
225
- timeout (0.4.1)
226
- tzinfo (2.0.6)
227
- concurrent-ruby (~> 1.0)
228
- unicode-display_width (2.5.0)
229
- useragent (0.16.10)
230
- webrick (1.8.1)
231
- websocket-driver (0.7.6)
232
- websocket-extensions (>= 0.1.0)
233
- websocket-extensions (0.1.5)
234
- zeitwerk (2.6.17)
235
-
236
- PLATFORMS
237
- ruby
238
-
239
- DEPENDENCIES
240
- bundler (>= 1.3.0)
241
- byebug
242
- nokogiri (>= 1.13.6)
243
- rack-proxy (>= 0.7.2)
244
- rails (>= 6.1.6)
245
- rails-html-sanitizer (>= 1.4.3)
246
- rake (>= 11.1)
247
- rbs (~> 3.0)
248
- rspec-rails (~> 6.0.2)
249
- rubocop
250
- rubocop-performance
251
- shakapacker!
252
-
253
- BUNDLED WITH
254
- 2.5.23