* Parses a Neo.mjs source file into granular chunks. * @param {String} content The raw file content. * @param {String} filePath The relative file path. * @param {String} [defaultType='src'] The type to assign to chunks (e.g., 'src', 'app', 'example'). * @param {Object} [hierarchy
(content, filePath, defaultType='src', hierarchy={})
| 44 | * @returns {Array<Object>} An array of chunks. |
| 45 | */ |
| 46 | parse(content, filePath, defaultType='src', hierarchy={}) { |
| 47 | const chunks = []; |
| 48 | let ast; |
| 49 | |
| 50 | // Strip shebang if present (acorn doesn't handle it) |
| 51 | if (content.startsWith('#!')) { |
| 52 | content = content.replace(/^#!.*\n/, ''); |
| 53 | } |
| 54 | |
| 55 | try { |
| 56 | // `ecmaVersion: 'latest'` lets acorn auto-track its highest-supported syntax |
| 57 | // rather than pinning a literal year that future TC39 features (e.g. import |
| 58 | // attributes `with {type: 'json'}`, decorators) would silently fail to parse. |
| 59 | ast = acorn.parse(content, { sourceType: 'module', locations: true, ecmaVersion: 'latest' }); |
| 60 | } catch (e) { |
| 61 | logger.warn(`Failed to parse source file ${filePath}: ${e.message}`); |
| 62 | return []; |
| 63 | } |
| 64 | |
| 65 | const contextNodes = []; |
| 66 | const propertyNodes = []; |
| 67 | let configNode = null; |
| 68 | const methodNodes = []; |
| 69 | let classStart = 0; |
| 70 | let classDefinition = ''; |
| 71 | let className = ''; |
| 72 | let superClass = ''; |
| 73 | |
| 74 | // 1. Traverse AST to categorize nodes |
| 75 | ast.body.forEach(node => { |
| 76 | if (node.type === 'ImportDeclaration' || node.type === 'VariableDeclaration') { |
| 77 | // Top-level imports and vars belong to Module Context |
| 78 | contextNodes.push(node); |
| 79 | } else if (node.type === 'ClassDeclaration' || node.type === 'ExportDefaultDeclaration') { |
| 80 | // Handle Class Definition |
| 81 | const classDecl = node.type === 'ExportDefaultDeclaration' ? node.declaration : node; |
| 82 | |
| 83 | if (classDecl.type === 'ClassDeclaration') { |
| 84 | classStart = classDecl.start; |
| 85 | // Capture JSDoc comments preceding the class |
| 86 | const classHeadEnd = classDecl.body.start + 1; // Include opening brace |
| 87 | classDefinition = content.substring(classDecl.start, classHeadEnd); |
| 88 | |
| 89 | if (classDecl.id) { |
| 90 | className = classDecl.id.name; |
| 91 | } |
| 92 | |
| 93 | // Iterate Class Body |
| 94 | classDecl.body.body.forEach(member => { |
| 95 | if (member.type === 'MethodDefinition') { |
| 96 | if (member.kind === 'constructor') { |
| 97 | methodNodes.push(member); |
| 98 | } else { |
| 99 | methodNodes.push(member); |
| 100 | } |
| 101 | } else if (member.type === 'PropertyDefinition') { |
| 102 | if (member.key.name === 'config' && member.static) { |
| 103 | configNode = member; |