@@ -1478,7 +1478,15 @@ export async function compareJsSemantically(
14781478 workingTsCode = normalizeNullishCoalescing ( workingTsCode )
14791479 workingOxcCode = removeNullishCoalescingParens ( workingOxcCode )
14801480
1481- // Fast path after template/inject/nullish normalization
1481+ // Normalize numeric literals: 1e3 → 1000 (OXC emits scientific notation)
1482+ workingOxcCode = normalizeNumericLiterals ( workingOxcCode )
1483+ workingTsCode = normalizeNumericLiterals ( workingTsCode )
1484+
1485+ // Normalize inline object literal formatting: collapse multi-line simple objects
1486+ workingOxcCode = collapseInlineObjectLiterals ( workingOxcCode )
1487+ workingTsCode = collapseInlineObjectLiterals ( workingTsCode )
1488+
1489+ // Fast path after template/inject/nullish/format normalization
14821490 if ( workingOxcCode === workingTsCode ) {
14831491 return { match : true }
14841492 }
@@ -2906,7 +2914,15 @@ export async function compareFullFileSemantically(
29062914 workingTsCode = normalizeNullishCoalescing ( workingTsCode )
29072915 workingOxcCode = removeNullishCoalescingParens ( workingOxcCode )
29082916
2909- // Fast path after template/inject/nullish normalization
2917+ // Normalize numeric literals: 1e3 → 1000 (OXC emits scientific notation)
2918+ workingOxcCode = normalizeNumericLiterals ( workingOxcCode )
2919+ workingTsCode = normalizeNumericLiterals ( workingTsCode )
2920+
2921+ // Normalize inline object literal formatting: collapse multi-line simple objects
2922+ workingOxcCode = collapseInlineObjectLiterals ( workingOxcCode )
2923+ workingTsCode = collapseInlineObjectLiterals ( workingTsCode )
2924+
2925+ // Fast path after template/inject/nullish/format normalization
29102926 if ( workingOxcCode === workingTsCode ) {
29112927 return { match : true }
29122928 }
@@ -3213,3 +3229,108 @@ async function formatCodeForComparison(code: string): Promise<string> {
32133229 return code
32143230 }
32153231}
3232+
3233+ /**
3234+ * AST node type used for traversal in normalization functions.
3235+ */
3236+ interface NormAstNode {
3237+ type ?: string
3238+ value ?: unknown
3239+ start ?: number
3240+ end ?: number
3241+ [ key : string ] : unknown
3242+ }
3243+
3244+ /**
3245+ * Recursively walk an AST node, calling the visitor for each object node.
3246+ */
3247+ function walkAst ( node : unknown , visitor : ( n : NormAstNode ) => void ) : void {
3248+ if ( node === null || typeof node !== 'object' ) return
3249+ if ( Array . isArray ( node ) ) {
3250+ for ( const item of node ) walkAst ( item , visitor )
3251+ return
3252+ }
3253+ const obj = node as NormAstNode
3254+ visitor ( obj )
3255+ for ( const value of Object . values ( obj ) ) {
3256+ if ( value !== null && typeof value === 'object' ) {
3257+ walkAst ( value , visitor )
3258+ }
3259+ }
3260+ }
3261+
3262+ /**
3263+ * Normalize numeric scientific notation to decimal form using oxc-parser.
3264+ * Finds Literal nodes (ESTree format) whose source text contains scientific notation
3265+ * (e.g. `1e3`) and replaces them with their decimal representation (`1000`).
3266+ */
3267+ function normalizeNumericLiterals ( code : string ) : string {
3268+ let ast
3269+ try {
3270+ ast = parseSync ( 'numeric.js' , code , { sourceType : 'module' } )
3271+ } catch {
3272+ return code
3273+ }
3274+
3275+ const replacements : Array < { start : number ; end : number ; replacement : string } > = [ ]
3276+
3277+ // oxc-parser returns character offsets (not byte offsets), so use start/end directly
3278+ walkAst ( ast . program , ( node ) => {
3279+ if ( node . type === 'Literal' && typeof node . value === 'number' ) {
3280+ if ( typeof node . start !== 'number' || typeof node . end !== 'number' ) return
3281+ const originalText = code . slice ( node . start as number , node . end as number )
3282+ // Check if the source text uses scientific notation (exclude hex/octal/binary prefixes)
3283+ if ( / e \+ ? \d / i. test ( originalText ) && ! / ^ 0 [ x o b ] / i. test ( originalText ) ) {
3284+ const num = node . value as number
3285+ if ( Number . isFinite ( num ) && Number . isSafeInteger ( num ) ) {
3286+ replacements . push ( {
3287+ start : node . start as number ,
3288+ end : node . end as number ,
3289+ replacement : String ( num ) ,
3290+ } )
3291+ }
3292+ }
3293+ }
3294+ } )
3295+
3296+ if ( replacements . length === 0 ) return code
3297+
3298+ // Apply replacements from end to start
3299+ replacements . sort ( ( a , b ) => b . start - a . start )
3300+ let result = code
3301+ for ( const { start, end, replacement } of replacements ) {
3302+ result = result . slice ( 0 , start ) + replacement + result . slice ( end )
3303+ }
3304+ return result
3305+ }
3306+
3307+ /**
3308+ * Collapse multi-line simple object literals in function call arguments to single lines.
3309+ *
3310+ * Matches patterns like:
3311+ * .emit({
3312+ * content: $event,
3313+ * format: item_r3.format,
3314+ * type: item_r3.id,
3315+ * })
3316+ *
3317+ * And collapses to:
3318+ * .emit({ content: $event, format: item_r3.format, type: item_r3.id})
3319+ *
3320+ * Only targets objects where every property value is a simple expression
3321+ * (identifiers, member access, $event) — no strings, nested objects, or calls.
3322+ */
3323+ const COLLAPSE_OBJ_RE = / \( \{ \s * \n ( (?: \s * \w + : \s * [ \w . $ ] + , ? \s * \n ) + ) \s * \} \) / g
3324+
3325+ function collapseInlineObjectLiterals ( code : string ) : string {
3326+ return code . replace ( COLLAPSE_OBJ_RE , ( _match , propsBlock : string ) => {
3327+ const lines = propsBlock . trim ( ) . split ( '\n' )
3328+ const props = lines . map ( ( l ) => l . trim ( ) ) . filter ( Boolean )
3329+ // Remove trailing comma from last property
3330+ const last = props . length - 1
3331+ if ( last >= 0 && props [ last ] . endsWith ( ',' ) ) {
3332+ props [ last ] = props [ last ] . slice ( 0 , - 1 )
3333+ }
3334+ return '({ ' + props . join ( ' ' ) + '})'
3335+ } )
3336+ }
0 commit comments