/** * di-markers.ts — shared idempotent marker-block splicing for the generated * project's DI host files (DependencyInjection.cs / ServiceCollectionExtensions * / *Module.cs) and Program.cs seams. * * Extracted VERBATIM from scaffold-core-seed's generate.ts (the original home * of the pattern — its tests keep guarding the behaviour through the re-import) * so every scaffolder that must land a registration line patches the SAME way: * - markers present → replace the whole block content, idempotently; * - markers absent → insert before the last `return services;` (the * canonical end-of-method marker of SmartStack DI extension methods), or, * last resort, before the file's final `}` — the upgrade path for projects * generated before the marker existed; * - already up to date → no rewrite (null / 'unchanged'), so re-running a * scaffolder never churns the file. * * `mergeLineIntoMarkerBlock` is the line-grained flavour scaffold-business * uses for `AddScoped()`: one shared block, one line * per entity, exact-dedupe union — with a guard for the line already living * OUTSIDE the block (a project where the agent registered the service by hand: * merging it again would double-register). */ /** * Replace the whole-line span between `begin` and `end` markers with `block`. * Returns: string (replaced), null (block already identical), undefined * (markers not found — caller picks its own insertion fallback). */ export function replaceMarkerBlock(source: string, begin: string, end: string, block: string): string | null | undefined { const beginIdx = source.indexOf(begin) const endIdx = source.indexOf(end) if (beginIdx < 0 || endIdx < 0 || endIdx <= beginIdx) return undefined const lineStart = source.lastIndexOf('\n', beginIdx) + 1 const lineEnd = source.indexOf('\n', endIdx) const before = source.slice(0, lineStart) const after = lineEnd >= 0 ? source.slice(lineEnd + 1) : '' const next = before + block + '\n' + after return next === source ? null : next } /** * Shared marker splice for DI host files: replace the BEGIN/END block when the * markers exist, otherwise insert before the last `return services;` (or, last * resort, before the file's final `}`). Returns null when already up to date. */ export function spliceDiMarkerBlock(source: string, begin: string, end: string, block: string): string | null { const replaced = replaceMarkerBlock(source, begin, end, block) if (replaced !== undefined) return replaced // No markers — try to insert before the LAST `return services;` statement // (the canonical end-of-method marker for SmartStack DI extension methods). const returnMatches = [...source.matchAll(/\n\s*return services;\s*\n/g)] if (returnMatches.length > 0) { const last = returnMatches[returnMatches.length - 1] const insertAt = last.index! const before = source.slice(0, insertAt) const after = source.slice(insertAt) return before + '\n' + block + after } // Last resort — append before the file's last `}`. The agent will see this // and may need to manually move it inside an extension method body. const lastBrace = source.lastIndexOf('}') if (lastBrace >= 0) { return source.slice(0, lastBrace) + block + '\n' + source.slice(lastBrace) } return null } export type MergeLineResult = | { status: 'unchanged'; reason: 'line-outside-block' | 'line-in-block' } | { status: 'patched'; source: string } | { status: 'failed'; reason: 'no-insertion-point' } /** * Merge ONE line into the `begin`/`end` marker block: exact-dedupe union with * the block's existing lines (their order preserved, the new line appended). * Guards: * - the exact line already lives OUTSIDE the block → no-op ('line-outside- * block'): the project registered it by hand, merging would double-register; * - markers absent → the whole block (markers + line) splices in through * `spliceDiMarkerBlock`'s fallbacks (upgrade path for pre-marker projects). * `indent` prefixes the marker/line rows on a fresh block (defaults to the * 8-space method-body indent of the DI templates). */ export function mergeLineIntoMarkerBlock( source: string, begin: string, end: string, line: string, indent = ' ', ): MergeLineResult { const beginIdx = source.indexOf(begin) const endIdx = source.indexOf(end) const hasBlock = beginIdx >= 0 && endIdx > beginIdx const lineKey = line.trim() if (hasBlock) { const blockSpan = source.slice(beginIdx, endIdx) const outside = source.slice(0, beginIdx) + source.slice(endIdx) if (outside.includes(lineKey)) return { status: 'unchanged', reason: 'line-outside-block' } const existing = blockSpan .split('\n') .slice(1) // the BEGIN marker row itself .map((l) => l.trim()) .filter((l) => l.length > 0 && !l.startsWith('//')) if (existing.includes(lineKey)) return { status: 'unchanged', reason: 'line-in-block' } const block = [ `${indent}${begin}`, ...existing.map((l) => `${indent}${l}`), `${indent}${lineKey}`, `${indent}${end}`, ].join('\n') const next = replaceMarkerBlock(source, begin, end, block) // markers verified present above — replaceMarkerBlock cannot return // undefined here; null would mean identical, excluded by the dedupe. return next ? { status: 'patched', source: next } : { status: 'unchanged', reason: 'line-in-block' } } if (source.includes(lineKey)) return { status: 'unchanged', reason: 'line-outside-block' } const block = [`${indent}${begin}`, `${indent}${lineKey}`, `${indent}${end}`].join('\n') const next = spliceDiMarkerBlock(source, begin, end, block) return next === null ? { status: 'failed', reason: 'no-insertion-point' } : { status: 'patched', source: next } }