Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Changes since the last non-beta release.

### Fixed

- **Improved type safety and error handling in configExporter module**. [PR #778](https://github.com/shakacode/shakapacker/pull/778) by [justin808](https://github.com/justin808). Resolves [#707](https://github.com/shakacode/shakapacker/issues/707).
- Enhanced type safety across configFile, buildValidator, and yamlSerializer modules
- Improved error message preservation for webpack/rspack build failures
- Fixed edge cases in YAML serialization (empty arrays, malformed objects)
- More robust constructor name detection for object serialization
- Better handling of Symbol, BigInt, and edge case types
- **Default template no longer triggers production warning**. [PR #774](https://github.com/shakacode/shakapacker/pull/774) by [justin808](https://github.com/justin808). Fixes [#703](https://github.com/shakacode/shakapacker/issues/703).
- Changed default `useContentHash` to `true` in `shakapacker.yml` template
- Eliminates confusing warning about `useContentHash: false` not being allowed in production
Expand Down
49 changes: 39 additions & 10 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ module.exports = [
// Temporarily ignore TypeScript files until technical debt is resolved
// See ESLINT_TECHNICAL_DEBT.md for tracking
// TODO: Remove this once ESLint issues are fixed (tracked in #723)
"package/**/*.ts"
// Exception: configExporter is being fixed in #707
"package/**/*.ts",
"!package/configExporter/**/*.ts" // Enable linting for configExporter (issue #707)
]
},

Expand Down Expand Up @@ -189,21 +191,48 @@ module.exports = [
}
},
{
// #707: Significant type safety improvements in configExporter module!
// - configFile.ts: ✅ Fully type-safe (0 type errors)
// - buildValidator.ts: ✅ Fully type-safe (0 type errors)
// - yamlSerializer.ts: ✅ Fully type-safe (0 type errors)
// - cli.ts: ⚠️ Partial (dynamic webpack config loading requires some `any`)
//
// Remaining overrides are for:
// 1. Code style/organization (not type safety)
// 2. Dynamic require() in cli.ts for webpack config loading
files: ["package/configExporter/**/*.ts"],
rules: {
// Code organization (functions before use due to large file)
"@typescript-eslint/no-use-before-define": "off",
// Import style (CommonJS require for dynamic imports)
"@typescript-eslint/no-require-imports": "off",
"import/no-dynamic-require": "off",
"global-require": "off",
// Class methods that are part of public API
"class-methods-use-this": "off",
// Template expressions (valid use cases with union types)
"@typescript-eslint/restrict-template-expressions": "off",
// Style preferences
"no-continue": "off",
"import/prefer-default-export": "off",
"no-await-in-loop": "off",
"no-underscore-dangle": "off",
"no-shadow": "off",
"no-restricted-globals": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/require-await": "off"
}
},
{
// cli.ts: Dynamic webpack config loading requires `any` types
// This is acceptable as webpack configs can have any shape
files: ["package/configExporter/cli.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/require-await": "off",
"no-await-in-loop": "off",
"import/prefer-default-export": "off",
"global-require": "off",
"no-underscore-dangle": "off"
"@typescript-eslint/no-unsafe-return": "off"
}
},
{
Expand Down
48 changes: 36 additions & 12 deletions package/configExporter/buildValidator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spawn } from "child_process"
import { existsSync } from "fs"
import { resolve, relative, sep } from "path"
import { resolve, relative } from "path"
import { ResolvedBuildConfig, BuildValidationResult } from "./types"

export interface ValidatorOptions {
Expand Down Expand Up @@ -397,8 +397,8 @@ export class BuildValidator {
})
}

child.stdout?.on("data", (data) => processOutput(data))
child.stderr?.on("data", (data) => processOutput(data))
child.stdout?.on("data", (data: Buffer) => processOutput(data))
child.stderr?.on("data", (data: Buffer) => processOutput(data))

child.on("exit", (code) => {
clearTimeout(timeoutId)
Expand Down Expand Up @@ -603,7 +603,7 @@ export class BuildValidator {

// Parse JSON output
try {
const jsonOutput: WebpackJsonOutput = JSON.parse(stdoutData)
const jsonOutput = JSON.parse(stdoutData) as WebpackJsonOutput

// Extract output path if available
if (jsonOutput.outputPath) {
Expand All @@ -613,10 +613,22 @@ export class BuildValidator {
// Check for errors in webpack/rspack JSON output
if (jsonOutput.errors && jsonOutput.errors.length > 0) {
jsonOutput.errors.forEach((error) => {
const errorMsg =
typeof error === "string"
? error
: error.message || String(error)
let errorMsg: string
if (typeof error === "string") {
errorMsg = error
} else if (error.message) {
errorMsg = error.message
} else {
// Attempt to extract useful info from malformed error using all enumerable props
try {
errorMsg = JSON.stringify(
error,
Object.getOwnPropertyNames(error)
)
} catch {
errorMsg = "[Error object with no message]"
}
}
result.errors.push(errorMsg)
// Also add to output for visibility
if (!this.options.verbose) {
Expand All @@ -628,10 +640,22 @@ export class BuildValidator {
// Check for warnings
if (jsonOutput.warnings && jsonOutput.warnings.length > 0) {
jsonOutput.warnings.forEach((warning) => {
const warningMsg =
typeof warning === "string"
? warning
: warning.message || String(warning)
let warningMsg: string
if (typeof warning === "string") {
warningMsg = warning
} else if (warning.message) {
warningMsg = warning.message
} else {
// Attempt to extract useful info from malformed warning using all enumerable props
try {
warningMsg = JSON.stringify(
warning,
Object.getOwnPropertyNames(warning)
)
} catch {
warningMsg = "[Warning object with no message]"
}
}
result.warnings.push(warningMsg)
})
}
Expand Down
3 changes: 1 addition & 2 deletions package/configExporter/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ let VERSION = "unknown"
try {
const packageJson = JSON.parse(
readFileSync(resolve(__dirname, "../../package.json"), "utf8")
)
) as { version?: string }
VERSION = packageJson.version || "unknown"
} catch (error) {
console.warn(
Expand Down Expand Up @@ -309,7 +309,6 @@ QUICK START (for troubleshooting):
description:
"Generate only server config (fallback when no config file exists)"
})
// eslint-disable-next-line no-shadow
.check((argv) => {
if (argv.webpack && argv.rspack) {
throw new Error(
Expand Down
32 changes: 18 additions & 14 deletions package/configExporter/configFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { resolve, relative, isAbsolute } from "path"
import { load as loadYaml, FAILSAFE_SCHEMA } from "js-yaml"
import {
BundlerConfigFile,
BuildConfig,
ResolvedBuildConfig,
ExportOptions,
DEFAULT_CONFIG_FILE
Expand Down Expand Up @@ -47,7 +46,7 @@ export class ConfigFileLoader {
// If file doesn't exist yet, just use the resolved path
realPath = absPath
}
} catch (error) {
} catch {
// If we can't resolve the path, use the original
realPath = absPath
}
Expand Down Expand Up @@ -95,9 +94,11 @@ export class ConfigFileLoader {

this.validate(parsed)
return parsed
} catch (error: any) {
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : String(error)
throw new Error(
`Failed to load config file ${this.configFilePath}: ${error.message}`
`Failed to load config file ${this.configFilePath}: ${errorMessage}`
)
}
}
Expand Down Expand Up @@ -294,7 +295,7 @@ export class ConfigFileLoader {
// Replace ${VAR:-default} with VAR value or default
expanded = expanded.replace(
/\$\{([^}:]+):-([^}]*)\}/g,
(_, varName, defaultValue) => {
(_: string, varName: string, defaultValue: string) => {
// Validate env var name to prevent regex injection
if (!this.isValidEnvVarName(varName)) {
console.warn(
Expand All @@ -307,16 +308,19 @@ export class ConfigFileLoader {
)

// Replace ${VAR} with VAR value
expanded = expanded.replace(/\$\{([^}:]+)\}/g, (_, varName) => {
// Validate env var name to prevent regex injection
if (!this.isValidEnvVarName(varName)) {
console.warn(
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
)
return `\${${varName}}`
expanded = expanded.replace(
/\$\{([^}:]+)\}/g,
(_: string, varName: string) => {
// Validate env var name to prevent regex injection
if (!this.isValidEnvVarName(varName)) {
console.warn(
`[Config Exporter] Warning: Invalid environment variable name: ${varName}`
)
return `\${${varName}}`
}
return process.env[varName] || ""
}
return process.env[varName] || ""
})
)

return expanded
}
Expand Down
Loading