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.
Files changed (265) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/address-review.md +206 -0
  3. data/.claude/commands/update-changelog.md +354 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +6 -9
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +6 -8
  6. data/.github/STATUS.md +1 -0
  7. data/.github/actionlint-matcher.json +17 -0
  8. data/.github/workflows/claude-code-review.yml +45 -0
  9. data/.github/workflows/claude.yml +55 -0
  10. data/.github/workflows/dummy.yml +18 -5
  11. data/.github/workflows/eslint-validation.yml +46 -0
  12. data/.github/workflows/generator.yml +38 -22
  13. data/.github/workflows/node.yml +116 -2
  14. data/.github/workflows/ruby.yml +57 -15
  15. data/.github/workflows/test-bundlers.yml +180 -0
  16. data/.gitignore +27 -0
  17. data/.husky/pre-commit +2 -0
  18. data/.npmignore +56 -0
  19. data/.prettierignore +7 -0
  20. data/.rubocop.yml +2 -0
  21. data/.yalcignore +26 -0
  22. data/CHANGELOG.md +487 -19
  23. data/CLAUDE.md +63 -0
  24. data/CONTRIBUTING.md +268 -21
  25. data/ESLINT_TECHNICAL_DEBT.md +165 -0
  26. data/README.md +497 -137
  27. data/Rakefile +44 -4
  28. data/TODO.md +58 -0
  29. data/TODO_v9.md +97 -0
  30. data/bin/conductor-exec +24 -0
  31. data/bin/shakapacker-config +11 -0
  32. data/conductor-setup.sh +147 -0
  33. data/conductor.json +9 -0
  34. data/docs/api-reference.md +519 -0
  35. data/docs/cdn_setup.md +384 -0
  36. data/docs/common-upgrades.md +695 -0
  37. data/docs/configuration.md +845 -0
  38. data/docs/css-modules-export-mode.md +566 -0
  39. data/docs/customizing_babel_config.md +16 -16
  40. data/docs/deployment.md +78 -7
  41. data/docs/developing_shakapacker.md +6 -0
  42. data/docs/early_hints.md +433 -0
  43. data/docs/early_hints_manual_api.md +454 -0
  44. data/docs/feature_testing.md +492 -0
  45. data/docs/node_package_api.md +70 -0
  46. data/docs/optional-peer-dependencies.md +203 -0
  47. data/docs/peer-dependencies.md +71 -0
  48. data/docs/precompile_hook.md +486 -0
  49. data/docs/preventing_fouc.md +132 -0
  50. data/docs/react.md +58 -48
  51. data/docs/releasing.md +288 -0
  52. data/docs/rspack.md +218 -0
  53. data/docs/rspack_migration_guide.md +862 -0
  54. data/docs/sprockets.md +1 -0
  55. data/docs/style_loader_vs_mini_css.md +12 -12
  56. data/docs/subresource_integrity.md +13 -7
  57. data/docs/transpiler-migration.md +212 -0
  58. data/docs/transpiler-performance.md +200 -0
  59. data/docs/troubleshooting.md +272 -24
  60. data/docs/typescript-migration.md +388 -0
  61. data/docs/typescript.md +103 -0
  62. data/docs/using_esbuild_loader.md +12 -12
  63. data/docs/using_swc_loader.md +121 -16
  64. data/docs/v6_upgrade.md +42 -19
  65. data/docs/v7_upgrade.md +8 -6
  66. data/docs/v8_upgrade.md +13 -12
  67. data/docs/v9_upgrade.md +616 -0
  68. data/eslint.config.fast.js +254 -0
  69. data/eslint.config.js +309 -0
  70. data/jest.config.js +8 -1
  71. data/knip.ts +61 -0
  72. data/lib/install/bin/shakapacker +4 -6
  73. data/lib/install/bin/shakapacker-config +11 -0
  74. data/lib/install/bin/shakapacker-dev-server +1 -1
  75. data/lib/install/binstubs.rb +6 -2
  76. data/lib/install/config/rspack/rspack.config.js +6 -0
  77. data/lib/install/config/rspack/rspack.config.ts +7 -0
  78. data/lib/install/config/shakapacker.yml +75 -12
  79. data/lib/install/config/webpack/webpack.config.ts +7 -0
  80. data/lib/install/package.json +38 -0
  81. data/lib/install/template.rb +207 -45
  82. data/lib/shakapacker/build_config_loader.rb +147 -0
  83. data/lib/shakapacker/bundler_switcher.rb +415 -0
  84. data/lib/shakapacker/compiler.rb +87 -0
  85. data/lib/shakapacker/configuration.rb +475 -6
  86. data/lib/shakapacker/dev_server.rb +88 -1
  87. data/lib/shakapacker/dev_server_runner.rb +240 -6
  88. data/lib/shakapacker/doctor.rb +1191 -0
  89. data/lib/shakapacker/env.rb +19 -3
  90. data/lib/shakapacker/helper.rb +411 -14
  91. data/lib/shakapacker/install/env.rb +33 -0
  92. data/lib/shakapacker/instance.rb +93 -4
  93. data/lib/shakapacker/manifest.rb +167 -30
  94. data/lib/shakapacker/railtie.rb +4 -0
  95. data/lib/shakapacker/rspack_runner.rb +19 -0
  96. data/lib/shakapacker/runner.rb +668 -9
  97. data/lib/shakapacker/swc_migrator.rb +384 -0
  98. data/lib/shakapacker/utils/manager.rb +2 -0
  99. data/lib/shakapacker/utils/version_syntax_converter.rb +1 -1
  100. data/lib/shakapacker/version.rb +1 -1
  101. data/lib/shakapacker/version_checker.rb +1 -1
  102. data/lib/shakapacker/webpack_runner.rb +4 -42
  103. data/lib/shakapacker.rb +159 -1
  104. data/lib/tasks/shakapacker/binstubs.rake +4 -2
  105. data/lib/tasks/shakapacker/check_binstubs.rake +2 -2
  106. data/lib/tasks/shakapacker/doctor.rake +48 -0
  107. data/lib/tasks/shakapacker/export_bundler_config.rake +68 -0
  108. data/lib/tasks/shakapacker/install.rake +16 -4
  109. data/lib/tasks/shakapacker/migrate_to_swc.rake +13 -0
  110. data/lib/tasks/shakapacker/switch_bundler.rake +72 -0
  111. data/lib/tasks/shakapacker.rake +2 -0
  112. data/package/.npmignore +4 -0
  113. data/package/babel/preset.ts +59 -0
  114. data/package/config.ts +189 -0
  115. data/package/configExporter/buildValidator.ts +906 -0
  116. data/package/configExporter/cli.ts +1748 -0
  117. data/package/configExporter/configDocs.ts +102 -0
  118. data/package/configExporter/configFile.ts +663 -0
  119. data/package/configExporter/fileWriter.ts +112 -0
  120. data/package/configExporter/index.ts +15 -0
  121. data/package/configExporter/types.ts +159 -0
  122. data/package/configExporter/yamlSerializer.ts +391 -0
  123. data/package/dev_server.ts +27 -0
  124. data/package/env.ts +92 -0
  125. data/package/environments/__type-tests__/rspack-plugin-compatibility.ts +36 -0
  126. data/package/environments/base.ts +147 -0
  127. data/package/environments/development.ts +88 -0
  128. data/package/environments/production.ts +82 -0
  129. data/package/environments/test.ts +55 -0
  130. data/package/environments/types.ts +98 -0
  131. data/package/esbuild/index.ts +40 -0
  132. data/package/index.d.ts +68 -93
  133. data/package/index.d.ts.template +72 -0
  134. data/package/index.ts +104 -0
  135. data/package/loaders.d.ts +28 -0
  136. data/package/optimization/rspack.ts +36 -0
  137. data/package/optimization/webpack.ts +55 -0
  138. data/package/plugins/envFilter.ts +82 -0
  139. data/package/plugins/rspack.ts +119 -0
  140. data/package/plugins/webpack.ts +82 -0
  141. data/package/rspack/index.ts +91 -0
  142. data/package/rules/{babel.js → babel.ts} +2 -2
  143. data/package/rules/{coffee.js → coffee.ts} +1 -1
  144. data/package/rules/css.ts +3 -0
  145. data/package/rules/{erb.js → erb.ts} +1 -1
  146. data/package/rules/esbuild.ts +10 -0
  147. data/package/rules/file.ts +41 -0
  148. data/package/rules/{jscommon.js → jscommon.ts} +5 -4
  149. data/package/rules/{less.js → less.ts} +4 -4
  150. data/package/rules/raw.ts +28 -0
  151. data/package/rules/rspack.ts +174 -0
  152. data/package/rules/sass.ts +21 -0
  153. data/package/rules/{stylus.js → stylus.ts} +4 -8
  154. data/package/rules/swc.ts +10 -0
  155. data/package/rules/{index.js → webpack.ts} +1 -2
  156. data/package/swc/index.ts +54 -0
  157. data/package/types/README.md +90 -0
  158. data/package/types/index.ts +69 -0
  159. data/package/types.ts +105 -0
  160. data/package/utils/bundlerUtils.ts +232 -0
  161. data/package/utils/configPath.ts +6 -0
  162. data/package/utils/debug.ts +45 -0
  163. data/package/utils/defaultConfigPath.ts +7 -0
  164. data/package/utils/ensureManifestExists.ts +17 -0
  165. data/package/utils/errorCodes.ts +249 -0
  166. data/package/utils/errorHelpers.ts +152 -0
  167. data/package/utils/getStyleRule.ts +75 -0
  168. data/package/utils/helpers.ts +99 -0
  169. data/package/utils/{inliningCss.js → inliningCss.ts} +3 -3
  170. data/package/utils/pathValidation.ts +207 -0
  171. data/package/utils/requireOrError.ts +24 -0
  172. data/package/utils/snakeToCamelCase.ts +5 -0
  173. data/package/utils/typeGuards.ts +388 -0
  174. data/package/utils/validateDependencies.ts +61 -0
  175. data/package/webpack-types.d.ts +33 -0
  176. data/package/webpackDevServerConfig.ts +130 -0
  177. data/package.json +157 -18
  178. data/scripts/remove-use-strict.js +44 -0
  179. data/scripts/type-check-no-emit.js +27 -0
  180. data/shakapacker.gemspec +4 -2
  181. data/sig/shakapacker/commands.rbs +35 -0
  182. data/sig/shakapacker/compiler.rbs +65 -0
  183. data/sig/shakapacker/compiler_strategy.rbs +41 -0
  184. data/sig/shakapacker/configuration.rbs +140 -0
  185. data/sig/shakapacker/dev_server.rbs +56 -0
  186. data/sig/shakapacker/env.rbs +25 -0
  187. data/sig/shakapacker/helper.rbs +98 -0
  188. data/sig/shakapacker/instance.rbs +46 -0
  189. data/sig/shakapacker/manifest.rbs +69 -0
  190. data/sig/shakapacker/version.rbs +4 -0
  191. data/sig/shakapacker.rbs +66 -0
  192. data/test/configExporter/buildValidator.test.js +1295 -0
  193. data/test/configExporter/configFile.test.js +393 -0
  194. data/test/configExporter/integration.test.js +262 -0
  195. data/test/helpers.js +1 -1
  196. data/test/package/bundlerUtils.rspack.test.js +145 -0
  197. data/test/package/bundlerUtils.test.js +97 -0
  198. data/test/package/config.test.js +14 -0
  199. data/test/package/configExporter/cli.test.js +440 -0
  200. data/test/package/configExporter/types.test.js +163 -0
  201. data/test/package/configExporter.test.js +491 -0
  202. data/test/package/env.test.js +42 -7
  203. data/test/package/environments/base.test.js +14 -4
  204. data/test/package/helpers.test.js +2 -2
  205. data/test/package/plugins/envFiltering.test.js +453 -0
  206. data/test/package/plugins/webpackSubresourceIntegrity.test.js +89 -0
  207. data/test/package/rspack/index.test.js +293 -0
  208. data/test/package/rspack/optimization.test.js +86 -0
  209. data/test/package/rspack/plugins.test.js +185 -0
  210. data/test/package/rspack/rules.test.js +229 -0
  211. data/test/package/rules/babel.test.js +65 -38
  212. data/test/package/rules/esbuild.test.js +13 -4
  213. data/test/package/rules/file.test.js +7 -1
  214. data/test/package/rules/raw.test.js +40 -7
  215. data/test/package/rules/sass-version-parsing.test.js +71 -0
  216. data/test/package/rules/sass.test.js +11 -6
  217. data/test/package/rules/sass1.test.js +8 -5
  218. data/test/package/rules/sass16.test.js +24 -0
  219. data/test/package/rules/swc.test.js +50 -39
  220. data/test/package/rules/webpack.test.js +35 -0
  221. data/test/package/staging.test.js +4 -3
  222. data/test/package/transpiler-defaults.test.js +169 -0
  223. data/test/package/utils/ensureManifestExists.test.js +51 -0
  224. data/test/package/yamlSerializer.test.js +204 -0
  225. data/test/peer-dependencies.sh +85 -0
  226. data/test/resolver.js +34 -3
  227. data/test/scripts/remove-use-strict.test.js +125 -0
  228. data/test/typescript/build.test.js +118 -0
  229. data/test/typescript/environments.test.js +107 -0
  230. data/test/typescript/pathValidation.test.js +186 -0
  231. data/test/typescript/requireOrError.test.js +49 -0
  232. data/test/typescript/securityValidation.test.js +182 -0
  233. data/tools/README.md +134 -0
  234. data/tools/css-modules-v9-codemod.js +179 -0
  235. data/tsconfig.eslint.json +9 -0
  236. data/tsconfig.json +38 -0
  237. data/yarn.lock +3202 -1097
  238. metadata +212 -44
  239. data/.eslintignore +0 -4
  240. data/.eslintrc.js +0 -36
  241. data/Gemfile.lock +0 -251
  242. data/package/babel/preset.js +0 -48
  243. data/package/config.js +0 -56
  244. data/package/dev_server.js +0 -23
  245. data/package/env.js +0 -48
  246. data/package/environments/base.js +0 -171
  247. data/package/environments/development.js +0 -13
  248. data/package/environments/production.js +0 -88
  249. data/package/environments/test.js +0 -3
  250. data/package/esbuild/index.js +0 -40
  251. data/package/index.js +0 -40
  252. data/package/rules/css.js +0 -3
  253. data/package/rules/esbuild.js +0 -10
  254. data/package/rules/file.js +0 -29
  255. data/package/rules/raw.js +0 -5
  256. data/package/rules/sass.js +0 -18
  257. data/package/rules/swc.js +0 -10
  258. data/package/swc/index.js +0 -50
  259. data/package/utils/configPath.js +0 -4
  260. data/package/utils/defaultConfigPath.js +0 -2
  261. data/package/utils/getStyleRule.js +0 -40
  262. data/package/utils/helpers.js +0 -62
  263. data/package/utils/snakeToCamelCase.js +0 -5
  264. data/package/webpackDevServerConfig.js +0 -71
  265. data/test/package/rules/index.test.js +0 -16
@@ -0,0 +1,112 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "fs"
2
+ import { resolve, dirname, relative, isAbsolute, basename } from "path"
3
+ import { tmpdir } from "os"
4
+ import { FileOutput } from "./types"
5
+
6
+ /**
7
+ * Handles writing config exports to files.
8
+ * Supports single file output or multiple files (one per config).
9
+ */
10
+ export class FileWriter {
11
+ /**
12
+ * Write multiple config files (one per config in array)
13
+ */
14
+ static writeMultipleFiles(outputs: FileOutput[], targetDir: string): void {
15
+ // Ensure directory exists
16
+ FileWriter.ensureDirectory(targetDir)
17
+
18
+ // Write each file
19
+ outputs.forEach((output) => {
20
+ const safeName = basename(output.filename)
21
+ const filePath = resolve(targetDir, safeName)
22
+ FileWriter.validateOutputPath(filePath)
23
+ FileWriter.writeFile(filePath, output.content)
24
+ console.log(`[Config Exporter] Created: ${filePath}`)
25
+ })
26
+
27
+ console.log(
28
+ `[Config Exporter] Exported ${outputs.length} config file(s) to ${targetDir}`
29
+ )
30
+ }
31
+
32
+ /**
33
+ * Write a single file
34
+ */
35
+ static writeSingleFile(filePath: string, content: string): void {
36
+ // Ensure parent directory exists
37
+ const dir = dirname(filePath)
38
+ FileWriter.ensureDirectory(dir)
39
+
40
+ FileWriter.validateOutputPath(filePath)
41
+ FileWriter.writeFile(filePath, content)
42
+ console.log(`[Config Exporter] Created: ${filePath}`)
43
+ }
44
+
45
+ /**
46
+ * Generate filename for a config export
47
+ * Format without build: {bundler}-{env}-{type}.{ext}
48
+ * Format with build: {bundler}-{build}-{type}.{ext}
49
+ *
50
+ * @param bundler - The bundler type (webpack, rspack)
51
+ * @param env - The environment (development, production, test)
52
+ * @param configType - Type of config. Built-in: "client", "server", "all", "client-hmr". Custom: any string from outputs array
53
+ * @param format - Output format (yaml, json, inspect)
54
+ * @param buildName - Optional build name that overrides env in filename
55
+ *
56
+ * @example
57
+ * // Built-in types
58
+ * generateFilename("webpack", "development", "client", "yaml")
59
+ * // => "webpack-development-client.yml"
60
+ *
61
+ * @example
62
+ * // Custom output names
63
+ * generateFilename("webpack", "development", "client-modern", "yaml", "dev-hmr")
64
+ * // => "webpack-dev-hmr-client-modern.yml"
65
+ */
66
+ static generateFilename(
67
+ bundler: string,
68
+ env: string,
69
+ configType: string,
70
+ format: "yaml" | "json" | "inspect",
71
+ buildName?: string
72
+ ): string {
73
+ let ext: string
74
+ if (format === "yaml") {
75
+ ext = "yml"
76
+ } else if (format === "json") {
77
+ ext = "json"
78
+ } else {
79
+ ext = "txt"
80
+ }
81
+ const name = buildName || env
82
+ return `${bundler}-${name}-${configType}.${ext}`
83
+ }
84
+
85
+ private static writeFile(filePath: string, content: string): void {
86
+ writeFileSync(filePath, content, "utf8")
87
+ }
88
+
89
+ private static ensureDirectory(dir: string): void {
90
+ if (!existsSync(dir)) {
91
+ mkdirSync(dir, { recursive: true })
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Validate output path and warn if writing outside cwd
97
+ */
98
+ private static validateOutputPath(outputPath: string): void {
99
+ const absPath = resolve(outputPath)
100
+ const cwd = process.cwd()
101
+
102
+ const isWithin = (base: string, target: string) => {
103
+ const rel = relative(base, target)
104
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel))
105
+ }
106
+ if (!isWithin(cwd, absPath) && !isWithin(tmpdir(), absPath)) {
107
+ console.warn(
108
+ `[Config Exporter] Warning: Writing to ${absPath} which is outside current directory (${cwd}) or temp (${tmpdir()})`
109
+ )
110
+ }
111
+ }
112
+ }
@@ -0,0 +1,15 @@
1
+ export { run } from "./cli"
2
+ export type {
3
+ ExportOptions,
4
+ ConfigMetadata,
5
+ FileOutput,
6
+ BundlerConfigFile,
7
+ BuildConfig,
8
+ ResolvedBuildConfig,
9
+ BuildValidationResult
10
+ } from "./types"
11
+ export { YamlSerializer } from "./yamlSerializer"
12
+ export { FileWriter } from "./fileWriter"
13
+ export { getDocForKey } from "./configDocs"
14
+ export { ConfigFileLoader, generateSampleConfigFile } from "./configFile"
15
+ export { BuildValidator } from "./buildValidator"
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Environment variable names that can be set by build configurations.
3
+ * These are the only environment variables that build configs are allowed to set.
4
+ * This whitelist prevents malicious configs from modifying critical system variables.
5
+ */
6
+ export const BUILD_ENV_VARS = [
7
+ "NODE_ENV",
8
+ "RAILS_ENV",
9
+ "NODE_OPTIONS",
10
+ "BABEL_ENV",
11
+ "WEBPACK_SERVE",
12
+ "CLIENT_BUNDLE_ONLY",
13
+ "SERVER_BUNDLE_ONLY"
14
+ ] as const
15
+
16
+ /**
17
+ * Environment variables that must never be set by build configurations.
18
+ * Setting these could compromise system security or cause unexpected behavior.
19
+ */
20
+ export const DANGEROUS_ENV_VARS = [
21
+ "PATH",
22
+ "HOME",
23
+ "LD_PRELOAD",
24
+ "LD_LIBRARY_PATH",
25
+ "DYLD_LIBRARY_PATH",
26
+ "DYLD_INSERT_LIBRARIES"
27
+ ] as const
28
+
29
+ /**
30
+ * Type predicate to check if a string is in the BUILD_ENV_VARS whitelist
31
+ *
32
+ * Note: The type assertion is necessary because TypeScript's type system cannot
33
+ * infer that .includes() on a readonly const array will properly narrow the type.
34
+ * The assertion is safe because we're only widening the type for the includes() check.
35
+ */
36
+ export function isBuildEnvVar(
37
+ key: string
38
+ ): key is (typeof BUILD_ENV_VARS)[number] {
39
+ return (BUILD_ENV_VARS as readonly string[]).includes(key)
40
+ }
41
+
42
+ /**
43
+ * Type predicate to check if a string is in the DANGEROUS_ENV_VARS blacklist
44
+ *
45
+ * Note: The type assertion is necessary because TypeScript's type system cannot
46
+ * infer that .includes() on a readonly const array will properly narrow the type.
47
+ * The assertion is safe because we're only widening the type for the includes() check.
48
+ */
49
+ export function isDangerousEnvVar(
50
+ key: string
51
+ ): key is (typeof DANGEROUS_ENV_VARS)[number] {
52
+ return (DANGEROUS_ENV_VARS as readonly string[]).includes(key)
53
+ }
54
+
55
+ /**
56
+ * Default directory for config exports when using --doctor or file output modes.
57
+ */
58
+ export const DEFAULT_EXPORT_DIR = "shakapacker-config-exports"
59
+
60
+ /**
61
+ * Default config file path for bundler build configurations.
62
+ */
63
+ export const DEFAULT_CONFIG_FILE = "config/shakapacker-builds.yml"
64
+
65
+ export interface ExportOptions {
66
+ doctor?: boolean
67
+ saveDir?: string
68
+ stdout?: boolean
69
+ bundler?: "webpack" | "rspack"
70
+ env?: "development" | "production" | "test"
71
+ clientOnly?: boolean
72
+ serverOnly?: boolean
73
+ output?: string
74
+ format?: "yaml" | "json" | "inspect"
75
+ annotate?: boolean
76
+ verbose?: boolean
77
+ depth?: number | null
78
+ help?: boolean
79
+ // New config file options
80
+ init?: boolean
81
+ ssr?: boolean
82
+ configFile?: string
83
+ build?: string
84
+ listBuilds?: boolean
85
+ allBuilds?: boolean
86
+ // Validation options
87
+ validate?: boolean
88
+ validateBuild?: string
89
+ }
90
+
91
+ export interface ConfigMetadata {
92
+ exportedAt: string
93
+ bundler: string
94
+ environment: string
95
+ configFile: string
96
+ /**
97
+ * Type of webpack/rspack config output.
98
+ * Built-in types: "client", "server", "all", "client-hmr"
99
+ * Custom types: Any string matching your outputs array (e.g., "client-modern", "client-legacy", "server-bundle")
100
+ */
101
+ configType: string
102
+ configCount: number
103
+ buildName?: string // New: name of the build from config file
104
+ environmentVariables: {
105
+ NODE_ENV?: string
106
+ RAILS_ENV?: string
107
+ CLIENT_BUNDLE_ONLY?: string
108
+ SERVER_BUNDLE_ONLY?: string
109
+ WEBPACK_SERVE?: string
110
+ HMR?: string
111
+ }
112
+ }
113
+
114
+ export interface FileOutput {
115
+ filename: string
116
+ content: string
117
+ metadata: ConfigMetadata
118
+ }
119
+
120
+ // Config file schema types
121
+ export interface BundlerConfigFile {
122
+ default_bundler?: "webpack" | "rspack"
123
+ shakapacker_doctor_default_builds_here?: boolean
124
+ builds: Record<string, BuildConfig>
125
+ }
126
+
127
+ export interface BuildConfig {
128
+ description?: string
129
+ bundler?: "webpack" | "rspack"
130
+ dev_server?: boolean
131
+ environment?: Record<string, string>
132
+ bundler_env?: Record<string, string | boolean>
133
+ outputs?: string[]
134
+ config?: string
135
+ }
136
+
137
+ export interface ResolvedBuildConfig {
138
+ name: string
139
+ description?: string
140
+ bundler: "webpack" | "rspack"
141
+ environment: Record<string, string>
142
+ bundlerEnvArgs: string[] // Converted bundler_env to CLI args
143
+ outputs: string[]
144
+ configFile?: string
145
+ }
146
+
147
+ export interface BuildValidationResult {
148
+ buildName: string
149
+ success: boolean
150
+ errors: string[]
151
+ warnings: string[]
152
+ output: string[]
153
+ outputs?: string[] // Build outputs (e.g., ["client", "server"])
154
+ configFile?: string // Config file path if specified
155
+ outputPath?: string // Output directory where files are written
156
+ startTime?: number // Unix timestamp in milliseconds
157
+ endTime?: number // Unix timestamp in milliseconds
158
+ duration?: number // Duration in milliseconds
159
+ }
@@ -0,0 +1,391 @@
1
+ import { relative, isAbsolute } from "path"
2
+ import { ConfigMetadata } from "./types"
3
+ import { getDocForKey } from "./configDocs"
4
+
5
+ /**
6
+ * Serializes webpack/rspack config to YAML format with optional inline documentation.
7
+ * Handles functions, RegExp, and special objects that don't serialize well to standard YAML.
8
+ */
9
+ export class YamlSerializer {
10
+ private annotate: boolean
11
+
12
+ private appRoot: string
13
+
14
+ constructor(options: { annotate: boolean; appRoot: string }) {
15
+ this.annotate = options.annotate
16
+ this.appRoot = options.appRoot
17
+ }
18
+
19
+ /**
20
+ * Serialize a config object to YAML string with metadata header
21
+ */
22
+ serialize(config: unknown, metadata: ConfigMetadata): string {
23
+ const output: string[] = []
24
+
25
+ // Add metadata header
26
+ output.push(YamlSerializer.createHeader(metadata))
27
+ output.push("")
28
+
29
+ // Serialize the config
30
+ output.push(this.serializeValue(config, 0, ""))
31
+
32
+ return output.join("\n")
33
+ }
34
+
35
+ private static createHeader(metadata: ConfigMetadata): string {
36
+ const lines: string[] = []
37
+ lines.push(`# ${"=".repeat(77)}`)
38
+ lines.push("# Webpack/Rspack Configuration Export")
39
+ lines.push(`# Generated: ${metadata.exportedAt}`)
40
+ lines.push(`# Environment: ${metadata.environment}`)
41
+ lines.push(`# Bundler: ${metadata.bundler}`)
42
+ lines.push(`# Config Type: ${metadata.configType}`)
43
+ if (metadata.configCount > 1) {
44
+ lines.push(`# Total Configs: ${metadata.configCount}`)
45
+ }
46
+ lines.push(`# ${"=".repeat(77)}`)
47
+ return lines.join("\n")
48
+ }
49
+
50
+ private serializeValue(
51
+ value: unknown,
52
+ indent: number,
53
+ keyPath: string
54
+ ): string {
55
+ if (value === null || value === undefined) {
56
+ return "null"
57
+ }
58
+
59
+ if (typeof value === "boolean") {
60
+ return value.toString()
61
+ }
62
+
63
+ if (typeof value === "number") {
64
+ return value.toString()
65
+ }
66
+
67
+ if (typeof value === "string") {
68
+ return this.serializeString(value, indent)
69
+ }
70
+
71
+ if (typeof value === "function") {
72
+ return this.serializeFunction(
73
+ value as (...args: unknown[]) => unknown,
74
+ indent
75
+ )
76
+ }
77
+
78
+ if (value instanceof RegExp) {
79
+ // Extract pattern without surrounding slashes: "/pattern/flags" -> "pattern"
80
+ // Include flags as inline comment when present for semantic clarity
81
+ const regexStr = value.toString()
82
+ const lastSlash = regexStr.lastIndexOf("/")
83
+ const pattern = regexStr.slice(1, lastSlash)
84
+ const flags = regexStr.slice(lastSlash + 1)
85
+
86
+ const serializedPattern = this.serializeString(pattern, indent)
87
+
88
+ // Add flags as inline comment if present (e.g., "pattern" # flags: gi)
89
+ if (flags) {
90
+ return `${serializedPattern} # flags: ${flags}`
91
+ }
92
+
93
+ return serializedPattern
94
+ }
95
+
96
+ if (Array.isArray(value)) {
97
+ return this.serializeArray(value, indent, keyPath)
98
+ }
99
+
100
+ if (typeof value === "object") {
101
+ return this.serializeObject(
102
+ value as Record<string, unknown>,
103
+ indent,
104
+ keyPath
105
+ )
106
+ }
107
+
108
+ // Handle remaining types explicitly
109
+ if (typeof value === "symbol") return value.toString()
110
+ if (typeof value === "bigint") return value.toString()
111
+
112
+ // All remaining types are primitives (string, number, boolean, null, undefined)
113
+ // that String() handles safely - cast to exclude objects since we've already handled them
114
+ return String(value as string | number | boolean | null | undefined)
115
+ }
116
+
117
+ private serializeString(str: string, indent: number = 0): string {
118
+ // Make absolute paths relative for cleaner output
119
+ const cleaned = this.makePathRelative(str)
120
+
121
+ // Handle multiline strings
122
+ if (cleaned.includes("\n")) {
123
+ const lines = cleaned.split("\n")
124
+ const lineIndent = " ".repeat(indent + 2)
125
+ return `|\n${lines.map((line) => lineIndent + line).join("\n")}`
126
+ }
127
+
128
+ // Escape strings that need quoting in YAML
129
+ // YAML has many special characters that can cause parsing errors:
130
+ // : # ' " (basic delimiters)
131
+ // [ ] { } (flow collections)
132
+ // * & ! @ ` (special constructs: aliases, anchors, tags)
133
+ if (
134
+ cleaned.includes(":") ||
135
+ cleaned.includes("#") ||
136
+ cleaned.includes("'") ||
137
+ cleaned.includes('"') ||
138
+ cleaned.includes("[") ||
139
+ cleaned.includes("]") ||
140
+ cleaned.includes("{") ||
141
+ cleaned.includes("}") ||
142
+ cleaned.includes("*") ||
143
+ cleaned.includes("&") ||
144
+ cleaned.includes("!") ||
145
+ cleaned.includes("@") ||
146
+ cleaned.includes("`") ||
147
+ cleaned.startsWith(" ") ||
148
+ cleaned.endsWith(" ")
149
+ ) {
150
+ // Escape backslashes first, then quotes to avoid double-escaping
151
+ return `"${cleaned.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
152
+ }
153
+
154
+ return cleaned
155
+ }
156
+
157
+ private serializeFunction(
158
+ fn: (...args: unknown[]) => unknown,
159
+ indent: number = 0
160
+ ): string {
161
+ // Get function source code
162
+ const source = fn.toString()
163
+
164
+ // Pretty-print function: maintain readable formatting
165
+ const lines = source.split("\n")
166
+
167
+ // For very long functions, truncate
168
+ const maxLines = 50
169
+ const truncated = lines.length > maxLines
170
+ const displayLines = truncated ? lines.slice(0, maxLines) : lines
171
+
172
+ // Clean up indentation while preserving structure
173
+ const indentLevels = displayLines
174
+ .filter((l) => l.trim().length > 0)
175
+ .map((l) => l.match(/^\s*/)?.[0].length || 0)
176
+ const minIndent = indentLevels.length > 0 ? Math.min(...indentLevels) : 0
177
+
178
+ const formatted =
179
+ displayLines.map((line) => line.substring(minIndent)).join("\n") +
180
+ (truncated ? "\n..." : "")
181
+
182
+ // Use serializeString to properly handle multiline with correct indentation
183
+ return this.serializeString(formatted, indent)
184
+ }
185
+
186
+ private serializeArray(
187
+ arr: unknown[],
188
+ indent: number,
189
+ keyPath: string
190
+ ): string {
191
+ if (arr.length === 0) {
192
+ return "[]"
193
+ }
194
+
195
+ const lines: string[] = []
196
+ const itemIndent = " ".repeat(indent + 2)
197
+ const contentIndent = " ".repeat(indent + 4)
198
+
199
+ arr.forEach((item, index) => {
200
+ const itemPath = `${keyPath}[${index}]`
201
+
202
+ // Check if this is a plugin object and add its name as a comment
203
+ const pluginName = YamlSerializer.getConstructorName(item)
204
+ const isPlugin = pluginName && /(^|\.)plugins\[\d+\]/.test(itemPath)
205
+ const isEmpty =
206
+ typeof item === "object" &&
207
+ item !== null &&
208
+ !Array.isArray(item) &&
209
+ Object.keys(item).length === 0
210
+
211
+ // For non-empty plugins, add comment before the plugin
212
+ // For empty plugins, the name will be shown inline
213
+ if (isPlugin && !isEmpty) {
214
+ lines.push(`${itemIndent}# ${pluginName}`)
215
+ }
216
+
217
+ const serialized = this.serializeValue(item, indent + 4, itemPath)
218
+
219
+ // Add documentation for array items if available
220
+ if (this.annotate) {
221
+ const doc = getDocForKey(itemPath)
222
+ if (doc) {
223
+ lines.push(`${itemIndent}# ${doc}`)
224
+ }
225
+ }
226
+
227
+ if (typeof item === "object" && !Array.isArray(item) && item !== null) {
228
+ // For objects in arrays, emit marker on its own line and indent content
229
+ lines.push(`${itemIndent}-`)
230
+ const nonEmptyLines = serialized
231
+ .split("\n")
232
+ .filter((line: string) => line.trim().length > 0)
233
+ // Compute minimum leading whitespace to preserve relative indentation
234
+ const indentLevels = nonEmptyLines.map(
235
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
236
+ )
237
+ const minIndent =
238
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
239
+ nonEmptyLines.forEach((line: string) => {
240
+ // Remove only the common indent, preserving relative indentation
241
+ lines.push(contentIndent + line.substring(minIndent))
242
+ })
243
+ } else if (serialized.includes("\n")) {
244
+ // For multiline values, emit marker on its own line and indent content
245
+ lines.push(`${itemIndent}-`)
246
+ const nonEmptyLines = serialized
247
+ .split("\n")
248
+ .filter((line: string) => line.trim().length > 0)
249
+ // Compute minimum leading whitespace to preserve relative indentation
250
+ const indentLevels = nonEmptyLines.map(
251
+ (line: string) => line.match(/^\s*/)?.[0].length || 0
252
+ )
253
+ const minIndent =
254
+ indentLevels.length > 0 ? Math.min(...indentLevels) : 0
255
+ nonEmptyLines.forEach((line: string) => {
256
+ // Remove only the common indent, preserving relative indentation
257
+ lines.push(contentIndent + line.substring(minIndent))
258
+ })
259
+ } else {
260
+ // For simple values, keep on same line
261
+ lines.push(`${itemIndent}- ${serialized}`)
262
+ }
263
+ })
264
+
265
+ return `\n${lines.join("\n")}`
266
+ }
267
+
268
+ private serializeObject(
269
+ obj: Record<string, unknown>,
270
+ indent: number,
271
+ keyPath: string
272
+ ): string {
273
+ const keys = Object.keys(obj).sort()
274
+ const constructorName = YamlSerializer.getConstructorName(obj)
275
+
276
+ // For empty objects, show constructor name if available
277
+ if (keys.length === 0) {
278
+ if (constructorName) {
279
+ return `{} # ${constructorName}`
280
+ }
281
+ return "{}"
282
+ }
283
+
284
+ const lines: string[] = []
285
+ const keyIndent = " ".repeat(indent)
286
+ const valueIndent = " ".repeat(indent + 2)
287
+
288
+ keys.forEach((key) => {
289
+ const value = obj[key]
290
+ const fullKeyPath = keyPath ? `${keyPath}.${key}` : key
291
+
292
+ // Add documentation comment if available and annotation is enabled
293
+ if (this.annotate) {
294
+ const doc = getDocForKey(fullKeyPath)
295
+ if (doc) {
296
+ lines.push(`${keyIndent}# ${doc}`)
297
+ }
298
+ }
299
+
300
+ // Handle multiline strings specially with block scalar
301
+ if (typeof value === "string" && value.includes("\n")) {
302
+ lines.push(`${keyIndent}${key}: |`)
303
+ for (const line of value.split("\n")) {
304
+ lines.push(`${valueIndent}${line}`)
305
+ }
306
+ } else if (value instanceof RegExp || typeof value === "function") {
307
+ // Handle RegExp and functions explicitly before the generic object check
308
+ // to prevent them from being treated as empty objects (RegExp/functions
309
+ // have no enumerable keys but should serialize as their string representation)
310
+ const serialized = this.serializeValue(value, indent + 2, fullKeyPath)
311
+ lines.push(`${keyIndent}${key}: ${serialized}`)
312
+ } else if (
313
+ typeof value === "object" &&
314
+ value !== null &&
315
+ !Array.isArray(value)
316
+ ) {
317
+ if (Object.keys(value).length === 0) {
318
+ lines.push(`${keyIndent}${key}: {}`)
319
+ } else {
320
+ lines.push(`${keyIndent}${key}:`)
321
+ const nestedLines = this.serializeObject(
322
+ value as Record<string, unknown>,
323
+ indent + 2,
324
+ fullKeyPath
325
+ )
326
+ lines.push(nestedLines)
327
+ }
328
+ } else if (Array.isArray(value)) {
329
+ if (value.length === 0) {
330
+ lines.push(`${keyIndent}${key}: []`)
331
+ } else {
332
+ lines.push(`${keyIndent}${key}:`)
333
+ const arrayLines = this.serializeArray(value, indent + 2, fullKeyPath)
334
+ lines.push(arrayLines)
335
+ }
336
+ } else {
337
+ const serialized = this.serializeValue(value, indent + 2, fullKeyPath)
338
+ lines.push(`${keyIndent}${key}: ${serialized}`)
339
+ }
340
+ })
341
+
342
+ return lines.join("\n")
343
+ }
344
+
345
+ private makePathRelative(str: string): string {
346
+ if (!isAbsolute(str)) return str
347
+
348
+ // Convert absolute paths to relative paths using path.relative
349
+ const rel = relative(this.appRoot, str)
350
+
351
+ if (rel === "") {
352
+ return "."
353
+ }
354
+
355
+ // If path is outside appRoot or already absolute, keep original
356
+ if (rel.startsWith("..") || isAbsolute(rel)) {
357
+ return str
358
+ }
359
+
360
+ return `./${rel}`
361
+ }
362
+
363
+ /**
364
+ * Extracts the constructor name from an object
365
+ * Returns null for plain objects (Object constructor) or objects without prototypes
366
+ */
367
+ private static getConstructorName(obj: unknown): string | null {
368
+ if (!obj || typeof obj !== "object") return null
369
+ if (Array.isArray(obj)) return null
370
+
371
+ // Use Object.getPrototypeOf for safer access to constructor
372
+ // This handles Object.create(null) and unusual prototypes correctly
373
+ try {
374
+ const proto = Object.getPrototypeOf(obj) as {
375
+ constructor?: { name?: string }
376
+ } | null
377
+ if (!proto || proto === Object.prototype) return null
378
+
379
+ const { constructor } = proto
380
+ if (!constructor || typeof constructor !== "function") return null
381
+
382
+ const constructorName = constructor.name
383
+ if (!constructorName || constructorName === "Object") return null
384
+
385
+ return constructorName
386
+ } catch {
387
+ // Handle frozen objects or other edge cases
388
+ return null
389
+ }
390
+ }
391
+ }
@@ -0,0 +1,27 @@
1
+ // These are the raw shakapacker dev server config settings from the YML file with ENV overrides applied.
2
+ import type { DevServerConfig, Config } from "./types"
3
+
4
+ const { isBoolean } = require("./utils/helpers")
5
+ const config = require("./config") as Config
6
+
7
+ const envFetch = (key: string): string | boolean | undefined => {
8
+ const value = process.env[key]
9
+ if (!value) return undefined
10
+ return isBoolean(value) ? JSON.parse(value) : value
11
+ }
12
+
13
+ const devServerConfig: DevServerConfig | undefined = config.dev_server
14
+
15
+ if (devServerConfig) {
16
+ const envPrefix = devServerConfig.env_prefix || "SHAKAPACKER_DEV_SERVER"
17
+
18
+ Object.keys(devServerConfig).forEach((key) => {
19
+ const envValue = envFetch(`${envPrefix}_${key.toUpperCase()}`)
20
+ if (envValue !== undefined) {
21
+ // Use bracket notation to avoid ASI issues
22
+ ;(devServerConfig as Record<string, unknown>)[key] = envValue
23
+ }
24
+ })
25
+ }
26
+
27
+ export = devServerConfig || {}