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,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
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const loadPluginsWithSriModule = (sriModule) => {
|
|
2
|
+
let getPlugins
|
|
3
|
+
|
|
4
|
+
jest.isolateModules(() => {
|
|
5
|
+
jest.doMock("../../../package/config", () => ({
|
|
6
|
+
manifestPath: "public/packs/manifest.json",
|
|
7
|
+
publicPathWithoutCDN: "/packs/",
|
|
8
|
+
integrity: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
hash_functions: ["sha256"]
|
|
11
|
+
},
|
|
12
|
+
css_extract_ignore_order_warnings: false,
|
|
13
|
+
useContentHash: false
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
jest.doMock("../../../package/env", () => ({
|
|
17
|
+
isProduction: true
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
jest.doMock("../../../package/utils/helpers", () => ({
|
|
21
|
+
moduleExists: (moduleName) =>
|
|
22
|
+
moduleName === "webpack-subresource-integrity"
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
jest.doMock("../../../package/utils/ensureManifestExists", () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
default: jest.fn()
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
jest.doMock("../../../package/utils/requireOrError", () => ({
|
|
31
|
+
requireOrError: (moduleName) => {
|
|
32
|
+
if (moduleName === "webpack-assets-manifest") {
|
|
33
|
+
return function WebpackAssetsManifest() {}
|
|
34
|
+
}
|
|
35
|
+
if (moduleName === "webpack") {
|
|
36
|
+
return {
|
|
37
|
+
EnvironmentPlugin: function EnvironmentPlugin() {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (moduleName === "webpack-subresource-integrity") {
|
|
41
|
+
return sriModule
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new Error(`Unexpected module request: ${moduleName}`)
|
|
45
|
+
}
|
|
46
|
+
}))
|
|
47
|
+
;({ getPlugins } = require("../../../package/plugins/webpack"))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return getPlugins
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe("webpack plugins - webpack-subresource-integrity compatibility", () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.clearAllMocks()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("supports webpack-subresource-integrity v5 named export", () => {
|
|
59
|
+
const SubresourceIntegrityPlugin = jest.fn(
|
|
60
|
+
function SubresourceIntegrityPluginMock(options) {
|
|
61
|
+
this.options = options
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
const getPlugins = loadPluginsWithSriModule({ SubresourceIntegrityPlugin })
|
|
65
|
+
|
|
66
|
+
getPlugins()
|
|
67
|
+
|
|
68
|
+
expect(SubresourceIntegrityPlugin).toHaveBeenCalledWith({
|
|
69
|
+
hashFuncNames: ["sha256"],
|
|
70
|
+
enabled: true
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("supports webpack-subresource-integrity default export", () => {
|
|
75
|
+
const SubresourceIntegrityPlugin = jest.fn(
|
|
76
|
+
function SubresourceIntegrityPluginMock(options) {
|
|
77
|
+
this.options = options
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
const getPlugins = loadPluginsWithSriModule(SubresourceIntegrityPlugin)
|
|
81
|
+
|
|
82
|
+
getPlugins()
|
|
83
|
+
|
|
84
|
+
expect(SubresourceIntegrityPlugin).toHaveBeenCalledWith({
|
|
85
|
+
hashFuncNames: ["sha256"],
|
|
86
|
+
enabled: true
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
})
|