Add typed ThemeCheckConfigError to theme-check-node#1241
Conversation
787b6c4 to
d893eec
Compare
graygilmore
left a comment
There was a problem hiding this comment.
Love that we're fixing this at the root. A few comments, nothing major. Feel free to push back.
| } | ||
| } | ||
|
|
||
| interface NodeErrnoException extends Error { |
There was a problem hiding this comment.
Since we're in theme-check-node we can assume we have access to things inside of Node. Instead of a custom interface here we should be able to use NodeJS.ErrnoException:
const errno = error instanceof Error ? (error as NodeJS.ErrnoException) : undefined;| if (error instanceof ThemeCheckConfigError) return error; | ||
|
|
||
| const errno = error instanceof Error ? (error as NodeErrnoException) : undefined; | ||
| const location = configPath ?? errno?.path; |
There was a problem hiding this comment.
Pretty sure this bot comment is correct. You could could have a nested file like this:
# /project/.theme-check.yml
extends: ./nested.ymlWhere nested.yml is the thing that fails to load but we'd set .theme-check.yml as configPath which could hide the underlying issue. Up to you how to solve this. I think if the consumer (the CLI in our case) inspects cause it'll show the correct issue.
🐛 Bug: When a config extends another config and the nested file fails to load, loadConfig passes the top-level configPath into toThemeCheckConfigError for the whole resolution operation. The helper then prefers that supplied path over a more specific path on the underlying error, such as errno.path, so both ThemeCheckConfigError.configPath and the from ... message can point at .theme-check.yml instead of the missing or malformed extended file. For recursively parsed configs, parse errors may also need the current nested config path attached before they bubble back out.
Suggestion: Prefer the concrete path from the cause before falling back to the entry config path:
| const location = configPath ?? errno?.path; | |
| const location = errno?.path ?? configPath; |
For parse errors in recursively extended configs, consider wrapping or annotating inside resolveConfig with the current config path so the existing idempotent guard preserves the inner path.
| it('wraps a missing config file in a ThemeCheckConfigError, preserving the original error', async () => { | ||
| const missingPath = path.join(tempDir, 'does-not-exist.yml'); | ||
| const error = await loadConfig(missingPath, tempDir).catch((e) => e); | ||
| expect(error).to.be.an.instanceof(ThemeCheckConfigError); |
There was a problem hiding this comment.
Might be good to add an expectation for the actual code since that'll be the contract for consumers.
expect(error.code).to.equal('THEME_CHECK_CONFIG_ERROR');| expect((error.cause as NodeJS.ErrnoException)?.code).to.equal('EISDIR'); | ||
| }); | ||
|
|
||
| it('wraps an invalid configuration shape in a ThemeCheckConfigError', async () => { |
There was a problem hiding this comment.
💡 Improvement: The case named around invalid configuration shape uses UnusedAssign: true, which fails in readYamlConfigDescription because the check value is not a plain object. That means it demonstrates wrapping for YAML/shape parsing, but not a later validateConfig failure. The generic wrapper should still catch validation errors, but the advertised validation path is asserted rather than directly exercised.
Suggestion: Either rename this case to reflect that it covers YAML/shape parsing, or add a separate case that reaches validateConfig and throws there, such as a check setting with the wrong type.
| validateConfig(config); | ||
| return config; | ||
| const configDescription = await resolveConfig(configPath ?? defaultChecks, true); | ||
| const config = await loadConfigDescription(configDescription, root); |
There was a problem hiding this comment.
(NEEDS VALIDATION)
🐛 Bug: Configured require entries appear to be delegated to loadThirdPartyChecks, which catches require() failures, logs them, and returns no checks. That means a user-authored config such as require: './missing-checks.js' can still load successfully instead of producing the new ThemeCheckConfigError. In the related CLI PR, bad config is expected to be catchable and surfaced as a friendly config failure, so explicit config require failures should probably be strict even if auto-discovered third-party packages remain best-effort.
Suggestion: Thread a strict/non-strict distinction through the loading path: explicit configDescription.require entries should throw when they cannot be loaded so this catch can wrap them, while auto-discovered third-party check packages can preserve the existing log-and-ignore behavior. Add a regression test for a missing relative require path that asserts ThemeCheckConfigError, code, configPath, and cause.
| const config = await loadConfigDescription(configDescription, root); | ||
| validateConfig(config); | ||
| return config; | ||
| } catch (error) { |
There was a problem hiding this comment.
I thought this one was worth flagging, too. I think this is a reasonable approach but it might be good to see if we can narrow it in any kind of way.
📐 Design: The catch wraps the entire resolution chain and converts every thrown value into a ThemeCheckConfigError. In the related CLI PR, that type is treated as an invalid-configuration condition and anything else remains reportable as a bug. If a future refactor introduces a genuine internal TypeError or other logic error in this path, it would be shown to users as bad config instead of surfacing as a reportable crash. This may be an intentional boundary, but it is worth making explicit because the consumer treats the type as authoritative for bug-vs-config triage.
Suggestion: Consider whether clearly internal precondition or logic errors should pass through unwrapped while expected failure shapes such as filesystem errors, YAML parse errors, and validation errors are wrapped. If any failure in the load path should be considered a config problem, document that in the class JSDoc or catch comment so the consumer-facing boundary is recorded.
loadConfig now wraps any failure while reading, parsing, or validating a Theme Check configuration in a ThemeCheckConfigError. The error attaches the configPath and preserves the underlying error (filesystem error, YAML parse error, validation error, etc.) on `cause`, and surfaces the original message inline. This lets callers recognize a configuration problem with a single `instanceof` check (or the stable `code`) instead of matching error-message text, while the original error's details remain available on `cause`. Assisted-By: devx/e547eed5-2a6b-4b20-972a-c6fe290bdf0c
d893eec to
f837dfd
Compare
What
Add an exported
ThemeCheckConfigErrorand makeloadConfigthrow it for anyconfiguration-resolution failure — reading, parsing,
extends/requireresolution, or validation.
The error preserves the original failure on
cause(so the filesystemcode,YAML line/column, and message stay available) and carries the
configPaththatfailed. It also exposes a stable
codeof'THEME_CHECK_CONFIG_ERROR'forrecognition across realm/duplicate-package boundaries where
instanceofcan beunreliable.
Why
Today every config failure throws a bare
Error(rawfserrors, ad-hocnew Error(...)from validation, an un-exportedUnresolvedAliasError).Consumers that want to treat "bad config" differently from a real bug have to
match error-message strings, which is brittle and couples them to this package's
wording.
With a single typed class, callers can do
if (e instanceof ThemeCheckConfigError)and read
e.causefor the detail.loadConfigis the one choke point all configloading flows through (
getThemeAndConfig→themeCheckRun→checkincluded),and the lower-level resolvers aren't exported, so this covers the whole surface.
This follows the common config-loader convention (e.g. Vite, cosmiconfig): wrap
the failure with where it came from, chain the original via
cause, and surfaceits message — rather than a bespoke error-code taxonomy.
Backwards compatible:
ThemeCheckConfigError extends Error, so existingcatch/instanceof Errorhandling is unaffected.Consumer
The Shopify CLI uses this to turn a bad
theme checkconfiguration into a clear,user-facing error instead of an unexpected crash: Shopify/cli#7868