@@ -153,28 +153,141 @@ function parsePackageJson(data: string): ModPackageInfo | null {
153153 }
154154}
155155
156+ /**
157+ * Detect tags from ModAPI function calls present in mod.js content
158+ */
159+ function detectTagsFromModJs ( data : string ) : string [ ] {
160+ const tagMap : Array < { pattern : RegExp ; tag : string } > = [
161+ // Items
162+ { pattern : / \b a d d I t e m \s * \( / , tag : 'Items' } ,
163+ { pattern : / \b a d d I t e m T o S h o p \s * \( / , tag : 'Items' } ,
164+ { pattern : / \b a d d I t e m T o G u i l d \s * \( / , tag : 'Items' } ,
165+ { pattern : / \b a d d I t e m T o A u c t i o n \s * \( / , tag : 'Items' } ,
166+ { pattern : / \b a d d I t e m T o F a l l e n S t a r \s * \( / , tag : 'Items' } ,
167+ { pattern : / \b a d d E n c h a n t m e n t \s * \( / , tag : 'Items' } ,
168+ { pattern : / \b a d d U n c u t S t o n e \s * \( / , tag : 'Items' } ,
169+ { pattern : / \b a d d M a n u a l \s * \( / , tag : 'Items' } ,
170+ // Crafting
171+ { pattern : / \b a d d R e c i p e T o L i b r a r y \s * \( / , tag : 'Crafting' } ,
172+ { pattern : / \b a d d R e c i p e T o R e s e a r c h \s * \( / , tag : 'Crafting' } ,
173+ { pattern : / \b a d d R e s e a r c h a b l e R e c i p e \s * \( / , tag : 'Crafting' } ,
174+ { pattern : / \b a d d C r a f t i n g T e c h n i q u e \s * \( / , tag : 'Crafting' } ,
175+ { pattern : / \b a d d H a r m o n y T y p e \s * \( / , tag : 'Crafting' } ,
176+ { pattern : / \b a d d C r a f t i n g M i s s i o n s T o L o c a t i o n \s * \( / , tag : 'Crafting' } ,
177+ // Characters
178+ { pattern : / \b a d d C h a r a c t e r \s * \( / , tag : 'Characters' } ,
179+ // Locations
180+ { pattern : / \b a d d L o c a t i o n \s * \( / , tag : 'Locations' } ,
181+ { pattern : / \b l i n k L o c a t i o n s \s * \( / , tag : 'Locations' } ,
182+ { pattern : / \b r e g i s t e r R o o t L o c a t i o n \s * \( / , tag : 'Locations' } ,
183+ { pattern : / \b a d d B u i l d i n g s T o L o c a t i o n \s * \( / , tag : 'Locations' } ,
184+ // Combat
185+ { pattern : / \b a d d E n e m i e s T o L o c a t i o n \s * \( / , tag : 'Combat' } ,
186+ { pattern : / \b a d d F a l l e n S t a r \s * \( / , tag : 'Combat' } ,
187+ { pattern : / \b a d d P u p p e t T y p e \s * \( / , tag : 'Combat' } ,
188+ // Techniques
189+ { pattern : / \b a d d T e c h n i q u e \s * \( / , tag : 'Techniques' } ,
190+ // Cultivation
191+ { pattern : / \b a d d B r e a k t h r o u g h \s * \( / , tag : 'Cultivation' } ,
192+ { pattern : / \b a d d D e s t i n y \s * \( / , tag : 'Cultivation' } ,
193+ // Events
194+ { pattern : / \b a d d T r i g g e r e d E v e n t \s * \( / , tag : 'Events' } ,
195+ { pattern : / \b a d d C a l e n d a r E v e n t \s * \( / , tag : 'Events' } ,
196+ { pattern : / \b a d d E v e n t s T o L o c a t i o n \s * \( / , tag : 'Events' } ,
197+ { pattern : / \b a d d E x p l o r a t i o n E v e n t s T o L o c a t i o n \s * \( / , tag : 'Events' } ,
198+ { pattern : / \b a d d M a p E v e n t s T o L o c a t i o n \s * \( / , tag : 'Events' } ,
199+ // Quests
200+ { pattern : / \b a d d Q u e s t \s * \( / , tag : 'Quests' } ,
201+ { pattern : / \b a d d M i s s i o n s T o L o c a t i o n \s * \( / , tag : 'Quests' } ,
202+ // Audio
203+ { pattern : / \b a d d M u s i c \s * \( / , tag : 'Audio' } ,
204+ { pattern : / \b a d d S f x \s * \( / , tag : 'Audio' } ,
205+ // Housing
206+ { pattern : / \b a d d R o o m \s * \( / , tag : 'Housing' } ,
207+ // Guilds
208+ { pattern : / \b a d d G u i l d \s * \( / , tag : 'Guilds' } ,
209+ // Relationships
210+ { pattern : / \b a d d D u a l C u l t i v a t i o n T e c h n i q u e \s * \( / , tag : 'Relationships' } ,
211+ // Cosmetics
212+ { pattern : / \b a d d P l a y e r S p r i t e \s * \( / , tag : 'Cosmetics' } ,
213+ // UI
214+ { pattern : / \b a d d S c r e e n \s * \( / , tag : 'UI' } ,
215+ { pattern : / \b a d d C u s t o m F o n t \s * \( / , tag : 'UI' } ,
216+ { pattern : / \b s e t C u s t o m F o n t F a m i l y \s * \( / , tag : 'UI' } ,
217+ { pattern : / \b a d d T h e m e O v e r r i d e \s * \( / , tag : 'UI' } ,
218+ // Character Creation
219+ { pattern : / \b a d d B i r t h B a c k g r o u n d \s * \( / , tag : 'Character Creation' } ,
220+ { pattern : / \b a d d C h i l d B a c k g r o u n d \s * \( / , tag : 'Character Creation' } ,
221+ { pattern : / \b a d d T e e n B a c k g r o u n d \s * \( / , tag : 'Character Creation' } ,
222+ { pattern : / \b a d d A l t e r n a t i v e S t a r t \s * \( / , tag : 'Alternative Start' } ,
223+ // Translation
224+ { pattern : / \b a d d T r a n s l a t i o n \s * \( / , tag : 'Translation' } ,
225+ // Exploration
226+ { pattern : / \b a d d M i n e C h a m b e r \s * \( / , tag : 'Exploration' } ,
227+ { pattern : / \b a d d M y s t i c a l R e g i o n B l e s s i n g \s * \( / , tag : 'Exploration' } ,
228+ // Farming
229+ { pattern : / \b a d d C r o p \s * \( / , tag : 'Farming' } ,
230+ ] ;
231+
232+ const detectedTags = new Set < string > ( ) ;
233+ for ( const { pattern, tag } of tagMap ) {
234+ if ( pattern . test ( data ) ) {
235+ detectedTags . add ( tag ) ;
236+ }
237+ }
238+
239+ const tags = Array . from ( detectedTags ) ;
240+ if ( tags . length > 0 ) {
241+ console . log ( 'Auto-detected tags from API usage:' , tags ) ;
242+ }
243+ return tags ;
244+ }
245+
156246/**
157247 * Parse mod.js content to extract metadata
158248 */
159249function parseModJsContent ( data : string ) : ModPackageInfo | null {
160250 console . log ( 'Parsing mod.js content...' ) ;
161251
252+ const detectedTags = detectTagsFromModJs ( data ) ;
253+
254+ const mergeDetectedTags = ( result : ModPackageInfo | null ) : ModPackageInfo | null => {
255+ if ( detectedTags . length === 0 ) return result ;
256+ // If metadata parsing failed but we have tags, return a minimal result with tags
257+ const base = result ?? { } ;
258+ const existing = base . tags ?? [ ] ;
259+ const merged = Array . from ( new Set ( [ ...existing , ...detectedTags ] ) ) ;
260+ return { ...base , tags : merged } ;
261+ } ;
262+
162263 try {
163- // Primary method: Look for getMetadata function and extract its return value
164- const metadataMatch = data . match (
264+ // Try all getMetadata syntax variants
265+ const metadataPatterns = [
266+ // Traditional: getMetadata: function() { return {...} }
165267 / g e t M e t a d a t a \s * : \s * f u n c t i o n \s * \( \s * \) \s * \{ \s * r e t u r n \s * ( \{ [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * \} ) / ,
166- ) ;
167-
168- if ( metadataMatch ) {
169- const result = parseMetadataObject ( data , metadataMatch ) ;
170- if ( result ) return result ;
268+ // Method shorthand: getMetadata() { return {...} }
269+ / g e t M e t a d a t a \s * \( \s * \) \s * \{ \s * r e t u r n \s * ( \{ [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * \} ) / ,
270+ // Arrow with body: getMetadata: () => { return {...} }
271+ / g e t M e t a d a t a \s * : \s * \( \s * \) \s * = > \s * \{ \s * r e t u r n \s * ( \{ [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * \} ) / ,
272+ // Arrow with parenthesized object: getMetadata: () => ({...})
273+ / g e t M e t a d a t a \s * : \s * \( \s * \) \s * = > \s * \( \s * ( \{ [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * \} ) \s * \) / ,
274+ // Arrow without parens returning object directly: getMetadata:()=>({...})
275+ / g e t M e t a d a t a \s * : \s * \( \s * \) \s * = > \s * ( \{ [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * \} ) / ,
276+ ] ;
277+
278+ for ( const pattern of metadataPatterns ) {
279+ const metadataMatch = data . match ( pattern ) ;
280+ if ( metadataMatch ) {
281+ const result = parseMetadataObject ( data , metadataMatch ) ;
282+ if ( result ) return mergeDetectedTags ( result ) ;
283+ }
171284 }
172285
173286 // Fallback: Try enhanced regex extraction
174- return extractWithFallbackMethods ( data ) ;
287+ return mergeDetectedTags ( extractWithFallbackMethods ( data ) ) ;
175288 } catch ( parseError ) {
176289 console . error ( 'Error parsing mod.js metadata:' , parseError ) ;
177- return extractWithFallbackMethods ( data ) ;
290+ return mergeDetectedTags ( extractWithFallbackMethods ( data ) ) ;
178291 }
179292}
180293
@@ -237,35 +350,94 @@ function parseMetadataObject(
237350 */
238351function extractWithFallbackMethods ( data : string ) : ModPackageInfo | null {
239352 try {
353+ // Method 0: Webpack compiled inline JSON — find the JSON object literal
354+ // after getMetadata and parse it directly (handles nested objects with brace counting)
355+ const getMetadataPos = data . indexOf ( 'getMetadata' ) ;
356+ if ( getMetadataPos !== - 1 ) {
357+ const searchArea = data . slice ( getMetadataPos , getMetadataPos + 3000 ) ;
358+ const jsonObjectStart = searchArea . search ( / \{ " n a m e " \s * : / ) ;
359+ if ( jsonObjectStart !== - 1 ) {
360+ const absoluteStart = getMetadataPos + jsonObjectStart ;
361+ let braceCount = 0 ;
362+ let jsonStr = '' ;
363+ for (
364+ let i = absoluteStart ;
365+ i < Math . min ( absoluteStart + 5000 , data . length ) ;
366+ i ++
367+ ) {
368+ const char = data [ i ] ;
369+ jsonStr += char ;
370+ if ( char === '{' ) braceCount ++ ;
371+ else if ( char === '}' ) {
372+ braceCount -- ;
373+ if ( braceCount === 0 ) break ;
374+ }
375+ }
376+ console . log ( 'DEBUG Method 0: extracted JSON string:' , jsonStr . slice ( 0 , 200 ) ) ;
377+ try {
378+ const parsed = JSON . parse ( jsonStr ) ;
379+ if ( parsed . name ) {
380+ console . log ( 'Successfully parsed metadata via inline JSON extraction' ) ;
381+ return {
382+ name : parsed . name ,
383+ title : formatModName ( parsed . title || parsed . name ) ,
384+ description : parsed . description ,
385+ version : parsed . version ,
386+ author :
387+ typeof parsed . author === 'string'
388+ ? parsed . author
389+ : parsed . author ?. name ,
390+ tags : parsed . tags || parsed . keywords ,
391+ } ;
392+ }
393+ } catch ( jsonError ) {
394+ console . error ( 'Failed to parse inline JSON object:' , jsonError ) ;
395+ }
396+ } else {
397+ console . log ( 'DEBUG Method 0: no {"name": pattern found near getMetadata' ) ;
398+ }
399+ }
400+
240401 // Method 1: Handle webpack inline JSON pattern
241402 // Pattern: name: {"name":"value",...}.name
242- const webpackJsonMatch = data . match (
403+ const webpackJsonPatterns = [
243404 / g e t M e t a d a t a \s * : \s * f u n c t i o n \s * \( \s * \) \s * \{ \s * r e t u r n \s * \( \s * \{ [ ^ ] * ?n a m e \s * : \s * ( \{ [ ^ } ] + \} ) \. n a m e / ,
244- ) ;
245-
246- if ( webpackJsonMatch ) {
247- console . log ( 'Found webpack inline JSON pattern, extracting...' ) ;
248- try {
249- const inlineJson = JSON . parse ( webpackJsonMatch [ 1 ] ) ;
250- return {
251- name : inlineJson . name ,
252- title : formatModName ( inlineJson . title || inlineJson . name ) ,
253- description : inlineJson . description ,
254- version : inlineJson . version ,
255- author :
256- typeof inlineJson . author === 'string'
257- ? inlineJson . author
258- : inlineJson . author ?. name ,
259- tags : inlineJson . tags || inlineJson . keywords ,
260- } ;
261- } catch ( jsonError ) {
262- console . error ( 'Failed to parse webpack inline JSON:' , jsonError ) ;
405+ / g e t M e t a d a t a \s * \( \s * \) \s * \{ \s * r e t u r n \s * \( \s * \{ [ ^ ] * ?n a m e \s * : \s * ( \{ [ ^ } ] + \} ) \. n a m e / ,
406+ / g e t M e t a d a t a \s * : \s * \( \s * \) \s * = > \s * \( ? \s * \{ [ ^ ] * ?n a m e \s * : \s * ( \{ [ ^ } ] + \} ) \. n a m e / ,
407+ ] ;
408+
409+ for ( const pattern of webpackJsonPatterns ) {
410+ const webpackJsonMatch = data . match ( pattern ) ;
411+ if ( webpackJsonMatch ) {
412+ console . log ( 'Found webpack inline JSON pattern, extracting...' ) ;
413+ try {
414+ const inlineJson = JSON . parse ( webpackJsonMatch [ 1 ] ) ;
415+ return {
416+ name : inlineJson . name ,
417+ title : formatModName ( inlineJson . title || inlineJson . name ) ,
418+ description : inlineJson . description ,
419+ version : inlineJson . version ,
420+ author :
421+ typeof inlineJson . author === 'string'
422+ ? inlineJson . author
423+ : inlineJson . author ?. name ,
424+ tags : inlineJson . tags || inlineJson . keywords ,
425+ } ;
426+ } catch ( jsonError ) {
427+ console . error ( 'Failed to parse webpack inline JSON:' , jsonError ) ;
428+ }
263429 }
264430 }
265431
266432 // Method 2: Look for the exact pattern with individual property extraction
267- const fullMatch = data . match (
433+ const fullMatchPatterns = [
268434 / g e t M e t a d a t a \s * : \s * f u n c t i o n \s * \( \s * \) \s * \{ \s * r e t u r n \s * \{ ( [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * ) } / ,
435+ / g e t M e t a d a t a \s * \( \s * \) \s * \{ \s * r e t u r n \s * \{ ( [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * ) } / ,
436+ / g e t M e t a d a t a \s * : \s * \( \s * \) \s * = > \s * \( ? \s * \{ ( [ ^ } ] * (?: \{ [ ^ } ] * \} [ ^ } ] * ) * ) \} ? \s * \) ? / ,
437+ ] ;
438+ const fullMatch = fullMatchPatterns . reduce < RegExpMatchArray | null > (
439+ ( found , pattern ) => found ?? data . match ( pattern ) ,
440+ null ,
269441 ) ;
270442
271443 if ( fullMatch ) {
0 commit comments