You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
I think it would be beneficial to modify the formatter logic to support pattern matching or compare against config defined extension rather than extracting it from the file path.
For ex. problematic extensions: .php + .blade.php
Currently the workaround for this is creating single rule:
I needed to rip out and analyst compare to give a few more options to compare for info to see if we missed anything that A.I. could give us to think about:
Alternative Approaches for Formatter Pattern Matching
This document explores different design approaches for solving the compound extension problem in the opencode formatter, each with trade-offs and use cases.
functionextractExtensions(filepath: string): string[]{constbasename=path.basename(filepath)constparts=basename.split('.')constextensions: string[]=[]// Extract all possible compound extensionsfor(leti=1;i<parts.length;i++){extensions.push('.'+parts.slice(i).join('.'))}returnextensions// [".spec.ts", ".ts"] for "file.spec.ts"}asyncfunctiongetFormatter(filepath: string){constextensions=extractExtensions(filepath)constformatters=awaitstate().then((x)=>x.formatters)constresult=[]for(constitemofObject.values(formatters)){// Check most specific extension firstfor(constextofextensions){if(item.extensions.includes(ext)){if(awaitisEnabled(item)){result.push(item)}break// Found match, stop checking this formatter}}}returnresult}
Pros:
✅ No new config fields needed
✅ Backward compatible
✅ Simple, intuitive behavior
✅ No external dependencies
✅ Efficient (simple string operations)
Cons:
Only works for extensions (can't match on directory paths)
Implementation:
Keep extensions for backward compatibility, add optional patterns for advanced cases.
extensions: z.array(z.string()).optional()
patterns: z.array(z.string()).optional()// Match order: patterns first, then compound extensions, then simple extensions
asyncfunctiongetFormatter(filepath: string){constformatters=awaitstate().then((x)=>x.formatters)constresult=[]for(constitemofObject.values(formatters)){letmatched=false// 1. Check patterns first (if defined)if(item.patterns){for(constpatternofitem.patterns){if(minimatch(filepath,pattern)){matched=truebreak}}}// 2. Fall back to compound extension matchingif(!matched&&item.extensions){constcompoundExts=extractExtensions(filepath)for(constextofcompoundExts){if(item.extensions.includes(ext)){matched=truebreak}}}if(matched&&awaitisEnabled(item)){result.push(item)}}returnresult}
Pros:
✅ Backward compatible
✅ Solves your use case elegantly
✅ Progressive enhancement (simple → advanced)
✅ Clear upgrade path for users
Cons:
Two ways to do similar things
Slightly more complex matching logic
Approach 6: Extension Suffix Matching (Simplest)
Implementation:
Just change how extensions are extracted - use suffix matching instead of path.extname().
// Current: path.extname("file.blade.php") → ".php"// New: checkExtension("file.blade.php", [".blade.php", ".php"]) → ".blade.php"functionmatchesExtension(filepath: string,extensions: string[]): string|null{constfilename=path.basename(filepath)// Sort by length (longest first) to match most specificconstsorted=[...extensions].sort((a,b)=>b.length-a.length)for(constextofsorted){if(filename.endsWith(ext)){returnext}}returnnull}
If you need path-based matching later, you could add Approach 5 (Hybrid) as an enhancement.
Comparison Matrix
Approach
Complexity
Backward Compat
Flexibility
Config Changes
1. Glob Patterns
Medium
✅
High
Optional
2. Priority Order
Low
✅
Medium
None
3. Regex
Medium
✅
Very High
Optional
4. Matcher Function
High
✅
Very High
Optional
5. Hybrid
Medium
✅
High
Optional
6. Suffix Match
Low
✅
Medium
None
Next Steps
Recommended: Implement Approach 6 for immediate value, with potential to add Approach 5 later if path-based patterns are needed.
Problem Context
Current Issue
The formatter currently uses path.extname() to extract file extensions, which only captures the last extension segment (e.g., .php from .blade.php). This creates issues for compound extensions like:
.blade.php → only matches .php
.html.erb → only matches .erb
.spec.ts → only matches .ts
Current Workaround
Users must create a wrapper script to handle pattern matching manually:
I've read through the discussion and the different approaches suggested (thanks @dkudos for the comprehensive analysis!).
I've implemented Approach 6: Extension Suffix Matching on my fork to solve the compound extension problem (.blade.php, .spec.ts, .html.erb, etc.).
Implementation Details
Problem: The current path.extname() only extracts the last extension:
component.blade.php → .php ❌
test.spec.ts → .ts ❌
Solution: Added extractExtensions() function that extracts ALL possible extensions from most specific to most general:
// For "component.blade.php"extractExtensions() returns: [".blade.php",".php"]// Matching logic then checks if any formatter extension// matches any of these file extensions
Changes made:
Modified packages/opencode/src/format/index.ts
Added extractExtensions(filepath: string): string[] helper function
Updated getFormatter() to accept full filepath instead of single extension
Changed matching logic from path.extname() to suffix matching
Why Approach 6:
✅ Zero breaking changes - fully backward compatible
✅ No config changes required - existing formatter configs work immediately
✅ Simplest implementation - only ~15 lines of code changed
✅ Intuitive behavior - more specific extensions match first
Activity
rekram1-node commentedon Oct 25, 2025
yeah I think this makes sense, i feel like pattern matching would be best probably
dkudos commentedon Nov 2, 2025
I needed to rip out and analyst compare to give a few more options to compare for info to see if we missed anything that A.I. could give us to think about:
Alternative Approaches for Formatter Pattern Matching
This document explores different design approaches for solving the compound extension problem in the opencode formatter, each with trade-offs and use cases.
Approach 1: Glob Pattern Matching (Original Proposal)
Implementation:
Example Config:
{ "formatter": { "blade": { "patterns": ["*.blade.php", "**/*.blade.php"], "command": ["blade-formatter", "$FILE"] } } }Pros:
Cons:
Approach 2: Extension Priority Order
Implementation:
Instead of extracting just the last extension, extract ALL potential extension combinations and check against a priority-ordered list.
Example Config:
{ "formatter": { "blade": { "extensions": [".blade.php"], "command": ["blade-formatter", "$FILE"] }, "php": { "extensions": [".php"], "command": ["pint", "$FILE"] } } }Implementation in format/index.ts:
Pros:
Cons:
Approach 3: Regex Pattern Matching
Implementation:
Example Config:
{ "formatter": { "blade": { "patterns": ["\\.blade\\.php$"], "command": ["blade-formatter", "$FILE"] }, "spec-files": { "patterns": ["\\.spec\\.(ts|js)$"], "command": ["spec-formatter", "$FILE"] } } }Pros:
Cons:
Approach 4: File Matcher Function (Most Flexible)
Implementation:
Allow custom matcher functions or predefined matchers.
Example Config:
{ "formatter": { "blade": { "matcher": { "type": "glob", "patterns": ["*.blade.php"] }, "command": ["blade-formatter", "$FILE"] }, "php": { "matcher": { "type": "extension", "values": [".php"], "compound": false }, "command": ["pint", "$FILE"] } } }Pros:
Cons:
Approach 5: Hybrid - Extensions + Optional Patterns
Implementation:
Keep
extensionsfor backward compatibility, add optionalpatternsfor advanced cases.Example Config:
{ "formatter": { "blade": { "patterns": ["*.blade.php"], "command": ["blade-formatter", "$FILE"] }, "typescript-spec": { "extensions": [".spec.ts", ".test.ts"], "command": ["spec-formatter", "$FILE"] }, "php": { "extensions": [".php"], "command": ["pint", "$FILE"] } } }Matching Logic:
Pros:
Cons:
Approach 6: Extension Suffix Matching (Simplest)
Implementation:
Just change how extensions are extracted - use suffix matching instead of
path.extname().Example Config (NO CHANGES NEEDED):
{ "formatter": { "blade": { "extensions": [".blade.php"], "command": ["blade-formatter", "$FILE"] }, "php": { "extensions": [".php"], "command": ["pint", "$FILE"] } } }Pros:
Cons:
My Recommendation: Approach 6 (Extension Suffix Matching)
Why?
Implementation Summary:
path.extname(file)with a suffix matching functionThis would allow your desired config to work immediately:
{ "formatter": { "blade": { "extensions": [".blade.php"], "command": ["blade-formatter", "$FILE"] }, "php": { "extensions": [".php"], "command": ["pint", "$FILE"] } } }If you need path-based matching later, you could add Approach 5 (Hybrid) as an enhancement.
Comparison Matrix
Next Steps
Recommended: Implement Approach 6 for immediate value, with potential to add Approach 5 later if path-based patterns are needed.
Problem Context
Current Issue
The formatter currently uses
path.extname()to extract file extensions, which only captures the last extension segment (e.g.,.phpfrom.blade.php). This creates issues for compound extensions like:.blade.php→ only matches.php.html.erb→ only matches.erb.spec.ts→ only matches.tsCurrent Workaround
Users must create a wrapper script to handle pattern matching manually:
With the recommended approach, this workaround would no longer be necessary.
I am taking what was said and looking though a few more options while noting the security concerns.
OpeOginni commentedon Nov 2, 2025
Made a PR, with Exact Matching of extentions
.tswill work for files likeindex.tsbut notindex.test.ts*.tswill work for bothindex.tsandindex.test.ts*.test.tswill work for onlyindex.test.tsmagicprinc commentedon Jan 11, 2026
+1 for .editorconfig support
https://editorconfig.org/
Taqinou commentedon Jan 31, 2026
Hi! 👋
I've read through the discussion and the different approaches suggested (thanks @dkudos for the comprehensive analysis!).
I've implemented Approach 6: Extension Suffix Matching on my fork to solve the compound extension problem (
.blade.php,.spec.ts,.html.erb, etc.).Implementation Details
Problem: The current
path.extname()only extracts the last extension:component.blade.php→.php❌test.spec.ts→.ts❌Solution: Added
extractExtensions()function that extracts ALL possible extensions from most specific to most general:Changes made:
packages/opencode/src/format/index.tsextractExtensions(filepath: string): string[]helper functiongetFormatter()to accept full filepath instead of single extensionpath.extname()to suffix matchingWhy Approach 6:
Example config that now works:
{ "formatter": { "blade": { "extensions": [".blade.php"], "command": ["blade-formatter", "$FILE"] }, "php": { "extensions": [".php"], "command": ["pint", "$FILE"] } } }For
component.blade.php, both formatters are now found (instead of just the PHP formatter).Testing:
.blade.php,.spec.ts,.html.erb.php,.ts,.js)Is this feature still considered open / up for grabs? I'd be happy to open a PR if you're interested in this direction!
Thanks!
fix: use suffix matching for formatter file extensions
github-actions commentedon Apr 16, 2026
To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.