Every `@Component` class a file declares, with its selectors and template.
(file: string, content: string, nodes: readonly Node[], ctx: ResolutionContext)
| 63 | |
| 64 | /** Every `@Component` class a file declares, with its selectors and template. */ |
| 65 | function componentsIn(file: string, content: string, nodes: readonly Node[], ctx: ResolutionContext): AngularComponent[] { |
| 66 | const out: AngularComponent[] = []; |
| 67 | const safe = stripCommentsForRegex(content, 'typescript'); |
| 68 | const decorator = /@Component\s*\(/g; |
| 69 | let m: RegExpExecArray | null; |
| 70 | while ((m = decorator.exec(safe)) !== null) { |
| 71 | const paren = m.index + m[0].length - 1; |
| 72 | const close = matchBracket(safe, paren); |
| 73 | if (close < 0) continue; |
| 74 | decorator.lastIndex = close; |
| 75 | const open = safe.indexOf('{', paren); |
| 76 | if (open < 0 || open > close) continue; |
| 77 | const objEnd = matchBracket(safe, open); |
| 78 | if (objEnd < 0) continue; |
| 79 | const cls = /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/.exec(safe.slice(close + 1, close + 400)); |
| 80 | if (!cls) continue; |
| 81 | const node = nodes.find((n) => n.kind === 'class' && n.name === cls[1]); |
| 82 | if (!node) continue; |
| 83 | const fields = readFields(safe, open, objEnd); |
| 84 | const selectorText = fields.get('selector')?.text; |
| 85 | const selector = selectorText ? staticString(selectorText) : null; |
| 86 | const selectors = (selector ?? '') |
| 87 | .split(',') |
| 88 | .map((s) => s.trim()) |
| 89 | .filter((s) => ELEMENT_SELECTOR.test(s)); |
| 90 | |
| 91 | let template: AngularComponent['template'] = null; |
| 92 | const url = fields.get('templateUrl')?.text; |
| 93 | const templateUrl = url ? staticString(url) : null; |
| 94 | if (templateUrl) { |
| 95 | const templateFile = templatePathFor(file, templateUrl); |
| 96 | const text = ctx.readFile(templateFile); |
| 97 | if (text) template = { file: templateFile, text, firstLine: 1, inline: false }; |
| 98 | } else { |
| 99 | const inline = fields.get('template'); |
| 100 | if (inline) { |
| 101 | // The template string's body, from the original source (stripping |
| 102 | // comments must not touch a template's text), at its own offset. |
| 103 | const quoteAt = content.indexOf(inline.text.trim()[0] ?? '`', inline.at + 'template'.length); |
| 104 | const end = quoteAt < 0 ? -1 : skipString(content, quoteAt); |
| 105 | if (end > quoteAt) { |
| 106 | template = { file, text: content.slice(quoteAt + 1, end), firstLine: lineOfOffset(content, quoteAt + 1), inline: true }; |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | out.push({ node, file, selectors, template }); |
| 111 | } |
| 112 | return out; |
| 113 | } |
| 114 | |
| 115 | /** Per context, renewed with the route list (whose identity changes when the resolver's caches do). */ |
| 116 | const componentIndexes = new WeakMap<ResolutionContext, { source: readonly Node[]; components: AngularComponent[] }>(); |
no test coverage detected