-
Notifications
You must be signed in to change notification settings - Fork 84
Add typed ThemeCheckConfigError to theme-check-node #1241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| '@shopify/theme-check-node': minor | ||
| --- | ||
|
|
||
| Add `ThemeCheckConfigError` so configuration-resolution failures are catchable by type. | ||
|
|
||
| `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`, | ||
| so callers can recognize a configuration problem with a single `instanceof` check — or the | ||
| stable `code` (`'THEME_CHECK_CONFIG_ERROR'`) — instead of matching error messages, while the | ||
| original error's details remain available on `cause`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /** | ||
| * Thrown by `loadConfig` when a Theme Check configuration cannot be resolved. | ||
| * | ||
| * Any failure while reading, parsing, or validating the configuration is | ||
| * surfaced as a `ThemeCheckConfigError`, so callers can recognize a | ||
| * configuration problem with a single `instanceof` check (or the stable | ||
| * `code`) instead of matching error messages. The underlying error — a | ||
| * filesystem error, a YAML parse error, a validation error, etc. — is | ||
| * preserved on `cause`, so its `code`, line/column, and message remain | ||
| * available to callers that want the detail. | ||
| */ | ||
| export class ThemeCheckConfigError extends Error { | ||
| /** | ||
| * Stable discriminator for recognizing this error across module/realm | ||
| * boundaries where `instanceof` can be unreliable (duplicate package copies). | ||
| */ | ||
| readonly code = 'THEME_CHECK_CONFIG_ERROR'; | ||
| /** The config path or modern identifier that failed to load, when known. */ | ||
| readonly configPath?: string; | ||
|
|
||
| constructor(message: string, options: { cause?: unknown; configPath?: string } = {}) { | ||
| super(message, { cause: options.cause }); | ||
| this.name = 'ThemeCheckConfigError'; | ||
| this.configPath = options.configPath; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Wraps any error thrown while resolving a configuration in a | ||
| * `ThemeCheckConfigError`, attaching the config path and preserving the | ||
| * original error on `cause`. The original message is surfaced inline so the | ||
| * default output stays useful without unwrapping `cause`. | ||
| */ | ||
| export function toThemeCheckConfigError( | ||
| error: unknown, | ||
| configPath?: string, | ||
| ): ThemeCheckConfigError { | ||
| if (error instanceof ThemeCheckConfigError) return error; | ||
|
|
||
| const errno = error instanceof Error ? (error as NodeJS.ErrnoException) : undefined; | ||
| const location = errno?.path ?? configPath; | ||
| const detail = errno?.message ?? String(error); | ||
|
|
||
| return new ThemeCheckConfigError( | ||
| `Failed to load Theme Check configuration${location ? ` from ${location}` : ''}: ${detail}`, | ||
| { cause: error, configPath: location }, | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| export { loadConfig } from './load-config'; | ||
| export { findConfigPath } from './find-config-path'; | ||
| export { ThemeCheckConfigError } from './errors'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { loadConfigDescription } from './load-config-description'; | |
| import { resolveConfig } from './resolve'; | ||
| import { ModernIdentifier } from './types'; | ||
| import { validateConfig } from './validation'; | ||
| import { toThemeCheckConfigError } from './errors'; | ||
| import fs from 'fs/promises'; | ||
|
|
||
| /** | ||
|
|
@@ -23,19 +24,27 @@ export async function loadConfig( | |
| root: AbsolutePath, | ||
| ): Promise<Config> { | ||
| if (!root) throw new Error('loadConfig cannot be called without a root argument'); | ||
| let defaultChecks = 'theme-check:recommended'; | ||
|
|
||
| if (!configPath) { | ||
| const files = await fs.readdir(root); | ||
| // *.extension.toml implies that we're already in the appropriate extensions | ||
| // directory. *.app.toml implies that we're inside the root of an app. | ||
| if (files.some((file) => file.endsWith('.extension.toml') || file.endsWith('.app.toml'))) { | ||
| defaultChecks = 'theme-check:theme-app-extension'; | ||
| // Config resolution is a single, well-defined operation: any failure while | ||
| // reading, parsing, or validating the configuration is a configuration | ||
| // problem, so we surface it as a typed `ThemeCheckConfigError`. | ||
| try { | ||
| let defaultChecks = 'theme-check:recommended'; | ||
|
|
||
| if (!configPath) { | ||
| const files = await fs.readdir(root); | ||
| // *.extension.toml implies that we're already in the appropriate extensions | ||
| // directory. *.app.toml implies that we're inside the root of an app. | ||
| if (files.some((file) => file.endsWith('.extension.toml') || file.endsWith('.app.toml'))) { | ||
| defaultChecks = 'theme-check:theme-app-extension'; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const configDescription = await resolveConfig(configPath ?? defaultChecks, true); | ||
| const config = await loadConfigDescription(configDescription, root); | ||
| validateConfig(config); | ||
| return config; | ||
| const configDescription = await resolveConfig(configPath ?? defaultChecks, true); | ||
| const config = await loadConfigDescription(configDescription, root); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (NEEDS VALIDATION) 🐛 Bug: Configured Suggestion: Thread a strict/non-strict distinction through the loading path: explicit |
||
| validateConfig(config); | ||
| return config; | ||
| } catch (error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 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. |
||
| throw toThemeCheckConfigError(error, typeof configPath === 'string' ? configPath : undefined); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to add an expectation for the actual code since that'll be the contract for consumers.