@contentrain/mcp
Advanced tools
| import { c as writeText, o as readText, r as pathExists } from "./fs-DLbVB-Ek.mjs"; | ||
| import { t as readConfig } from "./config-oxxgznz7.mjs"; | ||
| import { S as writeContent, _ as resolveMdFilePath, c as validateModelDefinition, h as resolveJsonFilePath, l as writeModel, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs"; | ||
| import { r as writeContext } from "./context-DjglDPvj.mjs"; | ||
| import { n as checkBranchHealth } from "./branch-lifecycle-BAfgSQBv.mjs"; | ||
| import { n as createTransaction, t as buildBranchName } from "./transaction-1SPznNt3.mjs"; | ||
| import { extname, join } from "node:path"; | ||
| //#region src/core/apply-manager.ts | ||
| const MAX_PATCHES = 100; | ||
| /** File extensions allowed for patching — scannable source files only */ | ||
| const PATCHABLE_EXTENSIONS = new Set([ | ||
| ".vue", | ||
| ".tsx", | ||
| ".jsx", | ||
| ".ts", | ||
| ".js", | ||
| ".mjs", | ||
| ".astro", | ||
| ".svelte" | ||
| ]); | ||
| /** Directories that must never be patched */ | ||
| const FORBIDDEN_PATH_SEGMENTS = new Set([ | ||
| ".contentrain", | ||
| "node_modules", | ||
| ".git", | ||
| "dist", | ||
| "build", | ||
| ".next", | ||
| ".nuxt" | ||
| ]); | ||
| function detectFileFramework(filePath) { | ||
| switch (extname(filePath).toLowerCase()) { | ||
| case ".vue": return "vue"; | ||
| case ".svelte": return "svelte"; | ||
| case ".tsx": | ||
| case ".jsx": return "jsx"; | ||
| case ".astro": return "astro"; | ||
| case ".ts": | ||
| case ".js": | ||
| case ".mjs": return "script"; | ||
| default: return "script"; | ||
| } | ||
| } | ||
| /** | ||
| * Validate that a replacement expression uses the correct template syntax | ||
| * for the target file's framework. Returns a warning string or null. | ||
| */ | ||
| function validateFrameworkExpression(filePath, newExpression, context) { | ||
| if (context !== "tag_text") return null; | ||
| switch (detectFileFramework(filePath)) { | ||
| case "vue": | ||
| if (!newExpression.includes("{{")) return `Vue file "${filePath}": tag text expression "${newExpression}" does not contain "{{" — expected Vue template syntax like {{ $t('key') }}`; | ||
| break; | ||
| case "jsx": | ||
| if (!newExpression.includes("{")) return `JSX file "${filePath}": tag text expression "${newExpression}" does not contain "{" — expected JSX syntax like {t('key')}`; | ||
| break; | ||
| case "svelte": | ||
| if (!newExpression.includes("{")) return `Svelte file "${filePath}": tag text expression "${newExpression}" does not contain "{" — expected Svelte syntax like {$t('key')}`; | ||
| break; | ||
| case "astro": | ||
| if (!newExpression.includes("{")) return `Astro file "${filePath}": tag text expression "${newExpression}" does not contain "{" — expected Astro syntax like {t('key')}`; | ||
| break; | ||
| case "script": return `Script file "${filePath}": tag text replacement not applicable for .ts/.js files`; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Validate that a patch file path is safe and within allowed scope. | ||
| * Returns an error message or null if valid. | ||
| */ | ||
| function validatePatchPath(filePath) { | ||
| const normalizedPath = filePath.replace(/\\/g, "/"); | ||
| if (normalizedPath.includes("..")) return `Path traversal detected: "${filePath}"`; | ||
| if (normalizedPath.startsWith("/")) return `Absolute path not allowed: "${filePath}"`; | ||
| const segments = normalizedPath.split("/"); | ||
| for (const seg of segments) if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return `Patching files inside "${seg}/" is not allowed: "${filePath}"`; | ||
| const ext = extname(filePath).toLowerCase(); | ||
| if (!PATCHABLE_EXTENSIONS.has(ext)) return `File extension "${ext}" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(", ")}. Path: "${filePath}"`; | ||
| return null; | ||
| } | ||
| /** | ||
| * Perform a basic syntax check on a patched file. | ||
| * Returns an error message or null if syntax appears valid. | ||
| */ | ||
| function checkSyntax(filePath, content) { | ||
| switch (extname(filePath).toLowerCase()) { | ||
| case ".ts": | ||
| case ".tsx": | ||
| case ".js": | ||
| case ".jsx": | ||
| case ".mjs": return checkJsSyntax(content); | ||
| case ".vue": return checkVueSyntax(content); | ||
| case ".svelte": | ||
| case ".astro": return checkTagBalance(content); | ||
| default: return null; | ||
| } | ||
| } | ||
| /** | ||
| * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure. | ||
| * This is intentionally conservative — it catches obvious breakage without | ||
| * requiring a full parser. | ||
| */ | ||
| function checkJsSyntax(content) { | ||
| const stack = []; | ||
| const pairs = { | ||
| ")": "(", | ||
| "]": "[", | ||
| "}": "{" | ||
| }; | ||
| let inString = null; | ||
| let escaped = false; | ||
| let inLineComment = false; | ||
| let inBlockComment = false; | ||
| for (let i = 0; i < content.length; i++) { | ||
| const ch = content[i]; | ||
| const next = content[i + 1]; | ||
| if (escaped) { | ||
| escaped = false; | ||
| continue; | ||
| } | ||
| if (ch === "\\" && inString !== null) { | ||
| escaped = true; | ||
| continue; | ||
| } | ||
| if (!inString && !inBlockComment && ch === "/" && next === "/") { | ||
| inLineComment = true; | ||
| continue; | ||
| } | ||
| if (inLineComment) { | ||
| if (ch === "\n") inLineComment = false; | ||
| continue; | ||
| } | ||
| if (!inString && !inBlockComment && ch === "/" && next === "*") { | ||
| inBlockComment = true; | ||
| i++; | ||
| continue; | ||
| } | ||
| if (inBlockComment) { | ||
| if (ch === "*" && next === "/") { | ||
| inBlockComment = false; | ||
| i++; | ||
| } | ||
| continue; | ||
| } | ||
| if (inString !== null) { | ||
| if (ch === inString) inString = null; | ||
| else if (inString !== "`" && ch === "\n") return `Unterminated string literal near offset ${i}`; | ||
| continue; | ||
| } | ||
| if (ch === "\"" || ch === "'" || ch === "`") { | ||
| inString = ch; | ||
| continue; | ||
| } | ||
| if (ch === "(" || ch === "[" || ch === "{") stack.push(ch); | ||
| else if (ch === ")" || ch === "]" || ch === "}") { | ||
| const expected = pairs[ch]; | ||
| if (stack.length === 0) return `Unmatched closing "${ch}" near offset ${i}`; | ||
| const top = stack.pop(); | ||
| if (top !== expected) return `Mismatched bracket: expected closing for "${top}" but found "${ch}" near offset ${i}`; | ||
| } | ||
| } | ||
| if (inString !== null) return `Unterminated string literal (opened with ${inString})`; | ||
| if (stack.length > 0) return `Unclosed bracket "${stack[stack.length - 1]}" — ${stack.length} unclosed bracket(s)`; | ||
| return null; | ||
| } | ||
| /** | ||
| * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced. | ||
| */ | ||
| function checkVueSyntax(content) { | ||
| const tagBalance = checkTagBalance(content); | ||
| if (tagBalance) return tagBalance; | ||
| for (const tag of ["template", "script"]) { | ||
| const openRe = new RegExp(`<${tag}[\\s>]`, "g"); | ||
| const closeRe = new RegExp(`</${tag}>`, "g"); | ||
| const opens = content.match(openRe)?.length ?? 0; | ||
| const closes = content.match(closeRe)?.length ?? 0; | ||
| if (opens !== closes) return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Basic tag balance check for HTML-like files. | ||
| * Checks that self-closing tags are handled and major structural tags are balanced. | ||
| */ | ||
| function checkTagBalance(content) { | ||
| for (const tag of [ | ||
| "div", | ||
| "section", | ||
| "main", | ||
| "header", | ||
| "footer", | ||
| "nav", | ||
| "article", | ||
| "aside", | ||
| "ul", | ||
| "ol", | ||
| "table" | ||
| ]) { | ||
| const openRe = new RegExp(`<${tag}[\\s>]`, "g"); | ||
| const closeRe = new RegExp(`</${tag}>`, "g"); | ||
| const opens = content.match(openRe)?.length ?? 0; | ||
| const closes = content.match(closeRe)?.length ?? 0; | ||
| if (opens !== closes) return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`; | ||
| } | ||
| return null; | ||
| } | ||
| async function applyExtract(projectRoot, input) { | ||
| const config = await readConfig(projectRoot); | ||
| if (!config) throw new Error("Project not initialized. Run contentrain_init first."); | ||
| const { extractions, dry_run } = input; | ||
| const existingModels = await listModels(projectRoot); | ||
| const existingIds = new Set(existingModels.map((m) => m.id)); | ||
| const modelsToCreate = []; | ||
| const modelsToUpdate = []; | ||
| const contentFiles = []; | ||
| let totalEntries = 0; | ||
| const validationErrors = []; | ||
| for (const ext of extractions) { | ||
| const modelErrors = validateModelDefinition({ | ||
| id: ext.model, | ||
| kind: ext.kind, | ||
| fields: ext.fields | ||
| }); | ||
| if (modelErrors.errors.length > 0) validationErrors.push(...modelErrors.errors.map((e) => `[${ext.model}] ${e}`)); | ||
| for (const entry of ext.entries) if (ext.kind === "dictionary") { | ||
| if (entry.data["id"] !== void 0 || entry.data["slug"] !== void 0) validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`); | ||
| for (const [key, val] of Object.entries(entry.data)) if (typeof val !== "string") validationErrors.push(`[${ext.model}] Dictionary entry value for key "${key}" must be a string, got ${typeof val}`); | ||
| } else if (ext.kind === "document") { | ||
| if (!entry.slug && !entry.data["slug"]) validationErrors.push(`[${ext.model}] Document entries must have a slug`); | ||
| } else if (ext.kind === "collection") { | ||
| if (entry.slug !== void 0 || entry.data["slug"] !== void 0) validationErrors.push(`[${ext.model}] Collection entries should not have slug`); | ||
| } else if (ext.kind === "singleton") { | ||
| if (entry.data["id"] !== void 0 || entry.slug !== void 0 || entry.data["slug"] !== void 0) validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`); | ||
| } | ||
| if (existingIds.has(ext.model)) modelsToUpdate.push(ext.model); | ||
| else modelsToCreate.push(ext.model); | ||
| totalEntries += ext.entries.length; | ||
| let previewModel; | ||
| if (existingIds.has(ext.model)) { | ||
| const real = await readModel(projectRoot, ext.model); | ||
| if (real) previewModel = real; | ||
| else previewModel = { | ||
| id: ext.model, | ||
| kind: ext.kind, | ||
| domain: ext.domain, | ||
| i18n: ext.i18n ?? true | ||
| }; | ||
| } else previewModel = { | ||
| id: ext.model, | ||
| kind: ext.kind, | ||
| domain: ext.domain, | ||
| i18n: ext.i18n ?? true | ||
| }; | ||
| const cDir = resolveContentDir(projectRoot, previewModel); | ||
| for (const entry of ext.entries) { | ||
| const locale = entry.locale ?? config.locales.default; | ||
| if (ext.kind === "document" && entry.slug) contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug)); | ||
| else contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale)); | ||
| } | ||
| } | ||
| const preview = { | ||
| models_to_create: modelsToCreate, | ||
| models_to_update: modelsToUpdate, | ||
| total_entries: totalEntries, | ||
| content_files: [...new Set(contentFiles)] | ||
| }; | ||
| if (dry_run !== false) return { | ||
| dry_run: true, | ||
| preview, | ||
| ...validationErrors.length > 0 ? { validation_errors: validationErrors } : {}, | ||
| next_steps: [ | ||
| ...validationErrors.length > 0 ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`] : [], | ||
| "Review the preview above", | ||
| "Call contentrain_apply with mode:extract and dry_run:false to execute" | ||
| ] | ||
| }; | ||
| if (validationErrors.length > 0) return { | ||
| dry_run: false, | ||
| error: "Model validation failed — cannot execute extract with invalid model definitions", | ||
| validation_errors: validationErrors, | ||
| next_steps: ["Fix the validation errors and retry"] | ||
| }; | ||
| const health = await checkBranchHealth(projectRoot); | ||
| if (health.blocked) return { | ||
| error: health.message, | ||
| action: "blocked", | ||
| hint: "Merge or delete old contentrain/* branches before executing normalize." | ||
| }; | ||
| const branchName = buildBranchName("normalize", "extract"); | ||
| const tx = await createTransaction(projectRoot, branchName, { workflowOverride: "review" }); | ||
| const sourceMap = []; | ||
| const modelsCreated = []; | ||
| const modelsUpdated = []; | ||
| let entriesWritten = 0; | ||
| try { | ||
| await tx.write(async (wt) => { | ||
| for (const ext of extractions) { | ||
| const existing = await readModel(wt, ext.model); | ||
| if (existing) { | ||
| if (ext.fields) { | ||
| const merged = { | ||
| ...existing.fields, | ||
| ...ext.fields | ||
| }; | ||
| if (Object.keys(ext.fields).filter((k) => !(k in (existing.fields ?? {}))).length > 0) { | ||
| existing.fields = merged; | ||
| await writeModel(wt, existing); | ||
| modelsUpdated.push(ext.model); | ||
| } | ||
| } | ||
| } else { | ||
| await writeModel(wt, { | ||
| id: ext.model, | ||
| name: ext.model.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "), | ||
| kind: ext.kind, | ||
| domain: ext.domain, | ||
| i18n: ext.i18n ?? true, | ||
| fields: ext.fields | ||
| }); | ||
| modelsCreated.push(ext.model); | ||
| } | ||
| const model = await readModel(wt, ext.model); | ||
| const entries = ext.entries.map((e) => ({ | ||
| locale: e.locale, | ||
| slug: e.slug, | ||
| data: e.data | ||
| })); | ||
| await writeContent(wt, model, entries, await readConfig(wt) ?? config); | ||
| entriesWritten += entries.length; | ||
| for (const entry of ext.entries) if (ext.kind === "dictionary" && entry.sources) for (const s of entry.sources) sourceMap.push({ | ||
| model: ext.model, | ||
| locale: entry.locale ?? config.locales.default, | ||
| value: s.value, | ||
| file: s.file, | ||
| line: s.line | ||
| }); | ||
| else if (entry.source) sourceMap.push({ | ||
| model: ext.model, | ||
| locale: entry.locale ?? config.locales.default, | ||
| value: entry.source.value, | ||
| file: entry.source.file, | ||
| line: entry.source.line | ||
| }); | ||
| } | ||
| if (sourceMap.length > 0) { | ||
| const sourcesByModel = {}; | ||
| for (const s of sourceMap) { | ||
| if (!sourcesByModel[s.model]) sourcesByModel[s.model] = { | ||
| source_files: [], | ||
| entry_count: 0 | ||
| }; | ||
| const modelEntry = sourcesByModel[s.model]; | ||
| if (!modelEntry.source_files.includes(s.file)) modelEntry.source_files.push(s.file); | ||
| modelEntry.entry_count++; | ||
| } | ||
| const sourcesJson = JSON.stringify({ | ||
| version: 1, | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString(), | ||
| models: sourcesByModel | ||
| }, null, 2) + "\n"; | ||
| await writeText(join(wt, ".contentrain", "normalize-sources.json"), sourcesJson); | ||
| } | ||
| await writeContext(wt, { | ||
| tool: "contentrain_apply", | ||
| model: extractions.map((e) => e.model).join(","), | ||
| locale: config.locales.default, | ||
| entries: extractions.flatMap((e) => e.entries.map((en) => en.slug ?? "entry")) | ||
| }); | ||
| }); | ||
| const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`; | ||
| await tx.commit(commitMsg); | ||
| const gitResult = { | ||
| branch: branchName, | ||
| action: "pending-review", | ||
| commit: "" | ||
| }; | ||
| try { | ||
| const completed = await tx.complete(); | ||
| gitResult.action = completed.action; | ||
| gitResult.commit = completed.commit; | ||
| if (completed.warning !== void 0) gitResult.warning = completed.warning; | ||
| } catch (error) { | ||
| gitResult.action = "incomplete"; | ||
| gitResult.warning = `Content was committed to "${branchName}" but the transaction could not be completed: ${error instanceof Error ? error.message : String(error)}. The branch is preserved — inspect it with contentrain_branch_list before retrying.`; | ||
| } finally { | ||
| await tx.cleanup(); | ||
| } | ||
| return { | ||
| dry_run: false, | ||
| results: { | ||
| models_created: modelsCreated, | ||
| models_updated: modelsUpdated, | ||
| entries_written: entriesWritten, | ||
| source_map: sourceMap | ||
| }, | ||
| git: gitResult, | ||
| context_updated: true, | ||
| next_steps: [ | ||
| "Run contentrain_validate to check the extracted content", | ||
| "Run contentrain_submit to push the branch for review", | ||
| "For browser-based review: ensure `contentrain serve` is running, direct user to http://localhost:3333/normalize", | ||
| "For terminal workflow: use contentrain_merge to merge the branch locally", | ||
| "After merge, run `npx contentrain generate` to update SDK client", | ||
| "After review, proceed with mode:reuse to patch source files" | ||
| ] | ||
| }; | ||
| } catch (error) { | ||
| await tx.cleanup(); | ||
| throw error; | ||
| } | ||
| } | ||
| async function applyReuse(projectRoot, input) { | ||
| const config = await readConfig(projectRoot); | ||
| if (!config) throw new Error("Project not initialized. Run contentrain_init first."); | ||
| const { scope, patches, dry_run } = input; | ||
| if (!scope.model && !scope.domain) throw new Error("Scope required: provide model or domain. Whole-project patching is not allowed."); | ||
| if (patches.length > MAX_PATCHES) throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`); | ||
| if (scope.model) { | ||
| if (!await readModel(projectRoot, scope.model)) throw new Error(`Model "${scope.model}" not found. Run extract phase first.`); | ||
| } | ||
| const scopeWarnings = []; | ||
| for (const patch of patches) { | ||
| const pathError = validatePatchPath(patch.file); | ||
| if (pathError) throw new Error(`Invalid patch path: ${pathError}`); | ||
| } | ||
| if (scope.model || scope.domain) { | ||
| const models = await listModels(projectRoot); | ||
| const scopeModels = scope.model ? models.filter((m) => m.id === scope.model) : scope.domain ? models.filter((m) => m.domain === scope.domain) : models; | ||
| if (scopeModels.length === 0) throw new Error(`No models found for scope ${scope.model ? `model="${scope.model}"` : `domain="${scope.domain}"`}`); | ||
| const { autoDetectSourceDirs } = await import("./core/scan-config.mjs"); | ||
| const sourceDirs = await autoDetectSourceDirs(projectRoot); | ||
| const allowedPrefixes = sourceDirs.map((d) => d === "." ? "" : d + "/"); | ||
| for (const patch of patches) { | ||
| const normalizedPath = patch.file.replace(/\\/g, "/"); | ||
| if (normalizedPath.startsWith(".contentrain/") || normalizedPath.includes("/.contentrain/")) throw new Error(`Cannot patch content/config files directly: "${patch.file}". Reuse patches source files only.`); | ||
| if (sourceDirs.length > 0 && sourceDirs[0] !== ".") { | ||
| if (!allowedPrefixes.some((prefix) => prefix === "" || normalizedPath.startsWith(prefix))) throw new Error(`Patch file "${patch.file}" is outside detected source directories (${sourceDirs.join(", ")}). Reuse patches must target source files within the project's source tree.`); | ||
| } | ||
| } | ||
| if (scope.model || scope.domain) { | ||
| const sourcesRaw = await readText(join(projectRoot, ".contentrain", "normalize-sources.json")); | ||
| if (!sourcesRaw) if (scopeModels.find((m) => m.id === scope.model)?.kind === "dictionary") scopeWarnings.push("normalize-sources.json not found. Dictionary models do not generate per-file source maps. Scope enforcement is based on source-tree locality only."); | ||
| else if (dry_run !== false) scopeWarnings.push("normalize-sources.json not found. Semantic scope enforcement is unavailable. Merge the extract branch first, then reuse will have full scope protection."); | ||
| else throw new Error("Cannot execute reuse: normalize-sources.json not found on base branch. The extract branch must be merged before reuse can execute. This ensures semantic scope enforcement protects against out-of-scope patching."); | ||
| else { | ||
| const sourcesData = JSON.parse(sourcesRaw); | ||
| let allowedSourceFiles = []; | ||
| let scopeLabel = ""; | ||
| if (scope.model) { | ||
| const modelSources = sourcesData.models?.[scope.model]?.source_files; | ||
| if (modelSources) allowedSourceFiles = modelSources; | ||
| scopeLabel = `model "${scope.model}"`; | ||
| } else if (scope.domain) { | ||
| const domainModelIds = scopeModels.map((m) => m.id); | ||
| for (const modelId of domainModelIds) { | ||
| const modelSources = sourcesData.models?.[modelId]?.source_files; | ||
| if (modelSources) allowedSourceFiles.push(...modelSources); | ||
| } | ||
| allowedSourceFiles = [...new Set(allowedSourceFiles)]; | ||
| scopeLabel = `domain "${scope.domain}"`; | ||
| } | ||
| if (allowedSourceFiles.length > 0) { | ||
| const outOfScopePatches = []; | ||
| for (const patch of patches) { | ||
| const normalizedPath = patch.file.replace(/\\/g, "/"); | ||
| if (!allowedSourceFiles.includes(normalizedPath)) outOfScopePatches.push(patch.file); | ||
| } | ||
| if (outOfScopePatches.length > 0) throw new Error(`Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. Out-of-scope files: ${outOfScopePatches.join(", ")}. Known source files: ${allowedSourceFiles.join(", ")}.`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| const patchesByFile = /* @__PURE__ */ new Map(); | ||
| for (const patch of patches) { | ||
| if (!patchesByFile.has(patch.file)) patchesByFile.set(patch.file, []); | ||
| patchesByFile.get(patch.file).push(patch); | ||
| } | ||
| const filesToModify = [...patchesByFile.keys()]; | ||
| const importsToAdd = patches.filter((p) => p.import_statement).length; | ||
| if (dry_run !== false) return { | ||
| dry_run: true, | ||
| preview: { | ||
| files_to_modify: filesToModify, | ||
| patches_count: patches.length, | ||
| imports_to_add: importsToAdd | ||
| }, | ||
| ...scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}, | ||
| next_steps: [ | ||
| ...scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : [], | ||
| "Review the files and patches above", | ||
| "Call contentrain_apply with mode:reuse and dry_run:false to execute" | ||
| ] | ||
| }; | ||
| const reuseHealth = await checkBranchHealth(projectRoot); | ||
| if (reuseHealth.blocked) return { | ||
| dry_run: false, | ||
| error: `Branch blocked: ${reuseHealth.message}`, | ||
| next_steps: ["Merge or delete old contentrain/* branches before executing reuse."] | ||
| }; | ||
| const scopeTarget = scope.model ?? scope.domain; | ||
| const branchName = buildBranchName("normalize/reuse", scopeTarget); | ||
| const tx = await createTransaction(projectRoot, branchName, { workflowOverride: "review" }); | ||
| const filesModified = []; | ||
| let patchesApplied = 0; | ||
| let importsAdded = 0; | ||
| const patchesSkipped = []; | ||
| const frameworkWarnings = []; | ||
| const syntaxErrors = []; | ||
| try { | ||
| await tx.write(async (wt) => { | ||
| for (const [relFile, filePatches] of patchesByFile) { | ||
| const absPath = join(wt, relFile); | ||
| if (!await pathExists(absPath)) { | ||
| for (const p of filePatches) patchesSkipped.push({ | ||
| file: relFile, | ||
| line: p.line, | ||
| reason: "file not found" | ||
| }); | ||
| continue; | ||
| } | ||
| const content = await readText(absPath); | ||
| if (content === null) { | ||
| for (const p of filePatches) patchesSkipped.push({ | ||
| file: relFile, | ||
| line: p.line, | ||
| reason: "file unreadable" | ||
| }); | ||
| continue; | ||
| } | ||
| const sorted = [...filePatches].toSorted((a, b) => b.line - a.line); | ||
| const lines = content.split("\n"); | ||
| let fileModified = false; | ||
| for (const patch of sorted) { | ||
| const isTagTextContext = (lines[patch.line - 1] ?? "").includes(`>${patch.old_value}<`); | ||
| const replacementContext = isTagTextContext ? "tag_text" : "other"; | ||
| if (isTagTextContext) { | ||
| const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext); | ||
| if (fwWarning) frameworkWarnings.push({ | ||
| file: relFile, | ||
| warning: fwWarning | ||
| }); | ||
| } | ||
| if (applyPatchToLines(lines, patch)) { | ||
| patchesApplied++; | ||
| fileModified = true; | ||
| } else patchesSkipped.push({ | ||
| file: relFile, | ||
| line: patch.line, | ||
| reason: "old_value not found at or near specified line" | ||
| }); | ||
| } | ||
| const importStatements = new Set(filePatches.filter((p) => p.import_statement).map((p) => p.import_statement)); | ||
| if (importStatements.size > 0) { | ||
| const added = addImportsToLines(lines, importStatements); | ||
| importsAdded += added; | ||
| if (added > 0) fileModified = true; | ||
| } | ||
| if (fileModified) { | ||
| const newContent = lines.join("\n"); | ||
| await writeText(absPath, newContent); | ||
| filesModified.push(relFile); | ||
| const syntaxError = checkSyntax(relFile, newContent); | ||
| if (syntaxError) syntaxErrors.push({ | ||
| file: relFile, | ||
| error: syntaxError | ||
| }); | ||
| } | ||
| } | ||
| await writeContext(wt, { | ||
| tool: "contentrain_apply", | ||
| model: scopeTarget, | ||
| locale: config.locales.default | ||
| }); | ||
| }); | ||
| if (filesModified.length === 0) { | ||
| await tx.cleanup(); | ||
| return { | ||
| dry_run: false, | ||
| results: { | ||
| files_modified: [], | ||
| patches_applied: 0, | ||
| patches_skipped: patchesSkipped, | ||
| imports_added: 0, | ||
| framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : void 0 | ||
| }, | ||
| next_steps: ["No files were modified. Check patch definitions and try again."] | ||
| }; | ||
| } | ||
| const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`; | ||
| await tx.commit(commitMsg); | ||
| const gitResult = { | ||
| branch: branchName, | ||
| action: "pending-review", | ||
| commit: "" | ||
| }; | ||
| try { | ||
| const completed = await tx.complete(); | ||
| gitResult.action = completed.action; | ||
| gitResult.commit = completed.commit; | ||
| if (completed.warning !== void 0) gitResult.warning = completed.warning; | ||
| } catch (error) { | ||
| gitResult.action = "incomplete"; | ||
| gitResult.warning = `Content was committed to "${branchName}" but the transaction could not be completed: ${error instanceof Error ? error.message : String(error)}. The branch is preserved — inspect it with contentrain_branch_list before retrying.`; | ||
| } finally { | ||
| await tx.cleanup(); | ||
| } | ||
| return { | ||
| dry_run: false, | ||
| results: { | ||
| files_modified: filesModified, | ||
| patches_applied: patchesApplied, | ||
| patches_skipped: patchesSkipped, | ||
| imports_added: importsAdded, | ||
| framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : void 0, | ||
| syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : void 0 | ||
| }, | ||
| ...scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}, | ||
| git: gitResult, | ||
| next_steps: [ | ||
| "Run contentrain_validate to verify the patched files", | ||
| patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : "", | ||
| syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : "", | ||
| scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : "", | ||
| "Run contentrain_submit to push the branch for review", | ||
| "For review: direct user to http://localhost:3333/branches or use contentrain_merge", | ||
| "After all reuse phases complete, run `npx contentrain generate` to update SDK types" | ||
| ].filter(Boolean) | ||
| }; | ||
| } catch (error) { | ||
| await tx.cleanup(); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * Apply a single patch to a lines array. Mutates lines in place. | ||
| * Uses line hint for proximity matching — searches ±10 lines from hint. | ||
| */ | ||
| function applyPatchToLines(lines, patch) { | ||
| const { line, old_value, new_expression } = patch; | ||
| const lineIdx = line - 1; | ||
| const searchStart = Math.max(0, lineIdx - 10); | ||
| const searchEnd = Math.min(lines.length, lineIdx + 11); | ||
| if (lineIdx >= 0 && lineIdx < lines.length) { | ||
| const replaced = replaceInLine(lines[lineIdx], old_value, new_expression); | ||
| if (replaced !== null) { | ||
| lines[lineIdx] = replaced; | ||
| return true; | ||
| } | ||
| } | ||
| for (let i = searchStart; i < searchEnd; i++) { | ||
| if (i === lineIdx) continue; | ||
| const replaced = replaceInLine(lines[i], old_value, new_expression); | ||
| if (replaced !== null) { | ||
| lines[i] = replaced; | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * Replace old_value with new_expression in a single line. | ||
| * Matches the string literal (quoted or unquoted tag text). | ||
| * Returns the modified line, or null if not found. | ||
| * | ||
| * Guardrail #4: Safer patch matching — word boundary awareness and | ||
| * ambiguity rejection for plain text fallback. | ||
| */ | ||
| function replaceInLine(line, oldValue, newExpression) { | ||
| for (const quote of [ | ||
| "\"", | ||
| "'", | ||
| "`" | ||
| ]) { | ||
| const quoted = `${quote}${oldValue}${quote}`; | ||
| if (line.includes(quoted)) return line.replace(quoted, newExpression); | ||
| } | ||
| if (line.includes(`>${oldValue}<`)) return line.replace(`>${oldValue}<`, `>${newExpression}<`); | ||
| if (line.includes(oldValue)) { | ||
| if (countOccurrences(line, oldValue) > 1) return null; | ||
| const idx = line.indexOf(oldValue); | ||
| const charBefore = idx > 0 ? line[idx - 1] : ""; | ||
| const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length] : ""; | ||
| const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]); | ||
| const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]); | ||
| if (oldStartsWithWord && isWordChar(charBefore)) return null; | ||
| if (oldEndsWithWord && isWordChar(charAfter)) return null; | ||
| return line.replace(oldValue, newExpression); | ||
| } | ||
| return null; | ||
| } | ||
| /** Count non-overlapping occurrences of a substring */ | ||
| function countOccurrences(str, sub) { | ||
| let count = 0; | ||
| let pos = 0; | ||
| while (pos <= str.length - sub.length) { | ||
| const idx = str.indexOf(sub, pos); | ||
| if (idx === -1) break; | ||
| count++; | ||
| pos = idx + sub.length; | ||
| } | ||
| return count; | ||
| } | ||
| /** Check if a character is a word character (letter, digit, underscore) */ | ||
| function isWordChar(ch) { | ||
| return /\w/.test(ch); | ||
| } | ||
| /** | ||
| * Add import statements to the top of a file (after existing imports). | ||
| * Deduplicates — won't add if the import already exists. | ||
| * Returns number of imports actually added. | ||
| */ | ||
| function addImportsToLines(lines, imports) { | ||
| let added = 0; | ||
| const existingContent = lines.join("\n"); | ||
| let lastImportIdx = -1; | ||
| let inMultiLineImport = false; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const trimmed = lines[i].trim(); | ||
| if (inMultiLineImport) { | ||
| lastImportIdx = i; | ||
| if (trimmed.includes("}")) inMultiLineImport = false; | ||
| continue; | ||
| } | ||
| if (trimmed.startsWith("import ") || trimmed.startsWith("import{")) { | ||
| lastImportIdx = i; | ||
| if (trimmed.includes("{") && !trimmed.includes("}")) inMultiLineImport = true; | ||
| continue; | ||
| } | ||
| if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*")) break; | ||
| } | ||
| let insertAt; | ||
| if (lastImportIdx >= 0) insertAt = lastImportIdx + 1; | ||
| else { | ||
| insertAt = 0; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const trimmed = lines[i].trim(); | ||
| if (i === 0 && trimmed.startsWith("#!")) { | ||
| insertAt = i + 1; | ||
| continue; | ||
| } | ||
| if (trimmed === "'use client'" || trimmed === "\"use client\"" || trimmed === "'use server'" || trimmed === "\"use server\"" || trimmed === "'use client';" || trimmed === "\"use client\";" || trimmed === "'use server';" || trimmed === "\"use server\";") { | ||
| insertAt = i + 1; | ||
| continue; | ||
| } | ||
| if (insertAt > 0 && trimmed.length > 0) break; | ||
| if (insertAt === 0 && trimmed.length > 0) break; | ||
| } | ||
| } | ||
| const toInsert = []; | ||
| for (const imp of imports) if (!existingContent.includes(imp)) { | ||
| toInsert.push(imp); | ||
| added++; | ||
| } | ||
| if (toInsert.length > 0) lines.splice(insertAt, 0, ...toInsert); | ||
| return added; | ||
| } | ||
| //#endregion | ||
| export { detectFileFramework as a, validatePatchPath as c, checkSyntax as i, applyExtract as n, replaceInLine as o, applyReuse as r, validateFrameworkExpression as s, PATCHABLE_EXTENSIONS as t }; | ||
| //# sourceMappingURL=apply-manager-SLCRLHN_.mjs.map |
| {"version":3,"file":"apply-manager-SLCRLHN_.mjs","names":[],"sources":["../src/core/apply-manager.ts"],"sourcesContent":["import type { ModelDefinition, FieldDef, FileFramework } from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText, writeText, pathExists } from '../util/fs.js'\nimport { readModel, writeModel, listModels, validateModelDefinition } from './model-manager.js'\nimport { writeContent, resolveContentDir, resolveJsonFilePath, resolveMdFilePath, type ContentEntry } from './content-manager.js'\nimport { readConfig } from './config.js'\nimport { writeContext } from './context.js'\nimport { createTransaction, buildBranchName } from '../git/transaction.js'\nimport { checkBranchHealth } from '../git/branch-lifecycle.js'\n\n// ─── Types ───\n\nexport interface ExtractionEntry {\n model: string\n kind: 'singleton' | 'collection' | 'dictionary' | 'document'\n domain: string\n i18n?: boolean\n fields?: Record<string, FieldDef>\n entries: Array<{\n locale?: string\n slug?: string\n data: Record<string, unknown>\n source?: { file: string; line: number; value: string }\n sources?: Array<{ file: string; line: number; key: string; value: string }>\n }>\n}\n\nexport interface ExtractionInput {\n extractions: ExtractionEntry[]\n dry_run?: boolean\n}\n\nexport interface ExtractionPreview {\n models_to_create: string[]\n models_to_update: string[]\n total_entries: number\n content_files: string[]\n}\n\nexport interface ExtractionResult {\n dry_run: boolean\n preview?: ExtractionPreview\n error?: string\n validation_errors?: string[]\n results?: {\n models_created: string[]\n models_updated: string[]\n entries_written: number\n source_map: Array<{ model: string; locale: string; value: string; file: string; line: number }>\n }\n git?: { branch: string; action: string; commit: string; warning?: string }\n context_updated?: boolean\n next_steps: string[]\n}\n\nexport interface PatchEntry {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\nexport interface ReuseInput {\n scope: { model?: string; domain?: string }\n patches: PatchEntry[]\n dry_run?: boolean\n}\n\nexport interface SyntaxError {\n file: string\n error: string\n}\n\nexport interface ReuseResult {\n dry_run: boolean\n error?: string\n scope_warnings?: string[]\n preview?: {\n files_to_modify: string[]\n patches_count: number\n imports_to_add: number\n }\n results?: {\n files_modified: string[]\n patches_applied: number\n patches_skipped: Array<{ file: string; line: number; reason: string }>\n imports_added: number\n framework_warnings?: Array<{ file: string; warning: string }>\n syntax_errors?: SyntaxError[]\n }\n git?: { branch: string; action: string; commit: string; warning?: string }\n next_steps: string[]\n}\n\n// ─── Constants ───\n\nconst MAX_PATCHES = 100\n\n/** File extensions allowed for patching — scannable source files only */\nexport const PATCHABLE_EXTENSIONS = new Set([\n '.vue', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directories that must never be patched */\nconst FORBIDDEN_PATH_SEGMENTS = new Set([\n '.contentrain', 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',\n])\n\n// ─── Framework Detection (Guardrail #2) ───\n\nexport type { FileFramework } from '@contentrain/types'\n\nexport function detectFileFramework(filePath: string): FileFramework {\n const ext = extname(filePath).toLowerCase()\n switch (ext) {\n case '.vue': return 'vue'\n case '.svelte': return 'svelte'\n case '.tsx':\n case '.jsx': return 'jsx'\n case '.astro': return 'astro'\n case '.ts':\n case '.js':\n case '.mjs': return 'script'\n default: return 'script'\n }\n}\n\n/**\n * Validate that a replacement expression uses the correct template syntax\n * for the target file's framework. Returns a warning string or null.\n */\nexport function validateFrameworkExpression(\n filePath: string,\n newExpression: string,\n context: 'tag_text' | 'other',\n): string | null {\n if (context !== 'tag_text') return null\n\n const framework = detectFileFramework(filePath)\n\n switch (framework) {\n case 'vue':\n if (!newExpression.includes('{{')) {\n return `Vue file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{{\" — expected Vue template syntax like {{ $t('key') }}`\n }\n break\n case 'jsx':\n if (!newExpression.includes('{')) {\n return `JSX file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected JSX syntax like {t('key')}`\n }\n break\n case 'svelte':\n if (!newExpression.includes('{')) {\n return `Svelte file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Svelte syntax like {$t('key')}`\n }\n break\n case 'astro':\n if (!newExpression.includes('{')) {\n return `Astro file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Astro syntax like {t('key')}`\n }\n break\n case 'script':\n // Script files don't have template interpolation — warn if attempting tag text replacement\n return `Script file \"${filePath}\": tag text replacement not applicable for .ts/.js files`\n }\n\n return null\n}\n\n// ─── Scope Validation (Guardrail #1) ───\n\n/**\n * Validate that a patch file path is safe and within allowed scope.\n * Returns an error message or null if valid.\n */\nexport function validatePatchPath(filePath: string): string | null {\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n\n // Reject path traversal\n if (normalizedPath.includes('..')) {\n return `Path traversal detected: \"${filePath}\"`\n }\n\n // Reject absolute paths\n if (normalizedPath.startsWith('/')) {\n return `Absolute path not allowed: \"${filePath}\"`\n }\n\n // Reject forbidden directories\n const segments = normalizedPath.split('/')\n for (const seg of segments) {\n if (FORBIDDEN_PATH_SEGMENTS.has(seg)) {\n return `Patching files inside \"${seg}/\" is not allowed: \"${filePath}\"`\n }\n }\n\n // Reject non-scannable extensions\n const ext = extname(filePath).toLowerCase()\n if (!PATCHABLE_EXTENSIONS.has(ext)) {\n return `File extension \"${ext}\" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(', ')}. Path: \"${filePath}\"`\n }\n\n return null\n}\n\n// ─── Syntax Check (Guardrail #5) ───\n\n/**\n * Perform a basic syntax check on a patched file.\n * Returns an error message or null if syntax appears valid.\n */\nexport function checkSyntax(filePath: string, content: string): string | null {\n const ext = extname(filePath).toLowerCase()\n\n switch (ext) {\n case '.ts':\n case '.tsx':\n case '.js':\n case '.jsx':\n case '.mjs':\n return checkJsSyntax(content)\n case '.vue':\n return checkVueSyntax(content)\n case '.svelte':\n case '.astro':\n return checkTagBalance(content)\n default:\n return null\n }\n}\n\n/**\n * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure.\n * This is intentionally conservative — it catches obvious breakage without\n * requiring a full parser.\n */\nfunction checkJsSyntax(content: string): string | null {\n const stack: string[] = []\n const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' }\n let inString: string | null = null\n let escaped = false\n let inLineComment = false\n let inBlockComment = false\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i]!\n const next = content[i + 1]\n\n // Handle escape sequences inside strings\n if (escaped) {\n escaped = false\n continue\n }\n\n if (ch === '\\\\' && inString !== null) {\n escaped = true\n continue\n }\n\n // Line comment\n if (!inString && !inBlockComment && ch === '/' && next === '/') {\n inLineComment = true\n continue\n }\n if (inLineComment) {\n if (ch === '\\n') inLineComment = false\n continue\n }\n\n // Block comment\n if (!inString && !inBlockComment && ch === '/' && next === '*') {\n inBlockComment = true\n i++ // skip *\n continue\n }\n if (inBlockComment) {\n if (ch === '*' && next === '/') {\n inBlockComment = false\n i++ // skip /\n }\n continue\n }\n\n // String handling\n if (inString !== null) {\n if (ch === inString) {\n // Template literal allows multi-line, others don't\n inString = null\n } else if (inString !== '`' && ch === '\\n') {\n return `Unterminated string literal near offset ${i}`\n }\n continue\n }\n\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n continue\n }\n\n // Bracket matching\n if (ch === '(' || ch === '[' || ch === '{') {\n stack.push(ch)\n } else if (ch === ')' || ch === ']' || ch === '}') {\n const expected = pairs[ch]!\n if (stack.length === 0) {\n return `Unmatched closing \"${ch}\" near offset ${i}`\n }\n const top = stack.pop()!\n if (top !== expected) {\n return `Mismatched bracket: expected closing for \"${top}\" but found \"${ch}\" near offset ${i}`\n }\n }\n }\n\n if (inString !== null) {\n return `Unterminated string literal (opened with ${inString})`\n }\n\n if (stack.length > 0) {\n return `Unclosed bracket \"${stack[stack.length - 1]}\" — ${stack.length} unclosed bracket(s)`\n }\n\n return null\n}\n\n/**\n * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced.\n */\nfunction checkVueSyntax(content: string): string | null {\n const tagBalance = checkTagBalance(content)\n if (tagBalance) return tagBalance\n\n // Vue-specific: check that SFC root tags are present and balanced\n for (const tag of ['template', 'script']) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n/**\n * Basic tag balance check for HTML-like files.\n * Checks that self-closing tags are handled and major structural tags are balanced.\n */\nfunction checkTagBalance(content: string): string | null {\n // Check for common structural tags balance\n const structuralTags = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'ul', 'ol', 'table']\n\n for (const tag of structuralTags) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n// ─── Extract Mode ───\n\nexport async function applyExtract(\n projectRoot: string,\n input: ExtractionInput,\n): Promise<ExtractionResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { extractions, dry_run } = input\n\n // Analyze what will happen\n const existingModels = await listModels(projectRoot)\n const existingIds = new Set(existingModels.map(m => m.id))\n\n const modelsToCreate: string[] = []\n const modelsToUpdate: string[] = []\n const contentFiles: string[] = []\n let totalEntries = 0\n\n const validationErrors: string[] = []\n\n for (const ext of extractions) {\n // Validate model definition with same rules as model_save\n const modelErrors = validateModelDefinition({\n id: ext.model,\n kind: ext.kind,\n fields: ext.fields as Record<string, unknown> | undefined,\n })\n if (modelErrors.errors.length > 0) {\n validationErrors.push(...modelErrors.errors.map(e => `[${ext.model}] ${e}`))\n }\n\n for (const entry of ext.entries) {\n if (ext.kind === 'dictionary') {\n if (entry.data['id'] !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`)\n }\n for (const [key, val] of Object.entries(entry.data)) {\n if (typeof val !== 'string') {\n validationErrors.push(`[${ext.model}] Dictionary entry value for key \"${key}\" must be a string, got ${typeof val}`)\n }\n }\n } else if (ext.kind === 'document') {\n if (!entry.slug && !entry.data['slug']) {\n validationErrors.push(`[${ext.model}] Document entries must have a slug`)\n }\n } else if (ext.kind === 'collection') {\n if (entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Collection entries should not have slug`)\n }\n } else if (ext.kind === 'singleton') {\n if (entry.data['id'] !== undefined || entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`)\n }\n }\n }\n\n if (existingIds.has(ext.model)) {\n modelsToUpdate.push(ext.model)\n } else {\n modelsToCreate.push(ext.model)\n }\n totalEntries += ext.entries.length\n\n // Guardrail #3: Preview-Execute Parity — use real model metadata if it exists\n let previewModel: ModelDefinition\n if (existingIds.has(ext.model)) {\n const real = await readModel(projectRoot, ext.model)\n if (real) {\n previewModel = real\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n\n const cDir = resolveContentDir(projectRoot, previewModel)\n for (const entry of ext.entries) {\n const locale = entry.locale ?? config.locales.default\n if (ext.kind === 'document' && entry.slug) {\n contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug))\n } else {\n contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale))\n }\n }\n }\n\n const preview: ExtractionPreview = {\n models_to_create: modelsToCreate,\n models_to_update: modelsToUpdate,\n total_entries: totalEntries,\n content_files: [...new Set(contentFiles)],\n }\n\n // Dry run — return preview only (include validation errors if any)\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview,\n ...(validationErrors.length > 0 ? { validation_errors: validationErrors } : {}),\n next_steps: [\n ...(validationErrors.length > 0\n ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`]\n : []),\n 'Review the preview above',\n 'Call contentrain_apply with mode:extract and dry_run:false to execute',\n ],\n }\n }\n\n // Block execute if validation errors exist\n if (validationErrors.length > 0) {\n return {\n dry_run: false,\n error: 'Model validation failed — cannot execute extract with invalid model definitions',\n validation_errors: validationErrors,\n next_steps: ['Fix the validation errors and retry'],\n }\n }\n\n // Branch health gate\n const health = await checkBranchHealth(projectRoot)\n if (health.blocked) {\n return {\n error: health.message,\n action: 'blocked' as const,\n hint: 'Merge or delete old contentrain/* branches before executing normalize.',\n } as unknown as ExtractionResult\n }\n\n // Execute — git transaction (always review mode for normalize)\n const branchName = buildBranchName('normalize', 'extract')\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n const sourceMap: Array<{ model: string; locale: string; value: string; file: string; line: number }> = []\n const modelsCreated: string[] = []\n const modelsUpdated: string[] = []\n let entriesWritten = 0\n\n try {\n await tx.write(async (wt) => {\n for (const ext of extractions) {\n // Create or merge model\n const existing = await readModel(wt, ext.model)\n if (existing) {\n // Merge fields: add new, keep existing\n if (ext.fields) {\n const merged = { ...existing.fields, ...ext.fields }\n // Only overwrite if new fields were actually added\n const newFieldNames = Object.keys(ext.fields).filter(k => !(k in (existing.fields ?? {})))\n if (newFieldNames.length > 0) {\n existing.fields = merged\n await writeModel(wt, existing)\n modelsUpdated.push(ext.model)\n }\n }\n } else {\n // Create new model\n const newModel: ModelDefinition = {\n id: ext.model,\n name: ext.model.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),\n kind: ext.kind,\n domain: ext.domain,\n i18n: ext.i18n ?? true,\n fields: ext.fields,\n }\n await writeModel(wt, newModel)\n modelsCreated.push(ext.model)\n }\n\n // Write content entries\n const model = (await readModel(wt, ext.model))!\n const entries: ContentEntry[] = ext.entries.map(e => ({\n locale: e.locale,\n slug: e.slug,\n data: e.data,\n }))\n\n const wtConfig = await readConfig(wt) ?? config\n await writeContent(wt, model, entries, wtConfig)\n entriesWritten += entries.length\n\n // Track source map\n for (const entry of ext.entries) {\n // For dictionary entries with per-key source tracking\n if (ext.kind === 'dictionary' && entry.sources) {\n for (const s of entry.sources) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: s.value,\n file: s.file,\n line: s.line,\n })\n }\n } else if (entry.source) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: entry.source.value,\n file: entry.source.file,\n line: entry.source.line,\n })\n }\n }\n }\n\n // Write source map for reuse scope enforcement\n if (sourceMap.length > 0) {\n const sourcesByModel: Record<string, { source_files: string[]; entry_count: number }> = {}\n for (const s of sourceMap) {\n if (!sourcesByModel[s.model]) {\n sourcesByModel[s.model] = { source_files: [], entry_count: 0 }\n }\n const modelEntry = sourcesByModel[s.model]!\n if (!modelEntry.source_files.includes(s.file)) {\n modelEntry.source_files.push(s.file)\n }\n modelEntry.entry_count++\n }\n const sourcesJson = JSON.stringify({\n version: 1,\n created_at: new Date().toISOString(),\n models: sourcesByModel,\n }, null, 2) + '\\n'\n // Write to worktree only — merge brings it to main\n // Phase 2 (reuse) must wait for extract branch to be merged first\n await writeText(join(wt, '.contentrain', 'normalize-sources.json'), sourcesJson)\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: extractions.map(e => e.model).join(','),\n locale: config.locales.default,\n entries: extractions.flatMap(e => e.entries.map(en => en.slug ?? 'entry')),\n })\n })\n\n const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`\n await tx.commit(commitMsg)\n\n const gitResult: { branch: string; action: string; commit: string; warning?: string } = {\n branch: branchName,\n action: 'pending-review',\n commit: '',\n }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n if (completed.warning !== undefined) gitResult.warning = completed.warning\n } catch (error) {\n // Never fall through to a value that mimics success. The commit landed\n // but publishing it did not, and 'pending-review' with an empty commit\n // hash is indistinguishable from a healthy review branch.\n gitResult.action = 'incomplete'\n gitResult.warning = `Content was committed to \"${branchName}\" but the transaction could not be completed: `\n + `${error instanceof Error ? error.message : String(error)}. `\n + `The branch is preserved — inspect it with contentrain_branch_list before retrying.`\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n models_created: modelsCreated,\n models_updated: modelsUpdated,\n entries_written: entriesWritten,\n source_map: sourceMap,\n },\n git: gitResult,\n context_updated: true,\n next_steps: [\n 'Run contentrain_validate to check the extracted content',\n 'Run contentrain_submit to push the branch for review',\n 'For browser-based review: ensure `contentrain serve` is running, direct user to http://localhost:3333/normalize',\n 'For terminal workflow: use contentrain_merge to merge the branch locally',\n 'After merge, run `npx contentrain generate` to update SDK client',\n 'After review, proceed with mode:reuse to patch source files',\n ],\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Reuse Mode ───\n\nexport async function applyReuse(\n projectRoot: string,\n input: ReuseInput,\n): Promise<ReuseResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { scope, patches, dry_run } = input\n\n // Validate scope\n if (!scope.model && !scope.domain) {\n throw new Error('Scope required: provide model or domain. Whole-project patching is not allowed.')\n }\n\n // Validate patch count\n if (patches.length > MAX_PATCHES) {\n throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`)\n }\n\n // Check content exists for scope (soft warning)\n if (scope.model) {\n const model = await readModel(projectRoot, scope.model)\n if (!model) {\n throw new Error(`Model \"${scope.model}\" not found. Run extract phase first.`)\n }\n }\n\n const scopeWarnings: string[] = []\n\n // Guardrail #1: Scope Real Enforcement\n // Step 1: Path safety — every patch must target a valid, patchable source file\n for (const patch of patches) {\n const pathError = validatePatchPath(patch.file)\n if (pathError) {\n throw new Error(`Invalid patch path: ${pathError}`)\n }\n }\n\n // Step 2: Semantic scope — verify scope model/domain exists and cross-check patch files\n if (scope.model || scope.domain) {\n const models = await listModels(projectRoot)\n const scopeModels = scope.model\n ? models.filter(m => m.id === scope.model)\n : scope.domain\n ? models.filter(m => m.domain === scope.domain)\n : models\n\n if (scopeModels.length === 0) {\n throw new Error(`No models found for scope ${scope.model ? `model=\"${scope.model}\"` : `domain=\"${scope.domain}\"`}`)\n }\n\n // Step 3: Verify patch files are source files (not content/config/meta files)\n // and belong to detectable source directories (not random locations)\n const { autoDetectSourceDirs } = await import('./scan-config.js')\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n\n // Build allowed file prefixes from source dirs\n const allowedPrefixes = sourceDirs.map(d => d === '.' ? '' : d + '/')\n\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n\n // Reject patches targeting .contentrain/ directory (content files should never be patched by reuse)\n if (normalizedPath.startsWith('.contentrain/') || normalizedPath.includes('/.contentrain/')) {\n throw new Error(`Cannot patch content/config files directly: \"${patch.file}\". Reuse patches source files only.`)\n }\n\n // If source dirs were detected (not just \".\"), verify patch files are within them\n if (sourceDirs.length > 0 && sourceDirs[0] !== '.') {\n const inSourceDir = allowedPrefixes.some(prefix =>\n prefix === '' || normalizedPath.startsWith(prefix),\n )\n if (!inSourceDir) {\n throw new Error(\n `Patch file \"${patch.file}\" is outside detected source directories (${sourceDirs.join(', ')}). ` +\n `Reuse patches must target source files within the project's source tree.`,\n )\n }\n }\n }\n\n // Step 4: Semantic source→model cross-check via normalize-sources.json\n // Source map is written by extract into the review branch worktree.\n // It only exists on base after extract branch is merged.\n // If missing: dry_run proceeds with warning, execute is blocked.\n if (scope.model || scope.domain) {\n const sourcesPath = join(projectRoot, '.contentrain', 'normalize-sources.json')\n const sourcesRaw = await readText(sourcesPath)\n\n if (!sourcesRaw) {\n // Source map not found — check if scoped model is a dictionary\n // Dictionaries store all keys in one entry, so per-file source tracking\n // is not available. Allow reuse with a warning instead of blocking.\n const scopedModel = scopeModels.find(m => m.id === scope.model)\n const isDictionary = scopedModel?.kind === 'dictionary'\n\n if (isDictionary) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Dictionary models do not generate per-file source maps. ' +\n 'Scope enforcement is based on source-tree locality only.',\n )\n } else if (dry_run !== false) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Semantic scope enforcement is unavailable. ' +\n 'Merge the extract branch first, then reuse will have full scope protection.',\n )\n } else {\n throw new Error(\n 'Cannot execute reuse: normalize-sources.json not found on base branch. ' +\n 'The extract branch must be merged before reuse can execute. ' +\n 'This ensures semantic scope enforcement protects against out-of-scope patching.',\n )\n }\n } else {\n const sourcesData = JSON.parse(sourcesRaw) as {\n models?: Record<string, { source_files?: string[] }>\n }\n\n // Collect all allowed source files for the scope\n let allowedSourceFiles: string[] = []\n let scopeLabel = ''\n\n if (scope.model) {\n const modelSources = sourcesData.models?.[scope.model]?.source_files\n if (modelSources) allowedSourceFiles = modelSources\n scopeLabel = `model \"${scope.model}\"`\n } else if (scope.domain) {\n const domainModelIds = scopeModels.map(m => m.id)\n for (const modelId of domainModelIds) {\n const modelSources = sourcesData.models?.[modelId]?.source_files\n if (modelSources) allowedSourceFiles.push(...modelSources)\n }\n allowedSourceFiles = [...new Set(allowedSourceFiles)]\n scopeLabel = `domain \"${scope.domain}\"`\n }\n\n if (allowedSourceFiles.length > 0) {\n const outOfScopePatches: string[] = []\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n if (!allowedSourceFiles.includes(normalizedPath)) {\n outOfScopePatches.push(patch.file)\n }\n }\n if (outOfScopePatches.length > 0) {\n throw new Error(\n `Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. ` +\n `Out-of-scope files: ${outOfScopePatches.join(', ')}. ` +\n `Known source files: ${allowedSourceFiles.join(', ')}.`,\n )\n }\n }\n }\n }\n }\n\n // Group patches by file\n const patchesByFile = new Map<string, PatchEntry[]>()\n for (const patch of patches) {\n if (!patchesByFile.has(patch.file)) {\n patchesByFile.set(patch.file, [])\n }\n patchesByFile.get(patch.file)!.push(patch)\n }\n\n const filesToModify = [...patchesByFile.keys()]\n const importsToAdd = patches.filter(p => p.import_statement).length\n\n // Dry run — return preview only\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview: {\n files_to_modify: filesToModify,\n patches_count: patches.length,\n imports_to_add: importsToAdd,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n next_steps: [\n ...(scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : []),\n 'Review the files and patches above',\n 'Call contentrain_apply with mode:reuse and dry_run:false to execute',\n ],\n }\n }\n\n // Branch health gate\n const reuseHealth = await checkBranchHealth(projectRoot)\n if (reuseHealth.blocked) {\n return {\n dry_run: false,\n error: `Branch blocked: ${reuseHealth.message}`,\n next_steps: ['Merge or delete old contentrain/* branches before executing reuse.'],\n }\n }\n\n // Execute — git transaction\n const scopeTarget = scope.model ?? scope.domain!\n const branchName = buildBranchName('normalize/reuse', scopeTarget)\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n\n const filesModified: string[] = []\n let patchesApplied = 0\n let importsAdded = 0\n const patchesSkipped: Array<{ file: string; line: number; reason: string }> = []\n const frameworkWarnings: Array<{ file: string; warning: string }> = []\n const syntaxErrors: SyntaxError[] = []\n\n try {\n await tx.write(async (wt) => {\n for (const [relFile, filePatches] of patchesByFile) {\n const absPath = join(wt, relFile)\n\n if (!(await pathExists(absPath))) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file not found' })\n }\n continue\n }\n\n const content = await readText(absPath)\n if (content === null) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file unreadable' })\n }\n continue\n }\n\n // Sort patches by line DESC (bottom-up to avoid line shifts)\n const sorted = [...filePatches].toSorted((a, b) => b.line - a.line)\n\n const lines = content.split('\\n')\n let fileModified = false\n\n for (const patch of sorted) {\n // Determine replacement context before applying\n const targetLine = lines[patch.line - 1] ?? ''\n const isTagTextContext = targetLine.includes(`>${patch.old_value}<`)\n const replacementContext = isTagTextContext ? 'tag_text' as const : 'other' as const\n\n // Guardrail #2: Framework-aware validation — only warn for tag text replacements\n if (isTagTextContext) {\n const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext)\n if (fwWarning) {\n frameworkWarnings.push({ file: relFile, warning: fwWarning })\n }\n }\n\n const applied = applyPatchToLines(lines, patch)\n if (applied) {\n patchesApplied++\n fileModified = true\n } else {\n patchesSkipped.push({ file: relFile, line: patch.line, reason: 'old_value not found at or near specified line' })\n }\n }\n\n // Add imports (deduplicate)\n const importStatements = new Set(\n filePatches\n .filter(p => p.import_statement)\n .map(p => p.import_statement!),\n )\n\n if (importStatements.size > 0) {\n const added = addImportsToLines(lines, importStatements)\n importsAdded += added\n if (added > 0) fileModified = true\n }\n\n if (fileModified) {\n const newContent = lines.join('\\n')\n await writeText(absPath, newContent)\n filesModified.push(relFile)\n\n // Guardrail #5: Syntax check after patching\n const syntaxError = checkSyntax(relFile, newContent)\n if (syntaxError) {\n syntaxErrors.push({ file: relFile, error: syntaxError })\n }\n }\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: scopeTarget,\n locale: config.locales.default,\n })\n })\n\n if (filesModified.length === 0) {\n await tx.cleanup()\n return {\n dry_run: false,\n results: {\n files_modified: [],\n patches_applied: 0,\n patches_skipped: patchesSkipped,\n imports_added: 0,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n },\n next_steps: ['No files were modified. Check patch definitions and try again.'],\n }\n }\n\n const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`\n await tx.commit(commitMsg)\n\n const gitResult: { branch: string; action: string; commit: string; warning?: string } = {\n branch: branchName,\n action: 'pending-review',\n commit: '',\n }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n if (completed.warning !== undefined) gitResult.warning = completed.warning\n } catch (error) {\n // Never fall through to a value that mimics success. The commit landed\n // but publishing it did not, and 'pending-review' with an empty commit\n // hash is indistinguishable from a healthy review branch.\n gitResult.action = 'incomplete'\n gitResult.warning = `Content was committed to \"${branchName}\" but the transaction could not be completed: `\n + `${error instanceof Error ? error.message : String(error)}. `\n + `The branch is preserved — inspect it with contentrain_branch_list before retrying.`\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n files_modified: filesModified,\n patches_applied: patchesApplied,\n patches_skipped: patchesSkipped,\n imports_added: importsAdded,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : undefined,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n git: gitResult,\n next_steps: [\n 'Run contentrain_validate to verify the patched files',\n patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : '',\n syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : '',\n scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : '',\n 'Run contentrain_submit to push the branch for review',\n 'For review: direct user to http://localhost:3333/branches or use contentrain_merge',\n 'After all reuse phases complete, run `npx contentrain generate` to update SDK types',\n ].filter(Boolean),\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Patch Helpers ───\n\n/**\n * Apply a single patch to a lines array. Mutates lines in place.\n * Uses line hint for proximity matching — searches ±10 lines from hint.\n */\nfunction applyPatchToLines(lines: string[], patch: PatchEntry): boolean {\n const { line, old_value, new_expression } = patch\n const lineIdx = line - 1 // 0-based\n\n // Search range: ±10 lines from hint\n const searchStart = Math.max(0, lineIdx - 10)\n const searchEnd = Math.min(lines.length, lineIdx + 11)\n\n // First pass: exact line match\n if (lineIdx >= 0 && lineIdx < lines.length) {\n const replaced = replaceInLine(lines[lineIdx]!, old_value, new_expression)\n if (replaced !== null) {\n lines[lineIdx] = replaced\n return true\n }\n }\n\n // Second pass: proximity search\n for (let i = searchStart; i < searchEnd; i++) {\n if (i === lineIdx) continue // already tried\n const replaced = replaceInLine(lines[i]!, old_value, new_expression)\n if (replaced !== null) {\n lines[i] = replaced\n return true\n }\n }\n\n return false\n}\n\n/**\n * Replace old_value with new_expression in a single line.\n * Matches the string literal (quoted or unquoted tag text).\n * Returns the modified line, or null if not found.\n *\n * Guardrail #4: Safer patch matching — word boundary awareness and\n * ambiguity rejection for plain text fallback.\n */\nexport function replaceInLine(line: string, oldValue: string, newExpression: string): string | null {\n // Try exact match of the value in quoted strings\n // Match: \"old_value\", 'old_value', `old_value`\n for (const quote of ['\"', \"'\", '`']) {\n const quoted = `${quote}${oldValue}${quote}`\n if (line.includes(quoted)) {\n return line.replace(quoted, newExpression)\n }\n }\n\n // Try unquoted tag text match: >old_value<\n // The agent provides the complete new_expression with correct framework syntax:\n // JSX: {t('key')}\n // Vue: {{ $t('key') }}\n // Svelte: {$t('key')}\n // So we insert the expression as-is between > and <, without wrapping in braces.\n if (line.includes(`>${oldValue}<`)) {\n return line.replace(`>${oldValue}<`, `>${newExpression}<`)\n }\n\n // Guardrail #4: Safer plain text fallback\n // Only match if old_value appears at a word boundary and is unambiguous\n if (line.includes(oldValue)) {\n // Count occurrences — if multiple, it's ambiguous\n const occurrences = countOccurrences(line, oldValue)\n if (occurrences > 1) {\n return null // ambiguous — let proximity search handle it\n }\n\n // Word boundary check: don't replace \"Submit\" inside \"SubmitButton\"\n // Reject if the old_value is a substring of a larger word — check if\n // adjacent characters are word characters that extend the token.\n const idx = line.indexOf(oldValue)\n const charBefore = idx > 0 ? line[idx - 1]! : ''\n const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length]! : ''\n\n const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]!)\n const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]!)\n\n // If old_value starts with a word char and char before is also word char, it's a partial match\n if (oldStartsWithWord && isWordChar(charBefore)) {\n return null\n }\n // If old_value ends with a word char and char after is also word char, it's a partial match\n if (oldEndsWithWord && isWordChar(charAfter)) {\n return null\n }\n\n return line.replace(oldValue, newExpression)\n }\n\n return null\n}\n\n/** Count non-overlapping occurrences of a substring */\nfunction countOccurrences(str: string, sub: string): number {\n let count = 0\n let pos = 0\n while (pos <= str.length - sub.length) {\n const idx = str.indexOf(sub, pos)\n if (idx === -1) break\n count++\n pos = idx + sub.length\n }\n return count\n}\n\n/** Check if a character is a word character (letter, digit, underscore) */\nfunction isWordChar(ch: string): boolean {\n return /\\w/.test(ch)\n}\n\n/**\n * Add import statements to the top of a file (after existing imports).\n * Deduplicates — won't add if the import already exists.\n * Returns number of imports actually added.\n */\nfunction addImportsToLines(lines: string[], imports: Set<string>): number {\n let added = 0\n const existingContent = lines.join('\\n')\n\n // Find the last import line position (handles multi-line imports)\n let lastImportIdx = -1\n let inMultiLineImport = false\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (inMultiLineImport) {\n lastImportIdx = i\n if (trimmed.includes('}')) inMultiLineImport = false\n continue\n }\n if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {\n lastImportIdx = i\n // Check if this is a multi-line import (has { but no closing })\n if (trimmed.includes('{') && !trimmed.includes('}')) {\n inMultiLineImport = true\n }\n continue\n }\n // Stop searching after a non-import, non-empty, non-comment line following imports\n if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {\n break\n }\n }\n\n let insertAt: number\n if (lastImportIdx >= 0) {\n insertAt = lastImportIdx + 1\n } else {\n // No existing imports found — insert after shebang and directive lines\n insertAt = 0\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (i === 0 && trimmed.startsWith('#!')) {\n insertAt = i + 1\n continue\n }\n if (trimmed === \"'use client'\" || trimmed === '\"use client\"'\n || trimmed === \"'use server'\" || trimmed === '\"use server\"'\n || trimmed === \"'use client';\" || trimmed === '\"use client\";'\n || trimmed === \"'use server';\" || trimmed === '\"use server\";') {\n insertAt = i + 1\n continue\n }\n if (insertAt > 0 && trimmed.length > 0) break\n if (insertAt === 0 && trimmed.length > 0) break\n }\n }\n const toInsert: string[] = []\n\n for (const imp of imports) {\n // Check if this import already exists (by checking the from clause)\n if (!existingContent.includes(imp)) {\n toInsert.push(imp)\n added++\n }\n }\n\n if (toInsert.length > 0) {\n lines.splice(insertAt, 0, ...toInsert)\n }\n\n return added\n}\n"],"mappings":"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"} |
| //#region src/core/ast-scanner/astro-parser.ts | ||
| let _compiler = null; | ||
| async function loadCompiler() { | ||
| if (_compiler) return _compiler; | ||
| try { | ||
| _compiler = await import("./node-BxujauDw.mjs"); | ||
| return _compiler; | ||
| } catch { | ||
| throw new Error("@astrojs/compiler is required to parse .astro files. Install it with: pnpm add -D @astrojs/compiler"); | ||
| } | ||
| } | ||
| let _tsxParser = null; | ||
| async function loadTsxParser() { | ||
| if (_tsxParser) return _tsxParser; | ||
| try { | ||
| _tsxParser = (await import("./tsx-parser-C3XsIrwU.mjs")).parseTsx; | ||
| return _tsxParser; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| const SURROUNDING_MAX = 120; | ||
| /** Attributes whose values are CSS, not content */ | ||
| const CSS_ATTRIBUTES = new Set([ | ||
| "class", | ||
| "style", | ||
| "className" | ||
| ]); | ||
| /** Event/code attribute prefixes — skip these */ | ||
| const CODE_ATTRIBUTE_PREFIXES = [ | ||
| "on", | ||
| "set:", | ||
| "define:", | ||
| "is:" | ||
| ]; | ||
| function getSurroundingByLine(content, line) { | ||
| const lines = content.split("\n"); | ||
| const idx = line - 1; | ||
| const start = Math.max(0, idx - 1); | ||
| const end = Math.min(lines.length - 1, idx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const l = lines[i]; | ||
| if (l !== void 0) parts.push(l); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| function walkAstroTemplate(node, content, results, parentTag = "") { | ||
| switch (node.type) { | ||
| case "root": { | ||
| const root = node; | ||
| for (const child of root.children) walkAstroTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "text": { | ||
| const textNode = node; | ||
| const trimmed = textNode.value.trim(); | ||
| if (trimmed.length > 0 && /\S/.test(trimmed)) { | ||
| const line = textNode.position?.start.line ?? 1; | ||
| const column = textNode.position?.start.column ?? 1; | ||
| results.push({ | ||
| value: trimmed, | ||
| line, | ||
| column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(content, line) | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| case "element": | ||
| case "component": | ||
| case "custom-element": { | ||
| const el = node; | ||
| const tag = el.name; | ||
| for (const attr of el.attributes) processAttribute(attr, tag, content, results); | ||
| for (const child of el.children) walkAstroTemplate(child, content, results, tag); | ||
| break; | ||
| } | ||
| case "fragment": { | ||
| const fragment = node; | ||
| for (const child of fragment.children) walkAstroTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "expression": break; | ||
| case "frontmatter": | ||
| case "comment": | ||
| case "doctype": break; | ||
| default: { | ||
| const unknownNode = node; | ||
| if (unknownNode.children) for (const child of unknownNode.children) walkAstroTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| function processAttribute(attr, parentTag, content, results) { | ||
| const attrName = attr.name; | ||
| if (attr.type === "expression" || attr.type === "spread" || attr.type === "shorthand") return; | ||
| for (const prefix of CODE_ATTRIBUTE_PREFIXES) if (attrName.startsWith(prefix)) return; | ||
| const attrValue = attr.value; | ||
| if (!attrValue || typeof attrValue === "string" && attrValue.trim().length === 0) return; | ||
| const line = attr.position?.start.line ?? 1; | ||
| const column = attr.position?.start.column ?? 1; | ||
| if (CSS_ATTRIBUTES.has(attrName)) { | ||
| results.push({ | ||
| value: attrValue, | ||
| line, | ||
| column, | ||
| context: "css_class", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, line) | ||
| }); | ||
| return; | ||
| } | ||
| results.push({ | ||
| value: attrValue, | ||
| line, | ||
| column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, line) | ||
| }); | ||
| } | ||
| function findFrontmatter(ast) { | ||
| for (const child of ast.children) if (child.type === "frontmatter") return child; | ||
| return null; | ||
| } | ||
| async function parseAstro(content, fileName) { | ||
| const compiler = await loadCompiler(); | ||
| const results = []; | ||
| let parseResult; | ||
| try { | ||
| parseResult = await compiler.parse(content, { position: true }); | ||
| } catch { | ||
| return []; | ||
| } | ||
| const ast = parseResult.ast; | ||
| const frontmatter = findFrontmatter(ast); | ||
| if (frontmatter && frontmatter.value.trim().length > 0) { | ||
| const tsxParser = await loadTsxParser(); | ||
| if (tsxParser) { | ||
| const frontmatterContent = frontmatter.value; | ||
| const contentStartLine = (frontmatter.position?.start.line ?? 1) + 1; | ||
| const scriptResults = tsxParser(frontmatterContent, fileName.replace(/\.astro$/, ".ts")); | ||
| for (const r of scriptResults) results.push({ | ||
| ...r, | ||
| line: r.line + contentStartLine - 1, | ||
| scope: "script" | ||
| }); | ||
| } | ||
| } | ||
| walkAstroTemplate(ast, content, results); | ||
| return results; | ||
| } | ||
| //#endregion | ||
| export { parseAstro }; | ||
| //# sourceMappingURL=astro-parser-LNjEspnf.mjs.map |
| {"version":3,"file":"astro-parser-LNjEspnf.mjs","names":[],"sources":["../src/core/ast-scanner/astro-parser.ts"],"sourcesContent":["// ─── Astro Parser for Scanner v2 ───\n// Parses .astro files using @astrojs/compiler.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\nimport type { ExtractedString } from './types.js'\n\n// ─── Lazy-loaded @astrojs/compiler ───\n\n// Astro AST node types from @astrojs/compiler\ninterface AstroBaseNode {\n type: string\n position?: {\n start: { line: number; column: number; offset: number }\n end?: { line: number; column: number; offset: number }\n }\n}\n\ninterface AstroRoot extends AstroBaseNode {\n type: 'root'\n children: AstroNode[]\n}\n\ninterface AstroElement extends AstroBaseNode {\n type: 'element'\n name: string\n attributes: AstroAttribute[]\n children: AstroNode[]\n}\n\ninterface AstroComponent extends AstroBaseNode {\n type: 'component'\n name: string\n attributes: AstroAttribute[]\n children: AstroNode[]\n}\n\ninterface AstroCustomElement extends AstroBaseNode {\n type: 'custom-element'\n name: string\n attributes: AstroAttribute[]\n children: AstroNode[]\n}\n\ninterface AstroFragment extends AstroBaseNode {\n type: 'fragment'\n children: AstroNode[]\n}\n\ninterface AstroText extends AstroBaseNode {\n type: 'text'\n value: string\n}\n\ninterface AstroExpression extends AstroBaseNode {\n type: 'expression'\n children: AstroNode[]\n}\n\ninterface AstroFrontmatter extends AstroBaseNode {\n type: 'frontmatter'\n value: string\n}\n\ninterface AstroComment extends AstroBaseNode {\n type: 'comment'\n value: string\n}\n\ninterface AstroDoctype extends AstroBaseNode {\n type: 'doctype'\n}\n\ninterface AstroAttribute {\n name: string\n type: 'attribute' | 'expression' | 'spread' | 'shorthand' | 'template-literal'\n kind: string\n value: string\n raw?: string\n position?: {\n start: { line: number; column: number; offset: number }\n end?: { line: number; column: number; offset: number }\n }\n}\n\ntype AstroNode =\n | AstroRoot\n | AstroElement\n | AstroComponent\n | AstroCustomElement\n | AstroFragment\n | AstroText\n | AstroExpression\n | AstroFrontmatter\n | AstroComment\n | AstroDoctype\n | AstroBaseNode\n\ninterface AstroParseResult {\n ast: AstroRoot\n}\n\ninterface AstroCompiler {\n parse: (source: string, options?: { position?: boolean }) => Promise<AstroParseResult>\n}\n\nlet _compiler: AstroCompiler | null = null\n\nasync function loadCompiler(): Promise<AstroCompiler> {\n if (_compiler) return _compiler\n try {\n const mod = await import('@astrojs/compiler')\n _compiler = mod as unknown as AstroCompiler\n return _compiler\n } catch {\n throw new Error(\n '@astrojs/compiler is required to parse .astro files. '\n + 'Install it with: pnpm add -D @astrojs/compiler',\n )\n }\n}\n\n// ─── tsx-parser delegation ───\n\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n return null\n }\n}\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n/** Attributes whose values are CSS, not content */\nconst CSS_ATTRIBUTES = new Set(['class', 'style', 'className'])\n\n/** Event/code attribute prefixes — skip these */\nconst CODE_ATTRIBUTE_PREFIXES = ['on', 'set:', 'define:', 'is:']\n\n// ─── Helpers ───\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n const start = Math.max(0, idx - 1)\n const end = Math.min(lines.length - 1, idx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const l = lines[i]\n if (l !== undefined) {\n parts.push(l)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\nfunction _getLineAndColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let lastNewline = -1\n\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n lastNewline = i\n }\n }\n\n return { line, column: offset - lastNewline }\n}\n\n// ─── Template AST Walker ───\n\nfunction walkAstroTemplate(\n node: AstroNode,\n content: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n const nodeType = node.type\n\n switch (nodeType) {\n case 'root': {\n const root = node as AstroRoot\n for (const child of root.children) {\n walkAstroTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'text': {\n const textNode = node as AstroText\n const trimmed = textNode.value.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n const line = textNode.position?.start.line ?? 1\n const column = textNode.position?.start.column ?? 1\n results.push({\n value: trimmed,\n line,\n column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(content, line),\n })\n }\n break\n }\n\n case 'element':\n case 'component':\n case 'custom-element': {\n const el = node as AstroElement | AstroComponent | AstroCustomElement\n const tag = el.name\n\n // Process attributes\n for (const attr of el.attributes) {\n processAttribute(attr, tag, content, results)\n }\n\n // Recurse into children\n for (const child of el.children) {\n walkAstroTemplate(child, content, results, tag)\n }\n break\n }\n\n case 'fragment': {\n const fragment = node as AstroFragment\n for (const child of fragment.children) {\n walkAstroTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'expression': {\n // JSX expressions like {variable} — code, skip\n // (The agent decides if embedded strings in expressions matter)\n break\n }\n\n case 'frontmatter':\n case 'comment':\n case 'doctype': {\n // Frontmatter is handled separately via tsx-parser delegation\n // Comments and doctype are skipped\n break\n }\n\n default: {\n // For unknown node types, try to walk children\n const unknownNode = node as AstroBaseNode & { children?: AstroNode[] }\n if (unknownNode.children) {\n for (const child of unknownNode.children) {\n walkAstroTemplate(child, content, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processAttribute(\n attr: AstroAttribute,\n parentTag: string,\n content: string,\n results: ExtractedString[],\n): void {\n const attrName = attr.name\n\n // Skip expression attributes (dynamic bindings) and spread attributes\n if (attr.type === 'expression' || attr.type === 'spread' || attr.type === 'shorthand') return\n\n // Skip code-related attributes (event handlers, directives)\n for (const prefix of CODE_ATTRIBUTE_PREFIXES) {\n if (attrName.startsWith(prefix)) return\n }\n\n // Skip boolean attributes (no value)\n const attrValue = attr.value\n if (!attrValue || (typeof attrValue === 'string' && attrValue.trim().length === 0)) return\n\n const line = attr.position?.start.line ?? 1\n const column = attr.position?.start.column ?? 1\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line,\n column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, line),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line,\n column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, line),\n })\n}\n\n// ─── Frontmatter Parsing ───\n\nfunction findFrontmatter(ast: AstroRoot): AstroFrontmatter | null {\n for (const child of ast.children) {\n if (child.type === 'frontmatter') {\n return child as AstroFrontmatter\n }\n }\n return null\n}\n\n// ─── Main Export ───\n\nexport async function parseAstro(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n let parseResult: AstroParseResult\n try {\n parseResult = await compiler.parse(content, { position: true })\n } catch {\n // If Astro parsing fails, return empty — malformed files shouldn't block scanning\n return []\n }\n\n const ast = parseResult.ast\n\n // ─── Frontmatter → tsx-parser ───\n const frontmatter = findFrontmatter(ast)\n if (frontmatter && frontmatter.value.trim().length > 0) {\n const tsxParser = await loadTsxParser()\n if (tsxParser) {\n const frontmatterContent = frontmatter.value\n // Frontmatter starts after the opening ---\n const frontmatterLine = frontmatter.position?.start.line ?? 1\n // The content starts on the line after ---\n const contentStartLine = frontmatterLine + 1\n\n // Astro frontmatter is always TypeScript — resolve filename accordingly\n const resolvedFileName = fileName.replace(/\\.astro$/, '.ts')\n const scriptResults = tsxParser(frontmatterContent, resolvedFileName)\n for (const r of scriptResults) {\n results.push({\n ...r,\n line: r.line + contentStartLine - 1,\n scope: 'script',\n })\n }\n }\n }\n\n // ─── Template (everything outside frontmatter) ───\n walkAstroTemplate(ast, content, results)\n\n return results\n}\n"],"mappings":"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"} |
| import { ContentrainConfig } from "@contentrain/types"; | ||
| import { SimpleGit } from "simple-git"; | ||
| //#region src/git/branch-lifecycle.d.ts | ||
| interface CleanupResult { | ||
| deleted: number; | ||
| remaining: number; | ||
| deletedBranches: string[]; | ||
| } | ||
| interface BranchHealthCheck { | ||
| total: number; | ||
| merged: number; | ||
| unmerged: number; | ||
| warning: boolean; | ||
| blocked: boolean; | ||
| message?: string; | ||
| } | ||
| /** | ||
| * Lists all local contentrain/* branches, deletes those already merged | ||
| * into the base branch, and returns the count of remaining unmerged ones. | ||
| */ | ||
| declare function cleanupMergedBranches(projectRoot: string): Promise<CleanupResult>; | ||
| /** | ||
| * Check branch health: count contentrain/* branches and return warning/blocked status. | ||
| * - 50+ branches: warning | ||
| * - 80+ branches: blocked | ||
| */ | ||
| declare function checkBranchHealth(projectRoot: string): Promise<BranchHealthCheck>; | ||
| interface BranchDiffResult { | ||
| /** The feature branch the diff was computed from. */ | ||
| branch: string; | ||
| /** The base ref the diff was computed against. Defaults to the `contentrain` branch. */ | ||
| base: string; | ||
| /** `git diff --stat` output — human-readable summary. */ | ||
| stat: string; | ||
| /** Raw unified diff. */ | ||
| patch: string; | ||
| /** Number of files touched in the diff. */ | ||
| filesChanged: number; | ||
| } | ||
| /** | ||
| * Compute the diff between a feature branch and its base. | ||
| * | ||
| * Defaults `base` to `CONTENTRAIN_BRANCH` — the singleton content- | ||
| * tracking branch every feature branch forks from. Passing the repo's | ||
| * default branch (e.g. `main`) is almost always a bug: when | ||
| * `contentrain` is ahead of `main`, the diff picks up unrelated | ||
| * historical content changes that the feature branch did not produce. | ||
| * | ||
| * Used by `contentrain serve` (branch detail view), the `contentrain | ||
| * diff` CLI command, and any Studio-side driver that needs to preview | ||
| * a feature branch before approving it. | ||
| */ | ||
| declare function branchDiff(projectRoot: string, opts: { | ||
| branch: string; | ||
| base?: string; | ||
| }): Promise<BranchDiffResult>; | ||
| /** | ||
| * Robust merged check for a single ref: ancestry fast-path, then a bounded | ||
| * `git cherry` (patch-id) fallback that survives base-history rewrites. | ||
| * Returns false when either ref cannot be resolved. | ||
| */ | ||
| declare function isRefMerged(git: SimpleGit, ref: string, into: string, opts?: { | ||
| maxCherryCommits?: number; | ||
| }): Promise<boolean>; | ||
| /** | ||
| * Classify which of the given local branches are merged into `into` | ||
| * (default: the contentrain branch). One `git branch --merged` call covers | ||
| * the ancestry-merged majority; only the remainder pays the patch-id | ||
| * fallback (bounded concurrency, verdicts cached). | ||
| * | ||
| * `opts.fallbackThreshold` skips the patch-id fallback entirely when fewer | ||
| * than that many branches are ancestry-unmerged — the fallback can only | ||
| * LOWER the unmerged count, so callers that merely compare the count | ||
| * against a limit (the hot pre-write gate) pay nothing in the normal case. | ||
| * | ||
| * Throws when `into` does not resolve — callers use this to fall back to | ||
| * the base branch (mirrors the previous `branch --merged` semantics). | ||
| */ | ||
| declare function classifyMergedBranches(projectRoot: string, branches: string[], into?: string, opts?: { | ||
| fallbackThreshold?: number; | ||
| }): Promise<Set<string>>; | ||
| interface RemoteDeleteResult { | ||
| deleted: boolean; | ||
| /** Why nothing was deleted, when that is expected (not a failure). */ | ||
| skipped?: 'disabled' | 'no-remote' | 'not-found' | 'protected'; | ||
| /** A real failure (offline, auth, protected ref) — surfaced, never thrown. */ | ||
| warning?: string; | ||
| } | ||
| /** | ||
| * Best-effort delete of a cr/* branch on the configured remote. Never | ||
| * throws: expected conditions land in `skipped`, real failures in | ||
| * `warning`. Gated by `config.remoteBranchCleanup` (default: on). | ||
| * | ||
| * Pass `opts.config` when the caller already read it (avoids a re-read); | ||
| * `null` means "no config" and applies the default gate. | ||
| */ | ||
| declare function deleteRemoteBranch(projectRoot: string, branch: string, opts?: { | ||
| config?: ContentrainConfig | null; | ||
| timeoutMs?: number; | ||
| }): Promise<RemoteDeleteResult>; | ||
| interface RemoteBranchList { | ||
| remote: string; | ||
| branches: { | ||
| name: string; | ||
| sha: string; | ||
| }[]; | ||
| /** ls-remote failed (offline/timeout) — branches is empty, not authoritative. */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * Authoritative list of cr/* branches on the configured remote via | ||
| * `ls-remote --heads` (no fetch, no stale remote-tracking refs). Returns | ||
| * null when no remote is configured. Never throws. | ||
| */ | ||
| declare function listRemoteCrBranches(projectRoot: string, opts?: { | ||
| timeoutMs?: number; | ||
| }): Promise<RemoteBranchList | null>; | ||
| interface RemotePruneResult { | ||
| /** Branches removed from the remote (in dryRun mode: the candidates). */ | ||
| deleted: string[]; | ||
| kept: string[]; | ||
| errors: string[]; | ||
| skipped?: 'disabled' | 'no-remote' | 'offline'; | ||
| } | ||
| /** | ||
| * Delete already-merged cr/* branches on the remote in batches. Merged-state | ||
| * uses the same ancestry + patch-id classification as the local cleanup, so | ||
| * branches leaked before a base-history rewrite are still recognised. | ||
| * Ignores `branchRetention` — a merged remote copy only produces phantom | ||
| * reviews. Never throws; gated by `config.remoteBranchCleanup`. | ||
| */ | ||
| declare function pruneMergedRemoteBranches(projectRoot: string, opts?: { | ||
| config?: ContentrainConfig | null; | ||
| max?: number; | ||
| dryRun?: boolean; | ||
| timeoutMs?: number; | ||
| }): Promise<RemotePruneResult>; | ||
| //#endregion | ||
| export { RemoteDeleteResult as a, checkBranchHealth as c, deleteRemoteBranch as d, isRefMerged as f, RemoteBranchList as i, classifyMergedBranches as l, pruneMergedRemoteBranches as m, BranchHealthCheck as n, RemotePruneResult as o, listRemoteCrBranches as p, CleanupResult as r, branchDiff as s, BranchDiffResult as t, cleanupMergedBranches as u }; | ||
| //# sourceMappingURL=branch-lifecycle-D4W0WIs4.d.mts.map |
| {"version":3,"file":"branch-lifecycle-D4W0WIs4.d.mts","names":[],"sources":["../src/git/branch-lifecycle.ts"],"mappings":";;;;UAKiB,aAAA;EACf,OAAA;EACA,SAAA;EACA,eAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EACA,OAAA;AAAA;;;;;iBAOoB,qBAAA,CAAsB,WAAA,WAAsB,OAAA,CAAQ,aAAA;;;;;;iBAoEpD,iBAAA,CAAkB,WAAA,WAAsB,OAAA,CAAQ,iBAAA;AAAA,UAkDrD,gBAAA;;EAEf,MAAA;EAxH0C;EA0H1C,IAAA;EA1HwE;EA4HxE,IAAA;EA5HqF;EA8HrF,KAAA;EA1DqC;EA4DrC,YAAA;AAAA;;;;;;AAVF;;;;;;;;iBA0BsB,UAAA,CACpB,WAAA,UACA,IAAA;EAAQ,MAAA;EAAgB,IAAA;AAAA,IACvB,OAAA,CAAQ,gBAAA;;;;;;iBA0FW,WAAA,CACpB,GAAA,EAAK,SAAA,EACL,GAAA,UACA,IAAA,UACA,IAAA;EAAS,gBAAA;AAAA,IACR,OAAA;;;;AALH;;;;;;;;;;;iBAiCsB,sBAAA,CACpB,WAAA,UACA,QAAA,YACA,IAAA,WACA,IAAA;EAAS,iBAAA;AAAA,IACR,OAAA,CAAQ,GAAA;AAAA,UAkFM,kBAAA;EACf,OAAA;EAnFQ;EAqFR,OAAA;EAxFA;EA0FA,OAAA;AAAA;;;;;;AALF;;;iBAgBsB,kBAAA,CACpB,WAAA,UACA,MAAA,UACA,IAAA;EAAS,MAAA,GAAS,iBAAA;EAA0B,SAAA;AAAA,IAC3C,OAAA,CAAQ,kBAAA;AAAA,UAuBM,gBAAA;EACf,MAAA;EACA,QAAA;IAAY,IAAA;IAAc,GAAA;EAAA;EAzBjB;EA2BT,KAAA;AAAA;;;;;;iBAQoB,oBAAA,CACpB,WAAA,UACA,IAAA;EAAS,SAAA;AAAA,IACR,OAAA,CAAQ,gBAAA;AAAA,UAoBM,iBAAA;EA1DY;EA4D3B,OAAA;EACA,IAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;;iBAYoB,yBAAA,CACpB,WAAA,UACA,IAAA;EAAS,MAAA,GAAS,iBAAA;EAA0B,GAAA;EAAc,MAAA;EAAkB,SAAA;AAAA,IAC3E,OAAA,CAAQ,iBAAA"} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { _ as RepoReader } from "./index-w8QHThNS.mjs"; | ||
| import { ContentrainConfig, LocaleStrategy, ModelDefinition, Vocabulary, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types"; | ||
| //#region src/core/content-manager.d.ts | ||
| declare function resolveContentDir(projectRoot: string, model: ModelDefinition): string; | ||
| declare function resolveLocaleStrategy(model: ModelDefinition): LocaleStrategy; | ||
| /** Build the file path for a JSON content file (singleton/collection/dictionary) */ | ||
| declare function resolveJsonFilePath(dir: string, model: ModelDefinition, locale: string): string; | ||
| /** Build the file path for a markdown document */ | ||
| declare function resolveMdFilePath(dir: string, model: ModelDefinition, locale: string, slug: string): string; | ||
| interface ContentEntry { | ||
| id?: string; | ||
| slug?: string; | ||
| locale?: string; | ||
| data: Record<string, unknown>; | ||
| } | ||
| interface WriteResult { | ||
| action: 'created' | 'updated'; | ||
| id?: string; | ||
| slug?: string; | ||
| locale: string; | ||
| advisories?: string[]; | ||
| } | ||
| interface DeleteOpts { | ||
| id?: string; | ||
| slug?: string; | ||
| locale?: string; | ||
| keys?: string[]; | ||
| /** `config.locales.default` — places a non-i18n model's single meta record. */ | ||
| defaultLocale: string; | ||
| } | ||
| interface ListOpts { | ||
| locale?: string; | ||
| filter?: Record<string, unknown>; | ||
| resolve?: boolean; | ||
| limit?: number; | ||
| offset?: number; | ||
| } | ||
| declare function writeContent(projectRoot: string, model: ModelDefinition, entries: ContentEntry[], config: ContentrainConfig, vocabulary?: Vocabulary | null): Promise<WriteResult[]>; | ||
| declare function deleteContent(projectRoot: string, model: ModelDefinition, opts: DeleteOpts): Promise<string[]>; | ||
| /** | ||
| * List content entries for a model. Dual signature: | ||
| * | ||
| * - `listContent(projectRoot, model, opts, config)` — legacy local flow, | ||
| * uses direct filesystem reads and supports `opts.resolve` for | ||
| * cross-model relation hydration. | ||
| * - `listContent(reader, model, opts, config)` — reader-backed flow for | ||
| * remote providers. Basic list works across all four model kinds. | ||
| * `opts.resolve: true` is rejected with an error on remote readers | ||
| * because the cross-model walk requires local filesystem access. | ||
| * | ||
| * The reader-based function lives in {@link listContentViaReader}; the | ||
| * projectRoot-based entry point continues to call the legacy body to | ||
| * preserve bit-for-bit behaviour for every existing caller. | ||
| */ | ||
| declare function listContent(projectRoot: string, model: ModelDefinition, opts: ListOpts, config: ContentrainConfig): Promise<unknown>; | ||
| declare function listContent(reader: RepoReader, model: ModelDefinition, opts: ListOpts, config: ContentrainConfig): Promise<unknown>; | ||
| declare function readContent(projectRoot: string, model: ModelDefinition, opts: { | ||
| locale: string; | ||
| entryId?: string; | ||
| slug?: string; | ||
| }): Promise<unknown>; | ||
| //#endregion | ||
| export { writeContent as _, deleteContent as a, readContent as c, resolveLocaleStrategy as d, resolveMdFilePath as f, validateSlug$1 as g, validateLocale$1 as h, WriteResult as i, resolveContentDir as l, validateEntryId$1 as m, DeleteOpts as n, listContent as o, serializeFrontmatter as p, ListOpts as r, parseFrontmatter as s, ContentEntry as t, resolveJsonFilePath as u }; | ||
| //# sourceMappingURL=content-manager-DWo3G64y.d.mts.map |
| {"version":3,"file":"content-manager-DWo3G64y.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;iBAgBgB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,eAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,eAAA,GAAkB,cAAA;;iBAK/C,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA;;iBAczD,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA,UAAgB,IAAA;AAAA,UAetE,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;AAAA;AAAA,UAGS,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAhDuE;EAkDvE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBA6IW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;AAzLH;;;;;;;;;;;AAOA;;iBA+TgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"} |
| import { a as readJson, i as readDir, o as readText, r as pathExists, t as contentrainDir } from "./fs-DLbVB-Ek.mjs"; | ||
| import { t as readConfig } from "./config-oxxgznz7.mjs"; | ||
| import { g as resolveLocaleStrategy, h as resolveJsonFilePath, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs"; | ||
| import { n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-BAfgSQBv.mjs"; | ||
| import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-BlNLRCMx.mjs"; | ||
| import { join } from "node:path"; | ||
| import { readdir, stat } from "node:fs/promises"; | ||
| import { simpleGit } from "simple-git"; | ||
| //#region src/core/doctor.ts | ||
| async function runDoctor(projectRoot, options = {}) { | ||
| const checks = []; | ||
| try { | ||
| const version = await simpleGit(projectRoot).version(); | ||
| checks.push({ | ||
| name: "Git", | ||
| pass: true, | ||
| detail: `v${version.major}.${version.minor}.${version.patch}` | ||
| }); | ||
| } catch { | ||
| checks.push({ | ||
| name: "Git", | ||
| pass: false, | ||
| detail: "Not installed or not in PATH", | ||
| severity: "error" | ||
| }); | ||
| } | ||
| const hasGit = await pathExists(join(projectRoot, ".git")); | ||
| checks.push({ | ||
| name: "Git repository", | ||
| pass: hasGit, | ||
| detail: hasGit ? projectRoot : "No .git directory found", | ||
| severity: hasGit ? void 0 : "error" | ||
| }); | ||
| const nodeVersion = process.versions.node; | ||
| const [major] = nodeVersion.split(".").map(Number); | ||
| const nodePass = (major ?? 0) >= 22; | ||
| checks.push({ | ||
| name: "Node.js", | ||
| pass: nodePass, | ||
| detail: `v${nodeVersion}${nodePass ? "" : " (requires ≥22)"}`, | ||
| severity: nodePass ? void 0 : "error" | ||
| }); | ||
| const crDir = contentrainDir(projectRoot); | ||
| const hasCrDir = await pathExists(crDir); | ||
| const hasConfig = await pathExists(join(crDir, "config.json")); | ||
| const hasModels = await pathExists(join(crDir, "models")); | ||
| const hasContent = await pathExists(join(crDir, "content")); | ||
| const structurePass = hasCrDir && hasConfig && hasModels && hasContent; | ||
| checks.push({ | ||
| name: ".contentrain/ structure", | ||
| pass: structurePass, | ||
| detail: !hasCrDir ? "Not initialized — run `contentrain init`" : [ | ||
| hasConfig ? null : "missing config.json", | ||
| hasModels ? null : "missing models/", | ||
| hasContent ? null : "missing content/" | ||
| ].filter(Boolean).join(", ") || "OK", | ||
| severity: structurePass ? void 0 : "error" | ||
| }); | ||
| let config = null; | ||
| if (hasConfig) { | ||
| config = await readConfig(projectRoot); | ||
| checks.push({ | ||
| name: "Config", | ||
| pass: config !== null, | ||
| detail: config ? `stack: ${config.stack}, locales: ${config.locales.supported.join(", ")}` : "Failed to parse config.json", | ||
| severity: config ? void 0 : "error" | ||
| }); | ||
| } | ||
| if (hasCrDir) try { | ||
| const models = await listModels(projectRoot); | ||
| const allParseable = (await Promise.all(models.map((m) => readModel(projectRoot, m.id)))).every((r) => r !== null); | ||
| checks.push({ | ||
| name: "Models", | ||
| pass: allParseable, | ||
| detail: `${models.length} model(s)${allParseable ? ", all valid" : ", some failed to parse"}`, | ||
| severity: allParseable ? void 0 : "error" | ||
| }); | ||
| } catch { | ||
| checks.push({ | ||
| name: "Models", | ||
| pass: false, | ||
| detail: "Failed to read models", | ||
| severity: "error" | ||
| }); | ||
| } | ||
| if (hasCrDir) { | ||
| const orphans = await findOrphanContent(projectRoot); | ||
| checks.push({ | ||
| name: "Orphan content", | ||
| pass: orphans.length === 0, | ||
| detail: orphans.length === 0 ? "None" : `Found: ${orphans.join(", ")}`, | ||
| severity: orphans.length === 0 ? void 0 : "warning" | ||
| }); | ||
| } | ||
| if (hasGit) { | ||
| try { | ||
| const health = await checkBranchHealth(projectRoot); | ||
| checks.push({ | ||
| name: "Pending branches", | ||
| pass: !health.blocked && !health.warning, | ||
| detail: health.message ?? (health.unmerged === 0 ? "None" : `${health.unmerged} active cr/* branch(es)`), | ||
| severity: health.blocked ? "error" : health.warning ? "warning" : void 0 | ||
| }); | ||
| } catch { | ||
| checks.push({ | ||
| name: "Pending branches", | ||
| pass: true, | ||
| detail: "Could not check" | ||
| }); | ||
| } | ||
| const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5e3 }); | ||
| if (remoteList) if (remoteList.error) checks.push({ | ||
| name: "Remote branches", | ||
| pass: true, | ||
| detail: `Could not check ${remoteList.remote} (offline?)`, | ||
| severity: "info" | ||
| }); | ||
| else { | ||
| const count = remoteList.branches.length; | ||
| const warn = count >= (config?.branchWarnLimit ?? 50); | ||
| checks.push({ | ||
| name: "Remote branches", | ||
| pass: !warn, | ||
| detail: count === 0 ? `None on ${remoteList.remote}` : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? " — run `contentrain prune` to remove merged leftovers" : ""}`, | ||
| severity: warn ? "warning" : void 0 | ||
| }); | ||
| } | ||
| } | ||
| const clientDir = join(crDir, "client"); | ||
| const modelsDir = join(crDir, "models"); | ||
| if (await pathExists(clientDir) && await pathExists(modelsDir)) try { | ||
| const [clientMtime, modelsMtime] = await Promise.all([newestFileMtime(clientDir), newestFileMtime(modelsDir)]); | ||
| if (clientMtime === null || modelsMtime === null) checks.push({ | ||
| name: "SDK client", | ||
| pass: true, | ||
| detail: "Could not check" | ||
| }); | ||
| else { | ||
| const fresh = clientMtime >= modelsMtime; | ||
| checks.push({ | ||
| name: "SDK client", | ||
| pass: fresh, | ||
| detail: fresh ? "Up to date" : "Stale — run `contentrain generate`", | ||
| severity: fresh ? void 0 : "warning" | ||
| }); | ||
| } | ||
| } catch { | ||
| checks.push({ | ||
| name: "SDK client", | ||
| pass: true, | ||
| detail: "Could not check" | ||
| }); | ||
| } | ||
| let usage; | ||
| if (options.usage && hasCrDir && config) { | ||
| const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([ | ||
| analyzeUnusedKeys(projectRoot, config), | ||
| analyzeDuplicateValues(projectRoot, config), | ||
| analyzeMissingLocaleKeys(projectRoot, config) | ||
| ]); | ||
| usage = { | ||
| unusedKeys, | ||
| duplicateValues, | ||
| missingLocaleKeys | ||
| }; | ||
| checks.push({ | ||
| name: "Unused content keys", | ||
| pass: unusedKeys.length === 0, | ||
| detail: unusedKeys.length === 0 ? "All keys referenced in source" : `${unusedKeys.length} key(s) not referenced in source code`, | ||
| severity: unusedKeys.length === 0 ? void 0 : "warning" | ||
| }); | ||
| checks.push({ | ||
| name: "Duplicate dictionary values", | ||
| pass: duplicateValues.length === 0, | ||
| detail: duplicateValues.length === 0 ? "No duplicate values" : `${duplicateValues.length} value(s) mapped to multiple keys`, | ||
| severity: duplicateValues.length === 0 ? void 0 : "warning" | ||
| }); | ||
| checks.push({ | ||
| name: "Locale key coverage", | ||
| pass: missingLocaleKeys.length === 0, | ||
| detail: missingLocaleKeys.length === 0 ? "All locales have matching keys" : `${missingLocaleKeys.length} key(s) missing in some locales`, | ||
| severity: missingLocaleKeys.length === 0 ? void 0 : "warning" | ||
| }); | ||
| } | ||
| const passed = checks.filter((c) => c.pass).length; | ||
| const failed = checks.length - passed; | ||
| const warnings = checks.filter((c) => !c.pass && c.severity === "warning").length; | ||
| const report = { | ||
| checks, | ||
| summary: { | ||
| total: checks.length, | ||
| passed, | ||
| failed, | ||
| warnings | ||
| } | ||
| }; | ||
| if (usage) report.usage = usage; | ||
| return report; | ||
| } | ||
| /** | ||
| * Newest mtime among the files under `dir`, recursively — null if it holds none. | ||
| * | ||
| * Stat the files, never the directory. A directory's mtime only moves when an | ||
| * entry is added, removed, or renamed inside it: `generate` rewrites the client | ||
| * files in place, so `.contentrain/client` never moves after the first run, | ||
| * while a selective sync recreates model files via `git checkout`, which does | ||
| * move `.contentrain/models`. Comparing the two directories therefore reported | ||
| * "stale" permanently after any model save. | ||
| */ | ||
| async function newestFileMtime(dir) { | ||
| const entries = await readdir(dir, { withFileTypes: true }).catch(() => []); | ||
| const known = (await Promise.all(entries.map(async (entry) => { | ||
| const full = join(dir, entry.name); | ||
| if (entry.isDirectory()) return newestFileMtime(full); | ||
| return stat(full).then((s) => s.mtimeMs, () => null); | ||
| }))).filter((m) => m !== null); | ||
| return known.length > 0 ? Math.max(...known) : null; | ||
| } | ||
| async function findOrphanContent(projectRoot) { | ||
| const crDir = contentrainDir(projectRoot); | ||
| const models = await listModels(projectRoot); | ||
| const orphans = []; | ||
| const knownContentDirs = /* @__PURE__ */ new Set(); | ||
| for (const m of models) { | ||
| const full = await readModel(projectRoot, m.id); | ||
| const modelForPath = full ? { | ||
| ...full, | ||
| content_path: full.content_path ?? m.content_path | ||
| } : { | ||
| id: m.id, | ||
| name: m.id, | ||
| kind: m.kind, | ||
| domain: m.domain, | ||
| i18n: m.i18n, | ||
| fields: {}, | ||
| content_path: m.content_path | ||
| }; | ||
| knownContentDirs.add(resolveContentDir(projectRoot, modelForPath)); | ||
| } | ||
| const contentDir = join(crDir, "content"); | ||
| if (await pathExists(contentDir)) { | ||
| const domains = await readDir(contentDir); | ||
| for (const domain of domains) { | ||
| const domainDir = join(contentDir, domain); | ||
| const entries = await readDir(domainDir); | ||
| for (const entry of entries) { | ||
| if (entry === ".gitkeep") continue; | ||
| const entryDir = join(domainDir, entry); | ||
| if (!knownContentDirs.has(entryDir)) orphans.push(`${domain}/${entry}`); | ||
| } | ||
| } | ||
| } | ||
| for (const dir of knownContentDirs) { | ||
| if (dir.startsWith(contentDir)) continue; | ||
| if (!await pathExists(dir)) { | ||
| orphans.push(`(missing custom path) ${dir}`); | ||
| continue; | ||
| } | ||
| await readDir(dir); | ||
| } | ||
| return orphans; | ||
| } | ||
| async function analyzeUnusedKeys(projectRoot, config) { | ||
| const files = await discoverFiles(projectRoot, { paths: await autoDetectSourceDirs(projectRoot) }); | ||
| if (files.length === 0) return []; | ||
| const allSource = (await Promise.all(files.map(async (relPath) => { | ||
| return await readText(join(projectRoot, relPath)) ?? ""; | ||
| }))).join("\n"); | ||
| const models = await listModels(projectRoot); | ||
| const defaultLocale = config.locales.default; | ||
| const unused = []; | ||
| for (const m of models) { | ||
| const fullModel = await readModel(projectRoot, m.id); | ||
| if (!fullModel) continue; | ||
| const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale); | ||
| for (const key of keys) if (!allSource.includes(key)) unused.push({ | ||
| model: m.id, | ||
| kind: m.kind, | ||
| key, | ||
| locale: defaultLocale | ||
| }); | ||
| } | ||
| return unused; | ||
| } | ||
| async function extractContentKeys(projectRoot, model, locale) { | ||
| const cDir = resolveContentDir(projectRoot, model); | ||
| if (!await pathExists(cDir)) return []; | ||
| switch (model.kind) { | ||
| case "dictionary": { | ||
| const data = await readJson(resolveJsonFilePath(cDir, model, locale)); | ||
| return data ? Object.keys(data) : []; | ||
| } | ||
| case "collection": { | ||
| const data = await readJson(resolveJsonFilePath(cDir, model, locale)); | ||
| return data ? Object.keys(data) : []; | ||
| } | ||
| case "document": { | ||
| const strategy = resolveLocaleStrategy(model); | ||
| const slugs = []; | ||
| if (!model.i18n) { | ||
| const files = await readDir(cDir); | ||
| for (const f of files) if (f.endsWith(".md")) slugs.push(f.replace(".md", "")); | ||
| } else if (strategy === "file") { | ||
| const dirs = await readDir(cDir); | ||
| for (const d of dirs) if (!d.startsWith(".")) slugs.push(d); | ||
| } else if (strategy === "suffix") { | ||
| const files = await readDir(cDir); | ||
| const suffix = `.${locale}.md`; | ||
| for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length)); | ||
| } else if (strategy === "directory") { | ||
| const localeDir = join(cDir, locale); | ||
| if (await pathExists(localeDir)) { | ||
| const files = await readDir(localeDir); | ||
| for (const f of files) if (f.endsWith(".md")) slugs.push(f.replace(".md", "")); | ||
| } | ||
| } else { | ||
| const files = await readDir(cDir); | ||
| for (const f of files) if (f.endsWith(".md")) slugs.push(f.replace(".md", "")); | ||
| } | ||
| return slugs; | ||
| } | ||
| case "singleton": return []; | ||
| default: return []; | ||
| } | ||
| } | ||
| async function analyzeDuplicateValues(projectRoot, config) { | ||
| const models = await listModels(projectRoot); | ||
| const result = []; | ||
| for (const m of models) { | ||
| if (m.kind !== "dictionary") continue; | ||
| const fullModel = await readModel(projectRoot, m.id); | ||
| if (!fullModel) continue; | ||
| const cDir = resolveContentDir(projectRoot, fullModel); | ||
| for (const locale of config.locales.supported) { | ||
| const data = await readJson(resolveJsonFilePath(cDir, fullModel, locale)); | ||
| if (!data) continue; | ||
| const valueToKeys = /* @__PURE__ */ new Map(); | ||
| for (const [key, value] of Object.entries(data)) { | ||
| const arr = valueToKeys.get(value); | ||
| if (arr) arr.push(key); | ||
| else valueToKeys.set(value, [key]); | ||
| } | ||
| for (const [value, keys] of valueToKeys) if (keys.length > 1) result.push({ | ||
| model: m.id, | ||
| locale, | ||
| value, | ||
| keys | ||
| }); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| async function analyzeMissingLocaleKeys(projectRoot, config) { | ||
| if (config.locales.supported.length < 2) return []; | ||
| const models = await listModels(projectRoot); | ||
| const result = []; | ||
| const defaultLocale = config.locales.default; | ||
| const otherLocales = config.locales.supported.filter((l) => l !== defaultLocale); | ||
| for (const m of models) { | ||
| if (m.kind !== "dictionary" && m.kind !== "collection") continue; | ||
| if (!m.i18n) continue; | ||
| const fullModel = await readModel(projectRoot, m.id); | ||
| if (!fullModel) continue; | ||
| const cDir = resolveContentDir(projectRoot, fullModel); | ||
| const defaultData = await readJson(resolveJsonFilePath(cDir, fullModel, defaultLocale)); | ||
| if (!defaultData) continue; | ||
| const defaultKeys = new Set(Object.keys(defaultData)); | ||
| for (const locale of otherLocales) { | ||
| const localeData = await readJson(resolveJsonFilePath(cDir, fullModel, locale)); | ||
| const localeKeys = localeData ? new Set(Object.keys(localeData)) : /* @__PURE__ */ new Set(); | ||
| for (const key of defaultKeys) if (!localeKeys.has(key)) result.push({ | ||
| model: m.id, | ||
| key, | ||
| missingIn: locale | ||
| }); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| //#endregion | ||
| export { runDoctor as t }; | ||
| //# sourceMappingURL=doctor-BwJ_nmqS.mjs.map |
| {"version":3,"file":"doctor-BwJ_nmqS.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport { simpleGit } from 'simple-git'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } from './model-manager.js'\nimport { resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy } from './content-manager.js'\nimport { autoDetectSourceDirs, discoverFiles } from './scan-config.js'\nimport { checkBranchHealth, listRemoteCrBranches } from '../git/branch-lifecycle.js'\nimport { contentrainDir, pathExists, readDir, readJson, readText } from '../util/fs.js'\n\n/**\n * Doctor — project health report.\n *\n * The public entry point is `runDoctor(projectRoot, { usage? })`. It is\n * inherently local-filesystem work (Node version, git install, file\n * mtimes, orphan directory detection), so the MCP tool surface gates\n * it behind the `localWorktree` capability — same pattern as\n * `contentrain_setup` and normalize.\n *\n * The report is structured JSON so three consumers can share it:\n *\n * - The `contentrain doctor` CLI command pretty-prints the checks.\n * - The Serve UI `/api/doctor` route returns the report to the\n * Dashboard's Doctor panel.\n * - Automation (CI, Studio) gets a deterministic JSON shape it can\n * assert against.\n *\n * Usage analysis (`--usage`) is a heavier, opt-in branch — it scans\n * every source file in the repo for content-key references. Kept\n * behind the flag so the default doctor run stays fast.\n */\n\nexport type CheckSeverity = 'error' | 'warning' | 'info'\n\nexport interface DoctorCheck {\n name: string\n pass: boolean\n detail: string\n /**\n * `error` — default for failing checks. Blocks a clean bill of health.\n * `warning` — failing-but-not-blocking (e.g. pending branches above\n * threshold, stale SDK client).\n * `info` — passed check; pure informational.\n */\n severity?: CheckSeverity\n}\n\nexport interface UnusedKeyEntry {\n model: string\n kind: string\n key: string\n locale: string\n}\n\nexport interface DuplicateValueEntry {\n model: string\n locale: string\n value: string\n keys: string[]\n}\n\nexport interface MissingLocaleEntry {\n model: string\n key: string\n missingIn: string\n}\n\nexport interface DoctorUsageAnalysis {\n unusedKeys: UnusedKeyEntry[]\n duplicateValues: DuplicateValueEntry[]\n missingLocaleKeys: MissingLocaleEntry[]\n}\n\nexport interface DoctorReport {\n checks: DoctorCheck[]\n summary: {\n total: number\n passed: number\n failed: number\n warnings: number\n }\n /** Present only when `options.usage === true`. */\n usage?: DoctorUsageAnalysis\n}\n\nexport interface RunDoctorOptions {\n /** Run heavier `--usage` analysis (unused keys, duplicates, locale gaps). */\n usage?: boolean\n}\n\nexport async function runDoctor(\n projectRoot: string,\n options: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n const checks: DoctorCheck[] = []\n\n // ─── 1. Git installed ───\n try {\n const git = simpleGit(projectRoot)\n const version = await git.version()\n checks.push({\n name: 'Git',\n pass: true,\n detail: `v${version.major}.${version.minor}.${version.patch}`,\n })\n } catch {\n checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' })\n }\n\n // ─── 2. Git repo initialized ───\n const hasGit = await pathExists(join(projectRoot, '.git'))\n checks.push({\n name: 'Git repository',\n pass: hasGit,\n detail: hasGit ? projectRoot : 'No .git directory found',\n severity: hasGit ? undefined : 'error',\n })\n\n // ─── 3. Node version ───\n const nodeVersion = process.versions.node\n const [major] = nodeVersion.split('.').map(Number)\n const nodePass = (major ?? 0) >= 22\n checks.push({\n name: 'Node.js',\n pass: nodePass,\n detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`,\n severity: nodePass ? undefined : 'error',\n })\n\n // ─── 4. .contentrain/ structure ───\n const crDir = contentrainDir(projectRoot)\n const hasCrDir = await pathExists(crDir)\n const hasConfig = await pathExists(join(crDir, 'config.json'))\n const hasModels = await pathExists(join(crDir, 'models'))\n const hasContent = await pathExists(join(crDir, 'content'))\n const structurePass = hasCrDir && hasConfig && hasModels && hasContent\n\n checks.push({\n name: '.contentrain/ structure',\n pass: structurePass,\n detail: !hasCrDir\n ? 'Not initialized — run `contentrain init`'\n : [\n hasConfig ? null : 'missing config.json',\n hasModels ? null : 'missing models/',\n hasContent ? null : 'missing content/',\n ].filter(Boolean).join(', ') || 'OK',\n severity: structurePass ? undefined : 'error',\n })\n\n // ─── 5. Config parseable ───\n let config: ContentrainConfig | null = null\n if (hasConfig) {\n config = await readConfig(projectRoot)\n checks.push({\n name: 'Config',\n pass: config !== null,\n detail: config\n ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}`\n : 'Failed to parse config.json',\n severity: config ? undefined : 'error',\n })\n }\n\n // ─── 6. Models all parseable ───\n if (hasCrDir) {\n try {\n const models = await listModels(projectRoot)\n const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const allParseable = parseResults.every(r => r !== null)\n checks.push({\n name: 'Models',\n pass: allParseable,\n detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`,\n severity: allParseable ? undefined : 'error',\n })\n } catch {\n checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' })\n }\n }\n\n // ─── 7. Orphan content ───\n if (hasCrDir) {\n const orphans = await findOrphanContent(projectRoot)\n checks.push({\n name: 'Orphan content',\n pass: orphans.length === 0,\n detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`,\n severity: orphans.length === 0 ? undefined : 'warning',\n })\n }\n\n // ─── 8. Stale contentrain branches ───\n if (hasGit) {\n try {\n const health = await checkBranchHealth(projectRoot)\n checks.push({\n name: 'Pending branches',\n pass: !health.blocked && !health.warning,\n detail: health.message\n ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`),\n severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined,\n })\n } catch {\n checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' })\n }\n\n // ─── 8b. Remote cr/* branches ───\n // Authoritative ls-remote count (local branch pressure cannot see the\n // remote pile). Best-effort: skipped entirely without a remote, and an\n // unreachable remote is informational — doctor never fails offline.\n const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5000 })\n if (remoteList) {\n if (remoteList.error) {\n checks.push({\n name: 'Remote branches',\n pass: true,\n detail: `Could not check ${remoteList.remote} (offline?)`,\n severity: 'info',\n })\n } else {\n const count = remoteList.branches.length\n const warnLimit = config?.branchWarnLimit ?? 50\n const warn = count >= warnLimit\n checks.push({\n name: 'Remote branches',\n pass: !warn,\n detail: count === 0\n ? `None on ${remoteList.remote}`\n : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? ' — run `contentrain prune` to remove merged leftovers' : ''}`,\n severity: warn ? 'warning' : undefined,\n })\n }\n }\n }\n\n // ─── 9. SDK client freshness ───\n const clientDir = join(crDir, 'client')\n const modelsDir = join(crDir, 'models')\n if (await pathExists(clientDir) && await pathExists(modelsDir)) {\n try {\n const [clientMtime, modelsMtime] = await Promise.all([\n newestFileMtime(clientDir),\n newestFileMtime(modelsDir),\n ])\n if (clientMtime === null || modelsMtime === null) {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n } else {\n const fresh = clientMtime >= modelsMtime\n checks.push({\n name: 'SDK client',\n pass: fresh,\n detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`',\n severity: fresh ? undefined : 'warning',\n })\n }\n } catch {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 10–12. Usage analysis (optional) ───\n let usage: DoctorUsageAnalysis | undefined\n if (options.usage && hasCrDir && config) {\n const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([\n analyzeUnusedKeys(projectRoot, config),\n analyzeDuplicateValues(projectRoot, config),\n analyzeMissingLocaleKeys(projectRoot, config),\n ])\n usage = { unusedKeys, duplicateValues, missingLocaleKeys }\n\n checks.push({\n name: 'Unused content keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All keys referenced in source'\n : `${unusedKeys.length} key(s) not referenced in source code`,\n severity: unusedKeys.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Duplicate dictionary values',\n pass: duplicateValues.length === 0,\n detail: duplicateValues.length === 0\n ? 'No duplicate values'\n : `${duplicateValues.length} value(s) mapped to multiple keys`,\n severity: duplicateValues.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Locale key coverage',\n pass: missingLocaleKeys.length === 0,\n detail: missingLocaleKeys.length === 0\n ? 'All locales have matching keys'\n : `${missingLocaleKeys.length} key(s) missing in some locales`,\n severity: missingLocaleKeys.length === 0 ? undefined : 'warning',\n })\n }\n\n const passed = checks.filter(c => c.pass).length\n const failed = checks.length - passed\n const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length\n\n const report: DoctorReport = {\n checks,\n summary: { total: checks.length, passed, failed, warnings },\n }\n if (usage) report.usage = usage\n return report\n}\n\n/**\n * Newest mtime among the files under `dir`, recursively — null if it holds none.\n *\n * Stat the files, never the directory. A directory's mtime only moves when an\n * entry is added, removed, or renamed inside it: `generate` rewrites the client\n * files in place, so `.contentrain/client` never moves after the first run,\n * while a selective sync recreates model files via `git checkout`, which does\n * move `.contentrain/models`. Comparing the two directories therefore reported\n * \"stale\" permanently after any model save.\n */\nasync function newestFileMtime(dir: string): Promise<number | null> {\n const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [])\n const mtimes = await Promise.all(entries.map(async (entry) => {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) return newestFileMtime(full)\n return stat(full).then(s => s.mtimeMs, () => null)\n }))\n const known = mtimes.filter((m): m is number => m !== null)\n return known.length > 0 ? Math.max(...known) : null\n}\n\nasync function findOrphanContent(projectRoot: string): Promise<string[]> {\n const crDir = contentrainDir(projectRoot)\n const models = await listModels(projectRoot)\n const orphans: string[] = []\n\n const knownContentDirs = new Set<string>()\n for (const m of models) {\n const full = await readModel(projectRoot, m.id)\n const modelForPath = full\n ? {\n ...full,\n content_path: full.content_path ?? (m as { content_path?: string }).content_path,\n }\n : {\n id: m.id,\n name: m.id,\n kind: m.kind,\n domain: m.domain,\n i18n: m.i18n,\n fields: {},\n content_path: (m as { content_path?: string }).content_path,\n }\n knownContentDirs.add(resolveContentDir(projectRoot, modelForPath))\n }\n\n const contentDir = join(crDir, 'content')\n if (await pathExists(contentDir)) {\n const domains = await readDir(contentDir)\n for (const domain of domains) {\n const domainDir = join(contentDir, domain)\n const entries = await readDir(domainDir)\n for (const entry of entries) {\n if (entry === '.gitkeep') continue\n const entryDir = join(domainDir, entry)\n if (!knownContentDirs.has(entryDir)) {\n orphans.push(`${domain}/${entry}`)\n }\n }\n }\n }\n\n for (const dir of knownContentDirs) {\n if (dir.startsWith(contentDir)) continue\n if (!await pathExists(dir)) {\n orphans.push(`(missing custom path) ${dir}`)\n continue\n }\n await readDir(dir)\n }\n\n return orphans\n}\n\nasync function analyzeUnusedKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<UnusedKeyEntry[]> {\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, { paths: sourceDirs })\n if (files.length === 0) return []\n\n const chunks = await Promise.all(\n files.map(async (relPath) => {\n const content = await readText(join(projectRoot, relPath))\n return content ?? ''\n }),\n )\n const allSource = chunks.join('\\n')\n\n const models = await listModels(projectRoot)\n const defaultLocale = config.locales.default\n const unused: UnusedKeyEntry[] = []\n\n for (const m of models) {\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale)\n for (const key of keys) {\n if (!allSource.includes(key)) {\n unused.push({ model: m.id, kind: m.kind, key, locale: defaultLocale })\n }\n }\n }\n\n return unused\n}\n\nasync function extractContentKeys(\n projectRoot: string,\n model: ModelDefinition,\n locale: string,\n): Promise<string[]> {\n const cDir = resolveContentDir(projectRoot, model)\n if (!await pathExists(cDir)) return []\n\n switch (model.kind) {\n case 'dictionary': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'collection': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'document': {\n const strategy = resolveLocaleStrategy(model)\n const slugs: string[] = []\n if (!model.i18n) {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n } else if (strategy === 'file') {\n const dirs = await readDir(cDir)\n for (const d of dirs) if (!d.startsWith('.')) slugs.push(d)\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length))\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n if (await pathExists(localeDir)) {\n const files = await readDir(localeDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n } else {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n return slugs\n }\n case 'singleton':\n return []\n default:\n return []\n }\n}\n\nasync function analyzeDuplicateValues(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<DuplicateValueEntry[]> {\n const models = await listModels(projectRoot)\n const result: DuplicateValueEntry[] = []\n\n for (const m of models) {\n if (m.kind !== 'dictionary') continue\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, fullModel, locale)\n const data = await readJson<Record<string, string>>(filePath)\n if (!data) continue\n\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n\n for (const [value, keys] of valueToKeys) {\n if (keys.length > 1) {\n result.push({ model: m.id, locale, value, keys })\n }\n }\n }\n }\n\n return result\n}\n\nasync function analyzeMissingLocaleKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<MissingLocaleEntry[]> {\n if (config.locales.supported.length < 2) return []\n\n const models = await listModels(projectRoot)\n const result: MissingLocaleEntry[] = []\n const defaultLocale = config.locales.default\n const otherLocales = config.locales.supported.filter(l => l !== defaultLocale)\n\n for (const m of models) {\n if (m.kind !== 'dictionary' && m.kind !== 'collection') continue\n if (!m.i18n) continue\n\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n const defaultPath = resolveJsonFilePath(cDir, fullModel, defaultLocale)\n const defaultData = await readJson<Record<string, unknown>>(defaultPath)\n if (!defaultData) continue\n const defaultKeys = new Set(Object.keys(defaultData))\n\n for (const locale of otherLocales) {\n const localePath = resolveJsonFilePath(cDir, fullModel, locale)\n const localeData = await readJson<Record<string, unknown>>(localePath)\n const localeKeys = localeData ? new Set(Object.keys(localeData)) : new Set<string>()\n\n for (const key of defaultKeys) {\n if (!localeKeys.has(key)) {\n result.push({ model: m.id, key, missingIn: locale })\n }\n }\n }\n }\n\n return result\n}\n"],"mappings":"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"} |
| //#region src/providers/shared/errors.ts | ||
| /** | ||
| * Error shapes for API-backed providers, and the mapping from those shapes | ||
| * into Contentrain's structured error envelope. | ||
| * | ||
| * Two common error shapes in the provider SDKs we use: | ||
| * | ||
| * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a | ||
| * top-level `.status` number set to the HTTP status code. | ||
| * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose | ||
| * `.cause` includes `{ response: { status } }`. | ||
| */ | ||
| /** | ||
| * HTTP status behind a provider SDK rejection, or `undefined` when the error | ||
| * is not an HTTP failure (a local git error, a programming mistake, …). | ||
| * | ||
| * We deliberately do NOT fall back to substring matching on the error | ||
| * message — that leniency can silently mask other failures (forbidden repo, | ||
| * deleted project, rate limits) and produce the wrong answer. If either SDK | ||
| * ever stops populating the status field, the regression surfaces in tests | ||
| * rather than being papered over at the reader layer. | ||
| */ | ||
| function extractHttpStatus(error) { | ||
| if (typeof error !== "object" || error === null) return void 0; | ||
| const direct = error.status; | ||
| if (typeof direct === "number") return direct; | ||
| const nested = error.cause?.response?.status; | ||
| return typeof nested === "number" ? nested : void 0; | ||
| } | ||
| /** Unified "is this a 404?" helper for API-backed providers. */ | ||
| function isNotFoundError(error) { | ||
| return extractHttpStatus(error) === 404; | ||
| } | ||
| /** | ||
| * Provider SDK messages are written for a human reading a terminal, not for an | ||
| * agent deciding what to do next. Octokit appends a documentation URL to most | ||
| * messages and embeds the raw JSON response body on validation failures. | ||
| * | ||
| * The vendor text still carries the only specifics we have ("Reference already | ||
| * exists", which field failed), so it is kept as a parenthetical detail rather | ||
| * than discarded — but the doc links are noise for an agent, and an embedded | ||
| * body can be arbitrarily long, so URLs are stripped and the result is capped. | ||
| */ | ||
| function sanitiseDetail(error) { | ||
| const collapsed = (error?.message ?? String(error)).replace(/\s*-?\s*https?:\/\/\S+/g, "").replace(/\s+/g, " ").trim(); | ||
| return collapsed.length > 200 ? `${collapsed.slice(0, 197)}…` : collapsed; | ||
| } | ||
| /** | ||
| * GitHub signals rate limiting with 403 plus an exhausted remaining-quota | ||
| * header (secondary limits use 403 too), and with 429 on some endpoints. | ||
| * Checking the header first keeps a genuine permission 403 from being | ||
| * reported as a rate limit. | ||
| */ | ||
| function isRateLimited(error, status) { | ||
| if (status === 429) return true; | ||
| if (status !== 403) return false; | ||
| const remaining = (error.response?.headers)?.["x-ratelimit-remaining"]; | ||
| if (remaining !== void 0) return String(remaining) === "0"; | ||
| return /\brate limit\b/i.test(error.message ?? ""); | ||
| } | ||
| /** | ||
| * Map a provider SDK rejection onto the same structured envelope the local git | ||
| * paths already produce (`code` + `agent_hint` + `developer_action`). Returns | ||
| * `undefined` for anything that is not a recognisable provider HTTP failure, | ||
| * so local git errors and ordinary exceptions pass through untouched. | ||
| */ | ||
| function mapProviderError(error) { | ||
| const status = extractHttpStatus(error); | ||
| if (status === void 0) return void 0; | ||
| const detail = sanitiseDetail(error); | ||
| const withDetail = (summary) => detail ? `${summary} (${detail})` : summary; | ||
| if (isRateLimited(error, status)) return { | ||
| error: withDetail("The git provider rejected the request because the API rate limit is exhausted."), | ||
| code: "PROVIDER_RATE_LIMITED", | ||
| agent_hint: "Do not retry immediately — the limit is time-based. Stop the current operation and report it to the developer.", | ||
| developer_action: "Wait for the rate-limit window to reset, or use a token with a higher quota." | ||
| }; | ||
| switch (status) { | ||
| case 401: return { | ||
| error: withDetail("The git provider rejected the credentials."), | ||
| code: "PROVIDER_UNAUTHORIZED", | ||
| agent_hint: "The token is missing, malformed, or expired. Not retryable — ask the developer to fix the credentials.", | ||
| developer_action: "Check the provider token configured for this Contentrain server and reissue it if it has expired." | ||
| }; | ||
| case 403: return { | ||
| error: withDetail("The git provider denied access to this resource."), | ||
| code: "PROVIDER_FORBIDDEN", | ||
| agent_hint: "The credentials are valid but lack permission for this operation. Not retryable — report which operation was refused.", | ||
| developer_action: "Grant the token write access to the repository, or check whether branch protection or SSO authorisation is blocking it." | ||
| }; | ||
| case 404: return { | ||
| error: withDetail("The git provider could not find the requested resource."), | ||
| code: "PROVIDER_NOT_FOUND", | ||
| agent_hint: "The repository, branch, or path does not exist — or the token cannot see it, which also returns 404. Verify the target before retrying.", | ||
| developer_action: "Confirm the repository and branch names in the Contentrain config, and that the token can reach a private repository." | ||
| }; | ||
| case 409: return { | ||
| error: withDetail("The git provider reported a conflict."), | ||
| code: "PROVIDER_CONFLICT", | ||
| agent_hint: "The branch moved between read and write. Re-read the current state and rebuild the change before retrying once.", | ||
| developer_action: "Usually a concurrent write to the same branch; retrying the operation normally resolves it." | ||
| }; | ||
| case 422: return { | ||
| error: withDetail("The git provider rejected the request as invalid."), | ||
| code: "PROVIDER_VALIDATION_FAILED", | ||
| agent_hint: "Well-formed but semantically rejected (e.g. the ref already exists, or a name is invalid). Do not retry unchanged.", | ||
| developer_action: "Inspect the detail above — a stale branch left over from an earlier run is the common cause." | ||
| }; | ||
| default: | ||
| if (status >= 500) return { | ||
| error: withDetail("The git provider is unavailable."), | ||
| code: "PROVIDER_UNAVAILABLE", | ||
| agent_hint: "A server-side failure on the provider. Safe to retry once after a short pause; if it persists, stop and report it.", | ||
| developer_action: "Check the provider status page." | ||
| }; | ||
| return { | ||
| error: withDetail(`The git provider returned HTTP ${status}.`), | ||
| code: "PROVIDER_REQUEST_FAILED", | ||
| agent_hint: "An unmapped provider failure. Report the status and detail to the developer rather than retrying blindly." | ||
| }; | ||
| } | ||
| } | ||
| //#endregion | ||
| export { mapProviderError as n, isNotFoundError as t }; | ||
| //# sourceMappingURL=errors-e0YdjooK.mjs.map |
| {"version":3,"file":"errors-e0YdjooK.mjs","names":[],"sources":["../src/providers/shared/errors.ts"],"sourcesContent":["/**\n * Error shapes for API-backed providers, and the mapping from those shapes\n * into Contentrain's structured error envelope.\n *\n * Two common error shapes in the provider SDKs we use:\n *\n * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a\n * top-level `.status` number set to the HTTP status code.\n * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose\n * `.cause` includes `{ response: { status } }`.\n */\n\n/**\n * HTTP status behind a provider SDK rejection, or `undefined` when the error\n * is not an HTTP failure (a local git error, a programming mistake, …).\n *\n * We deliberately do NOT fall back to substring matching on the error\n * message — that leniency can silently mask other failures (forbidden repo,\n * deleted project, rate limits) and produce the wrong answer. If either SDK\n * ever stops populating the status field, the regression surfaces in tests\n * rather than being papered over at the reader layer.\n */\nexport function extractHttpStatus(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const direct = (error as { status?: number }).status\n if (typeof direct === 'number') return direct\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return typeof nested === 'number' ? nested : undefined\n}\n\n/** Unified \"is this a 404?\" helper for API-backed providers. */\nexport function isNotFoundError(error: unknown): boolean {\n return extractHttpStatus(error) === 404\n}\n\n/**\n * Provider SDK messages are written for a human reading a terminal, not for an\n * agent deciding what to do next. Octokit appends a documentation URL to most\n * messages and embeds the raw JSON response body on validation failures.\n *\n * The vendor text still carries the only specifics we have (\"Reference already\n * exists\", which field failed), so it is kept as a parenthetical detail rather\n * than discarded — but the doc links are noise for an agent, and an embedded\n * body can be arbitrarily long, so URLs are stripped and the result is capped.\n */\nfunction sanitiseDetail(error: unknown): string {\n const raw = (error as { message?: string } | undefined)?.message ?? String(error)\n const withoutUrls = raw.replace(/\\s*-?\\s*https?:\\/\\/\\S+/g, '')\n const collapsed = withoutUrls.replace(/\\s+/g, ' ').trim()\n return collapsed.length > 200 ? `${collapsed.slice(0, 197)}…` : collapsed\n}\n\n/**\n * GitHub signals rate limiting with 403 plus an exhausted remaining-quota\n * header (secondary limits use 403 too), and with 429 on some endpoints.\n * Checking the header first keeps a genuine permission 403 from being\n * reported as a rate limit.\n */\nfunction isRateLimited(error: unknown, status: number): boolean {\n if (status === 429) return true\n if (status !== 403) return false\n const headers = (error as { response?: { headers?: Record<string, unknown> } }).response?.headers\n const remaining = headers?.['x-ratelimit-remaining']\n if (remaining !== undefined) return String(remaining) === '0'\n return /\\brate limit\\b/i.test((error as { message?: string }).message ?? '')\n}\n\nexport interface ProviderErrorInfo {\n error: string\n code: string\n agent_hint: string\n developer_action?: string\n}\n\n/**\n * Map a provider SDK rejection onto the same structured envelope the local git\n * paths already produce (`code` + `agent_hint` + `developer_action`). Returns\n * `undefined` for anything that is not a recognisable provider HTTP failure,\n * so local git errors and ordinary exceptions pass through untouched.\n */\nexport function mapProviderError(error: unknown): ProviderErrorInfo | undefined {\n const status = extractHttpStatus(error)\n if (status === undefined) return undefined\n\n const detail = sanitiseDetail(error)\n const withDetail = (summary: string): string => (detail ? `${summary} (${detail})` : summary)\n\n if (isRateLimited(error, status)) {\n return {\n error: withDetail('The git provider rejected the request because the API rate limit is exhausted.'),\n code: 'PROVIDER_RATE_LIMITED',\n agent_hint: 'Do not retry immediately — the limit is time-based. Stop the current operation and report it to the developer.',\n developer_action: 'Wait for the rate-limit window to reset, or use a token with a higher quota.',\n }\n }\n\n switch (status) {\n case 401:\n return {\n error: withDetail('The git provider rejected the credentials.'),\n code: 'PROVIDER_UNAUTHORIZED',\n agent_hint: 'The token is missing, malformed, or expired. Not retryable — ask the developer to fix the credentials.',\n developer_action: 'Check the provider token configured for this Contentrain server and reissue it if it has expired.',\n }\n case 403:\n return {\n error: withDetail('The git provider denied access to this resource.'),\n code: 'PROVIDER_FORBIDDEN',\n agent_hint: 'The credentials are valid but lack permission for this operation. Not retryable — report which operation was refused.',\n developer_action: 'Grant the token write access to the repository, or check whether branch protection or SSO authorisation is blocking it.',\n }\n case 404:\n return {\n error: withDetail('The git provider could not find the requested resource.'),\n code: 'PROVIDER_NOT_FOUND',\n agent_hint: 'The repository, branch, or path does not exist — or the token cannot see it, which also returns 404. Verify the target before retrying.',\n developer_action: 'Confirm the repository and branch names in the Contentrain config, and that the token can reach a private repository.',\n }\n case 409:\n return {\n error: withDetail('The git provider reported a conflict.'),\n code: 'PROVIDER_CONFLICT',\n agent_hint: 'The branch moved between read and write. Re-read the current state and rebuild the change before retrying once.',\n developer_action: 'Usually a concurrent write to the same branch; retrying the operation normally resolves it.',\n }\n case 422:\n return {\n error: withDetail('The git provider rejected the request as invalid.'),\n code: 'PROVIDER_VALIDATION_FAILED',\n agent_hint: 'Well-formed but semantically rejected (e.g. the ref already exists, or a name is invalid). Do not retry unchanged.',\n developer_action: 'Inspect the detail above — a stale branch left over from an earlier run is the common cause.',\n }\n default:\n if (status >= 500) {\n return {\n error: withDetail('The git provider is unavailable.'),\n code: 'PROVIDER_UNAVAILABLE',\n agent_hint: 'A server-side failure on the provider. Safe to retry once after a short pause; if it persists, stop and report it.',\n developer_action: 'Check the provider status page.',\n }\n }\n return {\n error: withDetail(`The git provider returned HTTP ${status}.`),\n code: 'PROVIDER_REQUEST_FAILED',\n agent_hint: 'An unmapped provider failure. Report the status and detail to the developer rather than retrying blindly.',\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,kBAAkB,OAAoC;AACpE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO,KAAA;CACxD,MAAM,SAAU,MAA8B;AAC9C,KAAI,OAAO,WAAW,SAAU,QAAO;CACvC,MAAM,SAAU,MAAyD,OAAO,UAAU;AAC1F,QAAO,OAAO,WAAW,WAAW,SAAS,KAAA;;;AAI/C,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,kBAAkB,MAAM,KAAK;;;;;;;;;;;;AAatC,SAAS,eAAe,OAAwB;CAG9C,MAAM,aAFO,OAA4C,WAAW,OAAO,MAAM,EACzD,QAAQ,2BAA2B,GAAG,CAChC,QAAQ,QAAQ,IAAI,CAAC,MAAM;AACzD,QAAO,UAAU,SAAS,MAAM,GAAG,UAAU,MAAM,GAAG,IAAI,CAAC,KAAK;;;;;;;;AASlE,SAAS,cAAc,OAAgB,QAAyB;AAC9D,KAAI,WAAW,IAAK,QAAO;AAC3B,KAAI,WAAW,IAAK,QAAO;CAE3B,MAAM,aADW,MAA+D,UAAU,WAC9D;AAC5B,KAAI,cAAc,KAAA,EAAW,QAAO,OAAO,UAAU,KAAK;AAC1D,QAAO,kBAAkB,KAAM,MAA+B,WAAW,GAAG;;;;;;;;AAgB9E,SAAgB,iBAAiB,OAA+C;CAC9E,MAAM,SAAS,kBAAkB,MAAM;AACvC,KAAI,WAAW,KAAA,EAAW,QAAO,KAAA;CAEjC,MAAM,SAAS,eAAe,MAAM;CACpC,MAAM,cAAc,YAA6B,SAAS,GAAG,QAAQ,IAAI,OAAO,KAAK;AAErF,KAAI,cAAc,OAAO,OAAO,CAC9B,QAAO;EACL,OAAO,WAAW,iFAAiF;EACnG,MAAM;EACN,YAAY;EACZ,kBAAkB;EACnB;AAGH,SAAQ,QAAR;EACE,KAAK,IACH,QAAO;GACL,OAAO,WAAW,6CAA6C;GAC/D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,mDAAmD;GACrE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,0DAA0D;GAC5E,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,wCAAwC;GAC1D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,oDAAoD;GACtE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH;AACE,OAAI,UAAU,IACZ,QAAO;IACL,OAAO,WAAW,mCAAmC;IACrD,MAAM;IACN,YAAY;IACZ,kBAAkB;IACnB;AAEH,UAAO;IACL,OAAO,WAAW,kCAAkC,OAAO,GAAG;IAC9D,MAAM;IACN,YAAY;IACb"} |
| import { o as readText, r as pathExists } from "./fs-DLbVB-Ek.mjs"; | ||
| import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles } from "./scan-config-BlNLRCMx.mjs"; | ||
| import { dirname, extname, join, relative } from "node:path"; | ||
| //#region src/core/graph-builder.ts | ||
| const MAX_ORPHANS = 10; | ||
| const IMPORT_RE = /(?:import|export)\s+.*?from\s+['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g; | ||
| function extractImportPaths(content) { | ||
| const paths = []; | ||
| let m; | ||
| IMPORT_RE.lastIndex = 0; | ||
| while ((m = IMPORT_RE.exec(content)) !== null) { | ||
| const p = m[1] ?? m[2]; | ||
| if (p && isLocalImport(p)) paths.push(p); | ||
| } | ||
| return paths; | ||
| } | ||
| function isLocalImport(p) { | ||
| return p.startsWith(".") || p.startsWith("/"); | ||
| } | ||
| const NAMED_IMPORT_RE = /import\s+(?:type\s+)?(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]*)\})?\s+from\s+['"][^'"]+['"]/g; | ||
| const DEFAULT_IMPORT_RE = /import\s+(?:type\s+)?(\w+)\s+from\s+['"][^'"]+['"]/g; | ||
| function isPascalCase(name) { | ||
| return /^[A-Z][a-zA-Z0-9]*$/.test(name); | ||
| } | ||
| function extractComponentNames(content) { | ||
| const names = /* @__PURE__ */ new Set(); | ||
| DEFAULT_IMPORT_RE.lastIndex = 0; | ||
| let m; | ||
| while ((m = DEFAULT_IMPORT_RE.exec(content)) !== null) { | ||
| const name = m[1]; | ||
| if (name && isPascalCase(name)) names.add(name); | ||
| } | ||
| NAMED_IMPORT_RE.lastIndex = 0; | ||
| while ((m = NAMED_IMPORT_RE.exec(content)) !== null) { | ||
| const defaultName = m[1]; | ||
| if (defaultName && isPascalCase(defaultName)) names.add(defaultName); | ||
| const namedPart = m[2]; | ||
| if (namedPart) for (const segment of namedPart.split(",")) { | ||
| const parts = segment.trim().split(/\s+as\s+/); | ||
| const localName = (parts[1] ?? parts[0])?.trim(); | ||
| if (localName && isPascalCase(localName)) names.add(localName); | ||
| } | ||
| } | ||
| return [...names]; | ||
| } | ||
| const STRING_SINGLE_RE = /'[^'\\]*(?:\\.[^'\\]*)*'/g; | ||
| const STRING_DOUBLE_RE = /"[^"\\]*(?:\\.[^"\\]*)*"/g; | ||
| const STRING_TEMPLATE_RE = /`[^`\\]*(?:\\.[^`\\]*)*`/g; | ||
| function countStrings(content) { | ||
| const stripped = content.replace(/^(?:import|export)\s+.*$/gm, "").replace(/\brequire\s*\([^)]*\)/g, ""); | ||
| const singles = stripped.match(STRING_SINGLE_RE)?.length ?? 0; | ||
| const doubles = stripped.match(STRING_DOUBLE_RE)?.length ?? 0; | ||
| const templates = stripped.match(STRING_TEMPLATE_RE)?.length ?? 0; | ||
| return singles + doubles + templates; | ||
| } | ||
| const RESOLVE_EXTENSIONS = [...SCAN_EXTENSIONS]; | ||
| const RESOLVE_INDEX_FILES = [ | ||
| "index.ts", | ||
| "index.tsx", | ||
| "index.js" | ||
| ]; | ||
| async function resolveImportPath(importPath, importerDir, projectRoot) { | ||
| const base = importPath.startsWith("/") ? join(projectRoot, importPath) : join(importerDir, importPath); | ||
| if (extname(base)) { | ||
| if (await pathExists(base)) return base; | ||
| return null; | ||
| } | ||
| for (const ext of RESOLVE_EXTENSIONS) { | ||
| const candidate = base + ext; | ||
| if (await pathExists(candidate)) return candidate; | ||
| } | ||
| for (const idx of RESOLVE_INDEX_FILES) { | ||
| const candidate = join(base, idx); | ||
| if (await pathExists(candidate)) return candidate; | ||
| } | ||
| return null; | ||
| } | ||
| async function buildGraph(projectRoot, options) { | ||
| const filePaths = await discoverFiles(projectRoot, { | ||
| paths: options?.paths ?? await autoDetectSourceDirs(projectRoot), | ||
| include: options?.include, | ||
| exclude: options?.exclude | ||
| }); | ||
| const fileMap = /* @__PURE__ */ new Map(); | ||
| const parsePromises = filePaths.map(async (relPath) => { | ||
| const content = await readText(join(projectRoot, relPath)); | ||
| if (content === null) return; | ||
| const imports = extractImportPaths(content); | ||
| const components = extractComponentNames(content); | ||
| const strings = countStrings(content); | ||
| const category = classifyFile(relPath); | ||
| fileMap.set(relPath, { | ||
| relPath, | ||
| imports, | ||
| components, | ||
| strings, | ||
| category | ||
| }); | ||
| }); | ||
| await Promise.all(parsePromises); | ||
| const usedByMap = /* @__PURE__ */ new Map(); | ||
| for (const relPath of fileMap.keys()) usedByMap.set(relPath, /* @__PURE__ */ new Set()); | ||
| const resolvePromises = []; | ||
| for (const [relPath, info] of fileMap) { | ||
| const importerAbsDir = dirname(join(projectRoot, relPath)); | ||
| for (const rawImport of info.imports) resolvePromises.push(resolveImportPath(rawImport, importerAbsDir, projectRoot).then((resolved) => { | ||
| if (!resolved) return; | ||
| const resolvedRel = relative(projectRoot, resolved); | ||
| const targetSet = usedByMap.get(resolvedRel); | ||
| if (targetSet) targetSet.add(relPath); | ||
| })); | ||
| } | ||
| await Promise.all(resolvePromises); | ||
| const pages = []; | ||
| const components = []; | ||
| const layouts = []; | ||
| const orphanCandidates = []; | ||
| let totalStrings = 0; | ||
| for (const [relPath, info] of fileMap) { | ||
| const usedBy = [...usedByMap.get(relPath) ?? []].toSorted((a, b) => a.localeCompare(b)); | ||
| totalStrings += info.strings; | ||
| if (info.strings === 0 && info.category !== "other") continue; | ||
| const node = { | ||
| file: relPath, | ||
| category: info.category, | ||
| imports: info.imports, | ||
| used_by: usedBy, | ||
| strings: info.strings | ||
| }; | ||
| if (info.category === "page" && info.components.length > 0) node.components = info.components; | ||
| switch (info.category) { | ||
| case "page": | ||
| pages.push(node); | ||
| break; | ||
| case "component": | ||
| components.push(node); | ||
| break; | ||
| case "layout": | ||
| layouts.push(node); | ||
| break; | ||
| default: | ||
| if (usedBy.length === 0 && info.imports.length === 0) orphanCandidates.push(relPath); | ||
| break; | ||
| } | ||
| } | ||
| return { | ||
| pages: pages.toSorted((a, b) => a.file.localeCompare(b.file)), | ||
| components: components.toSorted((a, b) => a.file.localeCompare(b.file)), | ||
| layouts: layouts.toSorted((a, b) => a.file.localeCompare(b.file)), | ||
| orphan_files: orphanCandidates.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_ORPHANS), | ||
| stats: { | ||
| total_files: fileMap.size, | ||
| total_components: components.length, | ||
| total_pages: pages.length, | ||
| total_strings_estimate: totalStrings | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { buildGraph as t }; | ||
| //# sourceMappingURL=graph-builder-DK4Mh8Tn.mjs.map |
| {"version":3,"file":"graph-builder-DK4Mh8Tn.mjs","names":[],"sources":["../src/core/graph-builder.ts"],"sourcesContent":["import type { FileCategory, GraphNode, ProjectGraph } from '@contentrain/types'\nimport { join, relative, dirname, extname } from 'node:path'\nimport { readText, pathExists } from '../util/fs.js'\nimport {\n SCAN_EXTENSIONS,\n classifyFile,\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\n\nexport interface BuildGraphOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\nconst MAX_ORPHANS = 10\n\n// ---------------------------------------------------------------------------\n// Import extraction\n// ---------------------------------------------------------------------------\n\nconst IMPORT_RE = /(?:import|export)\\s+.*?from\\s+['\"]([^'\"]+)['\"]|(?:import|require)\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g\n\nfunction extractImportPaths(content: string): string[] {\n const paths: string[] = []\n let m: RegExpExecArray | null\n // Reset lastIndex before use since we reuse the regex\n IMPORT_RE.lastIndex = 0\n while ((m = IMPORT_RE.exec(content)) !== null) {\n const p = m[1] ?? m[2]\n if (p && isLocalImport(p)) {\n paths.push(p)\n }\n }\n return paths\n}\n\nfunction isLocalImport(p: string): boolean {\n return p.startsWith('.') || p.startsWith('/')\n}\n\n// ---------------------------------------------------------------------------\n// Component name extraction from imports\n// ---------------------------------------------------------------------------\n\nconst NAMED_IMPORT_RE = /import\\s+(?:type\\s+)?(?:(\\w+)(?:\\s*,\\s*)?)?(?:\\{([^}]*)\\})?\\s+from\\s+['\"][^'\"]+['\"]/g\nconst DEFAULT_IMPORT_RE = /import\\s+(?:type\\s+)?(\\w+)\\s+from\\s+['\"][^'\"]+['\"]/g\n\nfunction isPascalCase(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name)\n}\n\nfunction extractComponentNames(content: string): string[] {\n const names = new Set<string>()\n\n // Default imports: `import Button from '...'`\n DEFAULT_IMPORT_RE.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = DEFAULT_IMPORT_RE.exec(content)) !== null) {\n const name = m[1]\n if (name && isPascalCase(name)) {\n names.add(name)\n }\n }\n\n // Named imports: `import { Button, useHook } from '...'`\n NAMED_IMPORT_RE.lastIndex = 0\n while ((m = NAMED_IMPORT_RE.exec(content)) !== null) {\n // Default part before destructuring\n const defaultName = m[1]\n if (defaultName && isPascalCase(defaultName)) {\n names.add(defaultName)\n }\n // Destructured names\n const namedPart = m[2]\n if (namedPart) {\n for (const segment of namedPart.split(',')) {\n // Handle `Foo as Bar` — take the local name (Bar)\n const parts = segment.trim().split(/\\s+as\\s+/)\n const localName = (parts[1] ?? parts[0])?.trim()\n if (localName && isPascalCase(localName)) {\n names.add(localName)\n }\n }\n }\n }\n\n return [...names]\n}\n\n// ---------------------------------------------------------------------------\n// String counting (estimate)\n// ---------------------------------------------------------------------------\n\nconst STRING_SINGLE_RE = /'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'/g\nconst STRING_DOUBLE_RE = /\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"/g\nconst STRING_TEMPLATE_RE = /`[^`\\\\]*(?:\\\\.[^`\\\\]*)*`/g\n\nfunction countStrings(content: string): number {\n // Strip import/export/require lines first so their string literals don't count\n const stripped = content.replace(/^(?:import|export)\\s+.*$/gm, '')\n .replace(/\\brequire\\s*\\([^)]*\\)/g, '')\n\n const singles = stripped.match(STRING_SINGLE_RE)?.length ?? 0\n const doubles = stripped.match(STRING_DOUBLE_RE)?.length ?? 0\n const templates = stripped.match(STRING_TEMPLATE_RE)?.length ?? 0\n\n return singles + doubles + templates\n}\n\n// ---------------------------------------------------------------------------\n// Import path resolution\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_EXTENSIONS = [...SCAN_EXTENSIONS]\nconst RESOLVE_INDEX_FILES = ['index.ts', 'index.tsx', 'index.js']\n\nasync function resolveImportPath(\n importPath: string,\n importerDir: string,\n projectRoot: string,\n): Promise<string | null> {\n const base = importPath.startsWith('/')\n ? join(projectRoot, importPath)\n : join(importerDir, importPath)\n\n // If it already has an extension, check directly\n if (extname(base)) {\n if (await pathExists(base)) {\n return base\n }\n return null\n }\n\n // Try adding extensions\n for (const ext of RESOLVE_EXTENSIONS) {\n const candidate = base + ext\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n // Try index files inside directory\n for (const idx of RESOLVE_INDEX_FILES) {\n const candidate = join(base, idx)\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\ninterface FileInfo {\n relPath: string\n imports: string[]\n components: string[]\n strings: number\n category: FileCategory\n}\n\nexport async function buildGraph(\n projectRoot: string,\n options?: BuildGraphOptions,\n): Promise<ProjectGraph> {\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n // ---- File discovery (shared with scanner) ----\n const filePaths = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // ---- Pass 1: Parse each file ----\n const fileMap = new Map<string, FileInfo>()\n\n const parsePromises = filePaths.map(async (relPath) => {\n const absPath = join(projectRoot, relPath)\n const content = await readText(absPath)\n if (content === null) return\n\n const imports = extractImportPaths(content)\n const components = extractComponentNames(content)\n const strings = countStrings(content)\n const category = classifyFile(relPath)\n\n fileMap.set(relPath, { relPath, imports, components, strings, category })\n })\n\n await Promise.all(parsePromises)\n\n // ---- Pass 2: Resolve imports and build reverse lookup ----\n const usedByMap = new Map<string, Set<string>>()\n\n // Initialize usedBy sets\n for (const relPath of fileMap.keys()) {\n usedByMap.set(relPath, new Set())\n }\n\n const resolvePromises: Promise<void>[] = []\n\n for (const [relPath, info] of fileMap) {\n const importerAbsDir = dirname(join(projectRoot, relPath))\n\n for (const rawImport of info.imports) {\n resolvePromises.push(\n resolveImportPath(rawImport, importerAbsDir, projectRoot).then((resolved) => {\n if (!resolved) return\n const resolvedRel = relative(projectRoot, resolved)\n const targetSet = usedByMap.get(resolvedRel)\n if (targetSet) {\n targetSet.add(relPath)\n }\n }),\n )\n }\n }\n\n await Promise.all(resolvePromises)\n\n // ---- Build nodes ----\n const pages: GraphNode[] = []\n const components: GraphNode[] = []\n const layouts: GraphNode[] = []\n const orphanCandidates: string[] = []\n\n let totalStrings = 0\n\n for (const [relPath, info] of fileMap) {\n const usedBy = [...(usedByMap.get(relPath) ?? [])].toSorted((a, b) => a.localeCompare(b))\n totalStrings += info.strings\n\n // Filter out nodes with 0 strings — not relevant for content extraction\n if (info.strings === 0 && info.category !== 'other') continue\n\n const node: GraphNode = {\n file: relPath,\n category: info.category,\n imports: info.imports,\n used_by: usedBy,\n strings: info.strings,\n }\n\n // Only attach components list for pages\n if (info.category === 'page' && info.components.length > 0) {\n node.components = info.components\n }\n\n switch (info.category) {\n case 'page':\n pages.push(node)\n break\n case 'component':\n components.push(node)\n break\n case 'layout':\n layouts.push(node)\n break\n default:\n // Orphan = \"other\" category with no inbound references and no outbound imports\n if (usedBy.length === 0 && info.imports.length === 0) {\n orphanCandidates.push(relPath)\n }\n break\n }\n }\n\n return {\n pages: pages.toSorted((a, b) => a.file.localeCompare(b.file)),\n components: components.toSorted((a, b) => a.file.localeCompare(b.file)),\n layouts: layouts.toSorted((a, b) => a.file.localeCompare(b.file)),\n orphan_files: orphanCandidates.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_ORPHANS),\n stats: {\n total_files: fileMap.size,\n total_components: components.length,\n total_pages: pages.length,\n total_strings_estimate: totalStrings,\n },\n }\n}\n"],"mappings":"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"} |
| import { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider, RepoReader, RepoWriter } from "@contentrain/types"; | ||
| export { RepoReader as _, FileChange as a, MediaAsset as c, MediaListResult as d, MediaProvider as f, RepoProvider as g, ProviderCapabilities$1 as h, CommitAuthor as i, MediaIngestInput as l, MergeResult as m, Branch as n, FileDiff as o, MediaUpdateInput as p, Commit as r, LOCAL_CAPABILITIES as s, ApplyPlanInput as t, MediaListOptions as u, RepoWriter as v }; |
| import g$1 from "crypto"; | ||
| import _, { promises } from "fs"; | ||
| import { TextDecoder, TextEncoder } from "util"; | ||
| import { fileURLToPath } from "url"; | ||
| //#region ../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/chunk-W5DTLHV4.js | ||
| globalThis.fs || Object.defineProperty(globalThis, "fs", { value: _ }); | ||
| globalThis.process || Object.defineProperties(globalThis, "process", { value: process }); | ||
| globalThis.crypto || Object.defineProperty(globalThis, "crypto", { value: g$1.webcrypto ? g$1.webcrypto : { getRandomValues(m) { | ||
| return g$1.randomFillSync(m); | ||
| } } }); | ||
| globalThis.performance || Object.defineProperty(globalThis, "performance", { value: { now() { | ||
| let [m, o] = process.hrtime(); | ||
| return m * 1e3 + o / 1e6; | ||
| } } }); | ||
| var y$1 = new TextEncoder("utf-8"), w$1 = new TextDecoder("utf-8"); | ||
| var d$1 = class { | ||
| constructor() { | ||
| this.argv = ["js"], this.env = {}, this.exit = (t) => { | ||
| t !== 0 && console.warn("exit code:", t); | ||
| }, this._exitPromise = new Promise((t) => { | ||
| this._resolveExitPromise = t; | ||
| }), this._pendingEvent = null, this._scheduledTimeouts = /* @__PURE__ */ new Map(), this._nextCallbackTimeoutID = 1; | ||
| let o = (t, e) => { | ||
| this.mem.setUint32(t + 0, e, !0), this.mem.setUint32(t + 4, Math.floor(e / 4294967296), !0); | ||
| }, n = (t) => { | ||
| return this.mem.getUint32(t + 0, !0) + this.mem.getInt32(t + 4, !0) * 4294967296; | ||
| }, r = (t) => { | ||
| let e = this.mem.getFloat64(t, !0); | ||
| if (e === 0) return; | ||
| if (!isNaN(e)) return e; | ||
| let s = this.mem.getUint32(t, !0); | ||
| return this._values[s]; | ||
| }, l = (t, e) => { | ||
| if (typeof e == "number" && e !== 0) { | ||
| if (isNaN(e)) { | ||
| this.mem.setUint32(t + 4, 2146959360, !0), this.mem.setUint32(t, 0, !0); | ||
| return; | ||
| } | ||
| this.mem.setFloat64(t, e, !0); | ||
| return; | ||
| } | ||
| if (e === void 0) { | ||
| this.mem.setFloat64(t, 0, !0); | ||
| return; | ||
| } | ||
| let i = this._ids.get(e); | ||
| i === void 0 && (i = this._idPool.pop(), i === void 0 && (i = this._values.length), this._values[i] = e, this._goRefCounts[i] = 0, this._ids.set(e, i)), this._goRefCounts[i]++; | ||
| let a = 0; | ||
| switch (typeof e) { | ||
| case "object": | ||
| e !== null && (a = 1); | ||
| break; | ||
| case "string": | ||
| a = 2; | ||
| break; | ||
| case "symbol": | ||
| a = 3; | ||
| break; | ||
| case "function": | ||
| a = 4; | ||
| break; | ||
| } | ||
| this.mem.setUint32(t + 4, 2146959360 | a, !0), this.mem.setUint32(t, i, !0); | ||
| }, c = (t) => { | ||
| let e = n(t + 0), s = n(t + 8); | ||
| return new Uint8Array(this._inst.exports.mem.buffer, e, s); | ||
| }, f = (t) => { | ||
| let e = n(t + 0), s = n(t + 8), i = new Array(s); | ||
| for (let a = 0; a < s; a++) i[a] = r(e + a * 8); | ||
| return i; | ||
| }, u = (t) => { | ||
| let e = n(t + 0), s = n(t + 8); | ||
| return w$1.decode(new DataView(this._inst.exports.mem.buffer, e, s)); | ||
| }, h = Date.now() - performance.now(); | ||
| this.importObject = { gojs: { | ||
| "runtime.wasmExit": (t) => { | ||
| t >>>= 0; | ||
| let e = this.mem.getInt32(t + 8, !0); | ||
| this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(e); | ||
| }, | ||
| "runtime.wasmWrite": (t) => { | ||
| t >>>= 0; | ||
| let e = n(t + 8), s = n(t + 16), i = this.mem.getInt32(t + 24, !0); | ||
| _.writeSync(e, new Uint8Array(this._inst.exports.mem.buffer, s, i)); | ||
| }, | ||
| "runtime.resetMemoryDataView": (t) => { | ||
| t >>>= 0, this.mem = new DataView(this._inst.exports.mem.buffer); | ||
| }, | ||
| "runtime.nanotime1": (t) => { | ||
| t >>>= 0, o(t + 8, (h + performance.now()) * 1e6); | ||
| }, | ||
| "runtime.walltime": (t) => { | ||
| t >>>= 0; | ||
| let e = (/* @__PURE__ */ new Date()).getTime(); | ||
| o(t + 8, e / 1e3), this.mem.setInt32(t + 16, e % 1e3 * 1e6, !0); | ||
| }, | ||
| "runtime.scheduleTimeoutEvent": (t) => { | ||
| t >>>= 0; | ||
| let e = this._nextCallbackTimeoutID; | ||
| this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(e, setTimeout(() => { | ||
| for (this._resume(); this._scheduledTimeouts.has(e);) console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume(); | ||
| }, n(t + 8) + 1)), this.mem.setInt32(t + 16, e, !0); | ||
| }, | ||
| "runtime.clearTimeoutEvent": (t) => { | ||
| t >>>= 0; | ||
| let e = this.mem.getInt32(t + 8, !0); | ||
| clearTimeout(this._scheduledTimeouts.get(e)), this._scheduledTimeouts.delete(e); | ||
| }, | ||
| "runtime.getRandomData": (t) => { | ||
| t >>>= 0, globalThis.crypto.getRandomValues(c(t + 8)); | ||
| }, | ||
| "syscall/js.finalizeRef": (t) => { | ||
| t >>>= 0; | ||
| let e = this.mem.getUint32(t + 8, !0); | ||
| if (this._goRefCounts[e]--, this._goRefCounts[e] === 0) { | ||
| let s = this._values[e]; | ||
| this._values[e] = null, this._ids.delete(s), this._idPool.push(e); | ||
| } | ||
| }, | ||
| "syscall/js.stringVal": (t) => { | ||
| t >>>= 0, l(t + 24, u(t + 8)); | ||
| }, | ||
| "syscall/js.valueGet": (t) => { | ||
| t >>>= 0; | ||
| let e = Reflect.get(r(t + 8), u(t + 16)); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 32, e); | ||
| }, | ||
| "syscall/js.valueSet": (t) => { | ||
| t >>>= 0, Reflect.set(r(t + 8), u(t + 16), r(t + 32)); | ||
| }, | ||
| "syscall/js.valueDelete": (t) => { | ||
| t >>>= 0, Reflect.deleteProperty(r(t + 8), u(t + 16)); | ||
| }, | ||
| "syscall/js.valueIndex": (t) => { | ||
| t >>>= 0, l(t + 24, Reflect.get(r(t + 8), n(t + 16))); | ||
| }, | ||
| "syscall/js.valueSetIndex": (t) => { | ||
| t >>>= 0, Reflect.set(r(t + 8), n(t + 16), r(t + 24)); | ||
| }, | ||
| "syscall/js.valueCall": (t) => { | ||
| t >>>= 0; | ||
| try { | ||
| let e = r(t + 8), s = Reflect.get(e, u(t + 16)), i = f(t + 32), a = Reflect.apply(s, e, i); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 56, a), this.mem.setUint8(t + 64, 1); | ||
| } catch (e) { | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 56, e), this.mem.setUint8(t + 64, 0); | ||
| } | ||
| }, | ||
| "syscall/js.valueInvoke": (t) => { | ||
| t >>>= 0; | ||
| try { | ||
| let e = r(t + 8), s = f(t + 16), i = Reflect.apply(e, void 0, s); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, i), this.mem.setUint8(t + 48, 1); | ||
| } catch (e) { | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, e), this.mem.setUint8(t + 48, 0); | ||
| } | ||
| }, | ||
| "syscall/js.valueNew": (t) => { | ||
| t >>>= 0; | ||
| try { | ||
| let e = r(t + 8), s = f(t + 16), i = Reflect.construct(e, s); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, i), this.mem.setUint8(t + 48, 1); | ||
| } catch (e) { | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, e), this.mem.setUint8(t + 48, 0); | ||
| } | ||
| }, | ||
| "syscall/js.valueLength": (t) => { | ||
| t >>>= 0, o(t + 16, Number.parseInt(r(t + 8).length)); | ||
| }, | ||
| "syscall/js.valuePrepareString": (t) => { | ||
| t >>>= 0; | ||
| let e = y$1.encode(String(r(t + 8))); | ||
| l(t + 16, e), o(t + 24, e.length); | ||
| }, | ||
| "syscall/js.valueLoadString": (t) => { | ||
| t >>>= 0; | ||
| let e = r(t + 8); | ||
| c(t + 16).set(e); | ||
| }, | ||
| "syscall/js.valueInstanceOf": (t) => { | ||
| t >>>= 0, this.mem.setUint8(t + 24, r(t + 8) instanceof r(t + 16) ? 1 : 0); | ||
| }, | ||
| "syscall/js.copyBytesToGo": (t) => { | ||
| t >>>= 0; | ||
| let e = c(t + 8), s = r(t + 32); | ||
| if (!(s instanceof Uint8Array || s instanceof Uint8ClampedArray)) { | ||
| this.mem.setUint8(t + 48, 0); | ||
| return; | ||
| } | ||
| let i = s.subarray(0, e.length); | ||
| e.set(i), o(t + 40, i.length), this.mem.setUint8(t + 48, 1); | ||
| }, | ||
| "syscall/js.copyBytesToJS": (t) => { | ||
| t >>>= 0; | ||
| let e = r(t + 8), s = c(t + 16); | ||
| if (!(e instanceof Uint8Array || e instanceof Uint8ClampedArray)) { | ||
| this.mem.setUint8(t + 48, 0); | ||
| return; | ||
| } | ||
| let i = s.subarray(0, e.length); | ||
| e.set(i), o(t + 40, i.length), this.mem.setUint8(t + 48, 1); | ||
| }, | ||
| debug: (t) => { | ||
| console.log(t); | ||
| } | ||
| } }; | ||
| } | ||
| async run(o) { | ||
| if (!(o instanceof WebAssembly.Instance)) throw new Error("Go.run: WebAssembly.Instance expected"); | ||
| this._inst = o, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [ | ||
| NaN, | ||
| 0, | ||
| null, | ||
| !0, | ||
| !1, | ||
| globalThis, | ||
| this | ||
| ], this._goRefCounts = new Array(this._values.length).fill(Number.POSITIVE_INFINITY), this._ids = new Map([ | ||
| [0, 1], | ||
| [null, 2], | ||
| [!0, 3], | ||
| [!1, 4], | ||
| [globalThis, 5], | ||
| [this, 6] | ||
| ]), this._idPool = [], this.exited = !1; | ||
| let n = 4096, r = (h) => { | ||
| let t = n, e = y$1.encode(`${h}\0`); | ||
| return new Uint8Array(this.mem.buffer, n, e.length).set(e), n += e.length, n % 8 !== 0 && (n += 8 - n % 8), t; | ||
| }, l = this.argv.length, c = []; | ||
| this.argv.forEach((h) => { | ||
| c.push(r(h)); | ||
| }), c.push(0), Object.keys(this.env).sort().forEach((h) => { | ||
| c.push(r(`${h}=${this.env[h]}`)); | ||
| }), c.push(0); | ||
| let u = n; | ||
| c.forEach((h) => { | ||
| this.mem.setUint32(n, h, !0), this.mem.setUint32(n + 4, 0, !0), n += 8; | ||
| }), this._inst.exports.run(l, u), this.exited && this._resolveExitPromise(), await this._exitPromise; | ||
| } | ||
| _resume() { | ||
| if (this.exited) throw new Error("Go program has already exited"); | ||
| this._inst.exports.resume(), this.exited && this._resolveExitPromise(); | ||
| } | ||
| _makeFuncWrapper(o) { | ||
| let n = this; | ||
| return function() { | ||
| let r = { | ||
| id: o, | ||
| this: this, | ||
| args: arguments | ||
| }; | ||
| return n._pendingEvent = r, n._resume(), r.result; | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/node/index.js | ||
| var w = async (t, s) => i().then((r) => r.transform(t, s)), l = async (t, s) => i().then((r) => r.parse(t, s)), b = async (t, s) => i().then((r) => r.convertToTSX(t, s)), P = async (t) => { | ||
| let { default: s } = await import(`data:text/javascript;charset=utf-8;base64,${Buffer.from(t).toString("base64")}`); | ||
| return s; | ||
| }, n, g = () => { | ||
| n = void 0, globalThis["@astrojs/compiler"] = void 0; | ||
| }, i = () => (n || (n = d().catch((t) => { | ||
| throw n = void 0, t; | ||
| })), n), y = async (t, s) => { | ||
| let r; | ||
| return r = await (async () => { | ||
| let o = await promises.readFile(t).then((e) => e.buffer); | ||
| return WebAssembly.instantiate(new Uint8Array(o), s); | ||
| })(), r; | ||
| }, d = async () => { | ||
| let t = new d$1(), s = await y(fileURLToPath(new URL("../astro.wasm", import.meta.url)), t.importObject); | ||
| t.run(s.instance); | ||
| let r = globalThis["@astrojs/compiler"]; | ||
| return { | ||
| transform: (a, o) => new Promise((e) => { | ||
| try { | ||
| e(r.transform(a, o || {})); | ||
| } catch (p) { | ||
| throw n = void 0, p; | ||
| } | ||
| }), | ||
| parse: (a, o) => new Promise((e) => e(r.parse(a, o || {}))).catch((e) => { | ||
| throw n = void 0, e; | ||
| }).then((e) => ({ | ||
| ...e, | ||
| ast: JSON.parse(e.ast) | ||
| })), | ||
| convertToTSX: (a, o) => new Promise((e) => e(r.convertToTSX(a, o || {}))).catch((e) => { | ||
| throw n = void 0, e; | ||
| }).then((e) => ({ | ||
| ...e, | ||
| map: JSON.parse(e.map) | ||
| })) | ||
| }; | ||
| }; | ||
| //#endregion | ||
| export { P as compile, b as convertToTSX, l as parse, g as teardown, w as transform }; | ||
| //# sourceMappingURL=node-BxujauDw.mjs.map |
| {"version":3,"file":"node-BxujauDw.mjs","names":["g","y","v","w","b","d","m","c","f"],"sources":["../../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/chunk-W5DTLHV4.js","../../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/node/index.js"],"sourcesContent":["import g from\"crypto\";import _ from\"fs\";import{TextDecoder as b,TextEncoder as v}from\"util\";globalThis.fs||Object.defineProperty(globalThis,\"fs\",{value:_});globalThis.process||Object.defineProperties(globalThis,\"process\",{value:process});globalThis.crypto||Object.defineProperty(globalThis,\"crypto\",{value:g.webcrypto?g.webcrypto:{getRandomValues(m){return g.randomFillSync(m)}}});globalThis.performance||Object.defineProperty(globalThis,\"performance\",{value:{now(){let[m,o]=process.hrtime();return m*1e3+o/1e6}}});var y=new v(\"utf-8\"),w=new b(\"utf-8\");var d=class{constructor(){this.argv=[\"js\"],this.env={},this.exit=t=>{t!==0&&console.warn(\"exit code:\",t)},this._exitPromise=new Promise(t=>{this._resolveExitPromise=t}),this._pendingEvent=null,this._scheduledTimeouts=new Map,this._nextCallbackTimeoutID=1;let o=(t,e)=>{this.mem.setUint32(t+0,e,!0),this.mem.setUint32(t+4,Math.floor(e/4294967296),!0)},n=t=>{let e=this.mem.getUint32(t+0,!0),s=this.mem.getInt32(t+4,!0);return e+s*4294967296},r=t=>{let e=this.mem.getFloat64(t,!0);if(e===0)return;if(!isNaN(e))return e;let s=this.mem.getUint32(t,!0);return this._values[s]},l=(t,e)=>{if(typeof e==\"number\"&&e!==0){if(isNaN(e)){this.mem.setUint32(t+4,2146959360,!0),this.mem.setUint32(t,0,!0);return}this.mem.setFloat64(t,e,!0);return}if(e===void 0){this.mem.setFloat64(t,0,!0);return}let i=this._ids.get(e);i===void 0&&(i=this._idPool.pop(),i===void 0&&(i=this._values.length),this._values[i]=e,this._goRefCounts[i]=0,this._ids.set(e,i)),this._goRefCounts[i]++;let a=0;switch(typeof e){case\"object\":e!==null&&(a=1);break;case\"string\":a=2;break;case\"symbol\":a=3;break;case\"function\":a=4;break}this.mem.setUint32(t+4,2146959360|a,!0),this.mem.setUint32(t,i,!0)},c=t=>{let e=n(t+0),s=n(t+8);return new Uint8Array(this._inst.exports.mem.buffer,e,s)},f=t=>{let e=n(t+0),s=n(t+8),i=new Array(s);for(let a=0;a<s;a++)i[a]=r(e+a*8);return i},u=t=>{let e=n(t+0),s=n(t+8);return w.decode(new DataView(this._inst.exports.mem.buffer,e,s))},h=Date.now()-performance.now();this.importObject={gojs:{\"runtime.wasmExit\":t=>{t>>>=0;let e=this.mem.getInt32(t+8,!0);this.exited=!0,delete this._inst,delete this._values,delete this._goRefCounts,delete this._ids,delete this._idPool,this.exit(e)},\"runtime.wasmWrite\":t=>{t>>>=0;let e=n(t+8),s=n(t+16),i=this.mem.getInt32(t+24,!0);_.writeSync(e,new Uint8Array(this._inst.exports.mem.buffer,s,i))},\"runtime.resetMemoryDataView\":t=>{t>>>=0,this.mem=new DataView(this._inst.exports.mem.buffer)},\"runtime.nanotime1\":t=>{t>>>=0,o(t+8,(h+performance.now())*1e6)},\"runtime.walltime\":t=>{t>>>=0;let e=new Date().getTime();o(t+8,e/1e3),this.mem.setInt32(t+16,e%1e3*1e6,!0)},\"runtime.scheduleTimeoutEvent\":t=>{t>>>=0;let e=this._nextCallbackTimeoutID;this._nextCallbackTimeoutID++,this._scheduledTimeouts.set(e,setTimeout(()=>{for(this._resume();this._scheduledTimeouts.has(e);)console.warn(\"scheduleTimeoutEvent: missed timeout event\"),this._resume()},n(t+8)+1)),this.mem.setInt32(t+16,e,!0)},\"runtime.clearTimeoutEvent\":t=>{t>>>=0;let e=this.mem.getInt32(t+8,!0);clearTimeout(this._scheduledTimeouts.get(e)),this._scheduledTimeouts.delete(e)},\"runtime.getRandomData\":t=>{t>>>=0,globalThis.crypto.getRandomValues(c(t+8))},\"syscall/js.finalizeRef\":t=>{t>>>=0;let e=this.mem.getUint32(t+8,!0);if(this._goRefCounts[e]--,this._goRefCounts[e]===0){let s=this._values[e];this._values[e]=null,this._ids.delete(s),this._idPool.push(e)}},\"syscall/js.stringVal\":t=>{t>>>=0,l(t+24,u(t+8))},\"syscall/js.valueGet\":t=>{t>>>=0;let e=Reflect.get(r(t+8),u(t+16));t=this._inst.exports.getsp()>>>0,l(t+32,e)},\"syscall/js.valueSet\":t=>{t>>>=0,Reflect.set(r(t+8),u(t+16),r(t+32))},\"syscall/js.valueDelete\":t=>{t>>>=0,Reflect.deleteProperty(r(t+8),u(t+16))},\"syscall/js.valueIndex\":t=>{t>>>=0,l(t+24,Reflect.get(r(t+8),n(t+16)))},\"syscall/js.valueSetIndex\":t=>{t>>>=0,Reflect.set(r(t+8),n(t+16),r(t+24))},\"syscall/js.valueCall\":t=>{t>>>=0;try{let e=r(t+8),s=Reflect.get(e,u(t+16)),i=f(t+32),a=Reflect.apply(s,e,i);t=this._inst.exports.getsp()>>>0,l(t+56,a),this.mem.setUint8(t+64,1)}catch(e){t=this._inst.exports.getsp()>>>0,l(t+56,e),this.mem.setUint8(t+64,0)}},\"syscall/js.valueInvoke\":t=>{t>>>=0;try{let e=r(t+8),s=f(t+16),i=Reflect.apply(e,void 0,s);t=this._inst.exports.getsp()>>>0,l(t+40,i),this.mem.setUint8(t+48,1)}catch(e){t=this._inst.exports.getsp()>>>0,l(t+40,e),this.mem.setUint8(t+48,0)}},\"syscall/js.valueNew\":t=>{t>>>=0;try{let e=r(t+8),s=f(t+16),i=Reflect.construct(e,s);t=this._inst.exports.getsp()>>>0,l(t+40,i),this.mem.setUint8(t+48,1)}catch(e){t=this._inst.exports.getsp()>>>0,l(t+40,e),this.mem.setUint8(t+48,0)}},\"syscall/js.valueLength\":t=>{t>>>=0,o(t+16,Number.parseInt(r(t+8).length))},\"syscall/js.valuePrepareString\":t=>{t>>>=0;let e=y.encode(String(r(t+8)));l(t+16,e),o(t+24,e.length)},\"syscall/js.valueLoadString\":t=>{t>>>=0;let e=r(t+8);c(t+16).set(e)},\"syscall/js.valueInstanceOf\":t=>{t>>>=0,this.mem.setUint8(t+24,r(t+8)instanceof r(t+16)?1:0)},\"syscall/js.copyBytesToGo\":t=>{t>>>=0;let e=c(t+8),s=r(t+32);if(!(s instanceof Uint8Array||s instanceof Uint8ClampedArray)){this.mem.setUint8(t+48,0);return}let i=s.subarray(0,e.length);e.set(i),o(t+40,i.length),this.mem.setUint8(t+48,1)},\"syscall/js.copyBytesToJS\":t=>{t>>>=0;let e=r(t+8),s=c(t+16);if(!(e instanceof Uint8Array||e instanceof Uint8ClampedArray)){this.mem.setUint8(t+48,0);return}let i=s.subarray(0,e.length);e.set(i),o(t+40,i.length),this.mem.setUint8(t+48,1)},debug:t=>{console.log(t)}}}}async run(o){if(!(o instanceof WebAssembly.Instance))throw new Error(\"Go.run: WebAssembly.Instance expected\");this._inst=o,this.mem=new DataView(this._inst.exports.mem.buffer),this._values=[Number.NaN,0,null,!0,!1,globalThis,this],this._goRefCounts=new Array(this._values.length).fill(Number.POSITIVE_INFINITY),this._ids=new Map([[0,1],[null,2],[!0,3],[!1,4],[globalThis,5],[this,6]]),this._idPool=[],this.exited=!1;let n=4096,r=h=>{let t=n,e=y.encode(`${h}\\0`);return new Uint8Array(this.mem.buffer,n,e.length).set(e),n+=e.length,n%8!==0&&(n+=8-n%8),t},l=this.argv.length,c=[];this.argv.forEach(h=>{c.push(r(h))}),c.push(0),Object.keys(this.env).sort().forEach(h=>{c.push(r(`${h}=${this.env[h]}`))}),c.push(0);let u=n;c.forEach(h=>{this.mem.setUint32(n,h,!0),this.mem.setUint32(n+4,0,!0),n+=8}),this._inst.exports.run(l,u),this.exited&&this._resolveExitPromise(),await this._exitPromise}_resume(){if(this.exited)throw new Error(\"Go program has already exited\");this._inst.exports.resume(),this.exited&&this._resolveExitPromise()}_makeFuncWrapper(o){let n=this;return function(){let r={id:o,this:this,args:arguments};return n._pendingEvent=r,n._resume(),r.result}}};export{d as a};\n","import{a as c}from\"../chunk-W5DTLHV4.js\";import{promises as m}from\"fs\";import{fileURLToPath as f}from\"url\";var w=async(t,s)=>i().then(r=>r.transform(t,s)),l=async(t,s)=>i().then(r=>r.parse(t,s)),b=async(t,s)=>i().then(r=>r.convertToTSX(t,s)),P=async t=>{let{default:s}=await import(`data:text/javascript;charset=utf-8;base64,${Buffer.from(t).toString(\"base64\")}`);return s},n,g=()=>{n=void 0,globalThis[\"@astrojs/compiler\"]=void 0},i=()=>(n||(n=d().catch(t=>{throw n=void 0,t})),n),y=async(t,s)=>{let r;return r=await(async()=>{let o=await m.readFile(t).then(e=>e.buffer);return WebAssembly.instantiate(new Uint8Array(o),s)})(),r},d=async()=>{let t=new c,s=await y(f(new URL(\"../astro.wasm\",import.meta.url)),t.importObject);t.run(s.instance);let r=globalThis[\"@astrojs/compiler\"];return{transform:(a,o)=>new Promise(e=>{try{e(r.transform(a,o||{}))}catch(p){throw n=void 0,p}}),parse:(a,o)=>new Promise(e=>e(r.parse(a,o||{}))).catch(e=>{throw n=void 0,e}).then(e=>({...e,ast:JSON.parse(e.ast)})),convertToTSX:(a,o)=>new Promise(e=>e(r.convertToTSX(a,o||{}))).catch(e=>{throw n=void 0,e}).then(e=>({...e,map:JSON.parse(e.map)}))}};export{P as compile,b as convertToTSX,l as parse,g as teardown,w as transform};\n"],"x_google_ignoreList":[0,1],"mappings":"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"} |
| //#region src/core/overlay-reader.ts | ||
| /** | ||
| * A RepoReader that overlays a set of pending FileChanges on top of an | ||
| * underlying reader. Used in the remote-provider write path so helpers | ||
| * like {@link import('./context.js').buildContextChange} and | ||
| * {@link import('./validator/project.js').validateProject} see the | ||
| * post-change state — the state the pending commit is about to produce | ||
| * — rather than the pre-change base branch. | ||
| * | ||
| * Semantics: | ||
| * | ||
| * - `readFile(path)` — returns pending `content` when the overlay maps | ||
| * the path; falls back to the base reader otherwise. A pending delete | ||
| * (`content: null`) surfaces as "missing" (throws, matching | ||
| * `RepoReader.readFile`'s missing-file contract). | ||
| * | ||
| * - `listDirectory(path)` — merges the base directory listing with | ||
| * pending additions that live directly in the same folder, removes | ||
| * entries whose pending change is a delete, and de-duplicates the | ||
| * result. Pending paths in nested subdirectories surface only at | ||
| * their own listings. | ||
| * | ||
| * - `fileExists(path)` — pending adds → `true`, pending deletes → | ||
| * `false`, otherwise delegates. | ||
| * | ||
| * The overlay keys are canonicalised to match the FileChange contract: | ||
| * forward slashes, no leading `/`, no `..` segments (FileChanges are | ||
| * required to respect these invariants). | ||
| */ | ||
| var OverlayReader = class { | ||
| overlay; | ||
| constructor(base, pendingChanges) { | ||
| this.base = base; | ||
| this.overlay = /* @__PURE__ */ new Map(); | ||
| for (const change of pendingChanges) this.overlay.set(normalise(change.path), change); | ||
| } | ||
| async readFile(path, ref) { | ||
| const key = normalise(path); | ||
| const pending = this.overlay.get(key); | ||
| if (pending) { | ||
| if (pending.content === null) throw new Error(`OverlayReader: "${path}" is marked for deletion`); | ||
| return pending.content; | ||
| } | ||
| return this.base.readFile(path, ref); | ||
| } | ||
| async listDirectory(path, ref) { | ||
| const baseEntries = await this.base.listDirectory(path, ref); | ||
| const dirKey = normalise(path); | ||
| const prefix = dirKey === "" ? "" : `${dirKey}/`; | ||
| const direct = [...this.overlay.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, change]) => ({ | ||
| name: key.slice(prefix.length).split("/")[0] ?? "", | ||
| isNested: key.slice(prefix.length).includes("/"), | ||
| deleted: change.content === null | ||
| })).filter((entry) => entry.name.length > 0); | ||
| const deleted = new Set(direct.filter((e) => e.deleted && !e.isNested).map((e) => e.name)); | ||
| const added = direct.filter((e) => !e.deleted).map((e) => e.name); | ||
| const result = baseEntries.filter((n) => !deleted.has(n)); | ||
| for (const name of added) if (!result.includes(name)) result.push(name); | ||
| return result; | ||
| } | ||
| async fileExists(path, ref) { | ||
| const key = normalise(path); | ||
| const pending = this.overlay.get(key); | ||
| if (pending) return pending.content !== null; | ||
| return this.base.fileExists(path, ref); | ||
| } | ||
| }; | ||
| function normalise(path) { | ||
| return path.replace(/^\/+/, ""); | ||
| } | ||
| //#endregion | ||
| export { OverlayReader as t }; | ||
| //# sourceMappingURL=overlay-reader-DNaVsgiS.mjs.map |
| {"version":3,"file":"overlay-reader-DNaVsgiS.mjs","names":[],"sources":["../src/core/overlay-reader.ts"],"sourcesContent":["import type { FileChange, RepoReader } from './contracts/index.js'\n\n/**\n * A RepoReader that overlays a set of pending FileChanges on top of an\n * underlying reader. Used in the remote-provider write path so helpers\n * like {@link import('./context.js').buildContextChange} and\n * {@link import('./validator/project.js').validateProject} see the\n * post-change state — the state the pending commit is about to produce\n * — rather than the pre-change base branch.\n *\n * Semantics:\n *\n * - `readFile(path)` — returns pending `content` when the overlay maps\n * the path; falls back to the base reader otherwise. A pending delete\n * (`content: null`) surfaces as \"missing\" (throws, matching\n * `RepoReader.readFile`'s missing-file contract).\n *\n * - `listDirectory(path)` — merges the base directory listing with\n * pending additions that live directly in the same folder, removes\n * entries whose pending change is a delete, and de-duplicates the\n * result. Pending paths in nested subdirectories surface only at\n * their own listings.\n *\n * - `fileExists(path)` — pending adds → `true`, pending deletes →\n * `false`, otherwise delegates.\n *\n * The overlay keys are canonicalised to match the FileChange contract:\n * forward slashes, no leading `/`, no `..` segments (FileChanges are\n * required to respect these invariants).\n */\nexport class OverlayReader implements RepoReader {\n private readonly overlay: Map<string, FileChange>\n\n constructor(\n private readonly base: RepoReader,\n pendingChanges: FileChange[],\n ) {\n this.overlay = new Map()\n for (const change of pendingChanges) {\n this.overlay.set(normalise(change.path), change)\n }\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) {\n if (pending.content === null) {\n throw new Error(`OverlayReader: \"${path}\" is marked for deletion`)\n }\n return pending.content\n }\n return this.base.readFile(path, ref)\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const baseEntries = await this.base.listDirectory(path, ref)\n const dirKey = normalise(path)\n const prefix = dirKey === '' ? '' : `${dirKey}/`\n\n const direct = [...this.overlay.entries()]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, change]) => ({\n name: key.slice(prefix.length).split('/')[0] ?? '',\n isNested: key.slice(prefix.length).includes('/'),\n deleted: change.content === null,\n }))\n .filter(entry => entry.name.length > 0)\n\n // For nested pending paths (e.g. overlay at `dir/sub/a.json` when\n // listing `dir`), the immediate child directory `sub` must surface\n // even though no pending change targets it directly.\n const deleted = new Set(\n direct.filter(e => e.deleted && !e.isNested).map(e => e.name),\n )\n const added = direct\n .filter(e => !e.deleted)\n .map(e => e.name)\n\n const result = baseEntries.filter(n => !deleted.has(n))\n for (const name of added) {\n if (!result.includes(name)) result.push(name)\n }\n return result\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) return pending.content !== null\n return this.base.fileExists(path, ref)\n }\n}\n\nfunction normalise(path: string): string {\n return path.replace(/^\\/+/, '')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAAiD;CAC/C;CAEA,YACE,MACA,gBACA;AAFiB,OAAA,OAAA;AAGjB,OAAK,0BAAU,IAAI,KAAK;AACxB,OAAK,MAAM,UAAU,eACnB,MAAK,QAAQ,IAAI,UAAU,OAAO,KAAK,EAAE,OAAO;;CAIpD,MAAM,SAAS,MAAc,KAA+B;EAC1D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,SAAS;AACX,OAAI,QAAQ,YAAY,KACtB,OAAM,IAAI,MAAM,mBAAmB,KAAK,0BAA0B;AAEpE,UAAO,QAAQ;;AAEjB,SAAO,KAAK,KAAK,SAAS,MAAM,IAAI;;CAGtC,MAAM,cAAc,MAAc,KAAiC;EACjE,MAAM,cAAc,MAAM,KAAK,KAAK,cAAc,MAAM,IAAI;EAC5D,MAAM,SAAS,UAAU,KAAK;EAC9B,MAAM,SAAS,WAAW,KAAK,KAAK,GAAG,OAAO;EAE9C,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,SAAS,CAAC,CACvC,QAAQ,CAAC,SAAS,IAAI,WAAW,OAAO,CAAC,CACzC,KAAK,CAAC,KAAK,aAAa;GACvB,MAAM,IAAI,MAAM,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM;GAChD,UAAU,IAAI,MAAM,OAAO,OAAO,CAAC,SAAS,IAAI;GAChD,SAAS,OAAO,YAAY;GAC7B,EAAE,CACF,QAAO,UAAS,MAAM,KAAK,SAAS,EAAE;EAKzC,MAAM,UAAU,IAAI,IAClB,OAAO,QAAO,MAAK,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,KAAI,MAAK,EAAE,KAAK,CAC9D;EACD,MAAM,QAAQ,OACX,QAAO,MAAK,CAAC,EAAE,QAAQ,CACvB,KAAI,MAAK,EAAE,KAAK;EAEnB,MAAM,SAAS,YAAY,QAAO,MAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;AACvD,OAAK,MAAM,QAAQ,MACjB,KAAI,CAAC,OAAO,SAAS,KAAK,CAAE,QAAO,KAAK,KAAK;AAE/C,SAAO;;CAGT,MAAM,WAAW,MAAc,KAAgC;EAC7D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,QAAS,QAAO,QAAQ,YAAY;AACxC,SAAO,KAAK,KAAK,WAAW,MAAM,IAAI;;;AAI1C,SAAS,UAAU,MAAsB;AACvC,QAAO,KAAK,QAAQ,QAAQ,GAAG"} |
| //#region src/providers/shared/paths.ts | ||
| /** | ||
| * Normalise an optional contentRoot — strip leading/trailing slashes, | ||
| * treat `''`, `/` and `undefined` as "no prefix". Used by API-backed | ||
| * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative | ||
| * paths against a repo subdirectory when Contentrain lives under a | ||
| * monorepo path like `apps/web/.contentrain/`. | ||
| */ | ||
| function normaliseContentRoot(raw) { | ||
| if (!raw || raw === "/" || raw === "") return ""; | ||
| return raw.replace(/^\/+|\/+$/g, ""); | ||
| } | ||
| /** | ||
| * Resolve a content-root-relative path to a repo-relative path. The result | ||
| * always uses forward slashes and has no leading slash — the form every | ||
| * REST git API consumes for `file_path` / `path` query parameters and the | ||
| * Git Data API tree entries. | ||
| */ | ||
| function resolveRepoPath(contentRoot, relativePath) { | ||
| const prefix = normaliseContentRoot(contentRoot); | ||
| const cleanPath = relativePath.replace(/^\/+/, ""); | ||
| return prefix ? `${prefix}/${cleanPath}` : cleanPath; | ||
| } | ||
| //#endregion | ||
| export { resolveRepoPath as t }; | ||
| //# sourceMappingURL=paths-enT2coeX.mjs.map |
| {"version":3,"file":"paths-enT2coeX.mjs","names":[],"sources":["../src/providers/shared/paths.ts"],"sourcesContent":["/**\n * Normalise an optional contentRoot — strip leading/trailing slashes,\n * treat `''`, `/` and `undefined` as \"no prefix\". Used by API-backed\n * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative\n * paths against a repo subdirectory when Contentrain lives under a\n * monorepo path like `apps/web/.contentrain/`.\n */\nexport function normaliseContentRoot(raw?: string): string {\n if (!raw || raw === '/' || raw === '') return ''\n return raw.replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Resolve a content-root-relative path to a repo-relative path. The result\n * always uses forward slashes and has no leading slash — the form every\n * REST git API consumes for `file_path` / `path` query parameters and the\n * Git Data API tree entries.\n */\nexport function resolveRepoPath(contentRoot: string | undefined, relativePath: string): string {\n const prefix = normaliseContentRoot(contentRoot)\n const cleanPath = relativePath.replace(/^\\/+/, '')\n return prefix ? `${prefix}/${cleanPath}` : cleanPath\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,qBAAqB,KAAsB;AACzD,KAAI,CAAC,OAAO,QAAQ,OAAO,QAAQ,GAAI,QAAO;AAC9C,QAAO,IAAI,QAAQ,cAAc,GAAG;;;;;;;;AAStC,SAAgB,gBAAgB,aAAiC,cAA8B;CAC7F,MAAM,SAAS,qBAAqB,YAAY;CAChD,MAAM,YAAY,aAAa,QAAQ,QAAQ,GAAG;AAClD,QAAO,SAAS,GAAG,OAAO,GAAG,cAAc"} |
| import { r as pathExists } from "./fs-DLbVB-Ek.mjs"; | ||
| import { basename, extname, join } from "node:path"; | ||
| import { readdir } from "node:fs/promises"; | ||
| //#region src/core/scan-config.ts | ||
| /** File extensions to scan across all JS/TS ecosystem projects */ | ||
| const SCAN_EXTENSIONS = new Set([ | ||
| ".tsx", | ||
| ".jsx", | ||
| ".vue", | ||
| ".ts", | ||
| ".js", | ||
| ".mjs", | ||
| ".astro", | ||
| ".svelte" | ||
| ]); | ||
| /** Directory names to always exclude from scanning */ | ||
| const SCAN_IGNORE_DIRS = new Set([ | ||
| "node_modules", | ||
| ".pnpm", | ||
| "dist", | ||
| "build", | ||
| "out", | ||
| ".output", | ||
| ".nuxt", | ||
| ".next", | ||
| ".svelte-kit", | ||
| ".expo", | ||
| ".turbo", | ||
| ".parcel-cache", | ||
| ".vercel", | ||
| ".netlify", | ||
| "coverage", | ||
| "__tests__", | ||
| "__mocks__", | ||
| ".git", | ||
| ".vscode", | ||
| ".idea", | ||
| ".contentrain" | ||
| ]); | ||
| /** Max files per scan operation */ | ||
| const MAX_SCAN_FILES = 500; | ||
| /** File patterns to skip regardless of extension */ | ||
| const SKIP_FILE_RE = /\.(test|spec)\.[^.]+$|\.d\.ts$|\.min\.[^.]+$/; | ||
| /** Directories that represent entry points / pages / screens */ | ||
| const PAGE_DIR_NAMES = new Set([ | ||
| "pages", | ||
| "routes", | ||
| "screens", | ||
| "views", | ||
| "controllers", | ||
| "handlers", | ||
| "resolvers" | ||
| ]); | ||
| /** Directories that represent reusable components / modules */ | ||
| const COMPONENT_DIR_NAMES = new Set([ | ||
| "components", | ||
| "ui", | ||
| "widgets", | ||
| "elements", | ||
| "features", | ||
| "modules", | ||
| "services", | ||
| "providers", | ||
| "shared", | ||
| "common" | ||
| ]); | ||
| /** Directories that represent layouts / templates */ | ||
| const LAYOUT_DIR_NAMES = new Set(["layouts", "templates"]); | ||
| /** Next.js App Router special files that are page-like */ | ||
| const NEXTJS_PAGE_FILES = new Set([ | ||
| "page.tsx", | ||
| "page.jsx", | ||
| "page.ts", | ||
| "page.js", | ||
| "layout.tsx", | ||
| "layout.jsx", | ||
| "layout.ts", | ||
| "layout.js", | ||
| "error.tsx", | ||
| "error.jsx", | ||
| "loading.tsx", | ||
| "loading.jsx", | ||
| "not-found.tsx", | ||
| "not-found.jsx" | ||
| ]); | ||
| /** Classify a file into page/component/layout/other based on path heuristics */ | ||
| function classifyFile(relPath) { | ||
| const parts = relPath.split("/"); | ||
| const fileName = parts[parts.length - 1] ?? ""; | ||
| if (parts.includes("app") && NEXTJS_PAGE_FILES.has(fileName)) return fileName.startsWith("layout") ? "layout" : "page"; | ||
| for (const part of parts) { | ||
| const lower = part.toLowerCase(); | ||
| if (LAYOUT_DIR_NAMES.has(lower)) return "layout"; | ||
| if (PAGE_DIR_NAMES.has(lower)) return "page"; | ||
| if (COMPONENT_DIR_NAMES.has(lower)) return "component"; | ||
| } | ||
| return "other"; | ||
| } | ||
| /** Common source directories across all JS/TS project types */ | ||
| const AUTO_DETECT_DIRS = [ | ||
| "src", | ||
| "app", | ||
| "lib", | ||
| "pages", | ||
| "components", | ||
| "layouts", | ||
| "views", | ||
| "screens", | ||
| "modules", | ||
| "routes", | ||
| "controllers", | ||
| "services", | ||
| "features", | ||
| "shared", | ||
| "common", | ||
| "hooks", | ||
| "composables", | ||
| "stores" | ||
| ]; | ||
| /** Auto-detect which source directories exist in the project */ | ||
| async function autoDetectSourceDirs(projectRoot) { | ||
| const found = []; | ||
| for (const dir of AUTO_DETECT_DIRS) if (await pathExists(join(projectRoot, dir))) found.push(dir); | ||
| return found.length > 0 ? found : ["."]; | ||
| } | ||
| /** | ||
| * Discover source files matching scan criteria. | ||
| * Returns relative paths (relative to projectRoot). | ||
| */ | ||
| async function discoverFiles(projectRoot, options) { | ||
| const extensions = options?.include ? new Set(options.include.map((e) => e.startsWith(".") ? e : `.${e}`)) : SCAN_EXTENSIONS; | ||
| const extraExcludes = new Set(options?.exclude ?? []); | ||
| const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot); | ||
| const files = []; | ||
| for (const dir of scanDirs) { | ||
| const absDir = join(projectRoot, dir); | ||
| if (!await pathExists(absDir)) continue; | ||
| let entries; | ||
| try { | ||
| entries = await readdir(absDir, { recursive: true }); | ||
| } catch { | ||
| continue; | ||
| } | ||
| for (const entry of entries) { | ||
| if (files.length >= 500) break; | ||
| const fileName = basename(entry); | ||
| if (entry.split("/").some((seg) => SCAN_IGNORE_DIRS.has(seg) || extraExcludes.has(seg))) continue; | ||
| if (!extensions.has(extname(fileName))) continue; | ||
| if (SKIP_FILE_RE.test(fileName)) continue; | ||
| files.push(join(dir, entry)); | ||
| } | ||
| if (files.length >= 500) break; | ||
| } | ||
| return files.toSorted((a, b) => a.localeCompare(b)).slice(0, 500); | ||
| } | ||
| //#endregion | ||
| export { classifyFile as a, autoDetectSourceDirs as i, SCAN_EXTENSIONS as n, discoverFiles as o, SCAN_IGNORE_DIRS as r, MAX_SCAN_FILES as t }; | ||
| //# sourceMappingURL=scan-config-BlNLRCMx.mjs.map |
| {"version":3,"file":"scan-config-BlNLRCMx.mjs","names":[],"sources":["../src/core/scan-config.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises'\nimport { join, extname, basename } from 'node:path'\nimport { pathExists } from '../util/fs.js'\n\n// ─── Shared Scan Constants ───\n\n/** File extensions to scan across all JS/TS ecosystem projects */\nexport const SCAN_EXTENSIONS = new Set([\n '.tsx', '.jsx', '.vue', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directory names to always exclude from scanning */\nexport const SCAN_IGNORE_DIRS = new Set([\n // Package managers / deps\n 'node_modules', '.pnpm',\n // Build outputs\n 'dist', 'build', 'out', '.output',\n // Framework caches\n '.nuxt', '.next', '.svelte-kit', '.expo', '.turbo', '.parcel-cache', '.vercel', '.netlify',\n // Test / coverage\n 'coverage', '__tests__', '__mocks__',\n // VCS / IDE\n '.git', '.vscode', '.idea',\n // Contentrain\n '.contentrain',\n])\n\n/** Max files per scan operation */\nexport const MAX_SCAN_FILES = 500\n\n/** File patterns to skip regardless of extension */\nconst SKIP_FILE_RE = /\\.(test|spec)\\.[^.]+$|\\.d\\.ts$|\\.min\\.[^.]+$/\n\n// ─── File Classification ───\n// Covers: React, Next.js, Nuxt, Vue, Astro, SvelteKit, Remix,\n// React Native/Expo, NestJS, Express, Fastify, Koa, Hapi\n\n/** Directories that represent entry points / pages / screens */\nconst PAGE_DIR_NAMES = new Set([\n // Frontend routing\n 'pages', 'routes', 'screens', 'views',\n // Backend entry points\n 'controllers', 'handlers', 'resolvers',\n])\n\n/** Directories that represent reusable components / modules */\nconst COMPONENT_DIR_NAMES = new Set([\n // UI components\n 'components', 'ui', 'widgets', 'elements',\n // Feature modules\n 'features', 'modules',\n // Backend services\n 'services', 'providers',\n // Shared / common\n 'shared', 'common',\n])\n\n/** Directories that represent layouts / templates */\nconst LAYOUT_DIR_NAMES = new Set([\n 'layouts', 'templates',\n])\n\n/** Next.js App Router special files that are page-like */\nconst NEXTJS_PAGE_FILES = new Set([\n 'page.tsx', 'page.jsx', 'page.ts', 'page.js',\n 'layout.tsx', 'layout.jsx', 'layout.ts', 'layout.js',\n 'error.tsx', 'error.jsx',\n 'loading.tsx', 'loading.jsx',\n 'not-found.tsx', 'not-found.jsx',\n])\n\n/** Classify a file into page/component/layout/other based on path heuristics */\nexport function classifyFile(relPath: string): 'page' | 'component' | 'layout' | 'other' {\n const parts = relPath.split('/')\n const fileName = parts[parts.length - 1] ?? ''\n\n // Next.js App Router: files in app/ with special names are pages\n if (parts.includes('app') && NEXTJS_PAGE_FILES.has(fileName)) {\n return fileName.startsWith('layout') ? 'layout' : 'page'\n }\n\n // Check directory names in path (check layout first — more specific)\n for (const part of parts) {\n const lower = part.toLowerCase()\n if (LAYOUT_DIR_NAMES.has(lower)) return 'layout'\n if (PAGE_DIR_NAMES.has(lower)) return 'page'\n if (COMPONENT_DIR_NAMES.has(lower)) return 'component'\n }\n\n return 'other'\n}\n\n// ─── Source Directory Detection ───\n\n/** Common source directories across all JS/TS project types */\nconst AUTO_DETECT_DIRS = [\n // Standard source\n 'src', 'app', 'lib',\n // Frontend specific\n 'pages', 'components', 'layouts', 'views',\n // Mobile\n 'screens',\n // Backend\n 'modules', 'routes', 'controllers', 'services',\n // Shared\n 'features', 'shared', 'common',\n // Hooks / composables\n 'hooks', 'composables',\n // Stores\n 'stores',\n]\n\n/** Auto-detect which source directories exist in the project */\nexport async function autoDetectSourceDirs(projectRoot: string): Promise<string[]> {\n const found: string[] = []\n for (const dir of AUTO_DETECT_DIRS) {\n if (await pathExists(join(projectRoot, dir))) {\n found.push(dir)\n }\n }\n return found.length > 0 ? found : ['.']\n}\n\n// ─── File Discovery ───\n\nexport interface DiscoverFilesOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\n/**\n * Discover source files matching scan criteria.\n * Returns relative paths (relative to projectRoot).\n */\nexport async function discoverFiles(\n projectRoot: string,\n options?: DiscoverFilesOptions,\n): Promise<string[]> {\n const extensions = options?.include\n ? new Set(options.include.map(e => e.startsWith('.') ? e : `.${e}`))\n : SCAN_EXTENSIONS\n const extraExcludes = new Set(options?.exclude ?? [])\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n const files: string[] = []\n\n for (const dir of scanDirs) {\n const absDir = join(projectRoot, dir)\n if (!(await pathExists(absDir))) continue\n\n let entries: string[]\n try {\n entries = await readdir(absDir, { recursive: true }) as unknown as string[]\n } catch {\n continue\n }\n\n for (const entry of entries) {\n if (files.length >= MAX_SCAN_FILES) break\n\n const fileName = basename(entry)\n\n // Check if any path segment is excluded\n const pathSegments = entry.split('/')\n if (pathSegments.some(seg => SCAN_IGNORE_DIRS.has(seg) || extraExcludes.has(seg))) continue\n\n // Check extension\n if (!extensions.has(extname(fileName))) continue\n\n // Skip test/spec/declaration/minified files\n if (SKIP_FILE_RE.test(fileName)) continue\n\n files.push(join(dir, entry))\n }\n\n if (files.length >= MAX_SCAN_FILES) break\n }\n\n return files.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_SCAN_FILES)\n}\n"],"mappings":"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"} |
| import { o as readText } from "./fs-DLbVB-Ek.mjs"; | ||
| import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-BlNLRCMx.mjs"; | ||
| import { t as parseTsx } from "./tsx-parser-md1N0Niu.mjs"; | ||
| import { extname, join } from "node:path"; | ||
| //#region src/core/ast-scanner/pre-filter.ts | ||
| const PURE_NUMBER_RE = /^-?\d+(\.\d+)?$/; | ||
| const HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i; | ||
| const FILE_EXT_RE = /\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i; | ||
| const SVG_PATH_DATA_RE = /^[Mm][\d\s.,LHVCSQTAZlhvcsqtazmMzZ-]+$/; | ||
| const SVG_VIEWBOX_RE = /^\d+(\.\d+)?\s+\d+(\.\d+)?\s+\d+(\.\d+)?\s+\d+(\.\d+)?$/; | ||
| const I18N_KEY_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/; | ||
| const TECHNICAL_IDENTIFIER_RE = /^[_a-z][a-z0-9_-]*$/; | ||
| const ERROR_CODE_RE = /^[A-Z][A-Z0-9_]+$/; | ||
| const PLACEHOLDER_RE = /^\{\d+\}$|^\.{2,}$/; | ||
| const CAMEL_CASE_RE = /^[a-z]+[A-Z]/; | ||
| const LOCALE_CODE_RE = /^[a-z]{2}[-_][A-Z]{2}$/; | ||
| const DIMENSION_RE = /^\d+[x×]\d+$/; | ||
| const REPEAT_CHAR_RE = /^(.)\1{3,}$/; | ||
| const MIME_TYPE_RE = /^(application|text|image|audio|video|multipart|font)\/[\w.+-]+$/; | ||
| const PASCAL_CASE_RE = /^[A-Z][a-z]+[A-Z]/; | ||
| const HTML_TARGETS = new Set([ | ||
| "_blank", | ||
| "_self", | ||
| "_parent", | ||
| "_top" | ||
| ]); | ||
| function isURLLike(str) { | ||
| if (/^(https?|ftp|file|mailto|data):/.test(str)) return true; | ||
| if (/^(\.\.?\/|\/|[A-Za-z]:\\)/.test(str)) return true; | ||
| if (/^['"]?[@a-z][\w-]*/.test(str.toLowerCase()) && !str.includes(" ") && (str.includes("/") || str.includes("."))) return true; | ||
| return false; | ||
| } | ||
| const TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/; | ||
| function isCssClassList(value) { | ||
| const segments = value.trim().split(/\s+/); | ||
| if (segments.length < 2) return false; | ||
| let matched = 0; | ||
| for (const seg of segments) if (TAILWIND_SEGMENT_RE.test(seg)) matched++; | ||
| return matched / segments.length >= .5; | ||
| } | ||
| function isSingleCssUtility(value) { | ||
| const trimmed = value.trim(); | ||
| if (trimmed.includes(" ")) return false; | ||
| return TAILWIND_SEGMENT_RE.test(trimmed); | ||
| } | ||
| const SVG_TECHNICAL_ATTRIBUTES = new Set([ | ||
| "d", | ||
| "viewBox", | ||
| "points", | ||
| "transform", | ||
| "pathLength", | ||
| "xmlns", | ||
| "preserveAspectRatio", | ||
| "stroke-linecap", | ||
| "stroke-linejoin", | ||
| "stroke-width", | ||
| "stroke-dasharray", | ||
| "stroke-dashoffset", | ||
| "stroke-miterlimit", | ||
| "fill-rule", | ||
| "clip-rule" | ||
| ]); | ||
| const SVG_GRAPHIC_ELEMENTS = new Set([ | ||
| "svg", | ||
| "path", | ||
| "circle", | ||
| "rect", | ||
| "line", | ||
| "polyline", | ||
| "polygon", | ||
| "ellipse", | ||
| "g", | ||
| "defs", | ||
| "use", | ||
| "symbol", | ||
| "clipPath", | ||
| "mask", | ||
| "pattern", | ||
| "linearGradient", | ||
| "radialGradient", | ||
| "stop", | ||
| "marker", | ||
| "animate", | ||
| "animateTransform", | ||
| "image" | ||
| ]); | ||
| const I18N_FUNCTIONS = new Set([ | ||
| "t", | ||
| "$t", | ||
| "i18n", | ||
| "translate", | ||
| "formatMessage", | ||
| "msg" | ||
| ]); | ||
| const EMIT_FUNCTIONS = new Set(["emit", "$emit"]); | ||
| const TRANSLATABLE_ATTRIBUTES = new Set([ | ||
| "title", | ||
| "alt", | ||
| "placeholder", | ||
| "label", | ||
| "summary", | ||
| "caption", | ||
| "abbr", | ||
| "accesskey", | ||
| "content", | ||
| "description", | ||
| "aria-label", | ||
| "aria-description", | ||
| "aria-placeholder", | ||
| "aria-roledescription", | ||
| "aria-valuetext", | ||
| "accessibilityLabel", | ||
| "accessibilityHint", | ||
| "accessibilityValue", | ||
| "heading", | ||
| "subheading", | ||
| "message", | ||
| "hint", | ||
| "tooltip", | ||
| "helper-text", | ||
| "error-message", | ||
| "success-message", | ||
| "confirm-text", | ||
| "cancel-text", | ||
| "empty-text", | ||
| "loading-text", | ||
| "no-data-text", | ||
| "no-results-text" | ||
| ]); | ||
| const TRANSLATABLE_PROPERTIES = new Set([ | ||
| "label", | ||
| "title", | ||
| "description", | ||
| "text", | ||
| "message", | ||
| "placeholder", | ||
| "caption", | ||
| "summary", | ||
| "heading", | ||
| "subheading", | ||
| "subtitle", | ||
| "tooltip", | ||
| "hint", | ||
| "helpText", | ||
| "errorMessage", | ||
| "successMessage", | ||
| "name" | ||
| ]); | ||
| /** | ||
| * Determines if a string is definitely NOT user-visible content. | ||
| * Returns skip reason if it should be filtered, null if it should proceed to scoring. | ||
| * | ||
| * Conservative for template_text/jsx_text (tag-between text is almost always content). | ||
| * Aggressive for everything else (technical tokens, config values, framework artifacts). | ||
| */ | ||
| function shouldSkip(str) { | ||
| if (str.context === "import_path") return "import_path"; | ||
| if (str.context === "type_annotation") return "type_annotation"; | ||
| if (str.context === "css_class") return "css_class"; | ||
| if (str.context === "css_utility_call") return "css_utility_call"; | ||
| if (str.context === "console_call") return "console_call"; | ||
| if (str.context === "test_assertion") return "test_assertion"; | ||
| if (str.context === "switch_case") return "switch_case"; | ||
| const v = str.value; | ||
| if (v.length <= 1) return "single_char"; | ||
| if (/^\s+$/.test(v)) return "whitespace"; | ||
| if (PURE_NUMBER_RE.test(v)) return "pure_number"; | ||
| if (HEX_COLOR_RE.test(v)) return "hex_color"; | ||
| if (FILE_EXT_RE.test(v)) return "file_extension"; | ||
| if (v.startsWith("--")) return "cli_flag"; | ||
| if (I18N_KEY_RE.test(v)) return "i18n_key"; | ||
| if (MIME_TYPE_RE.test(v)) return "mime_type"; | ||
| if (isURLLike(v)) return "url_path"; | ||
| if (isCssClassList(v)) return "css_class_list"; | ||
| if (isSingleCssUtility(v)) return "css_utility_token"; | ||
| if (v.length > 3 && SVG_PATH_DATA_RE.test(v)) return "svg_path_data"; | ||
| if (SVG_VIEWBOX_RE.test(v)) return "svg_viewbox"; | ||
| if (str.parentProperty !== void 0 && SVG_TECHNICAL_ATTRIBUTES.has(str.parentProperty)) return "svg_technical_attr"; | ||
| if (str.context === "template_attribute" && SVG_GRAPHIC_ELEMENTS.has(str.parent)) return "svg_element_attr"; | ||
| if (v.startsWith("update:")) return "vue_emit_event"; | ||
| if (PLACEHOLDER_RE.test(v)) return "placeholder"; | ||
| if (LOCALE_CODE_RE.test(v)) return "locale_code"; | ||
| if (DIMENSION_RE.test(v)) return "dimension"; | ||
| if (REPEAT_CHAR_RE.test(v)) return "repeat_chars"; | ||
| if (HTML_TARGETS.has(v)) return "html_target"; | ||
| if (str.context === "function_argument" && I18N_FUNCTIONS.has(str.parent)) { | ||
| if (/^[a-z][a-z0-9_.-]*$/.test(v)) return "i18n_function_arg"; | ||
| } | ||
| if (str.context === "function_argument" && EMIT_FUNCTIONS.has(str.parent)) return "emit_event_arg"; | ||
| if (str.context !== "template_text" && str.context !== "jsx_text") { | ||
| if (TECHNICAL_IDENTIFIER_RE.test(v) && v.length < 30) return "technical_identifier"; | ||
| } | ||
| if (ERROR_CODE_RE.test(v) && v.includes("_") && v.length > 3) return "error_code"; | ||
| return null; | ||
| } | ||
| /** | ||
| * Calculates a content confidence score (0-1) for a string that passed shouldSkip. | ||
| * Uses AST context metadata (our advantage over offset-based tools) combined with | ||
| * value-based signals proven by i18next-cli. | ||
| * | ||
| * Base score: 0.5. Boosted/penalized by context and value characteristics. | ||
| */ | ||
| function calculateContentScore(str) { | ||
| let score = .5; | ||
| if (str.context === "template_text" || str.context === "jsx_text") score += .3; | ||
| if (str.context === "template_attribute" || str.context === "jsx_attribute") if (str.parentProperty && TRANSLATABLE_ATTRIBUTES.has(str.parentProperty)) score += .2; | ||
| else score -= .2; | ||
| if (str.context === "object_property") { | ||
| if (str.parentProperty && TRANSLATABLE_PROPERTIES.has(str.parentProperty)) score += .25; | ||
| } | ||
| const wordCount = str.value.split(/\s+/).length; | ||
| if (wordCount >= 3) score += .2; | ||
| else if (wordCount === 2) score += .1; | ||
| if (/[.!?:;]$/.test(str.value)) score += .1; | ||
| if (/[\u0080-\uFFFF]/.test(str.value)) score += .15; | ||
| if (/^[A-Z]/.test(str.value) && /[a-z]/.test(str.value)) score += .1; | ||
| if (CAMEL_CASE_RE.test(str.value)) score -= .3; | ||
| if (PASCAL_CASE_RE.test(str.value) && !str.value.includes(" ")) score -= .25; | ||
| if (/^[A-Z]{2,5}$/.test(str.value)) score -= .15; | ||
| if (str.value.includes("/") && !str.value.includes(" ")) score -= .2; | ||
| return Math.max(0, Math.min(1, score)); | ||
| } | ||
| /** | ||
| * Two-phase pre-filter: | ||
| * 1. shouldSkip(): Binary removal of definite non-content | ||
| * 2. calculateContentScore(): 0-1 confidence scoring for ambiguous strings | ||
| * | ||
| * Returns candidates that passed both phases, with content scores attached. | ||
| */ | ||
| function applyPreFilter(strings, minScore = .4) { | ||
| const candidates = []; | ||
| const skipReasons = {}; | ||
| let skipped = 0; | ||
| let lowConfidence = 0; | ||
| for (const str of strings) { | ||
| const skipReason = shouldSkip(str); | ||
| if (skipReason) { | ||
| skipped++; | ||
| skipReasons[skipReason] = (skipReasons[skipReason] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| const contentScore = calculateContentScore(str); | ||
| if (contentScore < minScore) { | ||
| lowConfidence++; | ||
| skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| str.contentScore = contentScore; | ||
| candidates.push(str); | ||
| } | ||
| return { | ||
| candidates, | ||
| skipped, | ||
| lowConfidence, | ||
| skipReasons | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/core/ast-scanner/index.ts | ||
| const TSX_EXTENSIONS = new Set([ | ||
| ".tsx", | ||
| ".jsx", | ||
| ".ts", | ||
| ".js", | ||
| ".mjs" | ||
| ]); | ||
| const VUE_EXTENSIONS = new Set([".vue"]); | ||
| const SVELTE_EXTENSIONS = new Set([".svelte"]); | ||
| const ASTRO_EXTENSIONS = new Set([".astro"]); | ||
| /** | ||
| * Lazily import vue-parser. Returns undefined if not available | ||
| * (@vue/compiler-sfc is an optional dependency). | ||
| */ | ||
| async function loadVueParser() { | ||
| try { | ||
| return (await import("./vue-parser-CHxDohd8.mjs")).parseVue; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| /** | ||
| * Lazily import svelte-parser. Returns undefined if not available | ||
| * (svelte is an optional dependency). | ||
| */ | ||
| async function loadSvelteParser() { | ||
| try { | ||
| return (await import("./svelte-parser-DaFK9iY0.mjs")).parseSvelte; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| /** | ||
| * Lazily import astro-parser. Returns undefined if not available | ||
| * (@astrojs/compiler is an optional dependency). | ||
| */ | ||
| async function loadAstroParser() { | ||
| try { | ||
| return (await import("./astro-parser-LNjEspnf.mjs")).parseAstro; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| const SURROUNDING_MAX = 120; | ||
| /** | ||
| * Minimal regex-based extractor for file types without AST parsers. | ||
| * Extracts quoted strings and tag text — conservative, low accuracy. | ||
| * Used as fallback when dedicated parsers are unavailable. | ||
| */ | ||
| function extractWithRegex(content, _filePath) { | ||
| const lines = content.split("\n"); | ||
| const results = []; | ||
| const stringRe = /(['"`])(?:(?!\1|\\).|\\.)*?\1/g; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| const trimmed = line.trim(); | ||
| if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue; | ||
| if (/^\s*(import|export)\s/.test(line)) continue; | ||
| let match; | ||
| stringRe.lastIndex = 0; | ||
| while ((match = stringRe.exec(line)) !== null) { | ||
| const value = match[0].slice(1, -1); | ||
| if (value.length === 0) continue; | ||
| const surrounding = buildSurrounding(lines, i); | ||
| results.push({ | ||
| value, | ||
| line: i + 1, | ||
| column: match.index + 1, | ||
| context: "other", | ||
| scope: "script", | ||
| parent: "", | ||
| surrounding | ||
| }); | ||
| } | ||
| const tagTextRe = />([^<>{]+)</g; | ||
| let tagMatch; | ||
| tagTextRe.lastIndex = 0; | ||
| while ((tagMatch = tagTextRe.exec(line)) !== null) { | ||
| const text = tagMatch[1].trim(); | ||
| if (text.length === 0) continue; | ||
| if (/^[\s\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue; | ||
| results.push({ | ||
| value: text, | ||
| line: i + 1, | ||
| column: tagMatch.index + 1, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: "", | ||
| surrounding: buildSurrounding(lines, i) | ||
| }); | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| function buildSurrounding(lines, lineIdx) { | ||
| const start = Math.max(0, lineIdx - 1); | ||
| const end = Math.min(lines.length - 1, lineIdx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const line = lines[i]; | ||
| if (line !== void 0) parts.push(line); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| /** | ||
| * Extract strings from a source file with structural context. | ||
| * | ||
| * Routes to the correct parser based on file extension: | ||
| * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based) | ||
| * - .vue -> vue-parser (lazy-loaded, AST-based) | ||
| * - .svelte -> svelte-parser (lazy-loaded, AST-based) | ||
| * - .astro -> astro-parser (lazy-loaded, AST-based) | ||
| * - unknown -> empty array | ||
| * | ||
| * Falls back to regex extraction when the dedicated parser's | ||
| * optional dependency is not installed. | ||
| * | ||
| * Applies structural pre-filter to remove 100% non-content strings. | ||
| * Returns only candidates that should be sent to the agent. | ||
| */ | ||
| async function extractStrings(filePath, content, ext) { | ||
| const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`; | ||
| let rawStrings; | ||
| if (TSX_EXTENSIONS.has(normalizedExt)) rawStrings = parseTsx(content, filePath); | ||
| else if (VUE_EXTENSIONS.has(normalizedExt)) { | ||
| const parseVue = await loadVueParser(); | ||
| if (parseVue) rawStrings = await parseVue(content, filePath); | ||
| else rawStrings = extractWithRegex(content, filePath); | ||
| } else if (SVELTE_EXTENSIONS.has(normalizedExt)) { | ||
| const parseSvelte = await loadSvelteParser(); | ||
| if (parseSvelte) try { | ||
| rawStrings = await parseSvelte(content, filePath); | ||
| } catch { | ||
| rawStrings = extractWithRegex(content, filePath); | ||
| } | ||
| else rawStrings = extractWithRegex(content, filePath); | ||
| } else if (ASTRO_EXTENSIONS.has(normalizedExt)) { | ||
| const parseAstro = await loadAstroParser(); | ||
| if (parseAstro) try { | ||
| rawStrings = await parseAstro(content, filePath); | ||
| } catch { | ||
| rawStrings = extractWithRegex(content, filePath); | ||
| } | ||
| else rawStrings = extractWithRegex(content, filePath); | ||
| } else return []; | ||
| const { candidates } = applyPreFilter(rawStrings); | ||
| return candidates; | ||
| } | ||
| //#endregion | ||
| //#region src/core/scanner.ts | ||
| const DEFAULT_LIMIT = 50; | ||
| const DEFAULT_OFFSET = 0; | ||
| const DEFAULT_MIN_LENGTH = 2; | ||
| const DEFAULT_MAX_LENGTH = 500; | ||
| const DEFAULT_MIN_SCORE = .4; | ||
| const SUMMARY_SAMPLE_SIZE = 10; | ||
| const CONTEXT_MAP = { | ||
| "template_text": "template_text", | ||
| "template_attribute": "template_attribute", | ||
| "jsx_text": "jsx_text", | ||
| "jsx_attribute": "jsx_attribute", | ||
| "variable_assignment": "variable_assignment", | ||
| "object_property": "object_value", | ||
| "function_argument": "function_argument", | ||
| "array_element": "other", | ||
| "enum_value": "other", | ||
| "template_literal": "other", | ||
| "switch_case": "other", | ||
| "other": "other", | ||
| "import_path": "other", | ||
| "type_annotation": "other", | ||
| "css_class": "other", | ||
| "css_utility_call": "other", | ||
| "console_call": "other", | ||
| "test_assertion": "other" | ||
| }; | ||
| async function scanCandidates(projectRoot, options) { | ||
| const limit = options?.limit ?? DEFAULT_LIMIT; | ||
| const offset = options?.offset ?? DEFAULT_OFFSET; | ||
| const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH; | ||
| const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH; | ||
| const minScore = options?.min_score ?? DEFAULT_MIN_SCORE; | ||
| const files = await discoverFiles(projectRoot, { | ||
| paths: options?.paths ?? await autoDetectSourceDirs(projectRoot), | ||
| include: options?.include, | ||
| exclude: options?.exclude | ||
| }); | ||
| const filePromises = files.map(async (relPath) => { | ||
| const filePath = join(projectRoot, relPath); | ||
| const content = await readText(filePath); | ||
| if (!content) return { | ||
| relPath, | ||
| extractions: [] | ||
| }; | ||
| return { | ||
| relPath, | ||
| extractions: await extractStrings(filePath, content, extname(filePath)) | ||
| }; | ||
| }); | ||
| const fileResults = await Promise.all(filePromises); | ||
| let rawStringsFound = 0; | ||
| let skippedCount = 0; | ||
| let lowConfidenceCount = 0; | ||
| const skipReasons = {}; | ||
| const uniqueMap = /* @__PURE__ */ new Map(); | ||
| const dupeMap = /* @__PURE__ */ new Map(); | ||
| for (const { relPath, extractions } of fileResults) { | ||
| rawStringsFound += extractions.length; | ||
| for (const extraction of extractions) { | ||
| if (extraction.value.length < minLength || extraction.value.length > maxLength) { | ||
| skippedCount++; | ||
| skipReasons["length_filter"] = (skipReasons["length_filter"] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| const contentScore = extraction.contentScore ?? calculateContentScore(extraction); | ||
| if (contentScore < minScore) { | ||
| lowConfidenceCount++; | ||
| skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| const mappedContext = CONTEXT_MAP[extraction.context]; | ||
| const loc = { | ||
| file: relPath, | ||
| line: extraction.line | ||
| }; | ||
| if (!dupeMap.has(extraction.value)) dupeMap.set(extraction.value, []); | ||
| dupeMap.get(extraction.value).push(loc); | ||
| if (!uniqueMap.has(extraction.value)) uniqueMap.set(extraction.value, { | ||
| candidate: { | ||
| file: relPath, | ||
| line: extraction.line, | ||
| column: extraction.column, | ||
| value: extraction.value, | ||
| context: mappedContext, | ||
| surrounding: extraction.surrounding, | ||
| contentScore, | ||
| occurrences: [loc] | ||
| }, | ||
| maxScore: contentScore | ||
| }); | ||
| else { | ||
| const entry = uniqueMap.get(extraction.value); | ||
| entry.candidate.occurrences.push(loc); | ||
| if (contentScore > entry.maxScore) { | ||
| entry.maxScore = contentScore; | ||
| entry.candidate.contentScore = contentScore; | ||
| entry.candidate.file = relPath; | ||
| entry.candidate.line = extraction.line; | ||
| entry.candidate.column = extraction.column; | ||
| entry.candidate.context = mappedContext; | ||
| entry.candidate.surrounding = extraction.surrounding; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| const allUniqueCandidates = [...uniqueMap.values()].map((e) => e.candidate).toSorted((a, b) => b.contentScore - a.contentScore); | ||
| const duplicates = [...dupeMap.entries()].filter(([, occurrences]) => occurrences.length >= 2).map(([value, occurrences]) => ({ | ||
| value, | ||
| count: occurrences.length, | ||
| occurrences | ||
| })).toSorted((a, b) => b.count - a.count); | ||
| const uniqueCount = allUniqueCandidates.length; | ||
| const paginated = allUniqueCandidates.slice(offset, offset + limit); | ||
| const hasMore = uniqueCount > offset + limit; | ||
| return { | ||
| candidates: paginated, | ||
| duplicates, | ||
| stats: { | ||
| files_scanned: files.length, | ||
| raw_strings_found: rawStringsFound, | ||
| skipped: skippedCount, | ||
| low_confidence: lowConfidenceCount, | ||
| unique_candidates: uniqueCount, | ||
| candidates_returned: paginated.length, | ||
| has_more: hasMore, | ||
| skip_reasons: skipReasons | ||
| } | ||
| }; | ||
| } | ||
| async function scanSummary(projectRoot, options) { | ||
| const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH; | ||
| const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH; | ||
| const minScore = options?.min_score ?? DEFAULT_MIN_SCORE; | ||
| const files = await discoverFiles(projectRoot, { | ||
| paths: options?.paths ?? await autoDetectSourceDirs(projectRoot), | ||
| include: options?.include, | ||
| exclude: options?.exclude | ||
| }); | ||
| const dirFiles = /* @__PURE__ */ new Map(); | ||
| const fileTypes = {}; | ||
| for (const relPath of files) { | ||
| const dir = relPath.split("/").slice(0, -1).join("/") || "."; | ||
| const ext = extname(relPath); | ||
| if (!dirFiles.has(dir)) dirFiles.set(dir, []); | ||
| dirFiles.get(dir).push(relPath); | ||
| fileTypes[ext] = (fileTypes[ext] ?? 0) + 1; | ||
| } | ||
| const byDirectory = {}; | ||
| const freqMap = /* @__PURE__ */ new Map(); | ||
| let totalCandidatesEstimate = 0; | ||
| for (const [dir, dirFileList] of dirFiles) { | ||
| const totalInDir = dirFileList.length; | ||
| const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE); | ||
| let sampleCandidates = 0; | ||
| const samplePromises = sampleFiles.map(async (relPath) => { | ||
| const filePath = join(projectRoot, relPath); | ||
| const content = await readText(filePath); | ||
| if (!content) return []; | ||
| return extractStrings(filePath, content, extname(filePath)); | ||
| }); | ||
| const sampleResults = await Promise.all(samplePromises); | ||
| for (const extractions of sampleResults) for (const extraction of extractions) { | ||
| if (extraction.value.length < minLength || extraction.value.length > maxLength) continue; | ||
| if ((extraction.contentScore ?? calculateContentScore(extraction)) < minScore) continue; | ||
| sampleCandidates++; | ||
| const prev = freqMap.get(extraction.value) ?? 0; | ||
| freqMap.set(extraction.value, prev + 1); | ||
| } | ||
| const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0; | ||
| const estimatedCandidates = Math.round(avgPerFile * totalInDir); | ||
| byDirectory[dir] = { | ||
| files: totalInDir, | ||
| candidates: estimatedCandidates | ||
| }; | ||
| totalCandidatesEstimate += estimatedCandidates; | ||
| } | ||
| const topRepeated = [...freqMap.entries()].filter(([, count]) => count >= 2).toSorted((a, b) => b[1] - a[1]).slice(0, 20).map(([value, count]) => ({ | ||
| value, | ||
| count | ||
| })); | ||
| return { | ||
| total_files: files.length, | ||
| total_candidates_estimate: totalCandidatesEstimate, | ||
| by_directory: byDirectory, | ||
| top_repeated: topRepeated, | ||
| sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`, | ||
| file_types: fileTypes | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { scanSummary as n, scanCandidates as t }; | ||
| //# sourceMappingURL=scanner-CGWhmpDz.mjs.map |
| {"version":3,"file":"scanner-CGWhmpDz.mjs","names":[],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by shouldSkip */\n skipped: number\n /** Total number of strings removed by low content score */\n lowConfidence: number\n /** Breakdown: skip reason → count */\n skipReasons: Record<string, number>\n}\n\n// ─── Value-based regexes ───\n\nconst PURE_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\nconst SVG_PATH_DATA_RE = /^[Mm][\\d\\s.,LHVCSQTAZlhvcsqtazmMzZ-]+$/\nconst SVG_VIEWBOX_RE = /^\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?$/\nconst I18N_KEY_RE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/\nconst TECHNICAL_IDENTIFIER_RE = /^[_a-z][a-z0-9_-]*$/\nconst ERROR_CODE_RE = /^[A-Z][A-Z0-9_]+$/\nconst PLACEHOLDER_RE = /^\\{\\d+\\}$|^\\.{2,}$/\nconst CAMEL_CASE_RE = /^[a-z]+[A-Z]/\nconst LOCALE_CODE_RE = /^[a-z]{2}[-_][A-Z]{2}$/\nconst DIMENSION_RE = /^\\d+[x×]\\d+$/\nconst REPEAT_CHAR_RE = /^(.)\\1{3,}$/\nconst MIME_TYPE_RE = /^(application|text|image|audio|video|multipart|font)\\/[\\w.+-]+$/\nconst PASCAL_CASE_RE = /^[A-Z][a-z]+[A-Z]/\n\nconst HTML_TARGETS = new Set(['_blank', '_self', '_parent', '_top'])\n\n// ─── URL / path detection (consolidated from legacy isNonContent) ───\n\nfunction isURLLike(str: string): boolean {\n if (/^(https?|ftp|file|mailto|data):/.test(str)) return true\n if (/^(\\.\\.?\\/|\\/|[A-Za-z]:\\\\)/.test(str)) return true\n if (/^['\"]?[@a-z][\\w-]*/.test(str.toLowerCase()) && !str.includes(' ') && (str.includes('/') || str.includes('.'))) {\n return true\n }\n return false\n}\n\n// ─── CSS / Tailwind detection ───\n\nconst TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/\n\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── SVG technical attributes ───\n\nconst SVG_TECHNICAL_ATTRIBUTES = new Set([\n 'd', 'viewBox', 'points', 'transform', 'pathLength',\n 'xmlns', 'preserveAspectRatio',\n 'stroke-linecap', 'stroke-linejoin', 'stroke-width',\n 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit',\n 'fill-rule', 'clip-rule',\n])\n\nconst SVG_GRAPHIC_ELEMENTS = new Set([\n 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon',\n 'ellipse', 'g', 'defs', 'use', 'symbol', 'clipPath', 'mask',\n 'pattern', 'linearGradient', 'radialGradient', 'stop',\n 'marker', 'animate', 'animateTransform', 'image',\n])\n\n// ─── Known function names ───\n\nconst I18N_FUNCTIONS = new Set([\n 't', '$t', 'i18n', 'translate', 'formatMessage', 'msg',\n])\n\nconst EMIT_FUNCTIONS = new Set([\n 'emit', '$emit',\n])\n\n// ─── Translatable attribute whitelist (i18next-cli compatible + extended) ───\n\nconst TRANSLATABLE_ATTRIBUTES = new Set([\n // Standard HTML content attributes\n 'title', 'alt', 'placeholder', 'label', 'summary', 'caption',\n 'abbr', 'accesskey', 'content', 'description',\n // ARIA content\n 'aria-label', 'aria-description', 'aria-placeholder',\n 'aria-roledescription', 'aria-valuetext',\n // React Native accessibility (equivalent to aria-label)\n 'accessibilityLabel', 'accessibilityHint', 'accessibilityValue',\n // Common component content props\n 'heading', 'subheading', 'message', 'hint', 'tooltip',\n 'helper-text', 'error-message', 'success-message',\n 'confirm-text', 'cancel-text', 'empty-text', 'loading-text',\n 'no-data-text', 'no-results-text',\n])\n\n// ─── Translatable object property whitelist (i18next-cli compatible) ───\n\nconst TRANSLATABLE_PROPERTIES = new Set([\n 'label', 'title', 'description', 'text', 'message', 'placeholder',\n 'caption', 'summary', 'heading', 'subheading', 'subtitle', 'tooltip',\n 'hint', 'helpText', 'errorMessage', 'successMessage', 'name',\n])\n\n// ─── shouldSkip: Binary non-content detection ───\n\n/**\n * Determines if a string is definitely NOT user-visible content.\n * Returns skip reason if it should be filtered, null if it should proceed to scoring.\n *\n * Conservative for template_text/jsx_text (tag-between text is almost always content).\n * Aggressive for everything else (technical tokens, config values, framework artifacts).\n */\nexport function shouldSkip(str: ExtractedString): string | null {\n // ── Context-based rules (AST-determined, 100% accurate) ──\n\n if (str.context === 'import_path') return 'import_path'\n if (str.context === 'type_annotation') return 'type_annotation'\n if (str.context === 'css_class') return 'css_class'\n if (str.context === 'css_utility_call') return 'css_utility_call'\n if (str.context === 'console_call') return 'console_call'\n if (str.context === 'test_assertion') return 'test_assertion'\n if (str.context === 'switch_case') return 'switch_case'\n\n const v = str.value\n\n // ── Value-based rules (structural patterns) ──\n\n if (v.length <= 1) return 'single_char'\n if (/^\\s+$/.test(v)) return 'whitespace'\n if (PURE_NUMBER_RE.test(v)) return 'pure_number'\n if (HEX_COLOR_RE.test(v)) return 'hex_color'\n if (FILE_EXT_RE.test(v)) return 'file_extension'\n if (v.startsWith('--')) return 'cli_flag'\n\n // ── i18n key paths (checked before URL — both contain dots, but i18n keys are more specific) ──\n\n if (I18N_KEY_RE.test(v)) return 'i18n_key'\n\n // ── MIME types (checked before URL — both contain slash, MIME is more specific) ──\n\n if (MIME_TYPE_RE.test(v)) return 'mime_type'\n\n // ── URL/path patterns ──\n\n if (isURLLike(v)) return 'url_path'\n\n // ── CSS patterns ──\n\n if (isCssClassList(v)) return 'css_class_list'\n if (isSingleCssUtility(v)) return 'css_utility_token'\n\n // ── SVG patterns ──\n\n if (v.length > 3 && SVG_PATH_DATA_RE.test(v)) return 'svg_path_data'\n if (SVG_VIEWBOX_RE.test(v)) return 'svg_viewbox'\n if (str.parentProperty !== undefined && SVG_TECHNICAL_ATTRIBUTES.has(str.parentProperty)) return 'svg_technical_attr'\n if (str.context === 'template_attribute' && SVG_GRAPHIC_ELEMENTS.has(str.parent)) return 'svg_element_attr'\n\n // ── Framework event patterns ──\n\n if (v.startsWith('update:')) return 'vue_emit_event'\n\n // ── Placeholder / interpolation ──\n\n if (PLACEHOLDER_RE.test(v)) return 'placeholder'\n\n // ── Structural value patterns (100% non-content) ──\n\n if (LOCALE_CODE_RE.test(v)) return 'locale_code'\n if (DIMENSION_RE.test(v)) return 'dimension'\n if (REPEAT_CHAR_RE.test(v)) return 'repeat_chars'\n if (HTML_TARGETS.has(v)) return 'html_target'\n\n // ── Known function argument detection ──\n\n if (str.context === 'function_argument' && I18N_FUNCTIONS.has(str.parent)) {\n // i18n function args: filter lowercase identifiers (namespace/key), keep sentences\n if (/^[a-z][a-z0-9_.-]*$/.test(v)) return 'i18n_function_arg'\n }\n\n if (str.context === 'function_argument' && EMIT_FUNCTIONS.has(str.parent)) {\n return 'emit_event_arg'\n }\n\n // ── CRITICAL: Technical identifier detection (i18next-cli proven pattern) ──\n // Single lowercase ASCII word/kebab-case/snake_case < 30 chars → technical token\n // EXEMPT: template_text and jsx_text (tag-between text IS content, even lowercase)\n\n if (str.context !== 'template_text' && str.context !== 'jsx_text') {\n if (TECHNICAL_IDENTIFIER_RE.test(v) && v.length < 30) {\n return 'technical_identifier'\n }\n }\n\n // ── Error codes (SCREAMING_SNAKE_CASE with underscores) ──\n\n if (ERROR_CODE_RE.test(v) && v.includes('_') && v.length > 3) {\n return 'error_code'\n }\n\n return null\n}\n\n// ─── calculateContentScore: 0-1 confidence scoring ───\n\n/**\n * Calculates a content confidence score (0-1) for a string that passed shouldSkip.\n * Uses AST context metadata (our advantage over offset-based tools) combined with\n * value-based signals proven by i18next-cli.\n *\n * Base score: 0.5. Boosted/penalized by context and value characteristics.\n */\nexport function calculateContentScore(str: ExtractedString): number {\n let score = 0.5\n\n // ── Context signals (AST metadata advantage) ──\n\n // Template/JSX text = almost certainly user-visible content\n if (str.context === 'template_text' || str.context === 'jsx_text') {\n score += 0.3\n }\n\n // Content-bearing attribute (title, alt, placeholder, aria-label, etc.)\n if (str.context === 'template_attribute' || str.context === 'jsx_attribute') {\n if (str.parentProperty && TRANSLATABLE_ATTRIBUTES.has(str.parentProperty)) {\n score += 0.2\n } else {\n score -= 0.2 // Unknown/technical attribute\n }\n }\n\n // Content-bearing object property (message, label, description, etc.)\n if (str.context === 'object_property') {\n if (str.parentProperty && TRANSLATABLE_PROPERTIES.has(str.parentProperty)) {\n score += 0.25\n }\n }\n\n // ── Value signals (i18next-cli proven heuristics) ──\n\n // Multi-word strings are more likely content\n const wordCount = str.value.split(/\\s+/).length\n if (wordCount >= 3) score += 0.2\n else if (wordCount === 2) score += 0.1\n\n // Terminal punctuation suggests a sentence\n if (/[.!?:;]$/.test(str.value)) score += 0.1\n\n // Non-ASCII characters (Turkish, Chinese, Arabic, etc.) → almost certainly content\n if (/[\\u0080-\\uFFFF]/.test(str.value)) score += 0.15\n\n // Capitalized first letter with lowercase body (Dashboard, Kaydet, Settings)\n if (/^[A-Z]/.test(str.value) && /[a-z]/.test(str.value)) score += 0.1\n\n // camelCase → probably a technical identifier\n if (CAMEL_CASE_RE.test(str.value)) score -= 0.3\n\n // PascalCase with internal uppercase (PhGameController, GameCard) → likely component/icon name\n // Does NOT match single-uppercase words (Dashboard, Karadeniz, Settings)\n if (PASCAL_CASE_RE.test(str.value) && !str.value.includes(' ')) score -= 0.25\n\n // Short ALL-CAPS (TRY, GET, USD) → likely code/abbreviation, not content\n // In template_text the +0.3 context boost keeps real labels like \"FAQ\" above threshold\n if (/^[A-Z]{2,5}$/.test(str.value)) score -= 0.15\n\n // Contains slash without spaces → path-like\n if (str.value.includes('/') && !str.value.includes(' ')) score -= 0.2\n\n return Math.max(0, Math.min(1, score))\n}\n\n// ─── Public API ───\n\n/**\n * Two-phase pre-filter:\n * 1. shouldSkip(): Binary removal of definite non-content\n * 2. calculateContentScore(): 0-1 confidence scoring for ambiguous strings\n *\n * Returns candidates that passed both phases, with content scores attached.\n */\nexport function applyPreFilter(\n strings: ExtractedString[],\n minScore: number = 0.4,\n): PreFilterResult {\n const candidates: ExtractedString[] = []\n const skipReasons: Record<string, number> = {}\n let skipped = 0\n let lowConfidence = 0\n\n for (const str of strings) {\n // Phase 1: Binary skip\n const skipReason = shouldSkip(str)\n if (skipReason) {\n skipped++\n skipReasons[skipReason] = (skipReasons[skipReason] ?? 0) + 1\n continue\n }\n\n // Phase 2: Content scoring\n const contentScore = calculateContentScore(str)\n if (contentScore < minScore) {\n lowConfidence++\n skipReasons['low_confidence'] = (skipReasons['low_confidence'] ?? 0) + 1\n continue\n }\n\n // Attach score to the extraction for downstream use\n ;(str as ExtractedString & { contentScore: number }).contentScore = contentScore\n candidates.push(str)\n }\n\n return { candidates, skipped, lowConfidence, skipReasons }\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter, shouldSkip, calculateContentScore } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport { calculateContentScore } from './ast-scanner/pre-filter.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n /** Minimum content confidence score (0-1). Default: 0.4 */\n min_score?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst DEFAULT_MIN_SCORE = 0.4\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n const minScore = options?.min_score ?? DEFAULT_MIN_SCORE\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // ─── Phase 1: Extract strings from all files (pre-filter applied inside extractStrings) ───\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n // ─── Phase 2: Score + length filter + deduplicate ───\n\n let rawStringsFound = 0\n let skippedCount = 0\n let lowConfidenceCount = 0\n const skipReasons: Record<string, number> = {}\n\n // Deduplication map: value → first candidate + all occurrences\n const uniqueMap = new Map<string, {\n candidate: ScanCandidate\n maxScore: number\n }>()\n\n // Also track duplicates for backward compatibility\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Length filter\n if (extraction.value.length < minLength || extraction.value.length > maxLength) {\n skippedCount++\n skipReasons['length_filter'] = (skipReasons['length_filter'] ?? 0) + 1\n continue\n }\n\n // shouldSkip was already applied inside extractStrings (via applyPreFilter).\n // But applyPreFilter uses default minScore. Here we apply the user-configured minScore\n // on the contentScore that was attached during pre-filtering.\n\n // Get the contentScore attached by applyPreFilter\n const contentScore: number = (extraction as unknown as { contentScore?: number }).contentScore ?? calculateContentScore(extraction)\n\n if (contentScore < minScore) {\n lowConfidenceCount++\n skipReasons['low_confidence'] = (skipReasons['low_confidence'] ?? 0) + 1\n continue\n }\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n const loc = { file: relPath, line: extraction.line }\n\n // Track all occurrences for duplicates section\n if (!dupeMap.has(extraction.value)) {\n dupeMap.set(extraction.value, [])\n }\n dupeMap.get(extraction.value)!.push(loc)\n\n // Deduplication: keep first occurrence, accumulate locations\n if (!uniqueMap.has(extraction.value)) {\n uniqueMap.set(extraction.value, {\n candidate: {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n contentScore,\n occurrences: [loc],\n },\n maxScore: contentScore,\n })\n } else {\n const entry = uniqueMap.get(extraction.value)!\n entry.candidate.occurrences.push(loc)\n // Keep the highest score across occurrences\n if (contentScore > entry.maxScore) {\n entry.maxScore = contentScore\n entry.candidate.contentScore = contentScore\n entry.candidate.file = relPath\n entry.candidate.line = extraction.line\n entry.candidate.column = extraction.column\n entry.candidate.context = mappedContext\n entry.candidate.surrounding = extraction.surrounding\n }\n }\n }\n }\n\n // Build sorted unique candidates (highest score first)\n const allUniqueCandidates = [...uniqueMap.values()]\n .map(e => e.candidate)\n .toSorted((a, b) => b.contentScore - a.contentScore)\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination on unique candidates\n const uniqueCount = allUniqueCandidates.length\n const paginated = allUniqueCandidates.slice(offset, offset + limit)\n const hasMore = uniqueCount > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n skipped: skippedCount,\n low_confidence: lowConfidenceCount,\n unique_candidates: uniqueCount,\n candidates_returned: paginated.length,\n has_more: hasMore,\n skip_reasons: skipReasons,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n const minScore = options?.min_score ?? DEFAULT_MIN_SCORE\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n // Apply same filters as scanCandidates\n if (extraction.value.length < minLength || extraction.value.length > maxLength) continue\n\n const contentScore: number = (extraction as unknown as { contentScore?: number }).contentScore ?? calculateContentScore(extraction)\n if (contentScore < minScore) continue\n\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"} |
| import { g as RepoProvider } from "./index-w8QHThNS.mjs"; | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| //#region src/server.d.ts | ||
| /** | ||
| * The provider shape tool handlers consume. Now that every provider | ||
| * (Local, GitHub, GitLab) implements the full `RepoProvider`, tools can | ||
| * depend on the shared surface directly — no private alias required. | ||
| * Kept as a re-export so callers that already import `ToolProvider` do | ||
| * not need to migrate. | ||
| */ | ||
| type ToolProvider = RepoProvider; | ||
| /** | ||
| * Default MCP `instructions` surfaced to clients at initialize time. | ||
| * Deliberately kept under 512 characters — directory listings and client | ||
| * UIs truncate longer strings. Override via `CreateServerOptions.instructions`. | ||
| */ | ||
| declare const DEFAULT_INSTRUCTIONS: string; | ||
| interface CreateServerOptions { | ||
| /** | ||
| * Content provider — drives reads (and, in later phases, writes) through | ||
| * a reader surface. Required when `projectRoot` is omitted. Accepts the | ||
| * narrow `ToolProvider` shape so either `LocalProvider` or | ||
| * `GitHubProvider` satisfies the contract. | ||
| */ | ||
| provider?: ToolProvider; | ||
| /** | ||
| * Local project root. When the provider is a `LocalProvider`, its own | ||
| * `projectRoot` is used as the fallback. Tools that require local disk | ||
| * (normalize, setup, git submit/merge) are not registered when no | ||
| * projectRoot is available. | ||
| */ | ||
| projectRoot?: string; | ||
| /** | ||
| * MCP `instructions` string sent to clients in the `initialize` response. | ||
| * Defaults to `DEFAULT_INSTRUCTIONS`; pass an empty string to omit | ||
| * instructions entirely. | ||
| */ | ||
| instructions?: string; | ||
| } | ||
| /** | ||
| * Create an MCP server instance with every *available* Contentrain tool | ||
| * registered. | ||
| * | ||
| * Two signatures: | ||
| * | ||
| * - `createServer('/path/to/project')` — legacy stdio flow. A `LocalProvider` | ||
| * is constructed under the hood; every tool keeps behaving exactly as it | ||
| * did before phase 5.3. | ||
| * - `createServer({ provider, projectRoot? })` — phase 5.3 flow. Any | ||
| * `RepoProvider` (including `GitHubProvider`) drives reads and writes. If | ||
| * the provider is a `LocalProvider` and `projectRoot` is omitted, the | ||
| * provider's own `projectRoot` is used. | ||
| * | ||
| * Tool listing is capability-aware: tools whose requirements | ||
| * (`TOOL_REQUIREMENTS`) cannot be met by the resolved provider + | ||
| * projectRoot pair are not registered, so `tools/list` only advertises | ||
| * tools that can actually succeed. With a `LocalProvider` (stdio and CLI | ||
| * flows) all 19 tools remain registered — behavior there is unchanged. | ||
| */ | ||
| declare function createServer(projectRoot: string): McpServer; | ||
| declare function createServer(opts: CreateServerOptions): McpServer; | ||
| //#endregion | ||
| export { createServer as i, DEFAULT_INSTRUCTIONS as n, ToolProvider as r, CreateServerOptions as t }; | ||
| //# sourceMappingURL=server-DLauBiu2.d.mts.map |
| {"version":3,"file":"server-DLauBiu2.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;AAYA;;;;;KAAY,YAAA,GAAe,YAAA;;;;;AA2B3B;cAVa,oBAAA;AAAA,UAUI,mBAAA;EAOQ;;;;;;EAAvB,QAAA,GAAW,YAAA;EAiEG;;;;;AAChB;EA3DE,WAAA;;;;;;EAMA,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBAoDc,YAAA,CAAa,WAAA,WAAsB,SAAA;AAAA,iBACnC,YAAA,CAAa,IAAA,EAAM,mBAAA,GAAsB,SAAA"} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { t as __commonJSMin } from "./chunk-BEJ448es.mjs"; | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64.js | ||
| var require_base64 = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var intToCharMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split(""); | ||
| /** | ||
| * Encode an integer in the range of 0 to 63 to a single base 64 digit. | ||
| */ | ||
| exports.encode = function(number) { | ||
| if (0 <= number && number < intToCharMap.length) return intToCharMap[number]; | ||
| throw new TypeError("Must be between 0 and 63: " + number); | ||
| }; | ||
| /** | ||
| * Decode a single base 64 character code digit to an integer. Returns -1 on | ||
| * failure. | ||
| */ | ||
| exports.decode = function(charCode) { | ||
| var bigA = 65; | ||
| var bigZ = 90; | ||
| var littleA = 97; | ||
| var littleZ = 122; | ||
| var zero = 48; | ||
| var nine = 57; | ||
| var plus = 43; | ||
| var slash = 47; | ||
| var littleOffset = 26; | ||
| var numberOffset = 52; | ||
| if (bigA <= charCode && charCode <= bigZ) return charCode - bigA; | ||
| if (littleA <= charCode && charCode <= littleZ) return charCode - littleA + littleOffset; | ||
| if (zero <= charCode && charCode <= nine) return charCode - zero + numberOffset; | ||
| if (charCode == plus) return 62; | ||
| if (charCode == slash) return 63; | ||
| return -1; | ||
| }; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64-vlq.js | ||
| var require_base64_vlq = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var base64 = require_base64(); | ||
| var VLQ_BASE_SHIFT = 5; | ||
| var VLQ_BASE = 1 << VLQ_BASE_SHIFT; | ||
| var VLQ_BASE_MASK = VLQ_BASE - 1; | ||
| var VLQ_CONTINUATION_BIT = VLQ_BASE; | ||
| /** | ||
| * Converts from a two-complement value to a value where the sign bit is | ||
| * placed in the least significant bit. For example, as decimals: | ||
| * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) | ||
| * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) | ||
| */ | ||
| function toVLQSigned(aValue) { | ||
| return aValue < 0 ? (-aValue << 1) + 1 : (aValue << 1) + 0; | ||
| } | ||
| /** | ||
| * Converts to a two-complement value from a value where the sign bit is | ||
| * placed in the least significant bit. For example, as decimals: | ||
| * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 | ||
| * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 | ||
| */ | ||
| function fromVLQSigned(aValue) { | ||
| var isNegative = (aValue & 1) === 1; | ||
| var shifted = aValue >> 1; | ||
| return isNegative ? -shifted : shifted; | ||
| } | ||
| /** | ||
| * Returns the base 64 VLQ encoded value. | ||
| */ | ||
| exports.encode = function base64VLQ_encode(aValue) { | ||
| var encoded = ""; | ||
| var digit; | ||
| var vlq = toVLQSigned(aValue); | ||
| do { | ||
| digit = vlq & VLQ_BASE_MASK; | ||
| vlq >>>= VLQ_BASE_SHIFT; | ||
| if (vlq > 0) digit |= VLQ_CONTINUATION_BIT; | ||
| encoded += base64.encode(digit); | ||
| } while (vlq > 0); | ||
| return encoded; | ||
| }; | ||
| /** | ||
| * Decodes the next base 64 VLQ value from the given string and returns the | ||
| * value and the rest of the string via the out parameter. | ||
| */ | ||
| exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { | ||
| var strLen = aStr.length; | ||
| var result = 0; | ||
| var shift = 0; | ||
| var continuation, digit; | ||
| do { | ||
| if (aIndex >= strLen) throw new Error("Expected more digits in base 64 VLQ value."); | ||
| digit = base64.decode(aStr.charCodeAt(aIndex++)); | ||
| if (digit === -1) throw new Error("Invalid base64 digit: " + aStr.charAt(aIndex - 1)); | ||
| continuation = !!(digit & VLQ_CONTINUATION_BIT); | ||
| digit &= VLQ_BASE_MASK; | ||
| result = result + (digit << shift); | ||
| shift += VLQ_BASE_SHIFT; | ||
| } while (continuation); | ||
| aOutParam.value = fromVLQSigned(result); | ||
| aOutParam.rest = aIndex; | ||
| }; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/util.js | ||
| var require_util = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| /** | ||
| * This is a helper function for getting values from parameter/options | ||
| * objects. | ||
| * | ||
| * @param args The object we are extracting values from | ||
| * @param name The name of the property we are getting. | ||
| * @param defaultValue An optional value to return if the property is missing | ||
| * from the object. If this is not specified and the property is missing, an | ||
| * error will be thrown. | ||
| */ | ||
| function getArg(aArgs, aName, aDefaultValue) { | ||
| if (aName in aArgs) return aArgs[aName]; | ||
| else if (arguments.length === 3) return aDefaultValue; | ||
| else throw new Error("\"" + aName + "\" is a required argument."); | ||
| } | ||
| exports.getArg = getArg; | ||
| var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/; | ||
| var dataUrlRegexp = /^data:.+\,.+$/; | ||
| function urlParse(aUrl) { | ||
| var match = aUrl.match(urlRegexp); | ||
| if (!match) return null; | ||
| return { | ||
| scheme: match[1], | ||
| auth: match[2], | ||
| host: match[3], | ||
| port: match[4], | ||
| path: match[5] | ||
| }; | ||
| } | ||
| exports.urlParse = urlParse; | ||
| function urlGenerate(aParsedUrl) { | ||
| var url = ""; | ||
| if (aParsedUrl.scheme) url += aParsedUrl.scheme + ":"; | ||
| url += "//"; | ||
| if (aParsedUrl.auth) url += aParsedUrl.auth + "@"; | ||
| if (aParsedUrl.host) url += aParsedUrl.host; | ||
| if (aParsedUrl.port) url += ":" + aParsedUrl.port; | ||
| if (aParsedUrl.path) url += aParsedUrl.path; | ||
| return url; | ||
| } | ||
| exports.urlGenerate = urlGenerate; | ||
| var MAX_CACHED_INPUTS = 32; | ||
| /** | ||
| * Takes some function `f(input) -> result` and returns a memoized version of | ||
| * `f`. | ||
| * | ||
| * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The | ||
| * memoization is a dumb-simple, linear least-recently-used cache. | ||
| */ | ||
| function lruMemoize(f) { | ||
| var cache = []; | ||
| return function(input) { | ||
| for (var i = 0; i < cache.length; i++) if (cache[i].input === input) { | ||
| var temp = cache[0]; | ||
| cache[0] = cache[i]; | ||
| cache[i] = temp; | ||
| return cache[0].result; | ||
| } | ||
| var result = f(input); | ||
| cache.unshift({ | ||
| input, | ||
| result | ||
| }); | ||
| if (cache.length > MAX_CACHED_INPUTS) cache.pop(); | ||
| return result; | ||
| }; | ||
| } | ||
| /** | ||
| * Normalizes a path, or the path portion of a URL: | ||
| * | ||
| * - Replaces consecutive slashes with one slash. | ||
| * - Removes unnecessary '.' parts. | ||
| * - Removes unnecessary '<dir>/..' parts. | ||
| * | ||
| * Based on code in the Node.js 'path' core module. | ||
| * | ||
| * @param aPath The path or url to normalize. | ||
| */ | ||
| var normalize = lruMemoize(function normalize(aPath) { | ||
| var path = aPath; | ||
| var url = urlParse(aPath); | ||
| if (url) { | ||
| if (!url.path) return aPath; | ||
| path = url.path; | ||
| } | ||
| var isAbsolute = exports.isAbsolute(path); | ||
| var parts = []; | ||
| var start = 0; | ||
| var i = 0; | ||
| while (true) { | ||
| start = i; | ||
| i = path.indexOf("/", start); | ||
| if (i === -1) { | ||
| parts.push(path.slice(start)); | ||
| break; | ||
| } else { | ||
| parts.push(path.slice(start, i)); | ||
| while (i < path.length && path[i] === "/") i++; | ||
| } | ||
| } | ||
| for (var part, up = 0, i = parts.length - 1; i >= 0; i--) { | ||
| part = parts[i]; | ||
| if (part === ".") parts.splice(i, 1); | ||
| else if (part === "..") up++; | ||
| else if (up > 0) if (part === "") { | ||
| parts.splice(i + 1, up); | ||
| up = 0; | ||
| } else { | ||
| parts.splice(i, 2); | ||
| up--; | ||
| } | ||
| } | ||
| path = parts.join("/"); | ||
| if (path === "") path = isAbsolute ? "/" : "."; | ||
| if (url) { | ||
| url.path = path; | ||
| return urlGenerate(url); | ||
| } | ||
| return path; | ||
| }); | ||
| exports.normalize = normalize; | ||
| /** | ||
| * Joins two paths/URLs. | ||
| * | ||
| * @param aRoot The root path or URL. | ||
| * @param aPath The path or URL to be joined with the root. | ||
| * | ||
| * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a | ||
| * scheme-relative URL: Then the scheme of aRoot, if any, is prepended | ||
| * first. | ||
| * - Otherwise aPath is a path. If aRoot is a URL, then its path portion | ||
| * is updated with the result and aRoot is returned. Otherwise the result | ||
| * is returned. | ||
| * - If aPath is absolute, the result is aPath. | ||
| * - Otherwise the two paths are joined with a slash. | ||
| * - Joining for example 'http://' and 'www.example.com' is also supported. | ||
| */ | ||
| function join(aRoot, aPath) { | ||
| if (aRoot === "") aRoot = "."; | ||
| if (aPath === "") aPath = "."; | ||
| var aPathUrl = urlParse(aPath); | ||
| var aRootUrl = urlParse(aRoot); | ||
| if (aRootUrl) aRoot = aRootUrl.path || "/"; | ||
| if (aPathUrl && !aPathUrl.scheme) { | ||
| if (aRootUrl) aPathUrl.scheme = aRootUrl.scheme; | ||
| return urlGenerate(aPathUrl); | ||
| } | ||
| if (aPathUrl || aPath.match(dataUrlRegexp)) return aPath; | ||
| if (aRootUrl && !aRootUrl.host && !aRootUrl.path) { | ||
| aRootUrl.host = aPath; | ||
| return urlGenerate(aRootUrl); | ||
| } | ||
| var joined = aPath.charAt(0) === "/" ? aPath : normalize(aRoot.replace(/\/+$/, "") + "/" + aPath); | ||
| if (aRootUrl) { | ||
| aRootUrl.path = joined; | ||
| return urlGenerate(aRootUrl); | ||
| } | ||
| return joined; | ||
| } | ||
| exports.join = join; | ||
| exports.isAbsolute = function(aPath) { | ||
| return aPath.charAt(0) === "/" || urlRegexp.test(aPath); | ||
| }; | ||
| /** | ||
| * Make a path relative to a URL or another path. | ||
| * | ||
| * @param aRoot The root path or URL. | ||
| * @param aPath The path or URL to be made relative to aRoot. | ||
| */ | ||
| function relative(aRoot, aPath) { | ||
| if (aRoot === "") aRoot = "."; | ||
| aRoot = aRoot.replace(/\/$/, ""); | ||
| var level = 0; | ||
| while (aPath.indexOf(aRoot + "/") !== 0) { | ||
| var index = aRoot.lastIndexOf("/"); | ||
| if (index < 0) return aPath; | ||
| aRoot = aRoot.slice(0, index); | ||
| if (aRoot.match(/^([^\/]+:\/)?\/*$/)) return aPath; | ||
| ++level; | ||
| } | ||
| return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1); | ||
| } | ||
| exports.relative = relative; | ||
| var supportsNullProto = function() { | ||
| return !("__proto__" in Object.create(null)); | ||
| }(); | ||
| function identity(s) { | ||
| return s; | ||
| } | ||
| /** | ||
| * Because behavior goes wacky when you set `__proto__` on objects, we | ||
| * have to prefix all the strings in our set with an arbitrary character. | ||
| * | ||
| * See https://github.com/mozilla/source-map/pull/31 and | ||
| * https://github.com/mozilla/source-map/issues/30 | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| function toSetString(aStr) { | ||
| if (isProtoString(aStr)) return "$" + aStr; | ||
| return aStr; | ||
| } | ||
| exports.toSetString = supportsNullProto ? identity : toSetString; | ||
| function fromSetString(aStr) { | ||
| if (isProtoString(aStr)) return aStr.slice(1); | ||
| return aStr; | ||
| } | ||
| exports.fromSetString = supportsNullProto ? identity : fromSetString; | ||
| function isProtoString(s) { | ||
| if (!s) return false; | ||
| var length = s.length; | ||
| if (length < 9) return false; | ||
| if (s.charCodeAt(length - 1) !== 95 || s.charCodeAt(length - 2) !== 95 || s.charCodeAt(length - 3) !== 111 || s.charCodeAt(length - 4) !== 116 || s.charCodeAt(length - 5) !== 111 || s.charCodeAt(length - 6) !== 114 || s.charCodeAt(length - 7) !== 112 || s.charCodeAt(length - 8) !== 95 || s.charCodeAt(length - 9) !== 95) return false; | ||
| for (var i = length - 10; i >= 0; i--) if (s.charCodeAt(i) !== 36) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Comparator between two mappings where the original positions are compared. | ||
| * | ||
| * Optionally pass in `true` as `onlyCompareGenerated` to consider two | ||
| * mappings with the same original source/line/column, but different generated | ||
| * line and column the same. Useful when searching for a mapping with a | ||
| * stubbed out mapping. | ||
| */ | ||
| function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) { | ||
| var cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0 || onlyCompareOriginal) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByOriginalPositions = compareByOriginalPositions; | ||
| function compareByOriginalPositionsNoSource(mappingA, mappingB, onlyCompareOriginal) { | ||
| var cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0 || onlyCompareOriginal) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByOriginalPositionsNoSource = compareByOriginalPositionsNoSource; | ||
| /** | ||
| * Comparator between two mappings with deflated source and name indices where | ||
| * the generated positions are compared. | ||
| * | ||
| * Optionally pass in `true` as `onlyCompareGenerated` to consider two | ||
| * mappings with the same generated line and column, but different | ||
| * source/name/original line and column the same. Useful when searching for a | ||
| * mapping with a stubbed out mapping. | ||
| */ | ||
| function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) { | ||
| var cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0 || onlyCompareGenerated) return cmp; | ||
| cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated; | ||
| function compareByGeneratedPositionsDeflatedNoLine(mappingA, mappingB, onlyCompareGenerated) { | ||
| var cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0 || onlyCompareGenerated) return cmp; | ||
| cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByGeneratedPositionsDeflatedNoLine = compareByGeneratedPositionsDeflatedNoLine; | ||
| function strcmp(aStr1, aStr2) { | ||
| if (aStr1 === aStr2) return 0; | ||
| if (aStr1 === null) return 1; | ||
| if (aStr2 === null) return -1; | ||
| if (aStr1 > aStr2) return 1; | ||
| return -1; | ||
| } | ||
| /** | ||
| * Comparator between two mappings with inflated source and name strings where | ||
| * the generated positions are compared. | ||
| */ | ||
| function compareByGeneratedPositionsInflated(mappingA, mappingB) { | ||
| var cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated; | ||
| /** | ||
| * Strip any JSON XSSI avoidance prefix from the string (as documented | ||
| * in the source maps specification), and then parse the string as | ||
| * JSON. | ||
| */ | ||
| function parseSourceMapInput(str) { | ||
| return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, "")); | ||
| } | ||
| exports.parseSourceMapInput = parseSourceMapInput; | ||
| /** | ||
| * Compute the URL of a source given the the source root, the source's | ||
| * URL, and the source map's URL. | ||
| */ | ||
| function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { | ||
| sourceURL = sourceURL || ""; | ||
| if (sourceRoot) { | ||
| if (sourceRoot[sourceRoot.length - 1] !== "/" && sourceURL[0] !== "/") sourceRoot += "/"; | ||
| sourceURL = sourceRoot + sourceURL; | ||
| } | ||
| if (sourceMapURL) { | ||
| var parsed = urlParse(sourceMapURL); | ||
| if (!parsed) throw new Error("sourceMapURL could not be parsed"); | ||
| if (parsed.path) { | ||
| var index = parsed.path.lastIndexOf("/"); | ||
| if (index >= 0) parsed.path = parsed.path.substring(0, index + 1); | ||
| } | ||
| sourceURL = join(urlGenerate(parsed), sourceURL); | ||
| } | ||
| return normalize(sourceURL); | ||
| } | ||
| exports.computeSourceURL = computeSourceURL; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/array-set.js | ||
| var require_array_set = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var util = require_util(); | ||
| var has = Object.prototype.hasOwnProperty; | ||
| var hasNativeMap = typeof Map !== "undefined"; | ||
| /** | ||
| * A data structure which is a combination of an array and a set. Adding a new | ||
| * member is O(1), testing for membership is O(1), and finding the index of an | ||
| * element is O(1). Removing elements from the set is not supported. Only | ||
| * strings are supported for membership. | ||
| */ | ||
| function ArraySet() { | ||
| this._array = []; | ||
| this._set = hasNativeMap ? /* @__PURE__ */ new Map() : Object.create(null); | ||
| } | ||
| /** | ||
| * Static method for creating ArraySet instances from an existing array. | ||
| */ | ||
| ArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) { | ||
| var set = new ArraySet(); | ||
| for (var i = 0, len = aArray.length; i < len; i++) set.add(aArray[i], aAllowDuplicates); | ||
| return set; | ||
| }; | ||
| /** | ||
| * Return how many unique items are in this ArraySet. If duplicates have been | ||
| * added, than those do not count towards the size. | ||
| * | ||
| * @returns Number | ||
| */ | ||
| ArraySet.prototype.size = function ArraySet_size() { | ||
| return hasNativeMap ? this._set.size : Object.getOwnPropertyNames(this._set).length; | ||
| }; | ||
| /** | ||
| * Add the given string to this set. | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| ArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) { | ||
| var sStr = hasNativeMap ? aStr : util.toSetString(aStr); | ||
| var isDuplicate = hasNativeMap ? this.has(aStr) : has.call(this._set, sStr); | ||
| var idx = this._array.length; | ||
| if (!isDuplicate || aAllowDuplicates) this._array.push(aStr); | ||
| if (!isDuplicate) if (hasNativeMap) this._set.set(aStr, idx); | ||
| else this._set[sStr] = idx; | ||
| }; | ||
| /** | ||
| * Is the given string a member of this set? | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| ArraySet.prototype.has = function ArraySet_has(aStr) { | ||
| if (hasNativeMap) return this._set.has(aStr); | ||
| else { | ||
| var sStr = util.toSetString(aStr); | ||
| return has.call(this._set, sStr); | ||
| } | ||
| }; | ||
| /** | ||
| * What is the index of the given string in the array? | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) { | ||
| if (hasNativeMap) { | ||
| var idx = this._set.get(aStr); | ||
| if (idx >= 0) return idx; | ||
| } else { | ||
| var sStr = util.toSetString(aStr); | ||
| if (has.call(this._set, sStr)) return this._set[sStr]; | ||
| } | ||
| throw new Error("\"" + aStr + "\" is not in the set."); | ||
| }; | ||
| /** | ||
| * What is the element at the given index? | ||
| * | ||
| * @param Number aIdx | ||
| */ | ||
| ArraySet.prototype.at = function ArraySet_at(aIdx) { | ||
| if (aIdx >= 0 && aIdx < this._array.length) return this._array[aIdx]; | ||
| throw new Error("No element indexed by " + aIdx); | ||
| }; | ||
| /** | ||
| * Returns the array representation of this set (which has the proper indices | ||
| * indicated by indexOf). Note that this is a copy of the internal array used | ||
| * for storing the members so that no one can mess with internal state. | ||
| */ | ||
| ArraySet.prototype.toArray = function ArraySet_toArray() { | ||
| return this._array.slice(); | ||
| }; | ||
| exports.ArraySet = ArraySet; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/mapping-list.js | ||
| var require_mapping_list = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var util = require_util(); | ||
| /** | ||
| * Determine whether mappingB is after mappingA with respect to generated | ||
| * position. | ||
| */ | ||
| function generatedPositionAfter(mappingA, mappingB) { | ||
| var lineA = mappingA.generatedLine; | ||
| var lineB = mappingB.generatedLine; | ||
| var columnA = mappingA.generatedColumn; | ||
| var columnB = mappingB.generatedColumn; | ||
| return lineB > lineA || lineB == lineA && columnB >= columnA || util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0; | ||
| } | ||
| /** | ||
| * A data structure to provide a sorted view of accumulated mappings in a | ||
| * performance conscious manner. It trades a neglibable overhead in general | ||
| * case for a large speedup in case of mappings being added in order. | ||
| */ | ||
| function MappingList() { | ||
| this._array = []; | ||
| this._sorted = true; | ||
| this._last = { | ||
| generatedLine: -1, | ||
| generatedColumn: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Iterate through internal items. This method takes the same arguments that | ||
| * `Array.prototype.forEach` takes. | ||
| * | ||
| * NOTE: The order of the mappings is NOT guaranteed. | ||
| */ | ||
| MappingList.prototype.unsortedForEach = function MappingList_forEach(aCallback, aThisArg) { | ||
| this._array.forEach(aCallback, aThisArg); | ||
| }; | ||
| /** | ||
| * Add the given source mapping. | ||
| * | ||
| * @param Object aMapping | ||
| */ | ||
| MappingList.prototype.add = function MappingList_add(aMapping) { | ||
| if (generatedPositionAfter(this._last, aMapping)) { | ||
| this._last = aMapping; | ||
| this._array.push(aMapping); | ||
| } else { | ||
| this._sorted = false; | ||
| this._array.push(aMapping); | ||
| } | ||
| }; | ||
| /** | ||
| * Returns the flat, sorted array of mappings. The mappings are sorted by | ||
| * generated position. | ||
| * | ||
| * WARNING: This method returns internal data without copying, for | ||
| * performance. The return value must NOT be mutated, and should be treated as | ||
| * an immutable borrow. If you want to take ownership, you must make your own | ||
| * copy. | ||
| */ | ||
| MappingList.prototype.toArray = function MappingList_toArray() { | ||
| if (!this._sorted) { | ||
| this._array.sort(util.compareByGeneratedPositionsInflated); | ||
| this._sorted = true; | ||
| } | ||
| return this._array; | ||
| }; | ||
| exports.MappingList = MappingList; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/source-map-generator.js | ||
| var require_source_map_generator = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var base64VLQ = require_base64_vlq(); | ||
| var util = require_util(); | ||
| var ArraySet = require_array_set().ArraySet; | ||
| var MappingList = require_mapping_list().MappingList; | ||
| /** | ||
| * An instance of the SourceMapGenerator represents a source map which is | ||
| * being built incrementally. You may pass an object with the following | ||
| * properties: | ||
| * | ||
| * - file: The filename of the generated source. | ||
| * - sourceRoot: A root for all relative URLs in this source map. | ||
| */ | ||
| function SourceMapGenerator(aArgs) { | ||
| if (!aArgs) aArgs = {}; | ||
| this._file = util.getArg(aArgs, "file", null); | ||
| this._sourceRoot = util.getArg(aArgs, "sourceRoot", null); | ||
| this._skipValidation = util.getArg(aArgs, "skipValidation", false); | ||
| this._ignoreInvalidMapping = util.getArg(aArgs, "ignoreInvalidMapping", false); | ||
| this._sources = new ArraySet(); | ||
| this._names = new ArraySet(); | ||
| this._mappings = new MappingList(); | ||
| this._sourcesContents = null; | ||
| } | ||
| SourceMapGenerator.prototype._version = 3; | ||
| /** | ||
| * Creates a new SourceMapGenerator based on a SourceMapConsumer | ||
| * | ||
| * @param aSourceMapConsumer The SourceMap. | ||
| */ | ||
| SourceMapGenerator.fromSourceMap = function SourceMapGenerator_fromSourceMap(aSourceMapConsumer, generatorOps) { | ||
| var sourceRoot = aSourceMapConsumer.sourceRoot; | ||
| var generator = new SourceMapGenerator(Object.assign(generatorOps || {}, { | ||
| file: aSourceMapConsumer.file, | ||
| sourceRoot | ||
| })); | ||
| aSourceMapConsumer.eachMapping(function(mapping) { | ||
| var newMapping = { generated: { | ||
| line: mapping.generatedLine, | ||
| column: mapping.generatedColumn | ||
| } }; | ||
| if (mapping.source != null) { | ||
| newMapping.source = mapping.source; | ||
| if (sourceRoot != null) newMapping.source = util.relative(sourceRoot, newMapping.source); | ||
| newMapping.original = { | ||
| line: mapping.originalLine, | ||
| column: mapping.originalColumn | ||
| }; | ||
| if (mapping.name != null) newMapping.name = mapping.name; | ||
| } | ||
| generator.addMapping(newMapping); | ||
| }); | ||
| aSourceMapConsumer.sources.forEach(function(sourceFile) { | ||
| var sourceRelative = sourceFile; | ||
| if (sourceRoot !== null) sourceRelative = util.relative(sourceRoot, sourceFile); | ||
| if (!generator._sources.has(sourceRelative)) generator._sources.add(sourceRelative); | ||
| var content = aSourceMapConsumer.sourceContentFor(sourceFile); | ||
| if (content != null) generator.setSourceContent(sourceFile, content); | ||
| }); | ||
| return generator; | ||
| }; | ||
| /** | ||
| * Add a single mapping from original source line and column to the generated | ||
| * source's line and column for this source map being created. The mapping | ||
| * object should have the following properties: | ||
| * | ||
| * - generated: An object with the generated line and column positions. | ||
| * - original: An object with the original line and column positions. | ||
| * - source: The original source file (relative to the sourceRoot). | ||
| * - name: An optional original token name for this mapping. | ||
| */ | ||
| SourceMapGenerator.prototype.addMapping = function SourceMapGenerator_addMapping(aArgs) { | ||
| var generated = util.getArg(aArgs, "generated"); | ||
| var original = util.getArg(aArgs, "original", null); | ||
| var source = util.getArg(aArgs, "source", null); | ||
| var name = util.getArg(aArgs, "name", null); | ||
| if (!this._skipValidation) { | ||
| if (this._validateMapping(generated, original, source, name) === false) return; | ||
| } | ||
| if (source != null) { | ||
| source = String(source); | ||
| if (!this._sources.has(source)) this._sources.add(source); | ||
| } | ||
| if (name != null) { | ||
| name = String(name); | ||
| if (!this._names.has(name)) this._names.add(name); | ||
| } | ||
| this._mappings.add({ | ||
| generatedLine: generated.line, | ||
| generatedColumn: generated.column, | ||
| originalLine: original != null && original.line, | ||
| originalColumn: original != null && original.column, | ||
| source, | ||
| name | ||
| }); | ||
| }; | ||
| /** | ||
| * Set the source content for a source file. | ||
| */ | ||
| SourceMapGenerator.prototype.setSourceContent = function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { | ||
| var source = aSourceFile; | ||
| if (this._sourceRoot != null) source = util.relative(this._sourceRoot, source); | ||
| if (aSourceContent != null) { | ||
| if (!this._sourcesContents) this._sourcesContents = Object.create(null); | ||
| this._sourcesContents[util.toSetString(source)] = aSourceContent; | ||
| } else if (this._sourcesContents) { | ||
| delete this._sourcesContents[util.toSetString(source)]; | ||
| if (Object.keys(this._sourcesContents).length === 0) this._sourcesContents = null; | ||
| } | ||
| }; | ||
| /** | ||
| * Applies the mappings of a sub-source-map for a specific source file to the | ||
| * source map being generated. Each mapping to the supplied source file is | ||
| * rewritten using the supplied source map. Note: The resolution for the | ||
| * resulting mappings is the minimium of this map and the supplied map. | ||
| * | ||
| * @param aSourceMapConsumer The source map to be applied. | ||
| * @param aSourceFile Optional. The filename of the source file. | ||
| * If omitted, SourceMapConsumer's file property will be used. | ||
| * @param aSourceMapPath Optional. The dirname of the path to the source map | ||
| * to be applied. If relative, it is relative to the SourceMapConsumer. | ||
| * This parameter is needed when the two source maps aren't in the same | ||
| * directory, and the source map to be applied contains relative source | ||
| * paths. If so, those relative source paths need to be rewritten | ||
| * relative to the SourceMapGenerator. | ||
| */ | ||
| SourceMapGenerator.prototype.applySourceMap = function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) { | ||
| var sourceFile = aSourceFile; | ||
| if (aSourceFile == null) { | ||
| if (aSourceMapConsumer.file == null) throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, or the source map's \"file\" property. Both were omitted."); | ||
| sourceFile = aSourceMapConsumer.file; | ||
| } | ||
| var sourceRoot = this._sourceRoot; | ||
| if (sourceRoot != null) sourceFile = util.relative(sourceRoot, sourceFile); | ||
| var newSources = new ArraySet(); | ||
| var newNames = new ArraySet(); | ||
| this._mappings.unsortedForEach(function(mapping) { | ||
| if (mapping.source === sourceFile && mapping.originalLine != null) { | ||
| var original = aSourceMapConsumer.originalPositionFor({ | ||
| line: mapping.originalLine, | ||
| column: mapping.originalColumn | ||
| }); | ||
| if (original.source != null) { | ||
| mapping.source = original.source; | ||
| if (aSourceMapPath != null) mapping.source = util.join(aSourceMapPath, mapping.source); | ||
| if (sourceRoot != null) mapping.source = util.relative(sourceRoot, mapping.source); | ||
| mapping.originalLine = original.line; | ||
| mapping.originalColumn = original.column; | ||
| if (original.name != null) mapping.name = original.name; | ||
| } | ||
| } | ||
| var source = mapping.source; | ||
| if (source != null && !newSources.has(source)) newSources.add(source); | ||
| var name = mapping.name; | ||
| if (name != null && !newNames.has(name)) newNames.add(name); | ||
| }, this); | ||
| this._sources = newSources; | ||
| this._names = newNames; | ||
| aSourceMapConsumer.sources.forEach(function(sourceFile) { | ||
| var content = aSourceMapConsumer.sourceContentFor(sourceFile); | ||
| if (content != null) { | ||
| if (aSourceMapPath != null) sourceFile = util.join(aSourceMapPath, sourceFile); | ||
| if (sourceRoot != null) sourceFile = util.relative(sourceRoot, sourceFile); | ||
| this.setSourceContent(sourceFile, content); | ||
| } | ||
| }, this); | ||
| }; | ||
| /** | ||
| * A mapping can have one of the three levels of data: | ||
| * | ||
| * 1. Just the generated position. | ||
| * 2. The Generated position, original position, and original source. | ||
| * 3. Generated and original position, original source, as well as a name | ||
| * token. | ||
| * | ||
| * To maintain consistency, we validate that any new mapping being added falls | ||
| * in to one of these categories. | ||
| */ | ||
| SourceMapGenerator.prototype._validateMapping = function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource, aName) { | ||
| if (aOriginal && typeof aOriginal.line !== "number" && typeof aOriginal.column !== "number") { | ||
| var message = "original.line and original.column are not numbers -- you probably meant to omit the original mapping entirely and only map the generated position. If so, pass null for the original mapping instead of an object with empty or null values."; | ||
| if (this._ignoreInvalidMapping) { | ||
| if (typeof console !== "undefined" && console.warn) console.warn(message); | ||
| return false; | ||
| } else throw new Error(message); | ||
| } | ||
| if (aGenerated && "line" in aGenerated && "column" in aGenerated && aGenerated.line > 0 && aGenerated.column >= 0 && !aOriginal && !aSource && !aName) return; | ||
| else if (aGenerated && "line" in aGenerated && "column" in aGenerated && aOriginal && "line" in aOriginal && "column" in aOriginal && aGenerated.line > 0 && aGenerated.column >= 0 && aOriginal.line > 0 && aOriginal.column >= 0 && aSource) return; | ||
| else { | ||
| var message = "Invalid mapping: " + JSON.stringify({ | ||
| generated: aGenerated, | ||
| source: aSource, | ||
| original: aOriginal, | ||
| name: aName | ||
| }); | ||
| if (this._ignoreInvalidMapping) { | ||
| if (typeof console !== "undefined" && console.warn) console.warn(message); | ||
| return false; | ||
| } else throw new Error(message); | ||
| } | ||
| }; | ||
| /** | ||
| * Serialize the accumulated mappings in to the stream of base 64 VLQs | ||
| * specified by the source map format. | ||
| */ | ||
| SourceMapGenerator.prototype._serializeMappings = function SourceMapGenerator_serializeMappings() { | ||
| var previousGeneratedColumn = 0; | ||
| var previousGeneratedLine = 1; | ||
| var previousOriginalColumn = 0; | ||
| var previousOriginalLine = 0; | ||
| var previousName = 0; | ||
| var previousSource = 0; | ||
| var result = ""; | ||
| var next; | ||
| var mapping; | ||
| var nameIdx; | ||
| var sourceIdx; | ||
| var mappings = this._mappings.toArray(); | ||
| for (var i = 0, len = mappings.length; i < len; i++) { | ||
| mapping = mappings[i]; | ||
| next = ""; | ||
| if (mapping.generatedLine !== previousGeneratedLine) { | ||
| previousGeneratedColumn = 0; | ||
| while (mapping.generatedLine !== previousGeneratedLine) { | ||
| next += ";"; | ||
| previousGeneratedLine++; | ||
| } | ||
| } else if (i > 0) { | ||
| if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) continue; | ||
| next += ","; | ||
| } | ||
| next += base64VLQ.encode(mapping.generatedColumn - previousGeneratedColumn); | ||
| previousGeneratedColumn = mapping.generatedColumn; | ||
| if (mapping.source != null) { | ||
| sourceIdx = this._sources.indexOf(mapping.source); | ||
| next += base64VLQ.encode(sourceIdx - previousSource); | ||
| previousSource = sourceIdx; | ||
| next += base64VLQ.encode(mapping.originalLine - 1 - previousOriginalLine); | ||
| previousOriginalLine = mapping.originalLine - 1; | ||
| next += base64VLQ.encode(mapping.originalColumn - previousOriginalColumn); | ||
| previousOriginalColumn = mapping.originalColumn; | ||
| if (mapping.name != null) { | ||
| nameIdx = this._names.indexOf(mapping.name); | ||
| next += base64VLQ.encode(nameIdx - previousName); | ||
| previousName = nameIdx; | ||
| } | ||
| } | ||
| result += next; | ||
| } | ||
| return result; | ||
| }; | ||
| SourceMapGenerator.prototype._generateSourcesContent = function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) { | ||
| return aSources.map(function(source) { | ||
| if (!this._sourcesContents) return null; | ||
| if (aSourceRoot != null) source = util.relative(aSourceRoot, source); | ||
| var key = util.toSetString(source); | ||
| return Object.prototype.hasOwnProperty.call(this._sourcesContents, key) ? this._sourcesContents[key] : null; | ||
| }, this); | ||
| }; | ||
| /** | ||
| * Externalize the source map. | ||
| */ | ||
| SourceMapGenerator.prototype.toJSON = function SourceMapGenerator_toJSON() { | ||
| var map = { | ||
| version: this._version, | ||
| sources: this._sources.toArray(), | ||
| names: this._names.toArray(), | ||
| mappings: this._serializeMappings() | ||
| }; | ||
| if (this._file != null) map.file = this._file; | ||
| if (this._sourceRoot != null) map.sourceRoot = this._sourceRoot; | ||
| if (this._sourcesContents) map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); | ||
| return map; | ||
| }; | ||
| /** | ||
| * Render the source map being generated to a string. | ||
| */ | ||
| SourceMapGenerator.prototype.toString = function SourceMapGenerator_toString() { | ||
| return JSON.stringify(this.toJSON()); | ||
| }; | ||
| exports.SourceMapGenerator = SourceMapGenerator; | ||
| })); | ||
| //#endregion | ||
| export { require_base64_vlq as i, require_array_set as n, require_util as r, require_source_map_generator as t }; | ||
| //# sourceMappingURL=source-map-generator-Bvbu-kM5.mjs.map |
| {"version":3,"file":"source-map-generator-Bvbu-kM5.mjs","names":[],"sources":["../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64-vlq.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/util.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/array-set.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/mapping-list.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/source-map-generator.js"],"sourcesContent":["/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar intToCharMap = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');\n\n/**\n * Encode an integer in the range of 0 to 63 to a single base 64 digit.\n */\nexports.encode = function (number) {\n if (0 <= number && number < intToCharMap.length) {\n return intToCharMap[number];\n }\n throw new TypeError(\"Must be between 0 and 63: \" + number);\n};\n\n/**\n * Decode a single base 64 character code digit to an integer. Returns -1 on\n * failure.\n */\nexports.decode = function (charCode) {\n var bigA = 65; // 'A'\n var bigZ = 90; // 'Z'\n\n var littleA = 97; // 'a'\n var littleZ = 122; // 'z'\n\n var zero = 48; // '0'\n var nine = 57; // '9'\n\n var plus = 43; // '+'\n var slash = 47; // '/'\n\n var littleOffset = 26;\n var numberOffset = 52;\n\n // 0 - 25: ABCDEFGHIJKLMNOPQRSTUVWXYZ\n if (bigA <= charCode && charCode <= bigZ) {\n return (charCode - bigA);\n }\n\n // 26 - 51: abcdefghijklmnopqrstuvwxyz\n if (littleA <= charCode && charCode <= littleZ) {\n return (charCode - littleA + littleOffset);\n }\n\n // 52 - 61: 0123456789\n if (zero <= charCode && charCode <= nine) {\n return (charCode - zero + numberOffset);\n }\n\n // 62: +\n if (charCode == plus) {\n return 62;\n }\n\n // 63: /\n if (charCode == slash) {\n return 63;\n }\n\n // Invalid base64 digit.\n return -1;\n};\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n *\n * Based on the Base 64 VLQ implementation in Closure Compiler:\n * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java\n *\n * Copyright 2011 The Closure Compiler Authors. All rights reserved.\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above\n * copyright notice, this list of conditions and the following\n * disclaimer in the documentation and/or other materials provided\n * with the distribution.\n * * Neither the name of Google Inc. nor the names of its\n * contributors may be used to endorse or promote products derived\n * from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\nvar base64 = require('./base64');\n\n// A single base 64 digit can contain 6 bits of data. For the base 64 variable\n// length quantities we use in the source map spec, the first bit is the sign,\n// the next four bits are the actual value, and the 6th bit is the\n// continuation bit. The continuation bit tells us whether there are more\n// digits in this value following this digit.\n//\n// Continuation\n// | Sign\n// | |\n// V V\n// 101011\n\nvar VLQ_BASE_SHIFT = 5;\n\n// binary: 100000\nvar VLQ_BASE = 1 << VLQ_BASE_SHIFT;\n\n// binary: 011111\nvar VLQ_BASE_MASK = VLQ_BASE - 1;\n\n// binary: 100000\nvar VLQ_CONTINUATION_BIT = VLQ_BASE;\n\n/**\n * Converts from a two-complement value to a value where the sign bit is\n * placed in the least significant bit. For example, as decimals:\n * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)\n * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)\n */\nfunction toVLQSigned(aValue) {\n return aValue < 0\n ? ((-aValue) << 1) + 1\n : (aValue << 1) + 0;\n}\n\n/**\n * Converts to a two-complement value from a value where the sign bit is\n * placed in the least significant bit. For example, as decimals:\n * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1\n * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2\n */\nfunction fromVLQSigned(aValue) {\n var isNegative = (aValue & 1) === 1;\n var shifted = aValue >> 1;\n return isNegative\n ? -shifted\n : shifted;\n}\n\n/**\n * Returns the base 64 VLQ encoded value.\n */\nexports.encode = function base64VLQ_encode(aValue) {\n var encoded = \"\";\n var digit;\n\n var vlq = toVLQSigned(aValue);\n\n do {\n digit = vlq & VLQ_BASE_MASK;\n vlq >>>= VLQ_BASE_SHIFT;\n if (vlq > 0) {\n // There are still more digits in this value, so we must make sure the\n // continuation bit is marked.\n digit |= VLQ_CONTINUATION_BIT;\n }\n encoded += base64.encode(digit);\n } while (vlq > 0);\n\n return encoded;\n};\n\n/**\n * Decodes the next base 64 VLQ value from the given string and returns the\n * value and the rest of the string via the out parameter.\n */\nexports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) {\n var strLen = aStr.length;\n var result = 0;\n var shift = 0;\n var continuation, digit;\n\n do {\n if (aIndex >= strLen) {\n throw new Error(\"Expected more digits in base 64 VLQ value.\");\n }\n\n digit = base64.decode(aStr.charCodeAt(aIndex++));\n if (digit === -1) {\n throw new Error(\"Invalid base64 digit: \" + aStr.charAt(aIndex - 1));\n }\n\n continuation = !!(digit & VLQ_CONTINUATION_BIT);\n digit &= VLQ_BASE_MASK;\n result = result + (digit << shift);\n shift += VLQ_BASE_SHIFT;\n } while (continuation);\n\n aOutParam.value = fromVLQSigned(result);\n aOutParam.rest = aIndex;\n};\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\n/**\n * This is a helper function for getting values from parameter/options\n * objects.\n *\n * @param args The object we are extracting values from\n * @param name The name of the property we are getting.\n * @param defaultValue An optional value to return if the property is missing\n * from the object. If this is not specified and the property is missing, an\n * error will be thrown.\n */\nfunction getArg(aArgs, aName, aDefaultValue) {\n if (aName in aArgs) {\n return aArgs[aName];\n } else if (arguments.length === 3) {\n return aDefaultValue;\n } else {\n throw new Error('\"' + aName + '\" is a required argument.');\n }\n}\nexports.getArg = getArg;\n\nvar urlRegexp = /^(?:([\\w+\\-.]+):)?\\/\\/(?:(\\w+:\\w+)@)?([\\w.-]*)(?::(\\d+))?(.*)$/;\nvar dataUrlRegexp = /^data:.+\\,.+$/;\n\nfunction urlParse(aUrl) {\n var match = aUrl.match(urlRegexp);\n if (!match) {\n return null;\n }\n return {\n scheme: match[1],\n auth: match[2],\n host: match[3],\n port: match[4],\n path: match[5]\n };\n}\nexports.urlParse = urlParse;\n\nfunction urlGenerate(aParsedUrl) {\n var url = '';\n if (aParsedUrl.scheme) {\n url += aParsedUrl.scheme + ':';\n }\n url += '//';\n if (aParsedUrl.auth) {\n url += aParsedUrl.auth + '@';\n }\n if (aParsedUrl.host) {\n url += aParsedUrl.host;\n }\n if (aParsedUrl.port) {\n url += \":\" + aParsedUrl.port\n }\n if (aParsedUrl.path) {\n url += aParsedUrl.path;\n }\n return url;\n}\nexports.urlGenerate = urlGenerate;\n\nvar MAX_CACHED_INPUTS = 32;\n\n/**\n * Takes some function `f(input) -> result` and returns a memoized version of\n * `f`.\n *\n * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The\n * memoization is a dumb-simple, linear least-recently-used cache.\n */\nfunction lruMemoize(f) {\n var cache = [];\n\n return function(input) {\n for (var i = 0; i < cache.length; i++) {\n if (cache[i].input === input) {\n var temp = cache[0];\n cache[0] = cache[i];\n cache[i] = temp;\n return cache[0].result;\n }\n }\n\n var result = f(input);\n\n cache.unshift({\n input,\n result,\n });\n\n if (cache.length > MAX_CACHED_INPUTS) {\n cache.pop();\n }\n\n return result;\n };\n}\n\n/**\n * Normalizes a path, or the path portion of a URL:\n *\n * - Replaces consecutive slashes with one slash.\n * - Removes unnecessary '.' parts.\n * - Removes unnecessary '<dir>/..' parts.\n *\n * Based on code in the Node.js 'path' core module.\n *\n * @param aPath The path or url to normalize.\n */\nvar normalize = lruMemoize(function normalize(aPath) {\n var path = aPath;\n var url = urlParse(aPath);\n if (url) {\n if (!url.path) {\n return aPath;\n }\n path = url.path;\n }\n var isAbsolute = exports.isAbsolute(path);\n // Split the path into parts between `/` characters. This is much faster than\n // using `.split(/\\/+/g)`.\n var parts = [];\n var start = 0;\n var i = 0;\n while (true) {\n start = i;\n i = path.indexOf(\"/\", start);\n if (i === -1) {\n parts.push(path.slice(start));\n break;\n } else {\n parts.push(path.slice(start, i));\n while (i < path.length && path[i] === \"/\") {\n i++;\n }\n }\n }\n\n for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {\n part = parts[i];\n if (part === '.') {\n parts.splice(i, 1);\n } else if (part === '..') {\n up++;\n } else if (up > 0) {\n if (part === '') {\n // The first part is blank if the path is absolute. Trying to go\n // above the root is a no-op. Therefore we can remove all '..' parts\n // directly after the root.\n parts.splice(i + 1, up);\n up = 0;\n } else {\n parts.splice(i, 2);\n up--;\n }\n }\n }\n path = parts.join('/');\n\n if (path === '') {\n path = isAbsolute ? '/' : '.';\n }\n\n if (url) {\n url.path = path;\n return urlGenerate(url);\n }\n return path;\n});\nexports.normalize = normalize;\n\n/**\n * Joins two paths/URLs.\n *\n * @param aRoot The root path or URL.\n * @param aPath The path or URL to be joined with the root.\n *\n * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a\n * scheme-relative URL: Then the scheme of aRoot, if any, is prepended\n * first.\n * - Otherwise aPath is a path. If aRoot is a URL, then its path portion\n * is updated with the result and aRoot is returned. Otherwise the result\n * is returned.\n * - If aPath is absolute, the result is aPath.\n * - Otherwise the two paths are joined with a slash.\n * - Joining for example 'http://' and 'www.example.com' is also supported.\n */\nfunction join(aRoot, aPath) {\n if (aRoot === \"\") {\n aRoot = \".\";\n }\n if (aPath === \"\") {\n aPath = \".\";\n }\n var aPathUrl = urlParse(aPath);\n var aRootUrl = urlParse(aRoot);\n if (aRootUrl) {\n aRoot = aRootUrl.path || '/';\n }\n\n // `join(foo, '//www.example.org')`\n if (aPathUrl && !aPathUrl.scheme) {\n if (aRootUrl) {\n aPathUrl.scheme = aRootUrl.scheme;\n }\n return urlGenerate(aPathUrl);\n }\n\n if (aPathUrl || aPath.match(dataUrlRegexp)) {\n return aPath;\n }\n\n // `join('http://', 'www.example.com')`\n if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {\n aRootUrl.host = aPath;\n return urlGenerate(aRootUrl);\n }\n\n var joined = aPath.charAt(0) === '/'\n ? aPath\n : normalize(aRoot.replace(/\\/+$/, '') + '/' + aPath);\n\n if (aRootUrl) {\n aRootUrl.path = joined;\n return urlGenerate(aRootUrl);\n }\n return joined;\n}\nexports.join = join;\n\nexports.isAbsolute = function (aPath) {\n return aPath.charAt(0) === '/' || urlRegexp.test(aPath);\n};\n\n/**\n * Make a path relative to a URL or another path.\n *\n * @param aRoot The root path or URL.\n * @param aPath The path or URL to be made relative to aRoot.\n */\nfunction relative(aRoot, aPath) {\n if (aRoot === \"\") {\n aRoot = \".\";\n }\n\n aRoot = aRoot.replace(/\\/$/, '');\n\n // It is possible for the path to be above the root. In this case, simply\n // checking whether the root is a prefix of the path won't work. Instead, we\n // need to remove components from the root one by one, until either we find\n // a prefix that fits, or we run out of components to remove.\n var level = 0;\n while (aPath.indexOf(aRoot + '/') !== 0) {\n var index = aRoot.lastIndexOf(\"/\");\n if (index < 0) {\n return aPath;\n }\n\n // If the only part of the root that is left is the scheme (i.e. http://,\n // file:///, etc.), one or more slashes (/), or simply nothing at all, we\n // have exhausted all components, so the path is not relative to the root.\n aRoot = aRoot.slice(0, index);\n if (aRoot.match(/^([^\\/]+:\\/)?\\/*$/)) {\n return aPath;\n }\n\n ++level;\n }\n\n // Make sure we add a \"../\" for each component we removed from the root.\n return Array(level + 1).join(\"../\") + aPath.substr(aRoot.length + 1);\n}\nexports.relative = relative;\n\nvar supportsNullProto = (function () {\n var obj = Object.create(null);\n return !('__proto__' in obj);\n}());\n\nfunction identity (s) {\n return s;\n}\n\n/**\n * Because behavior goes wacky when you set `__proto__` on objects, we\n * have to prefix all the strings in our set with an arbitrary character.\n *\n * See https://github.com/mozilla/source-map/pull/31 and\n * https://github.com/mozilla/source-map/issues/30\n *\n * @param String aStr\n */\nfunction toSetString(aStr) {\n if (isProtoString(aStr)) {\n return '$' + aStr;\n }\n\n return aStr;\n}\nexports.toSetString = supportsNullProto ? identity : toSetString;\n\nfunction fromSetString(aStr) {\n if (isProtoString(aStr)) {\n return aStr.slice(1);\n }\n\n return aStr;\n}\nexports.fromSetString = supportsNullProto ? identity : fromSetString;\n\nfunction isProtoString(s) {\n if (!s) {\n return false;\n }\n\n var length = s.length;\n\n if (length < 9 /* \"__proto__\".length */) {\n return false;\n }\n\n if (s.charCodeAt(length - 1) !== 95 /* '_' */ ||\n s.charCodeAt(length - 2) !== 95 /* '_' */ ||\n s.charCodeAt(length - 3) !== 111 /* 'o' */ ||\n s.charCodeAt(length - 4) !== 116 /* 't' */ ||\n s.charCodeAt(length - 5) !== 111 /* 'o' */ ||\n s.charCodeAt(length - 6) !== 114 /* 'r' */ ||\n s.charCodeAt(length - 7) !== 112 /* 'p' */ ||\n s.charCodeAt(length - 8) !== 95 /* '_' */ ||\n s.charCodeAt(length - 9) !== 95 /* '_' */) {\n return false;\n }\n\n for (var i = length - 10; i >= 0; i--) {\n if (s.charCodeAt(i) !== 36 /* '$' */) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Comparator between two mappings where the original positions are compared.\n *\n * Optionally pass in `true` as `onlyCompareGenerated` to consider two\n * mappings with the same original source/line/column, but different generated\n * line and column the same. Useful when searching for a mapping with a\n * stubbed out mapping.\n */\nfunction compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {\n var cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0 || onlyCompareOriginal) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByOriginalPositions = compareByOriginalPositions;\n\nfunction compareByOriginalPositionsNoSource(mappingA, mappingB, onlyCompareOriginal) {\n var cmp\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0 || onlyCompareOriginal) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByOriginalPositionsNoSource = compareByOriginalPositionsNoSource;\n\n/**\n * Comparator between two mappings with deflated source and name indices where\n * the generated positions are compared.\n *\n * Optionally pass in `true` as `onlyCompareGenerated` to consider two\n * mappings with the same generated line and column, but different\n * source/name/original line and column the same. Useful when searching for a\n * mapping with a stubbed out mapping.\n */\nfunction compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {\n var cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0 || onlyCompareGenerated) {\n return cmp;\n }\n\n cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated;\n\nfunction compareByGeneratedPositionsDeflatedNoLine(mappingA, mappingB, onlyCompareGenerated) {\n var cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0 || onlyCompareGenerated) {\n return cmp;\n }\n\n cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByGeneratedPositionsDeflatedNoLine = compareByGeneratedPositionsDeflatedNoLine;\n\nfunction strcmp(aStr1, aStr2) {\n if (aStr1 === aStr2) {\n return 0;\n }\n\n if (aStr1 === null) {\n return 1; // aStr2 !== null\n }\n\n if (aStr2 === null) {\n return -1; // aStr1 !== null\n }\n\n if (aStr1 > aStr2) {\n return 1;\n }\n\n return -1;\n}\n\n/**\n * Comparator between two mappings with inflated source and name strings where\n * the generated positions are compared.\n */\nfunction compareByGeneratedPositionsInflated(mappingA, mappingB) {\n var cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated;\n\n/**\n * Strip any JSON XSSI avoidance prefix from the string (as documented\n * in the source maps specification), and then parse the string as\n * JSON.\n */\nfunction parseSourceMapInput(str) {\n return JSON.parse(str.replace(/^\\)]}'[^\\n]*\\n/, ''));\n}\nexports.parseSourceMapInput = parseSourceMapInput;\n\n/**\n * Compute the URL of a source given the the source root, the source's\n * URL, and the source map's URL.\n */\nfunction computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {\n sourceURL = sourceURL || '';\n\n if (sourceRoot) {\n // This follows what Chrome does.\n if (sourceRoot[sourceRoot.length - 1] !== '/' && sourceURL[0] !== '/') {\n sourceRoot += '/';\n }\n // The spec says:\n // Line 4: An optional source root, useful for relocating source\n // files on a server or removing repeated values in the\n // “sources” entry. This value is prepended to the individual\n // entries in the “source” field.\n sourceURL = sourceRoot + sourceURL;\n }\n\n // Historically, SourceMapConsumer did not take the sourceMapURL as\n // a parameter. This mode is still somewhat supported, which is why\n // this code block is conditional. However, it's preferable to pass\n // the source map URL to SourceMapConsumer, so that this function\n // can implement the source URL resolution algorithm as outlined in\n // the spec. This block is basically the equivalent of:\n // new URL(sourceURL, sourceMapURL).toString()\n // ... except it avoids using URL, which wasn't available in the\n // older releases of node still supported by this library.\n //\n // The spec says:\n // If the sources are not absolute URLs after prepending of the\n // “sourceRoot”, the sources are resolved relative to the\n // SourceMap (like resolving script src in a html document).\n if (sourceMapURL) {\n var parsed = urlParse(sourceMapURL);\n if (!parsed) {\n throw new Error(\"sourceMapURL could not be parsed\");\n }\n if (parsed.path) {\n // Strip the last path component, but keep the \"/\".\n var index = parsed.path.lastIndexOf('/');\n if (index >= 0) {\n parsed.path = parsed.path.substring(0, index + 1);\n }\n }\n sourceURL = join(urlGenerate(parsed), sourceURL);\n }\n\n return normalize(sourceURL);\n}\nexports.computeSourceURL = computeSourceURL;\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar util = require('./util');\nvar has = Object.prototype.hasOwnProperty;\nvar hasNativeMap = typeof Map !== \"undefined\";\n\n/**\n * A data structure which is a combination of an array and a set. Adding a new\n * member is O(1), testing for membership is O(1), and finding the index of an\n * element is O(1). Removing elements from the set is not supported. Only\n * strings are supported for membership.\n */\nfunction ArraySet() {\n this._array = [];\n this._set = hasNativeMap ? new Map() : Object.create(null);\n}\n\n/**\n * Static method for creating ArraySet instances from an existing array.\n */\nArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) {\n var set = new ArraySet();\n for (var i = 0, len = aArray.length; i < len; i++) {\n set.add(aArray[i], aAllowDuplicates);\n }\n return set;\n};\n\n/**\n * Return how many unique items are in this ArraySet. If duplicates have been\n * added, than those do not count towards the size.\n *\n * @returns Number\n */\nArraySet.prototype.size = function ArraySet_size() {\n return hasNativeMap ? this._set.size : Object.getOwnPropertyNames(this._set).length;\n};\n\n/**\n * Add the given string to this set.\n *\n * @param String aStr\n */\nArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) {\n var sStr = hasNativeMap ? aStr : util.toSetString(aStr);\n var isDuplicate = hasNativeMap ? this.has(aStr) : has.call(this._set, sStr);\n var idx = this._array.length;\n if (!isDuplicate || aAllowDuplicates) {\n this._array.push(aStr);\n }\n if (!isDuplicate) {\n if (hasNativeMap) {\n this._set.set(aStr, idx);\n } else {\n this._set[sStr] = idx;\n }\n }\n};\n\n/**\n * Is the given string a member of this set?\n *\n * @param String aStr\n */\nArraySet.prototype.has = function ArraySet_has(aStr) {\n if (hasNativeMap) {\n return this._set.has(aStr);\n } else {\n var sStr = util.toSetString(aStr);\n return has.call(this._set, sStr);\n }\n};\n\n/**\n * What is the index of the given string in the array?\n *\n * @param String aStr\n */\nArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) {\n if (hasNativeMap) {\n var idx = this._set.get(aStr);\n if (idx >= 0) {\n return idx;\n }\n } else {\n var sStr = util.toSetString(aStr);\n if (has.call(this._set, sStr)) {\n return this._set[sStr];\n }\n }\n\n throw new Error('\"' + aStr + '\" is not in the set.');\n};\n\n/**\n * What is the element at the given index?\n *\n * @param Number aIdx\n */\nArraySet.prototype.at = function ArraySet_at(aIdx) {\n if (aIdx >= 0 && aIdx < this._array.length) {\n return this._array[aIdx];\n }\n throw new Error('No element indexed by ' + aIdx);\n};\n\n/**\n * Returns the array representation of this set (which has the proper indices\n * indicated by indexOf). Note that this is a copy of the internal array used\n * for storing the members so that no one can mess with internal state.\n */\nArraySet.prototype.toArray = function ArraySet_toArray() {\n return this._array.slice();\n};\n\nexports.ArraySet = ArraySet;\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2014 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar util = require('./util');\n\n/**\n * Determine whether mappingB is after mappingA with respect to generated\n * position.\n */\nfunction generatedPositionAfter(mappingA, mappingB) {\n // Optimized for most common case\n var lineA = mappingA.generatedLine;\n var lineB = mappingB.generatedLine;\n var columnA = mappingA.generatedColumn;\n var columnB = mappingB.generatedColumn;\n return lineB > lineA || lineB == lineA && columnB >= columnA ||\n util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0;\n}\n\n/**\n * A data structure to provide a sorted view of accumulated mappings in a\n * performance conscious manner. It trades a neglibable overhead in general\n * case for a large speedup in case of mappings being added in order.\n */\nfunction MappingList() {\n this._array = [];\n this._sorted = true;\n // Serves as infimum\n this._last = {generatedLine: -1, generatedColumn: 0};\n}\n\n/**\n * Iterate through internal items. This method takes the same arguments that\n * `Array.prototype.forEach` takes.\n *\n * NOTE: The order of the mappings is NOT guaranteed.\n */\nMappingList.prototype.unsortedForEach =\n function MappingList_forEach(aCallback, aThisArg) {\n this._array.forEach(aCallback, aThisArg);\n };\n\n/**\n * Add the given source mapping.\n *\n * @param Object aMapping\n */\nMappingList.prototype.add = function MappingList_add(aMapping) {\n if (generatedPositionAfter(this._last, aMapping)) {\n this._last = aMapping;\n this._array.push(aMapping);\n } else {\n this._sorted = false;\n this._array.push(aMapping);\n }\n};\n\n/**\n * Returns the flat, sorted array of mappings. The mappings are sorted by\n * generated position.\n *\n * WARNING: This method returns internal data without copying, for\n * performance. The return value must NOT be mutated, and should be treated as\n * an immutable borrow. If you want to take ownership, you must make your own\n * copy.\n */\nMappingList.prototype.toArray = function MappingList_toArray() {\n if (!this._sorted) {\n this._array.sort(util.compareByGeneratedPositionsInflated);\n this._sorted = true;\n }\n return this._array;\n};\n\nexports.MappingList = MappingList;\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar base64VLQ = require('./base64-vlq');\nvar util = require('./util');\nvar ArraySet = require('./array-set').ArraySet;\nvar MappingList = require('./mapping-list').MappingList;\n\n/**\n * An instance of the SourceMapGenerator represents a source map which is\n * being built incrementally. You may pass an object with the following\n * properties:\n *\n * - file: The filename of the generated source.\n * - sourceRoot: A root for all relative URLs in this source map.\n */\nfunction SourceMapGenerator(aArgs) {\n if (!aArgs) {\n aArgs = {};\n }\n this._file = util.getArg(aArgs, 'file', null);\n this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);\n this._skipValidation = util.getArg(aArgs, 'skipValidation', false);\n this._ignoreInvalidMapping = util.getArg(aArgs, 'ignoreInvalidMapping', false);\n this._sources = new ArraySet();\n this._names = new ArraySet();\n this._mappings = new MappingList();\n this._sourcesContents = null;\n}\n\nSourceMapGenerator.prototype._version = 3;\n\n/**\n * Creates a new SourceMapGenerator based on a SourceMapConsumer\n *\n * @param aSourceMapConsumer The SourceMap.\n */\nSourceMapGenerator.fromSourceMap =\n function SourceMapGenerator_fromSourceMap(aSourceMapConsumer, generatorOps) {\n var sourceRoot = aSourceMapConsumer.sourceRoot;\n var generator = new SourceMapGenerator(Object.assign(generatorOps || {}, {\n file: aSourceMapConsumer.file,\n sourceRoot: sourceRoot\n }));\n aSourceMapConsumer.eachMapping(function (mapping) {\n var newMapping = {\n generated: {\n line: mapping.generatedLine,\n column: mapping.generatedColumn\n }\n };\n\n if (mapping.source != null) {\n newMapping.source = mapping.source;\n if (sourceRoot != null) {\n newMapping.source = util.relative(sourceRoot, newMapping.source);\n }\n\n newMapping.original = {\n line: mapping.originalLine,\n column: mapping.originalColumn\n };\n\n if (mapping.name != null) {\n newMapping.name = mapping.name;\n }\n }\n\n generator.addMapping(newMapping);\n });\n aSourceMapConsumer.sources.forEach(function (sourceFile) {\n var sourceRelative = sourceFile;\n if (sourceRoot !== null) {\n sourceRelative = util.relative(sourceRoot, sourceFile);\n }\n\n if (!generator._sources.has(sourceRelative)) {\n generator._sources.add(sourceRelative);\n }\n\n var content = aSourceMapConsumer.sourceContentFor(sourceFile);\n if (content != null) {\n generator.setSourceContent(sourceFile, content);\n }\n });\n return generator;\n };\n\n/**\n * Add a single mapping from original source line and column to the generated\n * source's line and column for this source map being created. The mapping\n * object should have the following properties:\n *\n * - generated: An object with the generated line and column positions.\n * - original: An object with the original line and column positions.\n * - source: The original source file (relative to the sourceRoot).\n * - name: An optional original token name for this mapping.\n */\nSourceMapGenerator.prototype.addMapping =\n function SourceMapGenerator_addMapping(aArgs) {\n var generated = util.getArg(aArgs, 'generated');\n var original = util.getArg(aArgs, 'original', null);\n var source = util.getArg(aArgs, 'source', null);\n var name = util.getArg(aArgs, 'name', null);\n\n if (!this._skipValidation) {\n if (this._validateMapping(generated, original, source, name) === false) {\n return;\n }\n }\n\n if (source != null) {\n source = String(source);\n if (!this._sources.has(source)) {\n this._sources.add(source);\n }\n }\n\n if (name != null) {\n name = String(name);\n if (!this._names.has(name)) {\n this._names.add(name);\n }\n }\n\n this._mappings.add({\n generatedLine: generated.line,\n generatedColumn: generated.column,\n originalLine: original != null && original.line,\n originalColumn: original != null && original.column,\n source: source,\n name: name\n });\n };\n\n/**\n * Set the source content for a source file.\n */\nSourceMapGenerator.prototype.setSourceContent =\n function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {\n var source = aSourceFile;\n if (this._sourceRoot != null) {\n source = util.relative(this._sourceRoot, source);\n }\n\n if (aSourceContent != null) {\n // Add the source content to the _sourcesContents map.\n // Create a new _sourcesContents map if the property is null.\n if (!this._sourcesContents) {\n this._sourcesContents = Object.create(null);\n }\n this._sourcesContents[util.toSetString(source)] = aSourceContent;\n } else if (this._sourcesContents) {\n // Remove the source file from the _sourcesContents map.\n // If the _sourcesContents map is empty, set the property to null.\n delete this._sourcesContents[util.toSetString(source)];\n if (Object.keys(this._sourcesContents).length === 0) {\n this._sourcesContents = null;\n }\n }\n };\n\n/**\n * Applies the mappings of a sub-source-map for a specific source file to the\n * source map being generated. Each mapping to the supplied source file is\n * rewritten using the supplied source map. Note: The resolution for the\n * resulting mappings is the minimium of this map and the supplied map.\n *\n * @param aSourceMapConsumer The source map to be applied.\n * @param aSourceFile Optional. The filename of the source file.\n * If omitted, SourceMapConsumer's file property will be used.\n * @param aSourceMapPath Optional. The dirname of the path to the source map\n * to be applied. If relative, it is relative to the SourceMapConsumer.\n * This parameter is needed when the two source maps aren't in the same\n * directory, and the source map to be applied contains relative source\n * paths. If so, those relative source paths need to be rewritten\n * relative to the SourceMapGenerator.\n */\nSourceMapGenerator.prototype.applySourceMap =\n function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {\n var sourceFile = aSourceFile;\n // If aSourceFile is omitted, we will use the file property of the SourceMap\n if (aSourceFile == null) {\n if (aSourceMapConsumer.file == null) {\n throw new Error(\n 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +\n 'or the source map\\'s \"file\" property. Both were omitted.'\n );\n }\n sourceFile = aSourceMapConsumer.file;\n }\n var sourceRoot = this._sourceRoot;\n // Make \"sourceFile\" relative if an absolute Url is passed.\n if (sourceRoot != null) {\n sourceFile = util.relative(sourceRoot, sourceFile);\n }\n // Applying the SourceMap can add and remove items from the sources and\n // the names array.\n var newSources = new ArraySet();\n var newNames = new ArraySet();\n\n // Find mappings for the \"sourceFile\"\n this._mappings.unsortedForEach(function (mapping) {\n if (mapping.source === sourceFile && mapping.originalLine != null) {\n // Check if it can be mapped by the source map, then update the mapping.\n var original = aSourceMapConsumer.originalPositionFor({\n line: mapping.originalLine,\n column: mapping.originalColumn\n });\n if (original.source != null) {\n // Copy mapping\n mapping.source = original.source;\n if (aSourceMapPath != null) {\n mapping.source = util.join(aSourceMapPath, mapping.source)\n }\n if (sourceRoot != null) {\n mapping.source = util.relative(sourceRoot, mapping.source);\n }\n mapping.originalLine = original.line;\n mapping.originalColumn = original.column;\n if (original.name != null) {\n mapping.name = original.name;\n }\n }\n }\n\n var source = mapping.source;\n if (source != null && !newSources.has(source)) {\n newSources.add(source);\n }\n\n var name = mapping.name;\n if (name != null && !newNames.has(name)) {\n newNames.add(name);\n }\n\n }, this);\n this._sources = newSources;\n this._names = newNames;\n\n // Copy sourcesContents of applied map.\n aSourceMapConsumer.sources.forEach(function (sourceFile) {\n var content = aSourceMapConsumer.sourceContentFor(sourceFile);\n if (content != null) {\n if (aSourceMapPath != null) {\n sourceFile = util.join(aSourceMapPath, sourceFile);\n }\n if (sourceRoot != null) {\n sourceFile = util.relative(sourceRoot, sourceFile);\n }\n this.setSourceContent(sourceFile, content);\n }\n }, this);\n };\n\n/**\n * A mapping can have one of the three levels of data:\n *\n * 1. Just the generated position.\n * 2. The Generated position, original position, and original source.\n * 3. Generated and original position, original source, as well as a name\n * token.\n *\n * To maintain consistency, we validate that any new mapping being added falls\n * in to one of these categories.\n */\nSourceMapGenerator.prototype._validateMapping =\n function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,\n aName) {\n // When aOriginal is truthy but has empty values for .line and .column,\n // it is most likely a programmer error. In this case we throw a very\n // specific error message to try to guide them the right way.\n // For example: https://github.com/Polymer/polymer-bundler/pull/519\n if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') {\n var message = 'original.line and original.column are not numbers -- you probably meant to omit ' +\n 'the original mapping entirely and only map the generated position. If so, pass ' +\n 'null for the original mapping instead of an object with empty or null values.'\n\n if (this._ignoreInvalidMapping) {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(message);\n }\n return false;\n } else {\n throw new Error(message);\n }\n }\n\n if (aGenerated && 'line' in aGenerated && 'column' in aGenerated\n && aGenerated.line > 0 && aGenerated.column >= 0\n && !aOriginal && !aSource && !aName) {\n // Case 1.\n return;\n }\n else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated\n && aOriginal && 'line' in aOriginal && 'column' in aOriginal\n && aGenerated.line > 0 && aGenerated.column >= 0\n && aOriginal.line > 0 && aOriginal.column >= 0\n && aSource) {\n // Cases 2 and 3.\n return;\n }\n else {\n var message = 'Invalid mapping: ' + JSON.stringify({\n generated: aGenerated,\n source: aSource,\n original: aOriginal,\n name: aName\n });\n\n if (this._ignoreInvalidMapping) {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(message);\n }\n return false;\n } else {\n throw new Error(message)\n }\n }\n };\n\n/**\n * Serialize the accumulated mappings in to the stream of base 64 VLQs\n * specified by the source map format.\n */\nSourceMapGenerator.prototype._serializeMappings =\n function SourceMapGenerator_serializeMappings() {\n var previousGeneratedColumn = 0;\n var previousGeneratedLine = 1;\n var previousOriginalColumn = 0;\n var previousOriginalLine = 0;\n var previousName = 0;\n var previousSource = 0;\n var result = '';\n var next;\n var mapping;\n var nameIdx;\n var sourceIdx;\n\n var mappings = this._mappings.toArray();\n for (var i = 0, len = mappings.length; i < len; i++) {\n mapping = mappings[i];\n next = ''\n\n if (mapping.generatedLine !== previousGeneratedLine) {\n previousGeneratedColumn = 0;\n while (mapping.generatedLine !== previousGeneratedLine) {\n next += ';';\n previousGeneratedLine++;\n }\n }\n else {\n if (i > 0) {\n if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {\n continue;\n }\n next += ',';\n }\n }\n\n next += base64VLQ.encode(mapping.generatedColumn\n - previousGeneratedColumn);\n previousGeneratedColumn = mapping.generatedColumn;\n\n if (mapping.source != null) {\n sourceIdx = this._sources.indexOf(mapping.source);\n next += base64VLQ.encode(sourceIdx - previousSource);\n previousSource = sourceIdx;\n\n // lines are stored 0-based in SourceMap spec version 3\n next += base64VLQ.encode(mapping.originalLine - 1\n - previousOriginalLine);\n previousOriginalLine = mapping.originalLine - 1;\n\n next += base64VLQ.encode(mapping.originalColumn\n - previousOriginalColumn);\n previousOriginalColumn = mapping.originalColumn;\n\n if (mapping.name != null) {\n nameIdx = this._names.indexOf(mapping.name);\n next += base64VLQ.encode(nameIdx - previousName);\n previousName = nameIdx;\n }\n }\n\n result += next;\n }\n\n return result;\n };\n\nSourceMapGenerator.prototype._generateSourcesContent =\n function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {\n return aSources.map(function (source) {\n if (!this._sourcesContents) {\n return null;\n }\n if (aSourceRoot != null) {\n source = util.relative(aSourceRoot, source);\n }\n var key = util.toSetString(source);\n return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)\n ? this._sourcesContents[key]\n : null;\n }, this);\n };\n\n/**\n * Externalize the source map.\n */\nSourceMapGenerator.prototype.toJSON =\n function SourceMapGenerator_toJSON() {\n var map = {\n version: this._version,\n sources: this._sources.toArray(),\n names: this._names.toArray(),\n mappings: this._serializeMappings()\n };\n if (this._file != null) {\n map.file = this._file;\n }\n if (this._sourceRoot != null) {\n map.sourceRoot = this._sourceRoot;\n }\n if (this._sourcesContents) {\n map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);\n }\n\n return map;\n };\n\n/**\n * Render the source map being generated to a string.\n */\nSourceMapGenerator.prototype.toString =\n function SourceMapGenerator_toString() {\n return JSON.stringify(this.toJSON());\n };\n\nexports.SourceMapGenerator = SourceMapGenerator;\n"],"x_google_ignoreList":[0,1,2,3,4,5],"mappings":"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"} |
| //#region src/core/ast-scanner/svelte-parser.ts | ||
| let _compiler = null; | ||
| async function loadCompiler() { | ||
| if (_compiler) return _compiler; | ||
| try { | ||
| _compiler = await import("./compiler-CNzm2Y2I.mjs"); | ||
| return _compiler; | ||
| } catch { | ||
| throw new Error("svelte is required to parse .svelte files. Install it with: pnpm add -D svelte"); | ||
| } | ||
| } | ||
| let _tsxParser = null; | ||
| async function loadTsxParser() { | ||
| if (_tsxParser) return _tsxParser; | ||
| try { | ||
| _tsxParser = (await import("./tsx-parser-C3XsIrwU.mjs")).parseTsx; | ||
| return _tsxParser; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| const SURROUNDING_MAX = 120; | ||
| /** Attributes whose values are CSS, not content */ | ||
| const CSS_ATTRIBUTES = new Set(["class", "style"]); | ||
| /** Directive types that contain code, not content */ | ||
| const CODE_DIRECTIVE_TYPES = new Set([ | ||
| "EventHandler", | ||
| "Binding", | ||
| "Action", | ||
| "Class", | ||
| "StyleDirective", | ||
| "Transition", | ||
| "Animation", | ||
| "Let", | ||
| "Ref" | ||
| ]); | ||
| function getLineAndColumn(content, offset) { | ||
| let line = 1; | ||
| let lastNewline = -1; | ||
| for (let i = 0; i < offset && i < content.length; i++) if (content[i] === "\n") { | ||
| line++; | ||
| lastNewline = i; | ||
| } | ||
| return { | ||
| line, | ||
| column: offset - lastNewline | ||
| }; | ||
| } | ||
| function getSurroundingByLine(content, line) { | ||
| const lines = content.split("\n"); | ||
| const idx = line - 1; | ||
| const start = Math.max(0, idx - 1); | ||
| const end = Math.min(lines.length - 1, idx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const l = lines[i]; | ||
| if (l !== void 0) parts.push(l); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| function walkTemplate(node, content, results, parentTag = "") { | ||
| switch (node.type) { | ||
| case "Fragment": { | ||
| const fragment = node; | ||
| for (const child of fragment.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "Text": { | ||
| const textNode = node; | ||
| const trimmed = textNode.data.trim(); | ||
| if (trimmed.length > 0 && /\S/.test(trimmed)) { | ||
| const pos = getLineAndColumn(content, textNode.start); | ||
| results.push({ | ||
| value: trimmed, | ||
| line: pos.line, | ||
| column: pos.column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(content, pos.line) | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| case "Element": | ||
| case "InlineComponent": | ||
| case "SlotTemplate": | ||
| case "Slot": | ||
| case "Head": | ||
| case "Title": | ||
| case "Window": | ||
| case "Document": | ||
| case "Body": { | ||
| const el = node; | ||
| const tag = el.name; | ||
| for (const attr of el.attributes) processAttribute(attr, tag, content, results); | ||
| for (const child of el.children) walkTemplate(child, content, results, tag); | ||
| break; | ||
| } | ||
| case "IfBlock": { | ||
| const ifBlock = node; | ||
| for (const child of ifBlock.children) walkTemplate(child, content, results, parentTag); | ||
| if (ifBlock.else) walkTemplate(ifBlock.else, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "ElseBlock": { | ||
| const elseBlock = node; | ||
| for (const child of elseBlock.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "EachBlock": { | ||
| const eachBlock = node; | ||
| for (const child of eachBlock.children) walkTemplate(child, content, results, parentTag); | ||
| if (eachBlock.else) walkTemplate(eachBlock.else, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "AwaitBlock": { | ||
| const awaitBlock = node; | ||
| if (awaitBlock.pending) walkTemplate(awaitBlock.pending, content, results, parentTag); | ||
| if (awaitBlock.then) walkTemplate(awaitBlock.then, content, results, parentTag); | ||
| if (awaitBlock.catch) walkTemplate(awaitBlock.catch, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "KeyBlock": { | ||
| const keyBlock = node; | ||
| for (const child of keyBlock.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "MustacheTag": | ||
| case "RawMustacheTag": break; | ||
| default: { | ||
| const unknownNode = node; | ||
| if (unknownNode.children) for (const child of unknownNode.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| function processAttribute(attr, parentTag, content, results) { | ||
| if (CODE_DIRECTIVE_TYPES.has(attr.type)) return; | ||
| if (attr.type !== "Attribute") return; | ||
| const attribute = attr; | ||
| const attrName = attribute.name; | ||
| if (!attribute.value || attribute.value.length === 0) return; | ||
| for (const valuePart of attribute.value) { | ||
| if (valuePart.type !== "Text") continue; | ||
| const textValue = valuePart.data; | ||
| if (!textValue || textValue.trim().length === 0) continue; | ||
| const pos = getLineAndColumn(content, valuePart.start); | ||
| if (CSS_ATTRIBUTES.has(attrName)) { | ||
| results.push({ | ||
| value: textValue, | ||
| line: pos.line, | ||
| column: pos.column, | ||
| context: "css_class", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, pos.line) | ||
| }); | ||
| continue; | ||
| } | ||
| results.push({ | ||
| value: textValue, | ||
| line: pos.line, | ||
| column: pos.column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, pos.line) | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Resolve script filename with correct extension for TypeScript parser. | ||
| * Svelte files with <script lang="ts"> need ScriptKind.TS, not ScriptKind.JS. | ||
| */ | ||
| function resolveScriptFileName(svelteFileName, lang) { | ||
| if (lang === "ts" || lang === "typescript") return svelteFileName.replace(/\.svelte$/, ".ts"); | ||
| return svelteFileName.replace(/\.svelte$/, ".js"); | ||
| } | ||
| function parseScriptBlock(scriptContent, scriptStartOffset, fullContent, fileName, parseTsx, lang) { | ||
| const scriptResults = parseTsx(scriptContent, resolveScriptFileName(fileName, lang)); | ||
| const scriptStartLine = getLineAndColumn(fullContent, scriptStartOffset).line; | ||
| return scriptResults.map((r) => { | ||
| r.line = r.line + scriptStartLine - 1; | ||
| r.scope = "script"; | ||
| return r; | ||
| }); | ||
| } | ||
| async function parseSvelte(content, fileName) { | ||
| const compiler = await loadCompiler(); | ||
| const results = []; | ||
| let ast; | ||
| try { | ||
| ast = compiler.parse(content, { filename: fileName }); | ||
| } catch { | ||
| return []; | ||
| } | ||
| if (ast.html) walkTemplate(ast.html, content, results); | ||
| const tsxParser = await loadTsxParser(); | ||
| if (tsxParser) { | ||
| if (ast.instance) { | ||
| const scriptStart = ast.instance.start; | ||
| const scriptEnd = ast.instance.end; | ||
| const scriptSource = content.slice(scriptStart, scriptEnd); | ||
| const scriptContentMatch = scriptSource.match(/<script[^>]*>([\s\S]*?)<\/script>/); | ||
| if (scriptContentMatch?.[1]) { | ||
| const scriptContentStr = scriptContentMatch[1]; | ||
| const scriptLang = scriptSource.match(/<script[^>]*\slang=["'](\w+)["']/)?.[1]; | ||
| const scriptResults = parseScriptBlock(scriptContentStr, scriptStart + (scriptSource.indexOf(">") + 1), content, fileName, tsxParser, scriptLang); | ||
| results.push(...scriptResults); | ||
| } | ||
| } | ||
| if (ast.module) { | ||
| const moduleStart = ast.module.start; | ||
| const moduleEnd = ast.module.end; | ||
| const moduleSource = content.slice(moduleStart, moduleEnd); | ||
| const moduleContentMatch = moduleSource.match(/<script[^>]*>([\s\S]*?)<\/script>/); | ||
| if (moduleContentMatch?.[1]) { | ||
| const moduleContentStr = moduleContentMatch[1]; | ||
| const moduleLang = moduleSource.match(/<script[^>]*\slang=["'](\w+)["']/)?.[1]; | ||
| const moduleResults = parseScriptBlock(moduleContentStr, moduleStart + (moduleSource.indexOf(">") + 1), content, fileName, tsxParser, moduleLang); | ||
| results.push(...moduleResults); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| //#endregion | ||
| export { parseSvelte }; | ||
| //# sourceMappingURL=svelte-parser-DaFK9iY0.mjs.map |
| {"version":3,"file":"svelte-parser-DaFK9iY0.mjs","names":[],"sources":["../src/core/ast-scanner/svelte-parser.ts"],"sourcesContent":["// ─── Svelte SFC Parser for Scanner v2 ───\n// Parses .svelte Single File Components using svelte/compiler.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\nimport type { ExtractedString } from './types.js'\n\n// ─── Lazy-loaded svelte/compiler ───\n\ninterface _SvelteLoc {\n start: number\n end: number\n line: number\n column: number\n}\n\n// Svelte AST node types from svelte/compiler parse()\ninterface SvelteBaseNode {\n type: string\n start: number\n end: number\n}\n\ninterface SvelteFragment extends SvelteBaseNode {\n type: 'Fragment'\n children: SvelteNode[]\n}\n\ninterface SvelteElement extends SvelteBaseNode {\n type: 'Element' | 'InlineComponent' | 'SlotTemplate' | 'Slot' | 'Head' | 'Title' | 'Window' | 'Document' | 'Body'\n name: string\n attributes: SvelteAttribute[]\n children: SvelteNode[]\n}\n\ninterface SvelteText extends SvelteBaseNode {\n type: 'Text'\n data: string\n raw: string\n}\n\ninterface SvelteAttribute extends SvelteBaseNode {\n type: 'Attribute'\n name: string\n value: SvelteAttributeValue[]\n}\n\ninterface SvelteAttributeText extends SvelteBaseNode {\n type: 'Text'\n data: string\n raw: string\n}\n\ntype SvelteAttributeValue = SvelteAttributeText | SvelteMustacheTag | SvelteBaseNode\n\ninterface _SvelteSpread extends SvelteBaseNode {\n type: 'Spread'\n}\n\ninterface SvelteMustacheTag extends SvelteBaseNode {\n type: 'MustacheTag'\n expression: SvelteBaseNode\n}\n\ninterface SvelteIfBlock extends SvelteBaseNode {\n type: 'IfBlock'\n expression: SvelteBaseNode\n children: SvelteNode[]\n else?: SvelteElseBlock\n}\n\ninterface SvelteElseBlock extends SvelteBaseNode {\n type: 'ElseBlock'\n children: SvelteNode[]\n}\n\ninterface SvelteEachBlock extends SvelteBaseNode {\n type: 'EachBlock'\n expression: SvelteBaseNode\n children: SvelteNode[]\n else?: SvelteElseBlock\n}\n\ninterface SvelteAwaitBlock extends SvelteBaseNode {\n type: 'AwaitBlock'\n pending: SvelteFragment | null\n then: SvelteFragment | null\n catch: SvelteFragment | null\n}\n\ninterface SvelteKeyBlock extends SvelteBaseNode {\n type: 'KeyBlock'\n children: SvelteNode[]\n}\n\ninterface SvelteRawMustacheTag extends SvelteBaseNode {\n type: 'RawMustacheTag'\n expression: SvelteBaseNode\n}\n\ninterface SvelteDirective extends SvelteBaseNode {\n type: 'EventHandler' | 'Binding' | 'Action' | 'Class' | 'StyleDirective' | 'Transition' | 'Animation' | 'Let' | 'Ref'\n name: string\n}\n\ninterface SvelteScript extends SvelteBaseNode {\n type: 'Script'\n content: string\n context?: string // \"module\" for <script context=\"module\">\n}\n\ninterface SvelteStyle extends SvelteBaseNode {\n type: 'Style'\n}\n\ntype SvelteNode =\n | SvelteFragment\n | SvelteElement\n | SvelteText\n | SvelteMustacheTag\n | SvelteIfBlock\n | SvelteEachBlock\n | SvelteAwaitBlock\n | SvelteKeyBlock\n | SvelteRawMustacheTag\n | SvelteBaseNode\n\ninterface SvelteAst {\n html: SvelteFragment\n instance?: SvelteScript\n module?: SvelteScript\n css?: SvelteStyle\n}\n\ninterface SvelteCompiler {\n parse: (source: string, options?: { filename?: string }) => SvelteAst\n}\n\nlet _compiler: SvelteCompiler | null = null\n\nasync function loadCompiler(): Promise<SvelteCompiler> {\n if (_compiler) return _compiler\n try {\n const mod = await import('svelte/compiler')\n _compiler = mod as unknown as SvelteCompiler\n return _compiler\n } catch {\n throw new Error(\n 'svelte is required to parse .svelte files. '\n + 'Install it with: pnpm add -D svelte',\n )\n }\n}\n\n// ─── tsx-parser delegation ───\n\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n return null\n }\n}\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n/** Attributes whose values are CSS, not content */\nconst CSS_ATTRIBUTES = new Set(['class', 'style'])\n\n/** Directive types that contain code, not content */\nconst CODE_DIRECTIVE_TYPES = new Set([\n 'EventHandler', // on:click\n 'Binding', // bind:value\n 'Action', // use:action\n 'Class', // class:name\n 'StyleDirective', // style:color\n 'Transition', // transition:fade\n 'Animation', // animate:flip\n 'Let', // let:item\n 'Ref', // ref (legacy)\n])\n\n// ─── Helpers ───\n\nfunction getLineAndColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let lastNewline = -1\n\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n lastNewline = i\n }\n }\n\n return { line, column: offset - lastNewline }\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n const start = Math.max(0, idx - 1)\n const end = Math.min(lines.length - 1, idx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const l = lines[i]\n if (l !== undefined) {\n parts.push(l)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: SvelteNode,\n content: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n const nodeType = node.type\n\n switch (nodeType) {\n case 'Fragment': {\n const fragment = node as SvelteFragment\n for (const child of fragment.children) {\n walkTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'Text': {\n const textNode = node as SvelteText\n const trimmed = textNode.data.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n const pos = getLineAndColumn(content, textNode.start)\n results.push({\n value: trimmed,\n line: pos.line,\n column: pos.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(content, pos.line),\n })\n }\n break\n }\n\n case 'Element':\n case 'InlineComponent':\n case 'SlotTemplate':\n case 'Slot':\n case 'Head':\n case 'Title':\n case 'Window':\n case 'Document':\n case 'Body': {\n const el = node as SvelteElement\n const tag = el.name\n\n // Process attributes\n for (const attr of el.attributes) {\n processAttribute(attr, tag, content, results)\n }\n\n // Recurse into children\n for (const child of el.children) {\n walkTemplate(child, content, results, tag)\n }\n break\n }\n\n case 'IfBlock': {\n const ifBlock = node as SvelteIfBlock\n // Skip the expression (code) — walk children for content\n for (const child of ifBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n // Walk else branch\n if (ifBlock.else) {\n walkTemplate(ifBlock.else, content, results, parentTag)\n }\n break\n }\n\n case 'ElseBlock': {\n const elseBlock = node as SvelteElseBlock\n for (const child of elseBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'EachBlock': {\n const eachBlock = node as SvelteEachBlock\n // Skip the expression — walk children for content\n for (const child of eachBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n if (eachBlock.else) {\n walkTemplate(eachBlock.else, content, results, parentTag)\n }\n break\n }\n\n case 'AwaitBlock': {\n const awaitBlock = node as SvelteAwaitBlock\n if (awaitBlock.pending) {\n walkTemplate(awaitBlock.pending, content, results, parentTag)\n }\n if (awaitBlock.then) {\n walkTemplate(awaitBlock.then, content, results, parentTag)\n }\n if (awaitBlock.catch) {\n walkTemplate(awaitBlock.catch, content, results, parentTag)\n }\n break\n }\n\n case 'KeyBlock': {\n const keyBlock = node as SvelteKeyBlock\n for (const child of keyBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'MustacheTag':\n case 'RawMustacheTag': {\n // {expression} or {@html expression} — code expressions, skip\n break\n }\n\n default: {\n // For unknown node types, try to walk children\n const unknownNode = node as SvelteBaseNode & { children?: SvelteNode[] }\n if (unknownNode.children) {\n for (const child of unknownNode.children) {\n walkTemplate(child, content, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processAttribute(\n attr: SvelteAttribute | SvelteDirective | SvelteBaseNode,\n parentTag: string,\n content: string,\n results: ExtractedString[],\n): void {\n // Skip directive types (EventHandler, Binding, etc.) — they contain code\n if (CODE_DIRECTIVE_TYPES.has(attr.type)) return\n\n // Only process regular Attribute nodes\n if (attr.type !== 'Attribute') return\n\n const attribute = attr as SvelteAttribute\n const attrName = attribute.name\n\n // Skip if no value or empty value array\n if (!attribute.value || attribute.value.length === 0) return\n\n // Process each value segment (attribute values can be arrays in Svelte)\n for (const valuePart of attribute.value) {\n if (valuePart.type !== 'Text') continue\n\n const textValue = (valuePart as SvelteAttributeText).data\n if (!textValue || textValue.trim().length === 0) continue\n\n const pos = getLineAndColumn(content, valuePart.start)\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: textValue,\n line: pos.line,\n column: pos.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, pos.line),\n })\n continue\n }\n\n results.push({\n value: textValue,\n line: pos.line,\n column: pos.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, pos.line),\n })\n }\n}\n\n// ─── Script Block Parsing ───\n\n/**\n * Resolve script filename with correct extension for TypeScript parser.\n * Svelte files with <script lang=\"ts\"> need ScriptKind.TS, not ScriptKind.JS.\n */\nfunction resolveScriptFileName(svelteFileName: string, lang?: string): string {\n if (lang === 'ts' || lang === 'typescript') return svelteFileName.replace(/\\.svelte$/, '.ts')\n return svelteFileName.replace(/\\.svelte$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartOffset: number,\n fullContent: string,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n const scriptStartPos = getLineAndColumn(fullContent, scriptStartOffset)\n const scriptStartLine = scriptStartPos.line\n\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseSvelte(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n let ast: SvelteAst\n try {\n ast = compiler.parse(content, { filename: fileName })\n } catch {\n // If Svelte parsing fails, return empty — malformed files shouldn't block scanning\n return []\n }\n\n // ─── Template (html) ───\n if (ast.html) {\n walkTemplate(ast.html, content, results)\n }\n\n // ─── Script Block ───\n const tsxParser = await loadTsxParser()\n if (tsxParser) {\n if (ast.instance) {\n // <script> block — extract its content from source\n const scriptStart = ast.instance.start\n const scriptEnd = ast.instance.end\n\n // Find the content between <script> tags\n const scriptSource = content.slice(scriptStart, scriptEnd)\n const scriptContentMatch = scriptSource.match(/<script[^>]*>([\\s\\S]*?)<\\/script>/)\n if (scriptContentMatch?.[1]) {\n const scriptContentStr = scriptContentMatch[1]\n // Extract lang attribute from <script lang=\"ts\">\n const langMatch = scriptSource.match(/<script[^>]*\\slang=[\"'](\\w+)[\"']/)\n const scriptLang = langMatch?.[1]\n // Calculate offset of script content within the file\n const scriptTagEnd = scriptSource.indexOf('>') + 1\n const contentOffset = scriptStart + scriptTagEnd\n const scriptResults = parseScriptBlock(\n scriptContentStr,\n contentOffset,\n content,\n fileName,\n tsxParser,\n scriptLang,\n )\n results.push(...scriptResults)\n }\n }\n\n if (ast.module) {\n // <script context=\"module\"> block\n const moduleStart = ast.module.start\n const moduleEnd = ast.module.end\n const moduleSource = content.slice(moduleStart, moduleEnd)\n const moduleContentMatch = moduleSource.match(/<script[^>]*>([\\s\\S]*?)<\\/script>/)\n if (moduleContentMatch?.[1]) {\n const moduleContentStr = moduleContentMatch[1]\n const moduleLangMatch = moduleSource.match(/<script[^>]*\\slang=[\"'](\\w+)[\"']/)\n const moduleLang = moduleLangMatch?.[1]\n const scriptTagEnd = moduleSource.indexOf('>') + 1\n const contentOffset = moduleStart + scriptTagEnd\n const moduleResults = parseScriptBlock(\n moduleContentStr,\n contentOffset,\n content,\n fileName,\n tsxParser,\n moduleLang,\n )\n results.push(...moduleResults)\n }\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"} |
| import { t as parseTsx } from "./tsx-parser-md1N0Niu.mjs"; | ||
| export { parseTsx }; |
| import ts from "typescript"; | ||
| //#region src/core/ast-scanner/tsx-parser.ts | ||
| const SURROUNDING_MAX = 120; | ||
| /** | ||
| * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context. | ||
| * | ||
| * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed). | ||
| * Walks the AST and classifies each string by its parent chain. | ||
| */ | ||
| function parseTsx(content, fileName) { | ||
| const scriptKind = getScriptKind(fileName); | ||
| const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind); | ||
| const results = []; | ||
| visit(sourceFile, sourceFile, content.split("\n"), results); | ||
| return results; | ||
| } | ||
| function getScriptKind(fileName) { | ||
| const lower = fileName.toLowerCase(); | ||
| if (lower.endsWith(".tsx")) return ts.ScriptKind.TSX; | ||
| if (lower.endsWith(".jsx")) return ts.ScriptKind.JSX; | ||
| if (lower.endsWith(".ts")) return ts.ScriptKind.TS; | ||
| return ts.ScriptKind.JS; | ||
| } | ||
| function visit(node, sourceFile, lines, results) { | ||
| if (ts.isJsxText(node)) { | ||
| const text = node.text.trim(); | ||
| if (text.length > 0) { | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); | ||
| const parent = getJsxParentTag(node); | ||
| results.push({ | ||
| value: text, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: "jsx_text", | ||
| scope: "script", | ||
| parent, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
| if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { | ||
| const value = node.text; | ||
| if (value.length === 0) return; | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); | ||
| const classification = classifyByParentChain(node, sourceFile); | ||
| results.push({ | ||
| value, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: classification.context, | ||
| scope: "script", | ||
| parent: classification.parent, | ||
| parentProperty: classification.parentProperty, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| return; | ||
| } | ||
| if (ts.isTemplateExpression(node)) { | ||
| extractTemplateParts(node, sourceFile, lines, results); | ||
| return; | ||
| } | ||
| ts.forEachChild(node, (child) => { | ||
| visit(child, sourceFile, lines, results); | ||
| }); | ||
| } | ||
| function extractTemplateParts(node, sourceFile, lines, results) { | ||
| const classification = classifyByParentChain(node, sourceFile); | ||
| const headText = node.head.text; | ||
| if (headText.length > 0) { | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile)); | ||
| results.push({ | ||
| value: headText, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: classification.context, | ||
| scope: "script", | ||
| parent: classification.parent, | ||
| parentProperty: classification.parentProperty, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| } | ||
| for (const span of node.templateSpans) { | ||
| const spanText = span.literal.text; | ||
| if (spanText.length > 0) { | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile)); | ||
| results.push({ | ||
| value: spanText, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: classification.context, | ||
| scope: "script", | ||
| parent: classification.parent, | ||
| parentProperty: classification.parentProperty, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| } | ||
| visit(span.expression, sourceFile, lines, results); | ||
| } | ||
| } | ||
| function classifyByParentChain(node, sourceFile) { | ||
| let current = node.parent; | ||
| while (current) { | ||
| if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) return { | ||
| context: "import_path", | ||
| parent: "import" | ||
| }; | ||
| if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) return { | ||
| context: "import_path", | ||
| parent: "import" | ||
| }; | ||
| if (ts.isCallExpression(current) && ts.isIdentifier(current.expression) && current.expression.text === "require") return { | ||
| context: "import_path", | ||
| parent: "require" | ||
| }; | ||
| if (ts.isJsxAttribute(current)) { | ||
| const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile); | ||
| if (attrName === "className" || attrName === "class" || attrName === "style") return { | ||
| context: "css_class", | ||
| parent: attrName | ||
| }; | ||
| return { | ||
| context: "jsx_attribute", | ||
| parent: attrName, | ||
| parentProperty: attrName | ||
| }; | ||
| } | ||
| if (ts.isVariableDeclaration(current)) return { | ||
| context: "variable_assignment", | ||
| parent: ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile) | ||
| }; | ||
| if (ts.isPropertyAssignment(current)) { | ||
| const key = ts.isIdentifier(current.name) ? current.name.text : ts.isStringLiteral(current.name) ? current.name.text : current.name.getText(sourceFile); | ||
| return { | ||
| context: "object_property", | ||
| parent: key, | ||
| parentProperty: key | ||
| }; | ||
| } | ||
| if (ts.isEnumMember(current)) return { | ||
| context: "enum_value", | ||
| parent: ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile) | ||
| }; | ||
| if (ts.isCallExpression(current)) { | ||
| const callee = getCalleeName(current.expression, sourceFile); | ||
| if ([ | ||
| "cn", | ||
| "clsx", | ||
| "classNames", | ||
| "twMerge", | ||
| "twJoin", | ||
| "cva", | ||
| "cx" | ||
| ].includes(callee)) return { | ||
| context: "css_utility_call", | ||
| parent: callee | ||
| }; | ||
| if (callee.startsWith("console.")) return { | ||
| context: "console_call", | ||
| parent: callee | ||
| }; | ||
| if ([ | ||
| "describe", | ||
| "it", | ||
| "test", | ||
| "expect", | ||
| "beforeEach", | ||
| "afterEach", | ||
| "beforeAll", | ||
| "afterAll" | ||
| ].includes(callee)) return { | ||
| context: "test_assertion", | ||
| parent: callee | ||
| }; | ||
| return { | ||
| context: "function_argument", | ||
| parent: callee | ||
| }; | ||
| } | ||
| if (ts.isArrayLiteralExpression(current)) return { | ||
| context: "array_element", | ||
| parent: "array" | ||
| }; | ||
| if (ts.isCaseClause(current)) return { | ||
| context: "switch_case", | ||
| parent: "case" | ||
| }; | ||
| if (ts.isTypeAliasDeclaration(current) || ts.isTypeReferenceNode(current) || ts.isInterfaceDeclaration(current) || ts.isTypeLiteralNode(current) || ts.isLiteralTypeNode(current) || ts.isUnionTypeNode(current) || ts.isIntersectionTypeNode(current)) return { | ||
| context: "type_annotation", | ||
| parent: "type" | ||
| }; | ||
| if (ts.isPropertySignature(current)) return { | ||
| context: "type_annotation", | ||
| parent: "type" | ||
| }; | ||
| current = current.parent; | ||
| } | ||
| return { | ||
| context: "other", | ||
| parent: "" | ||
| }; | ||
| } | ||
| function getCalleeName(expr, sourceFile) { | ||
| if (ts.isIdentifier(expr)) return expr.text; | ||
| if (ts.isPropertyAccessExpression(expr)) { | ||
| const obj = getCalleeName(expr.expression, sourceFile); | ||
| return obj ? `${obj}.${expr.name.text}` : expr.name.text; | ||
| } | ||
| return expr.getText(sourceFile); | ||
| } | ||
| function getJsxParentTag(node) { | ||
| let current = node.parent; | ||
| while (current) { | ||
| if (ts.isJsxElement(current)) return current.openingElement.tagName.getText(); | ||
| if (ts.isJsxFragment(current)) return "Fragment"; | ||
| current = current.parent; | ||
| } | ||
| return ""; | ||
| } | ||
| function buildSurrounding(lines, lineIdx) { | ||
| const start = Math.max(0, lineIdx - 1); | ||
| const end = Math.min(lines.length - 1, lineIdx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const line = lines[i]; | ||
| if (line !== void 0) parts.push(line); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| //#endregion | ||
| export { parseTsx as t }; | ||
| //# sourceMappingURL=tsx-parser-md1N0Niu.mjs.map |
| {"version":3,"file":"tsx-parser-md1N0Niu.mjs","names":[],"sources":["../src/core/ast-scanner/tsx-parser.ts"],"sourcesContent":["import ts from 'typescript'\nimport type { ExtractedString, StructuralContext } from './types.js'\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n// ─── Public API ───\n\n/**\n * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context.\n *\n * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed).\n * Walks the AST and classifies each string by its parent chain.\n */\nexport function parseTsx(content: string, fileName: string): ExtractedString[] {\n const scriptKind = getScriptKind(fileName)\n const sourceFile = ts.createSourceFile(\n fileName,\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n scriptKind,\n )\n\n const results: ExtractedString[] = []\n const lines = content.split('\\n')\n\n visit(sourceFile, sourceFile, lines, results)\n\n return results\n}\n\n// ─── Script kind detection ───\n\nfunction getScriptKind(fileName: string): ts.ScriptKind {\n const lower = fileName.toLowerCase()\n if (lower.endsWith('.tsx')) return ts.ScriptKind.TSX\n if (lower.endsWith('.jsx')) return ts.ScriptKind.JSX\n if (lower.endsWith('.ts')) return ts.ScriptKind.TS\n return ts.ScriptKind.JS\n}\n\n// ─── AST Walker ───\n\nfunction visit(\n node: ts.Node,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n // Handle JsxText nodes\n if (ts.isJsxText(node)) {\n const text = node.text.trim()\n // Skip whitespace-only or empty JsxText\n if (text.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const parent = getJsxParentTag(node)\n results.push({\n value: text,\n line: lineIdx + 1,\n column: character + 1,\n context: 'jsx_text',\n scope: 'script',\n parent,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n return\n }\n\n // Handle string literals\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {\n const value = node.text\n // Skip empty strings\n if (value.length === 0) return\n\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const classification = classifyByParentChain(node, sourceFile)\n\n results.push({\n value,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n return\n }\n\n // Handle template literals with expressions — extract static head/spans\n if (ts.isTemplateExpression(node)) {\n extractTemplateParts(node, sourceFile, lines, results)\n return\n }\n\n ts.forEachChild(node, (child) => {\n visit(child, sourceFile, lines, results)\n })\n}\n\n// ─── Template literal parts extraction ───\n\nfunction extractTemplateParts(\n node: ts.TemplateExpression,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n const classification = classifyByParentChain(node, sourceFile)\n\n // Head: text before first ${...}\n const headText = node.head.text\n if (headText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile))\n results.push({\n value: headText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Template spans: text after each ${...} expression\n for (const span of node.templateSpans) {\n const spanText = span.literal.text\n if (spanText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile))\n results.push({\n value: spanText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Visit the expression inside ${...} for nested strings\n visit(span.expression, sourceFile, lines, results)\n }\n}\n\n// ─── Parent chain classification ───\n\ninterface Classification {\n context: StructuralContext\n parent: string\n parentProperty?: string\n}\n\nfunction classifyByParentChain(node: ts.Node, sourceFile: ts.SourceFile): Classification {\n let current = node.parent\n\n while (current) {\n // Import / Export declaration → import_path\n if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // Import specifier module path (dynamic import)\n if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // require() calls\n if (\n ts.isCallExpression(current)\n && ts.isIdentifier(current.expression)\n && current.expression.text === 'require'\n ) {\n return { context: 'import_path', parent: 'require' }\n }\n\n // JSX attribute → jsx_attribute or css_class\n if (ts.isJsxAttribute(current)) {\n const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n // className / class / style → CSS, not content\n if (attrName === 'className' || attrName === 'class' || attrName === 'style') {\n return { context: 'css_class', parent: attrName }\n }\n return { context: 'jsx_attribute', parent: attrName, parentProperty: attrName }\n }\n\n // Variable declaration → variable_assignment\n if (ts.isVariableDeclaration(current)) {\n const varName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'variable_assignment', parent: varName }\n }\n\n // Property assignment → object_property\n if (ts.isPropertyAssignment(current)) {\n const key = ts.isIdentifier(current.name)\n ? current.name.text\n : ts.isStringLiteral(current.name)\n ? current.name.text\n : current.name.getText(sourceFile)\n return { context: 'object_property', parent: key, parentProperty: key }\n }\n\n // Enum member → enum_value\n if (ts.isEnumMember(current)) {\n const enumName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'enum_value', parent: enumName }\n }\n\n // Call expression → function_argument or css_utility_call or console_call or test_assertion\n if (ts.isCallExpression(current)) {\n const callee = getCalleeName(current.expression, sourceFile)\n // CSS utility functions\n if (['cn', 'clsx', 'classNames', 'twMerge', 'twJoin', 'cva', 'cx'].includes(callee)) {\n return { context: 'css_utility_call', parent: callee }\n }\n // Console calls\n if (callee.startsWith('console.')) {\n return { context: 'console_call', parent: callee }\n }\n // Test assertions\n if (['describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(callee)) {\n return { context: 'test_assertion', parent: callee }\n }\n return { context: 'function_argument', parent: callee }\n }\n\n // Array literal → array_element\n if (ts.isArrayLiteralExpression(current)) {\n return { context: 'array_element', parent: 'array' }\n }\n\n // Case clause → switch_case\n if (ts.isCaseClause(current)) {\n return { context: 'switch_case', parent: 'case' }\n }\n\n // Type contexts → type_annotation\n if (\n ts.isTypeAliasDeclaration(current)\n || ts.isTypeReferenceNode(current)\n || ts.isInterfaceDeclaration(current)\n || ts.isTypeLiteralNode(current)\n || ts.isLiteralTypeNode(current)\n || ts.isUnionTypeNode(current)\n || ts.isIntersectionTypeNode(current)\n ) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n // Property declaration with type context (e.g., `as const` typed properties)\n // Check if we're inside a type annotation specifically\n if (ts.isPropertySignature(current)) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n current = current.parent\n }\n\n return { context: 'other', parent: '' }\n}\n\n// ─── Callee name extraction ───\n\nfunction getCalleeName(expr: ts.Expression, sourceFile: ts.SourceFile): string {\n if (ts.isIdentifier(expr)) {\n return expr.text\n }\n if (ts.isPropertyAccessExpression(expr)) {\n // e.g., console.log → \"console.log\"\n const obj = getCalleeName(expr.expression, sourceFile)\n return obj ? `${obj}.${expr.name.text}` : expr.name.text\n }\n return expr.getText(sourceFile)\n}\n\n// ─── JSX parent tag extraction ───\n\nfunction getJsxParentTag(node: ts.Node): string {\n let current = node.parent\n\n while (current) {\n if (ts.isJsxElement(current)) {\n const tagName = current.openingElement.tagName.getText()\n return tagName\n }\n if (ts.isJsxFragment(current)) {\n return 'Fragment'\n }\n current = current.parent\n }\n\n return ''\n}\n\n// ─── Surrounding code builder ───\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n"],"mappings":";;AAKA,MAAM,kBAAkB;;;;;;;AAUxB,SAAgB,SAAS,SAAiB,UAAqC;CAC7E,MAAM,aAAa,cAAc,SAAS;CAC1C,MAAM,aAAa,GAAG,iBACpB,UACA,SACA,GAAG,aAAa,QACK,MACrB,WACD;CAED,MAAM,UAA6B,EAAE;AAGrC,OAAM,YAAY,YAFJ,QAAQ,MAAM,KAAK,EAEI,QAAQ;AAE7C,QAAO;;AAKT,SAAS,cAAc,UAAiC;CACtD,MAAM,QAAQ,SAAS,aAAa;AACpC,KAAI,MAAM,SAAS,OAAO,CAAE,QAAO,GAAG,WAAW;AACjD,KAAI,MAAM,SAAS,OAAO,CAAE,QAAO,GAAG,WAAW;AACjD,KAAI,MAAM,SAAS,MAAM,CAAE,QAAO,GAAG,WAAW;AAChD,QAAO,GAAG,WAAW;;AAKvB,SAAS,MACP,MACA,YACA,OACA,SACM;AAEN,KAAI,GAAG,UAAU,KAAK,EAAE;EACtB,MAAM,OAAO,KAAK,KAAK,MAAM;AAE7B,MAAI,KAAK,SAAS,GAAG;GACnB,MAAM,EAAE,MAAM,SAAS,cAAc,WAAW,8BAA8B,KAAK,SAAS,WAAW,CAAC;GACxG,MAAM,SAAS,gBAAgB,KAAK;AACpC,WAAQ,KAAK;IACX,OAAO;IACP,MAAM,UAAU;IAChB,QAAQ,YAAY;IACpB,SAAS;IACT,OAAO;IACP;IACA,aAAa,iBAAiB,OAAO,QAAQ;IAC9C,CAAC;;AAEJ;;AAIF,KAAI,GAAG,gBAAgB,KAAK,IAAI,GAAG,gCAAgC,KAAK,EAAE;EACxE,MAAM,QAAQ,KAAK;AAEnB,MAAI,MAAM,WAAW,EAAG;EAExB,MAAM,EAAE,MAAM,SAAS,cAAc,WAAW,8BAA8B,KAAK,SAAS,WAAW,CAAC;EACxG,MAAM,iBAAiB,sBAAsB,MAAM,WAAW;AAE9D,UAAQ,KAAK;GACX;GACA,MAAM,UAAU;GAChB,QAAQ,YAAY;GACpB,SAAS,eAAe;GACxB,OAAO;GACP,QAAQ,eAAe;GACvB,gBAAgB,eAAe;GAC/B,aAAa,iBAAiB,OAAO,QAAQ;GAC9C,CAAC;AACF;;AAIF,KAAI,GAAG,qBAAqB,KAAK,EAAE;AACjC,uBAAqB,MAAM,YAAY,OAAO,QAAQ;AACtD;;AAGF,IAAG,aAAa,OAAO,UAAU;AAC/B,QAAM,OAAO,YAAY,OAAO,QAAQ;GACxC;;AAKJ,SAAS,qBACP,MACA,YACA,OACA,SACM;CACN,MAAM,iBAAiB,sBAAsB,MAAM,WAAW;CAG9D,MAAM,WAAW,KAAK,KAAK;AAC3B,KAAI,SAAS,SAAS,GAAG;EACvB,MAAM,EAAE,MAAM,SAAS,cAAc,WAAW,8BAA8B,KAAK,KAAK,SAAS,WAAW,CAAC;AAC7G,UAAQ,KAAK;GACX,OAAO;GACP,MAAM,UAAU;GAChB,QAAQ,YAAY;GACpB,SAAS,eAAe;GACxB,OAAO;GACP,QAAQ,eAAe;GACvB,gBAAgB,eAAe;GAC/B,aAAa,iBAAiB,OAAO,QAAQ;GAC9C,CAAC;;AAIJ,MAAK,MAAM,QAAQ,KAAK,eAAe;EACrC,MAAM,WAAW,KAAK,QAAQ;AAC9B,MAAI,SAAS,SAAS,GAAG;GACvB,MAAM,EAAE,MAAM,SAAS,cAAc,WAAW,8BAA8B,KAAK,QAAQ,SAAS,WAAW,CAAC;AAChH,WAAQ,KAAK;IACX,OAAO;IACP,MAAM,UAAU;IAChB,QAAQ,YAAY;IACpB,SAAS,eAAe;IACxB,OAAO;IACP,QAAQ,eAAe;IACvB,gBAAgB,eAAe;IAC/B,aAAa,iBAAiB,OAAO,QAAQ;IAC9C,CAAC;;AAIJ,QAAM,KAAK,YAAY,YAAY,OAAO,QAAQ;;;AAYtD,SAAS,sBAAsB,MAAe,YAA2C;CACvF,IAAI,UAAU,KAAK;AAEnB,QAAO,SAAS;AAEd,MAAI,GAAG,oBAAoB,QAAQ,IAAI,GAAG,oBAAoB,QAAQ,CACpE,QAAO;GAAE,SAAS;GAAe,QAAQ;GAAU;AAIrD,MAAI,GAAG,iBAAiB,QAAQ,IAAI,QAAQ,WAAW,SAAS,GAAG,WAAW,cAC5E,QAAO;GAAE,SAAS;GAAe,QAAQ;GAAU;AAIrD,MACE,GAAG,iBAAiB,QAAQ,IACzB,GAAG,aAAa,QAAQ,WAAW,IACnC,QAAQ,WAAW,SAAS,UAE/B,QAAO;GAAE,SAAS;GAAe,QAAQ;GAAW;AAItD,MAAI,GAAG,eAAe,QAAQ,EAAE;GAC9B,MAAM,WAAW,GAAG,aAAa,QAAQ,KAAK,GAAG,QAAQ,KAAK,OAAO,QAAQ,KAAK,QAAQ,WAAW;AAErG,OAAI,aAAa,eAAe,aAAa,WAAW,aAAa,QACnE,QAAO;IAAE,SAAS;IAAa,QAAQ;IAAU;AAEnD,UAAO;IAAE,SAAS;IAAiB,QAAQ;IAAU,gBAAgB;IAAU;;AAIjF,MAAI,GAAG,sBAAsB,QAAQ,CAEnC,QAAO;GAAE,SAAS;GAAuB,QADzB,GAAG,aAAa,QAAQ,KAAK,GAAG,QAAQ,KAAK,OAAO,QAAQ,KAAK,QAAQ,WAAW;GAC1C;AAI5D,MAAI,GAAG,qBAAqB,QAAQ,EAAE;GACpC,MAAM,MAAM,GAAG,aAAa,QAAQ,KAAK,GACrC,QAAQ,KAAK,OACb,GAAG,gBAAgB,QAAQ,KAAK,GAC9B,QAAQ,KAAK,OACb,QAAQ,KAAK,QAAQ,WAAW;AACtC,UAAO;IAAE,SAAS;IAAmB,QAAQ;IAAK,gBAAgB;IAAK;;AAIzE,MAAI,GAAG,aAAa,QAAQ,CAE1B,QAAO;GAAE,SAAS;GAAc,QADf,GAAG,aAAa,QAAQ,KAAK,GAAG,QAAQ,KAAK,OAAO,QAAQ,KAAK,QAAQ,WAAW;GACnD;AAIpD,MAAI,GAAG,iBAAiB,QAAQ,EAAE;GAChC,MAAM,SAAS,cAAc,QAAQ,YAAY,WAAW;AAE5D,OAAI;IAAC;IAAM;IAAQ;IAAc;IAAW;IAAU;IAAO;IAAK,CAAC,SAAS,OAAO,CACjF,QAAO;IAAE,SAAS;IAAoB,QAAQ;IAAQ;AAGxD,OAAI,OAAO,WAAW,WAAW,CAC/B,QAAO;IAAE,SAAS;IAAgB,QAAQ;IAAQ;AAGpD,OAAI;IAAC;IAAY;IAAM;IAAQ;IAAU;IAAc;IAAa;IAAa;IAAW,CAAC,SAAS,OAAO,CAC3G,QAAO;IAAE,SAAS;IAAkB,QAAQ;IAAQ;AAEtD,UAAO;IAAE,SAAS;IAAqB,QAAQ;IAAQ;;AAIzD,MAAI,GAAG,yBAAyB,QAAQ,CACtC,QAAO;GAAE,SAAS;GAAiB,QAAQ;GAAS;AAItD,MAAI,GAAG,aAAa,QAAQ,CAC1B,QAAO;GAAE,SAAS;GAAe,QAAQ;GAAQ;AAInD,MACE,GAAG,uBAAuB,QAAQ,IAC/B,GAAG,oBAAoB,QAAQ,IAC/B,GAAG,uBAAuB,QAAQ,IAClC,GAAG,kBAAkB,QAAQ,IAC7B,GAAG,kBAAkB,QAAQ,IAC7B,GAAG,gBAAgB,QAAQ,IAC3B,GAAG,uBAAuB,QAAQ,CAErC,QAAO;GAAE,SAAS;GAAmB,QAAQ;GAAQ;AAKvD,MAAI,GAAG,oBAAoB,QAAQ,CACjC,QAAO;GAAE,SAAS;GAAmB,QAAQ;GAAQ;AAGvD,YAAU,QAAQ;;AAGpB,QAAO;EAAE,SAAS;EAAS,QAAQ;EAAI;;AAKzC,SAAS,cAAc,MAAqB,YAAmC;AAC7E,KAAI,GAAG,aAAa,KAAK,CACvB,QAAO,KAAK;AAEd,KAAI,GAAG,2BAA2B,KAAK,EAAE;EAEvC,MAAM,MAAM,cAAc,KAAK,YAAY,WAAW;AACtD,SAAO,MAAM,GAAG,IAAI,GAAG,KAAK,KAAK,SAAS,KAAK,KAAK;;AAEtD,QAAO,KAAK,QAAQ,WAAW;;AAKjC,SAAS,gBAAgB,MAAuB;CAC9C,IAAI,UAAU,KAAK;AAEnB,QAAO,SAAS;AACd,MAAI,GAAG,aAAa,QAAQ,CAE1B,QADgB,QAAQ,eAAe,QAAQ,SAAS;AAG1D,MAAI,GAAG,cAAc,QAAQ,CAC3B,QAAO;AAET,YAAU,QAAQ;;AAGpB,QAAO;;AAKT,SAAS,iBAAiB,OAAiB,SAAyB;CAClE,MAAM,QAAQ,KAAK,IAAI,GAAG,UAAU,EAAE;CACtC,MAAM,MAAM,KAAK,IAAI,MAAM,SAAS,GAAG,UAAU,EAAE;CAEnD,MAAM,QAAkB,EAAE;AAC1B,MAAK,IAAI,IAAI,OAAO,KAAK,KAAK,KAAK;EACjC,MAAM,OAAO,MAAM;AACnB,MAAI,SAAS,KAAA,EACX,OAAM,KAAK,KAAK;;CAIpB,MAAM,SAAS,MAAM,KAAK,KAAK;AAC/B,KAAI,OAAO,SAAS,gBAClB,QAAO,OAAO,MAAM,GAAG,gBAAgB;AAEzC,QAAO"} |
| import { i as metaFilePath, n as contentFilePath, r as documentFilePath, t as contentDirPath } from "./paths-CmVw5Cw2.mjs"; | ||
| import { c as writeText, s as writeJson } from "./fs-DLbVB-Ek.mjs"; | ||
| import { i as writeMeta } from "./meta-manager-CJUiTgP2.mjs"; | ||
| import { t as readConfig } from "./config-oxxgznz7.mjs"; | ||
| import { C as LocalReader, f as parseFrontmatter, g as resolveLocaleStrategy, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs"; | ||
| import { join } from "node:path"; | ||
| import { detectSecrets, validateFieldValue } from "@contentrain/types"; | ||
| import { rm } from "node:fs/promises"; | ||
| //#region src/core/validator/entry.ts | ||
| /** | ||
| * Validate a single content entry against its model's field schema. | ||
| * | ||
| * Merges the rule sets from MCP's legacy `validator.ts` (secret detection, | ||
| * schema validation, unique constraints) with Studio's `content-validation.ts` | ||
| * (email/url heuristics, polymorphic relation structure, nested object and | ||
| * array-of-object recursion). The union is the authoritative per-entry | ||
| * validator — both MCP's project validator and Studio's save path should | ||
| * converge on this function over time. | ||
| * | ||
| * Asynchronous relation-integrity checks (does the referenced entry exist?) | ||
| * live in `relation-integrity.ts` because they require I/O. | ||
| */ | ||
| function validateContent(data, fields, modelId, locale, entryId, ctx) { | ||
| const errors = []; | ||
| for (const [fieldId, def] of Object.entries(fields)) { | ||
| const value = data[fieldId]; | ||
| errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx)); | ||
| } | ||
| return { | ||
| valid: errors.filter((e) => e.severity === "error").length === 0, | ||
| errors | ||
| }; | ||
| } | ||
| /** Bounds `items`-inside-`items` nesting; far above any real schema. */ | ||
| const MAX_FIELD_DEPTH = 10; | ||
| function validateField(value, def, modelId, locale, entryId, fieldId, ctx, depth = 0) { | ||
| const errors = []; | ||
| const errCtx = { | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId | ||
| }; | ||
| if (depth > MAX_FIELD_DEPTH) return [{ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} exceeds the maximum nesting depth of ${MAX_FIELD_DEPTH}` | ||
| }]; | ||
| if (value !== null && value !== void 0 && value !== "") { | ||
| const secretErrors = detectSecrets(value); | ||
| for (const e of secretErrors) errors.push({ | ||
| ...e, | ||
| ...errCtx | ||
| }); | ||
| } | ||
| if (!(def.type === "relation" || def.type === "relations")) { | ||
| const fieldErrors = validateFieldValue(value, def); | ||
| if (fieldErrors.length > 0) { | ||
| for (const e of fieldErrors) errors.push({ | ||
| ...e, | ||
| ...errCtx | ||
| }); | ||
| if (fieldErrors.some((e) => e.severity === "error")) return errors; | ||
| } | ||
| } else if (def.required && (value === null || value === void 0 || value === "")) { | ||
| errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} is required` | ||
| }); | ||
| return errors; | ||
| } | ||
| if (value === null || value === void 0) return errors; | ||
| if (def.unique && ctx?.allEntries) { | ||
| const valueKey = String(value); | ||
| for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) { | ||
| if (otherId === ctx.currentEntryId) continue; | ||
| const otherValue = otherEntry[fieldId]; | ||
| if (otherValue !== null && otherValue !== void 0 && String(otherValue) === valueKey) { | ||
| errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be unique — "${String(value)}" already exists in entry ${otherId}` | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (def.type === "relation" && def.model) { | ||
| const targets = Array.isArray(def.model) ? def.model : [def.model]; | ||
| if (targets.length > 1) if (typeof value !== "object" || value === null || !("model" in value) || !("ref" in value)) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be { model, ref } for polymorphic relation` | ||
| }); | ||
| else { | ||
| const polyVal = value; | ||
| if (!targets.includes(polyVal.model)) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} target model "${polyVal.model}" must be one of: ${targets.join(", ")}` | ||
| }); | ||
| } | ||
| else if (typeof value !== "string") errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be a string (entry ID or slug)` | ||
| }); | ||
| } | ||
| if (def.type === "relations") if (!Array.isArray(value)) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be an array` | ||
| }); | ||
| else { | ||
| if (def.min !== void 0 && value.length < def.min) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must have at least ${def.min} items` | ||
| }); | ||
| if (def.max !== void 0 && value.length > def.max) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must have at most ${def.max} items` | ||
| }); | ||
| const targets = Array.isArray(def.model) ? def.model : def.model ? [def.model] : []; | ||
| const polymorphic = targets.length > 1; | ||
| for (let i = 0; i < value.length; i++) { | ||
| const item = value[i]; | ||
| const itemCtx = { | ||
| ...errCtx, | ||
| field: `${fieldId}[${i}]` | ||
| }; | ||
| if (polymorphic) if (typeof item !== "object" || item === null || !("model" in item) || !("ref" in item)) errors.push({ | ||
| severity: "error", | ||
| ...itemCtx, | ||
| message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` | ||
| }); | ||
| else { | ||
| const polyItem = item; | ||
| if (!targets.includes(polyItem.model)) errors.push({ | ||
| severity: "error", | ||
| ...itemCtx, | ||
| message: `${fieldId}[${i}] target model "${polyItem.model}" must be one of: ${targets.join(", ")}` | ||
| }); | ||
| } | ||
| else if (typeof item !== "string") errors.push({ | ||
| severity: "error", | ||
| ...itemCtx, | ||
| message: `${fieldId}[${i}] must be a string (entry ID or slug)` | ||
| }); | ||
| } | ||
| } | ||
| if (def.type === "array" && Array.isArray(value) && def.items) { | ||
| const itemDef = typeof def.items === "string" ? { type: def.items } : def.items; | ||
| for (let i = 0; i < value.length; i++) errors.push(...validateField(value[i], itemDef, modelId, locale, entryId, `${fieldId}[${i}]`, void 0, depth + 1)); | ||
| } | ||
| if (def.type === "object" && def.fields && typeof value === "object" && value !== null && !Array.isArray(value)) { | ||
| const nested = validateContent(value, def.fields, modelId, locale, entryId, ctx); | ||
| for (const e of nested.errors) errors.push({ | ||
| ...e, | ||
| field: e.field ? `${fieldId}.${e.field}` : fieldId | ||
| }); | ||
| } | ||
| return errors; | ||
| } | ||
| //#endregion | ||
| //#region src/core/validator/relation-integrity.ts | ||
| /** | ||
| * Verify that relation and relations fields reference targets that actually | ||
| * exist. Two severities are supported — Studio's per-save flow emits | ||
| * `warning` (the referenced entry may still be drafted), while MCP's | ||
| * project-wide validator emits `error` and additionally flags missing | ||
| * target models via `resolveTarget`. | ||
| * | ||
| * The loader abstraction keeps this function I/O-agnostic: MCP wires it | ||
| * to filesystem reads through LocalReader, Studio wires it to a | ||
| * `GitProvider.readFile`-backed loader, mocks pass in-memory maps. | ||
| */ | ||
| async function checkRelationIntegrity(data, fields, modelId, locale, entryId, loadContent, opts = {}) { | ||
| const errors = []; | ||
| const severity = opts.severity ?? "warning"; | ||
| const resolve = opts.resolveTarget ?? (async (id, loc) => { | ||
| return { | ||
| exists: true, | ||
| content: await loadContent(id, loc) | ||
| }; | ||
| }); | ||
| for (const [fieldId, def] of Object.entries(fields)) { | ||
| const value = data[fieldId]; | ||
| if (value === null || value === void 0) continue; | ||
| if (def.type === "relation" && def.model) { | ||
| const targets = Array.isArray(def.model) ? def.model : [def.model]; | ||
| if (targets.length > 1 && typeof value === "object" && value !== null) { | ||
| const polyVal = value; | ||
| if (polyVal.model && polyVal.ref) { | ||
| const resolved = await resolve(polyVal.model, locale); | ||
| if (!resolved.exists) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: target model "${polyVal.model}" not found` | ||
| }); | ||
| else if (resolved.content && !(polyVal.ref in resolved.content)) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: "${polyVal.ref}" not found in ${polyVal.model}` | ||
| }); | ||
| } | ||
| } else if (typeof value === "string" && targets[0]) { | ||
| const resolved = await resolve(targets[0], locale); | ||
| if (!resolved.exists) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: target model "${targets[0]}" not found` | ||
| }); | ||
| else if (resolved.content && !(value in resolved.content)) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: "${value}" not found in ${targets[0]}` | ||
| }); | ||
| } | ||
| } | ||
| if (def.type === "relations" && def.model && Array.isArray(value)) { | ||
| const target = Array.isArray(def.model) ? def.model[0] : def.model; | ||
| if (target) { | ||
| const resolved = await resolve(target, locale); | ||
| if (!resolved.exists) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: target model "${target}" not found` | ||
| }); | ||
| else if (resolved.content) { | ||
| for (const ref of value) if (typeof ref === "string" && !(ref in resolved.content)) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: "${ref}" not found in ${target}` | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return errors; | ||
| } | ||
| //#endregion | ||
| //#region src/core/validator/schedule.ts | ||
| /** | ||
| * Validate `publish_at` and `expire_at` meta fields. | ||
| * | ||
| * Rules (matches legacy `validator.ts:validateScheduleFields`): | ||
| * - `publish_at` must parse as a valid Date | ||
| * - `expire_at` must parse as a valid Date | ||
| * - When both are present, `expire_at` must be strictly after `publish_at` | ||
| */ | ||
| function validateScheduleFields(meta, ctx, issues) { | ||
| if (meta.publish_at !== void 0) { | ||
| const d = new Date(meta.publish_at); | ||
| if (Number.isNaN(d.getTime())) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| message: `Invalid publish_at date: "${meta.publish_at}". Must be a valid ISO 8601 date string.` | ||
| }); | ||
| } | ||
| if (meta.expire_at !== void 0) { | ||
| const d = new Date(meta.expire_at); | ||
| if (Number.isNaN(d.getTime())) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| message: `Invalid expire_at date: "${meta.expire_at}". Must be a valid ISO 8601 date string.` | ||
| }); | ||
| } | ||
| if (meta.publish_at !== void 0 && meta.expire_at !== void 0) { | ||
| const pubDate = new Date(meta.publish_at); | ||
| const expDate = new Date(meta.expire_at); | ||
| if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| message: `expire_at ("${meta.expire_at}") must be after publish_at ("${meta.publish_at}").` | ||
| }); | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/core/validator/project.ts | ||
| async function readJsonViaReader(reader, path) { | ||
| try { | ||
| return JSON.parse(await reader.readFile(path)); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function readTextViaReader(reader, path) { | ||
| try { | ||
| return await reader.readFile(path); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks | ||
| * the project's content store via the shared {@link RepoReader}. Mirrors | ||
| * the target-resolution shape the legacy `checkRelation` used — collection | ||
| * targets return their entry object-map, documents return a "slug exists" | ||
| * marker map, singletons and dictionaries return null content so the | ||
| * checker skips key enforcement for them. | ||
| */ | ||
| function buildProjectTargetResolver(reader, config) { | ||
| return async (targetModelId, targetLocale) => { | ||
| const targetModel = await readModel(reader, targetModelId); | ||
| if (!targetModel) return { exists: false }; | ||
| if (targetModel.kind === "document") { | ||
| const slugs = await discoverDocumentSlugs(reader, contentDirPath(targetModel), targetModel); | ||
| return { | ||
| exists: true, | ||
| content: Object.fromEntries(slugs.map((s) => [s, true])) | ||
| }; | ||
| } | ||
| if (targetModel.kind === "singleton" || targetModel.kind === "dictionary") return { | ||
| exists: true, | ||
| content: null | ||
| }; | ||
| const merged = {}; | ||
| const primary = await readJsonViaReader(reader, contentFilePath(targetModel, targetLocale)); | ||
| if (primary) Object.assign(merged, primary); | ||
| if (targetModel.i18n && targetLocale !== config.locales.default) { | ||
| const fallback = await readJsonViaReader(reader, contentFilePath(targetModel, config.locales.default)); | ||
| if (fallback) Object.assign(merged, fallback); | ||
| } | ||
| return { | ||
| exists: true, | ||
| content: merged | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Scan an entry's data fields for detected secrets in UNDECLARED keys — | ||
| * the legacy validator also flagged stray/rogue fields that were not in | ||
| * `model.fields`. `validateContent` only knows about declared fields, so | ||
| * this complementary pass preserves that coverage. | ||
| */ | ||
| function scanUndeclaredFieldsForSecrets(data, declared, ctx, issues) { | ||
| for (const [fieldName, value] of Object.entries(data)) { | ||
| if (declared && fieldName in declared) continue; | ||
| if (detectSecrets(value).length > 0) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| field: fieldName, | ||
| message: `Potential secret detected in field "${fieldName}"` | ||
| }); | ||
| } | ||
| } | ||
| async function validateCollectionModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| const locales = model.i18n ? config.locales.supported : [config.locales.default]; | ||
| const localeEntryIds = {}; | ||
| const allEntryIds = /* @__PURE__ */ new Set(); | ||
| const resolveTarget = buildProjectTargetResolver(reader, config); | ||
| for (const locale of locales) { | ||
| const filePath = contentFilePath(model, locale); | ||
| const data = await readJsonViaReader(reader, filePath); | ||
| if (!data) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| message: `Locale file missing: ${locale}.json` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| await writeJson(join(projectRoot, filePath), {}); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| const ids = new Set(Object.keys(data)); | ||
| localeEntryIds[locale] = ids; | ||
| for (const id of ids) allEntryIds.add(id); | ||
| const keys = Object.keys(data); | ||
| const sorted = [...keys].toSorted(); | ||
| if (keys.join(",") !== sorted.join(",")) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: "Content file keys not in canonical order" | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const resorted = {}; | ||
| for (const key of sorted) resorted[key] = data[key]; | ||
| await writeJson(join(projectRoot, filePath), resorted); | ||
| fixed++; | ||
| } | ||
| } | ||
| for (const [entryId, fields] of Object.entries(data)) { | ||
| entriesChecked++; | ||
| scanUndeclaredFieldsForSecrets(fields, model.fields, { | ||
| model: model.id, | ||
| locale, | ||
| entry: entryId | ||
| }, issues); | ||
| if (!model.fields) continue; | ||
| const entryResult = validateContent(fields, model.fields, model.id, locale, entryId, { | ||
| allEntries: data, | ||
| currentEntryId: entryId | ||
| }); | ||
| issues.push(...entryResult.errors); | ||
| const relationErrors = await checkRelationIntegrity(fields, model.fields, model.id, locale, entryId, async () => null, { | ||
| severity: "error", | ||
| resolveTarget | ||
| }); | ||
| issues.push(...relationErrors); | ||
| } | ||
| } | ||
| if (model.i18n && Object.keys(localeEntryIds).length > 1) { | ||
| const localeKeys = Object.keys(localeEntryIds); | ||
| for (let i = 1; i < localeKeys.length; i++) { | ||
| const locA = localeKeys[0]; | ||
| const locB = localeKeys[i]; | ||
| const idsA = localeEntryIds[locA]; | ||
| const idsB = localeEntryIds[locB]; | ||
| for (const id of idsA) if (!idsB.has(id)) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale: locB, | ||
| entry: id, | ||
| message: `Entry parity: entry "${id}" exists in ${locA} but missing in ${locB}` | ||
| }); | ||
| for (const id of idsB) if (!idsA.has(id)) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale: locA, | ||
| entry: id, | ||
| message: `Entry parity: entry "${id}" exists in ${locB} but missing in ${locA}` | ||
| }); | ||
| } | ||
| } | ||
| const strayResult = await checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix); | ||
| fixed += strayResult.fixed; | ||
| for (const locale of locales) { | ||
| const metaRelPath = metaFilePath(model, locale, config.locales.default); | ||
| const metaData = await readJsonViaReader(reader, metaRelPath); | ||
| const contentData = await readJsonViaReader(reader, contentFilePath(model, locale)) ?? {}; | ||
| if (metaData) for (const metaEntryId of Object.keys(metaData)) { | ||
| validateScheduleFields(metaData[metaEntryId], { | ||
| model: model.id, | ||
| locale, | ||
| entry: metaEntryId | ||
| }, issues); | ||
| if (!(metaEntryId in contentData)) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| entry: metaEntryId, | ||
| message: `Orphan meta: meta entry "${metaEntryId}" exists but content entry missing` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| delete metaData[metaEntryId]; | ||
| const metaAbs = join(projectRoot, metaRelPath); | ||
| if (Object.keys(metaData).length > 0) await writeJson(metaAbs, metaData); | ||
| else await rm(metaAbs, { force: true }); | ||
| fixed++; | ||
| } | ||
| } | ||
| } | ||
| for (const entryId of Object.keys(contentData)) if (!metaData || !(entryId in metaData)) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| entry: entryId, | ||
| message: `Orphan content: entry "${entryId}" has no metadata` | ||
| }); | ||
| if (fix && projectRoot && !strayResult.unresolved) { | ||
| await writeMeta(projectRoot, model, { | ||
| locale, | ||
| entryId, | ||
| defaultLocale: config.locales.default | ||
| }, { | ||
| status: "draft", | ||
| source: "import", | ||
| updated_by: "contentrain-mcp" | ||
| }); | ||
| fixed++; | ||
| } | ||
| } | ||
| if (metaData) { | ||
| const entries = Object.entries(metaData); | ||
| const draftIds = entries.filter(([, m]) => m.status === "draft").map(([id]) => id); | ||
| const publishedCount = entries.filter(([, m]) => m.status === "published").length; | ||
| if (publishedCount > 0 && draftIds.length > 0) issues.push({ | ||
| severity: "notice", | ||
| model: model.id, | ||
| locale, | ||
| message: `Publish-state drift: ${draftIds.length} draft entr${draftIds.length === 1 ? "y" : "ies"} alongside ${publishedCount} published in the same collection — [${draftIds.join(", ")}]. If these were published before, restore them with contentrain_bulk update_status.` | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| /** | ||
| * Flag — and, with `fix`, remediate — meta files a non-i18n model should not have. | ||
| * | ||
| * Such a model keeps all content in one `data.json` and therefore exactly one | ||
| * meta record, at the default locale. Earlier writes derived the meta path from | ||
| * the caller's locale, so saving under a non-default locale left a second meta | ||
| * file, and readers disagreed about which was authoritative. | ||
| * | ||
| * The `fix` remediation is deterministic and never decides a status: | ||
| * - default-locale meta present → the strays are redundant, so delete them | ||
| * (the default-locale record stays authoritative — no status is merged). | ||
| * - default-locale meta absent, exactly one stray → that stray holds the only | ||
| * record, so migrate it to the default path (move) rather than orphan the | ||
| * content. | ||
| * - default-locale meta absent, several strays → which is authoritative is | ||
| * ambiguous, so leave the warning for the agent to resolve by hand. | ||
| * | ||
| * Returns the count of files remediated and whether strays remain unresolved. | ||
| * `unresolved` gates the caller's orphan-content fabrication: while a non-i18n | ||
| * model's meta still lives in a stray, the content is not truly orphaned, so | ||
| * minting a draft default-locale record would both be wrong and set up a trap | ||
| * (a later fix pass would then treat the real stray as redundant and delete it). | ||
| */ | ||
| async function checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix) { | ||
| if (model.i18n) return { | ||
| fixed: 0, | ||
| unresolved: false | ||
| }; | ||
| const metaDir = `.contentrain/meta/${model.id}`; | ||
| const expected = `${config.locales.default}.json`; | ||
| let files; | ||
| try { | ||
| files = await reader.listDirectory(metaDir); | ||
| } catch { | ||
| return { | ||
| fixed: 0, | ||
| unresolved: false | ||
| }; | ||
| } | ||
| const strays = files.filter((f) => f.endsWith(".json") && f !== expected); | ||
| if (strays.length === 0) return { | ||
| fixed: 0, | ||
| unresolved: false | ||
| }; | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| message: `Meta layout mismatch: "${model.id}" has i18n disabled, so its content lives in a single data.json and its meta belongs at ${expected} alone — but [${strays.join(", ")}] also exist. Readers may disagree about which file is authoritative. Run contentrain_validate fix:true to prune the extras (the default-locale meta stays authoritative).` | ||
| }); | ||
| if (!fix || !projectRoot) return { | ||
| fixed: 0, | ||
| unresolved: true | ||
| }; | ||
| if (await readJsonViaReader(reader, `${metaDir}/${expected}`) === null) { | ||
| if (strays.length !== 1) return { | ||
| fixed: 0, | ||
| unresolved: true | ||
| }; | ||
| const stray = strays[0]; | ||
| const content = await readJsonViaReader(reader, `${metaDir}/${stray}`); | ||
| if (content === null) return { | ||
| fixed: 0, | ||
| unresolved: true | ||
| }; | ||
| await writeJson(join(projectRoot, metaDir, expected), content); | ||
| await rm(join(projectRoot, metaDir, stray), { force: true }); | ||
| return { | ||
| fixed: 1, | ||
| unresolved: false | ||
| }; | ||
| } | ||
| await Promise.all(strays.map((stray) => rm(join(projectRoot, metaDir, stray), { force: true }))); | ||
| return { | ||
| fixed: strays.length, | ||
| unresolved: false | ||
| }; | ||
| } | ||
| async function validateSingletonModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) { | ||
| const filePath = contentFilePath(model, locale); | ||
| const data = await readJsonViaReader(reader, filePath); | ||
| if (!data) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| message: `Locale file missing: ${locale}.json` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| await writeJson(join(projectRoot, filePath), {}); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| entriesChecked++; | ||
| scanUndeclaredFieldsForSecrets(data, model.fields, { | ||
| model: model.id, | ||
| locale | ||
| }, issues); | ||
| if (model.fields) { | ||
| const entryResult = validateContent(data, model.fields, model.id, locale); | ||
| issues.push(...entryResult.errors); | ||
| const resolveTarget = buildProjectTargetResolver(reader, config); | ||
| const relationErrors = await checkRelationIntegrity(data, model.fields, model.id, locale, void 0, async () => null, { | ||
| severity: "error", | ||
| resolveTarget | ||
| }); | ||
| issues.push(...relationErrors); | ||
| } | ||
| const keys = Object.keys(data); | ||
| const sorted = [...keys].toSorted(); | ||
| if (keys.join(",") !== sorted.join(",")) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: "Content file keys not in canonical order" | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const resorted = {}; | ||
| for (const key of sorted) resorted[key] = data[key]; | ||
| await writeJson(join(projectRoot, filePath), resorted); | ||
| fixed++; | ||
| } | ||
| } | ||
| const singletonMetaData = await readJsonViaReader(reader, metaFilePath(model, locale, config.locales.default)); | ||
| if (singletonMetaData) validateScheduleFields(singletonMetaData, { | ||
| model: model.id, | ||
| locale | ||
| }, issues); | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| async function validateDictionaryModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| const localeKeys = {}; | ||
| for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) { | ||
| const filePath = contentFilePath(model, locale); | ||
| const data = await readJsonViaReader(reader, filePath); | ||
| if (!data) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| message: `Locale file missing: ${locale}.json` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| await writeJson(join(projectRoot, filePath), {}); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| entriesChecked++; | ||
| localeKeys[locale] = new Set(Object.keys(data)); | ||
| for (const [key, value] of Object.entries(data)) if (detectSecrets(value).length > 0) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| field: key, | ||
| message: `Potential secret detected in key "${key}"` | ||
| }); | ||
| const valueToKeys = /* @__PURE__ */ new Map(); | ||
| for (const [key, value] of Object.entries(data)) { | ||
| const arr = valueToKeys.get(value); | ||
| if (arr) arr.push(key); | ||
| else valueToKeys.set(value, [key]); | ||
| } | ||
| for (const [value, dupeKeys] of valueToKeys) if (dupeKeys.length > 1) { | ||
| const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value; | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: `Duplicate value "${truncated}" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(", ")}]` | ||
| }); | ||
| } | ||
| const keys = Object.keys(data); | ||
| const sorted = [...keys].toSorted(); | ||
| if (keys.join(",") !== sorted.join(",")) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: "Content file keys not in canonical order" | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const resorted = {}; | ||
| for (const key of sorted) resorted[key] = data[key]; | ||
| await writeJson(join(projectRoot, filePath), resorted); | ||
| fixed++; | ||
| } | ||
| } | ||
| } | ||
| if (model.i18n && Object.keys(localeKeys).length > 1) { | ||
| const localeNames = Object.keys(localeKeys); | ||
| for (let i = 1; i < localeNames.length; i++) { | ||
| const locA = localeNames[0]; | ||
| const locB = localeNames[i]; | ||
| const keysA = localeKeys[locA]; | ||
| const keysB = localeKeys[locB]; | ||
| for (const k of keysA) if (!keysB.has(k)) issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale: locB, | ||
| field: k, | ||
| message: `Key parity: key "${k}" exists in ${locA} but missing in ${locB}` | ||
| }); | ||
| for (const k of keysB) if (!keysA.has(k)) issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale: locA, | ||
| field: k, | ||
| message: `Key parity: key "${k}" exists in ${locB} but missing in ${locA}` | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| async function discoverDocumentSlugs(reader, cDir, model) { | ||
| const strategy = resolveLocaleStrategy(model); | ||
| const entries = await reader.listDirectory(cDir); | ||
| if (!model.i18n) return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")); | ||
| if (strategy === "file") return entries.filter((e) => !e.startsWith(".")); | ||
| if (strategy === "suffix") { | ||
| const slugs = /* @__PURE__ */ new Set(); | ||
| for (const f of entries) { | ||
| if (!f.endsWith(".md")) continue; | ||
| const parts = f.replace(/\.md$/, "").split("."); | ||
| if (parts.length >= 2) { | ||
| parts.pop(); | ||
| slugs.add(parts.join(".")); | ||
| } | ||
| } | ||
| return [...slugs]; | ||
| } | ||
| if (strategy === "directory") { | ||
| const slugs = /* @__PURE__ */ new Set(); | ||
| const localeLists = await Promise.all(entries.filter((localeDir) => !localeDir.startsWith(".")).map((localeDir) => reader.listDirectory(`${cDir}/${localeDir}`))); | ||
| for (const files of localeLists) for (const f of files) if (f.endsWith(".md")) slugs.add(f.replace(/\.md$/, "")); | ||
| return [...slugs]; | ||
| } | ||
| return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")); | ||
| } | ||
| async function validateDocumentModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| const cDir = contentDirPath(model); | ||
| if (!await reader.fileExists(cDir)) return { | ||
| entries: 0, | ||
| fixed: 0 | ||
| }; | ||
| const slugs = await discoverDocumentSlugs(reader, cDir, model); | ||
| const locales = model.i18n ? config.locales.supported : [config.locales.default]; | ||
| const rawByKey = /* @__PURE__ */ new Map(); | ||
| const frontmatterByLocale = {}; | ||
| for (const slug of slugs) { | ||
| if (slug.startsWith(".")) continue; | ||
| for (const locale of locales) { | ||
| const raw = await readTextViaReader(reader, documentFilePath(model, locale, slug)); | ||
| if (!raw) continue; | ||
| rawByKey.set(`${slug}\u0000${locale}`, raw); | ||
| const { frontmatter } = parseFrontmatter(raw); | ||
| frontmatterByLocale[locale] ??= {}; | ||
| frontmatterByLocale[locale][slug] = frontmatter; | ||
| } | ||
| } | ||
| for (const slug of slugs) { | ||
| if (slug.startsWith(".")) continue; | ||
| for (const locale of locales) { | ||
| const filePath = documentFilePath(model, locale, slug); | ||
| const raw = rawByKey.get(`${slug}\u0000${locale}`) ?? null; | ||
| if (!raw) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| slug, | ||
| message: `Missing translation: document "${slug}" missing ${locale} locale file` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const template = `---\nslug: ${slug}\n---\n`; | ||
| await writeText(join(projectRoot, filePath), template); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| entriesChecked++; | ||
| const { frontmatter, body } = parseFrontmatter(raw); | ||
| scanUndeclaredFieldsForSecrets(frontmatter, { | ||
| ...model.fields, | ||
| body: true | ||
| }, { | ||
| model: model.id, | ||
| locale, | ||
| slug | ||
| }, issues); | ||
| if (detectSecrets(body).length > 0) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| slug, | ||
| field: "body", | ||
| message: "Potential secret detected in document body" | ||
| }); | ||
| if (model.fields) { | ||
| const fieldsWithoutBody = Object.fromEntries(Object.entries(model.fields).filter(([name]) => name !== "body")); | ||
| const entryResult = validateContent(frontmatter, fieldsWithoutBody, model.id, locale, void 0, { | ||
| allEntries: frontmatterByLocale[locale] ?? {}, | ||
| currentEntryId: slug | ||
| }); | ||
| for (const err of entryResult.errors) issues.push({ | ||
| ...err, | ||
| slug | ||
| }); | ||
| const resolveTarget = buildProjectTargetResolver(reader, config); | ||
| const relationErrors = await checkRelationIntegrity(frontmatter, fieldsWithoutBody, model.id, locale, void 0, async () => null, { | ||
| severity: "error", | ||
| resolveTarget | ||
| }); | ||
| for (const err of relationErrors) issues.push({ | ||
| ...err, | ||
| slug | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| async function checkOrphanContent(reader, validModelIds, issues, _fix) { | ||
| const fixed = 0; | ||
| const contentBase = ".contentrain/content"; | ||
| const domains = await reader.listDirectory(contentBase); | ||
| const modelLists = await Promise.all(domains.filter((d) => !d.startsWith(".")).map(async (d) => ({ | ||
| domain: d, | ||
| dirs: await reader.listDirectory(`${contentBase}/${d}`) | ||
| }))); | ||
| for (const { dirs } of modelLists) for (const modelDir of dirs) { | ||
| if (modelDir.startsWith(".")) continue; | ||
| if (!validModelIds.has(modelDir)) issues.push({ | ||
| severity: "warning", | ||
| model: modelDir, | ||
| message: `Orphan content: content directory exists for deleted model "${modelDir}"` | ||
| }); | ||
| } | ||
| return fixed; | ||
| } | ||
| async function validateProject(input, options) { | ||
| const reader = typeof input === "string" ? new LocalReader(input) : input; | ||
| const projectRoot = typeof input === "string" ? input : void 0; | ||
| const fix = Boolean(options?.fix) && projectRoot !== void 0; | ||
| const issues = []; | ||
| let totalEntries = 0; | ||
| let totalFixed = 0; | ||
| let modelsChecked = 0; | ||
| const config = await readConfig(reader); | ||
| if (!config) return { | ||
| valid: false, | ||
| summary: { | ||
| errors: 1, | ||
| warnings: 0, | ||
| notices: 0, | ||
| models_checked: 0, | ||
| entries_checked: 0 | ||
| }, | ||
| issues: [{ | ||
| severity: "error", | ||
| message: "Project not initialized: config.json missing" | ||
| }], | ||
| fixed: 0 | ||
| }; | ||
| const modelSummaries = await listModels(reader); | ||
| const validModelIds = new Set(modelSummaries.map((m) => m.id)); | ||
| const modelsToCheck = options?.model ? modelSummaries.filter((m) => m.id === options.model) : modelSummaries; | ||
| for (const summary of modelsToCheck) { | ||
| const model = await readModel(reader, summary.id); | ||
| if (!model) continue; | ||
| modelsChecked++; | ||
| let result; | ||
| switch (model.kind) { | ||
| case "collection": | ||
| result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| case "singleton": | ||
| result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| case "dictionary": | ||
| result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| case "document": | ||
| result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| default: result = { | ||
| entries: 0, | ||
| fixed: 0 | ||
| }; | ||
| } | ||
| totalEntries += result.entries; | ||
| totalFixed += result.fixed; | ||
| } | ||
| if (!options?.model) totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix); | ||
| if (!options?.model) { | ||
| const dictModels = modelsToCheck.filter((m) => m.kind === "dictionary"); | ||
| if (dictModels.length > 1) { | ||
| const globalValueMap = {}; | ||
| for (const summary of dictModels) { | ||
| const model = await readModel(reader, summary.id); | ||
| if (!model) continue; | ||
| for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) { | ||
| if (!globalValueMap[locale]) globalValueMap[locale] = /* @__PURE__ */ new Map(); | ||
| const data = await readJsonViaReader(reader, contentFilePath(model, locale)); | ||
| if (!data) continue; | ||
| for (const [key, value] of Object.entries(data)) { | ||
| const refs = globalValueMap[locale].get(value); | ||
| if (refs) refs.push({ | ||
| model: model.id, | ||
| key | ||
| }); | ||
| else globalValueMap[locale].set(value, [{ | ||
| model: model.id, | ||
| key | ||
| }]); | ||
| } | ||
| } | ||
| } | ||
| for (const [locale, valueMap] of Object.entries(globalValueMap)) for (const [value, refs] of valueMap) if (new Set(refs.map((r) => r.model)).size > 1) { | ||
| const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value; | ||
| issues.push({ | ||
| severity: "notice", | ||
| locale, | ||
| message: `Cross-model duplicate value "${truncated}" in ${refs.map((r) => `${r.model}/${r.key}`).join(", ")}` | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| const errors = issues.filter((i) => i.severity === "error").length; | ||
| const warnings = issues.filter((i) => i.severity === "warning").length; | ||
| const notices = issues.filter((i) => i.severity === "notice").length; | ||
| return { | ||
| valid: errors === 0, | ||
| summary: { | ||
| errors, | ||
| warnings, | ||
| notices, | ||
| models_checked: modelsChecked, | ||
| entries_checked: totalEntries | ||
| }, | ||
| issues, | ||
| fixed: totalFixed | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { validateContent as i, validateScheduleFields as n, checkRelationIntegrity as r, validateProject as t }; | ||
| //# sourceMappingURL=validator-ChiYk6ap.mjs.map |
Sorry, the diff of this file is too big to display
| import { i as __toESM } from "./chunk-BEJ448es.mjs"; | ||
| //#region src/core/ast-scanner/vue-parser.ts | ||
| const NodeTypes = { | ||
| ROOT: 0, | ||
| ELEMENT: 1, | ||
| TEXT: 2, | ||
| COMMENT: 3, | ||
| SIMPLE_EXPRESSION: 4, | ||
| INTERPOLATION: 5, | ||
| ATTRIBUTE: 6, | ||
| DIRECTIVE: 7, | ||
| COMPOUND_EXPRESSION: 8, | ||
| IF: 9, | ||
| IF_BRANCH: 10, | ||
| FOR: 11, | ||
| TEXT_CALL: 12, | ||
| VNODE_CALL: 13, | ||
| JS_CALL_EXPRESSION: 14 | ||
| }; | ||
| let _compiler = null; | ||
| async function loadCompiler() { | ||
| if (_compiler) return _compiler; | ||
| try { | ||
| _compiler = await import("./compiler-sfc.cjs-y0B0L3MH.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1)); | ||
| return _compiler; | ||
| } catch { | ||
| throw new Error("@vue/compiler-sfc is required to parse .vue files. Install it with: pnpm add -D @vue/compiler-sfc"); | ||
| } | ||
| } | ||
| const CODE_DIRECTIVES = new Set([ | ||
| "if", | ||
| "else-if", | ||
| "else", | ||
| "show", | ||
| "for", | ||
| "on", | ||
| "model", | ||
| "memo", | ||
| "once", | ||
| "pre", | ||
| "cloak", | ||
| "is", | ||
| "slot", | ||
| "key" | ||
| ]); | ||
| const CODE_DIRECTIVE_ARGS = new Set([ | ||
| "class", | ||
| "style", | ||
| "key", | ||
| "ref", | ||
| "is" | ||
| ]); | ||
| const CSS_ATTRIBUTES = new Set(["class", "style"]); | ||
| const SURROUNDING_MAX = 120; | ||
| function getSurroundingByLine(content, line) { | ||
| const lines = content.split("\n"); | ||
| const idx = line - 1; | ||
| if (idx >= 0 && idx < lines.length) return (lines[idx] ?? "").slice(0, SURROUNDING_MAX); | ||
| return ""; | ||
| } | ||
| function walkTemplate(node, templateLineOffset, sfcContent, results, parentTag = "") { | ||
| switch (node.type) { | ||
| case NodeTypes.TEXT: { | ||
| const trimmed = (typeof node.content === "string" ? node.content : node.loc?.source ?? "").trim(); | ||
| if (trimmed.length > 0 && /\S/.test(trimmed)) results.push({ | ||
| value: trimmed, | ||
| line: node.loc.start.line + templateLineOffset - 1, | ||
| column: node.loc.start.column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| break; | ||
| } | ||
| case NodeTypes.INTERPOLATION: | ||
| if (node.content && typeof node.content !== "string" && node.content.type === NodeTypes.SIMPLE_EXPRESSION) { | ||
| const stringMatch = (typeof node.content.content === "string" ? node.content.content : "").match(/^(['"`])(.+)\1$/); | ||
| if (stringMatch?.[2]) results.push({ | ||
| value: stringMatch[2], | ||
| line: node.loc.start.line + templateLineOffset - 1, | ||
| column: node.loc.start.column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| } | ||
| break; | ||
| case NodeTypes.ELEMENT: { | ||
| const tag = node.tag ?? ""; | ||
| if (node.props) for (const prop of node.props) processProp(prop, tag, templateLineOffset, sfcContent, results); | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, tag); | ||
| break; | ||
| } | ||
| case NodeTypes.IF: | ||
| if (node.branches) { | ||
| for (const branch of node.branches) if (branch.children) for (const child of branch.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| } | ||
| break; | ||
| case NodeTypes.IF_BRANCH: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| case NodeTypes.FOR: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| case NodeTypes.ROOT: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| case NodeTypes.COMPOUND_EXPRESSION: | ||
| if (node.children) { | ||
| for (const child of node.children) if (typeof child !== "string") walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| } | ||
| break; | ||
| case NodeTypes.TEXT_CALL: | ||
| if (node.content && typeof node.content !== "string") walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag); | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| default: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| } | ||
| } | ||
| function processProp(prop, parentTag, templateLineOffset, sfcContent, results) { | ||
| if (prop.type === NodeTypes.ATTRIBUTE) { | ||
| if (!prop.value) return; | ||
| const attrName = prop.name; | ||
| const attrValue = prop.value.content; | ||
| if (!attrValue || attrValue.trim().length === 0) return; | ||
| if (CSS_ATTRIBUTES.has(attrName)) { | ||
| results.push({ | ||
| value: attrValue, | ||
| line: prop.value.loc.start.line + templateLineOffset - 1, | ||
| column: prop.value.loc.start.column, | ||
| context: "css_class", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| return; | ||
| } | ||
| results.push({ | ||
| value: attrValue, | ||
| line: prop.value.loc.start.line + templateLineOffset - 1, | ||
| column: prop.value.loc.start.column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| } else if (prop.type === NodeTypes.DIRECTIVE) { | ||
| const directiveName = prop.name; | ||
| if (CODE_DIRECTIVES.has(directiveName)) return; | ||
| if (directiveName === "bind" && prop.arg) { | ||
| const argName = typeof prop.arg.content === "string" ? prop.arg.content : ""; | ||
| if (CODE_DIRECTIVE_ARGS.has(argName)) return; | ||
| } | ||
| if (prop.exp) { | ||
| const expr = typeof prop.exp.content === "string" ? prop.exp.content : ""; | ||
| const argName = prop.arg && typeof prop.arg.content === "string" ? prop.arg.content : ""; | ||
| const stringMatch = expr.match(/^(['"`])(.+)\1$/); | ||
| if (stringMatch?.[2]) results.push({ | ||
| value: stringMatch[2], | ||
| line: prop.exp.loc.start.line + templateLineOffset - 1, | ||
| column: prop.exp.loc.start.column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: argName || directiveName, | ||
| surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| let _tsxParser = null; | ||
| async function loadTsxParser() { | ||
| if (_tsxParser) return _tsxParser; | ||
| try { | ||
| _tsxParser = (await import("./tsx-parser-C3XsIrwU.mjs")).parseTsx; | ||
| return _tsxParser; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Map script block lang to a file extension that tsx-parser's getScriptKind understands. | ||
| * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations | ||
| * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals. | ||
| */ | ||
| function resolveScriptFileName(vueFileName, lang) { | ||
| if (lang === "tsx") return vueFileName.replace(/\.vue$/, ".tsx"); | ||
| if (lang === "ts") return vueFileName.replace(/\.vue$/, ".ts"); | ||
| if (lang === "jsx") return vueFileName.replace(/\.vue$/, ".jsx"); | ||
| return vueFileName.replace(/\.vue$/, ".js"); | ||
| } | ||
| function parseScriptBlock(scriptContent, scriptStartLine, fileName, parseTsx, lang) { | ||
| return parseTsx(scriptContent, resolveScriptFileName(fileName, lang)).map((r) => { | ||
| r.line = r.line + scriptStartLine - 1; | ||
| r.scope = "script"; | ||
| return r; | ||
| }); | ||
| } | ||
| async function parseVue(content, fileName) { | ||
| const compiler = await loadCompiler(); | ||
| const results = []; | ||
| const { descriptor } = compiler.parse(content, { filename: fileName }); | ||
| if (descriptor.template) { | ||
| const templateLineOffset = descriptor.template.loc.start.line; | ||
| const templateAst = descriptor.template.ast; | ||
| if (templateAst) walkTemplate(templateAst, templateLineOffset, content, results); | ||
| else try { | ||
| const compiled = compiler.compileTemplate({ | ||
| source: descriptor.template.content, | ||
| filename: fileName, | ||
| id: "ast-scanner" | ||
| }); | ||
| if (compiled.ast) walkTemplate(compiled.ast, templateLineOffset, content, results); | ||
| } catch {} | ||
| } | ||
| const scriptBlock = descriptor.scriptSetup ?? descriptor.script; | ||
| if (scriptBlock) { | ||
| const parseTsx = await loadTsxParser(); | ||
| if (parseTsx) { | ||
| const scriptStartLine = scriptBlock.loc.start.line; | ||
| const scriptResults = parseScriptBlock(scriptBlock.content, scriptStartLine, fileName, parseTsx, scriptBlock.lang); | ||
| results.push(...scriptResults); | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| //#endregion | ||
| export { parseVue }; | ||
| //# sourceMappingURL=vue-parser-CHxDohd8.mjs.map |
| {"version":3,"file":"vue-parser-CHxDohd8.mjs","names":[],"sources":["../src/core/ast-scanner/vue-parser.ts"],"sourcesContent":["// ─── Vue SFC Parser for Scanner v2 ───\n// Parses .vue Single File Components using @vue/compiler-sfc.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\n// ─── Types ───\n\nimport type { ExtractedString } from './types.js'\nexport type { ExtractedString }\n\n// ─── Lazy-loaded @vue/compiler-sfc ───\n\ninterface VueCompilerSFC {\n parse: (source: string, options?: { filename?: string }) => {\n descriptor: {\n template: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n ast?: VueTemplateNode\n } | null\n script: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n scriptSetup: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n }\n }\n compileTemplate: (options: {\n source: string\n filename: string\n id: string\n }) => {\n ast?: VueTemplateNode\n }\n}\n\n// Vue template AST node types\nconst NodeTypes = {\n ROOT: 0,\n ELEMENT: 1,\n TEXT: 2,\n COMMENT: 3,\n SIMPLE_EXPRESSION: 4,\n INTERPOLATION: 5,\n ATTRIBUTE: 6,\n DIRECTIVE: 7,\n COMPOUND_EXPRESSION: 8,\n IF: 9,\n IF_BRANCH: 10,\n FOR: 11,\n TEXT_CALL: 12,\n VNODE_CALL: 13,\n JS_CALL_EXPRESSION: 14,\n} as const\n\ninterface VueTemplateLoc {\n start: { line: number; column: number; offset: number }\n end: { line: number; column: number; offset: number }\n source: string\n}\n\ninterface VueTemplateNode {\n type: number\n loc: VueTemplateLoc\n children?: VueTemplateNode[]\n tag?: string // for ELEMENT\n props?: VueTemplateProp[]\n content?: VueTemplateNode | string // for INTERPOLATION, TEXT, SIMPLE_EXPRESSION\n branches?: VueTemplateNode[] // for IF\n}\n\ninterface VueTemplateProp {\n type: number // ATTRIBUTE (6) or DIRECTIVE (7)\n name: string\n loc: VueTemplateLoc\n value?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n exp?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n arg?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n}\n\nlet _compiler: VueCompilerSFC | null = null\n\nasync function loadCompiler(): Promise<VueCompilerSFC> {\n if (_compiler) return _compiler\n try {\n // Dynamic import — @vue/compiler-sfc is optional\n const mod = await import('@vue/compiler-sfc')\n _compiler = mod as unknown as VueCompilerSFC\n return _compiler\n } catch {\n throw new Error(\n '@vue/compiler-sfc is required to parse .vue files. '\n + 'Install it with: pnpm add -D @vue/compiler-sfc',\n )\n }\n}\n\n// ─── Directives that contain code expressions (not user content) ───\n\nconst CODE_DIRECTIVES = new Set([\n 'if', 'else-if', 'else', 'show',\n 'for',\n 'on', // @click, v-on:click\n 'model',\n 'memo',\n 'once',\n 'pre',\n 'cloak',\n 'is',\n 'slot',\n 'key',\n])\n\n// Directive args that are code/binding expressions, not content\nconst CODE_DIRECTIVE_ARGS = new Set([\n 'class', 'style', // :class, :style → CSS bindings\n 'key',\n 'ref',\n 'is',\n])\n\n// Static attributes whose values are CSS, not content\nconst CSS_ATTRIBUTES = new Set([\n 'class', 'style',\n])\n\n// ─── Surrounding text helper ───\n\nconst SURROUNDING_MAX = 120\n\nfunction _getSurrounding(content: string, offset: number): string {\n const lines = content.split('\\n')\n let charCount = 0\n for (let i = 0; i < lines.length; i++) {\n const lineLen = (lines[i]?.length ?? 0) + 1 // +1 for newline\n if (charCount + lineLen > offset) {\n return (lines[i] ?? '').slice(0, SURROUNDING_MAX)\n }\n charCount += lineLen\n }\n return ''\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n if (idx >= 0 && idx < lines.length) {\n return (lines[idx] ?? '').slice(0, SURROUNDING_MAX)\n }\n return ''\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: VueTemplateNode,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n switch (node.type) {\n case NodeTypes.TEXT: {\n // Static text between tags\n const text = typeof node.content === 'string'\n ? node.content\n : (node.loc?.source ?? '')\n const trimmed = text.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n results.push({\n value: trimmed,\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n break\n }\n\n case NodeTypes.INTERPOLATION: {\n // {{ expression }} — extract string literals from within\n if (node.content && typeof node.content !== 'string' && node.content.type === NodeTypes.SIMPLE_EXPRESSION) {\n const expr = typeof node.content.content === 'string'\n ? node.content.content\n : ''\n // Only extract if the expression itself is a string literal\n // e.g., {{ 'Hello' }} or {{ \"World\" }}\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable reference like {{ greeting }}, skip — that's code\n }\n break\n }\n\n case NodeTypes.ELEMENT: {\n const tag = node.tag ?? ''\n\n // Process props/attributes\n if (node.props) {\n for (const prop of node.props) {\n processProp(prop, tag, templateLineOffset, sfcContent, results)\n }\n }\n\n // Recurse into children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, tag)\n }\n }\n break\n }\n\n case NodeTypes.IF: {\n // v-if creates branches — walk each branch's children\n if (node.branches) {\n for (const branch of node.branches) {\n if (branch.children) {\n for (const child of branch.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n }\n break\n }\n\n case NodeTypes.IF_BRANCH: {\n // Walk children of if branch\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.FOR: {\n // v-for node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.ROOT: {\n // Root node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.COMPOUND_EXPRESSION: {\n // Compound expression — walk children\n if (node.children) {\n for (const child of node.children) {\n if (typeof child !== 'string') {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n break\n }\n\n case NodeTypes.TEXT_CALL: {\n // Text call node (wrapper for text in v-if, etc.) — walk content\n if (node.content && typeof node.content !== 'string') {\n walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag)\n }\n // Also walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n default: {\n // For any unknown node type, try to walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processProp(\n prop: VueTemplateProp,\n parentTag: string,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n): void {\n if (prop.type === NodeTypes.ATTRIBUTE) {\n // Static attribute: title=\"Hello\"\n if (!prop.value) return\n\n const attrName = prop.name\n const attrValue = prop.value.content\n\n if (!attrValue || attrValue.trim().length === 0) return\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n } else if (prop.type === NodeTypes.DIRECTIVE) {\n // Dynamic directive: :title=\"expr\", v-bind:title=\"expr\", @click=\"handler\"\n const directiveName = prop.name // 'bind', 'on', 'if', 'for', etc.\n\n // Skip code directives entirely (v-if, v-for, @click, etc.)\n if (CODE_DIRECTIVES.has(directiveName)) return\n\n // For v-bind (:attr=\"expr\"), check if the arg is a code binding\n if (directiveName === 'bind' && prop.arg) {\n const argName = typeof prop.arg.content === 'string' ? prop.arg.content : ''\n if (CODE_DIRECTIVE_ARGS.has(argName)) return\n }\n\n // Extract string literals from directive expressions\n if (prop.exp) {\n const expr = typeof prop.exp.content === 'string' ? prop.exp.content : ''\n const argName = prop.arg && typeof prop.arg.content === 'string' ? prop.arg.content : ''\n\n // Check if expression is a simple string literal: 'text' or \"text\"\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: prop.exp.loc.start.line + templateLineOffset - 1,\n column: prop.exp.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: argName || directiveName,\n surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable or complex expression, skip — scanner doesn't interpret code\n }\n }\n}\n\n// ─── Script Block Parsing ───\n\n// tsx-parser is being built in parallel; define the interface we expect\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n // tsx-parser not yet available — fall back to no script parsing\n return null\n }\n}\n\n/**\n * Map script block lang to a file extension that tsx-parser's getScriptKind understands.\n * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations\n * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals.\n */\nfunction resolveScriptFileName(vueFileName: string, lang?: string): string {\n if (lang === 'tsx') return vueFileName.replace(/\\.vue$/, '.tsx')\n if (lang === 'ts') return vueFileName.replace(/\\.vue$/, '.ts')\n if (lang === 'jsx') return vueFileName.replace(/\\.vue$/, '.jsx')\n return vueFileName.replace(/\\.vue$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartLine: number,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n // Resolve filename with correct extension for TypeScript parser\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n\n // Adjust line numbers by script block offset\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseVue(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n const { descriptor } = compiler.parse(content, { filename: fileName })\n\n // ─── Template Block ───\n if (descriptor.template) {\n const templateLineOffset = descriptor.template.loc.start.line\n const templateAst = descriptor.template.ast\n\n if (templateAst) {\n walkTemplate(templateAst, templateLineOffset, content, results)\n } else {\n // Fallback: compile template to get AST\n try {\n const compiled = compiler.compileTemplate({\n source: descriptor.template.content,\n filename: fileName,\n id: 'ast-scanner',\n })\n if (compiled.ast) {\n walkTemplate(compiled.ast, templateLineOffset, content, results)\n }\n } catch {\n // If template compilation fails, we skip template extraction\n // This is acceptable — malformed templates shouldn't block scanning\n }\n }\n }\n\n // ─── Script Block ───\n const scriptBlock = descriptor.scriptSetup ?? descriptor.script\n if (scriptBlock) {\n const parseTsx = await loadTsxParser()\n if (parseTsx) {\n const scriptStartLine = scriptBlock.loc.start.line\n const scriptResults = parseScriptBlock(\n scriptBlock.content,\n scriptStartLine,\n fileName,\n parseTsx,\n scriptBlock.lang,\n )\n results.push(...scriptResults)\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"} |
@@ -10,3 +10,3 @@ import "../serialization-B1CEzR4H.mjs"; | ||
| import "../transaction-1SPznNt3.mjs"; | ||
| import { a as detectFileFramework, c as validatePatchPath, i as checkSyntax, n as applyExtract, o as replaceInLine, r as applyReuse, s as validateFrameworkExpression, t as PATCHABLE_EXTENSIONS } from "../apply-manager-B7BrL-ZW.mjs"; | ||
| import { a as detectFileFramework, c as validatePatchPath, i as checkSyntax, n as applyExtract, o as replaceInLine, r as applyReuse, s as validateFrameworkExpression, t as PATCHABLE_EXTENSIONS } from "../apply-manager-SLCRLHN_.mjs"; | ||
| export { PATCHABLE_EXTENSIONS, applyExtract, applyReuse, checkSyntax, detectFileFramework, replaceInLine, validateFrameworkExpression, validatePatchPath }; |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader } from "../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader } from "../index-w8QHThNS.mjs"; | ||
| import { ContentrainConfig, Vocabulary } from "@contentrain/types"; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,2 @@ | ||
| import { _ as writeContent, a as deleteContent, c as readContent, d as resolveLocaleStrategy, f as resolveMdFilePath, g as validateSlug, h as validateLocale, i as WriteResult, l as resolveContentDir, m as validateEntryId, n as DeleteOpts, o as listContent, p as serializeFrontmatter, r as ListOpts, s as parseFrontmatter, t as ContentEntry, u as resolveJsonFilePath } from "../content-manager-BgxF3dHu.mjs"; | ||
| import { _ as writeContent, a as deleteContent, c as readContent, d as resolveLocaleStrategy, f as resolveMdFilePath, g as validateSlug, h as validateLocale, i as WriteResult, l as resolveContentDir, m as validateEntryId, n as DeleteOpts, o as listContent, p as serializeFrontmatter, r as ListOpts, s as parseFrontmatter, t as ContentEntry, u as resolveJsonFilePath } from "../content-manager-DWo3G64y.mjs"; | ||
| export { ContentEntry, DeleteOpts, ListOpts, WriteResult, deleteContent, listContent, parseFrontmatter, readContent, resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy, resolveMdFilePath, serializeFrontmatter, validateEntryId, validateLocale, validateSlug, writeContent }; |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader, a as FileChange } from "../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader, a as FileChange } from "../index-w8QHThNS.mjs"; | ||
| import { ContextJson, ContextSource } from "@contentrain/types"; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter } from "../../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter } from "../../index-w8QHThNS.mjs"; | ||
| export { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities, RepoProvider, RepoReader, RepoWriter }; |
@@ -6,4 +6,4 @@ import "../fs-DLbVB-Ek.mjs"; | ||
| import "../branch-lifecycle-BAfgSQBv.mjs"; | ||
| import "../scan-config-BGUflS8t.mjs"; | ||
| import { t as runDoctor } from "../doctor-pw8EWRFR.mjs"; | ||
| import "../scan-config-BlNLRCMx.mjs"; | ||
| import { t as runDoctor } from "../doctor-BwJ_nmqS.mjs"; | ||
| export { runDoctor }; |
| import "../fs-DLbVB-Ek.mjs"; | ||
| import "../scan-config-BGUflS8t.mjs"; | ||
| import { t as buildGraph } from "../graph-builder-CRUX_8mA.mjs"; | ||
| import "../scan-config-BlNLRCMx.mjs"; | ||
| import { t as buildGraph } from "../graph-builder-DK4Mh8Tn.mjs"; | ||
| export { buildGraph }; |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader } from "../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader } from "../index-w8QHThNS.mjs"; | ||
| import { ModelDefinition, ModelSummary, ModelSummary as ModelSummary$1 } from "@contentrain/types"; | ||
@@ -3,0 +3,0 @@ import { z } from "zod"; |
@@ -1,3 +0,3 @@ | ||
| import { _ as RepoReader, a as FileChange } from "../../index-DDX-qYNw.mjs"; | ||
| import { t as ContentEntry } from "../../content-manager-BgxF3dHu.mjs"; | ||
| import { _ as RepoReader, a as FileChange } from "../../index-w8QHThNS.mjs"; | ||
| import { t as ContentEntry } from "../../content-manager-DWo3G64y.mjs"; | ||
| import { ContentrainConfig, ModelDefinition, Vocabulary } from "@contentrain/types"; | ||
@@ -4,0 +4,0 @@ |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader, a as FileChange } from "../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader, a as FileChange } from "../index-w8QHThNS.mjs"; | ||
@@ -3,0 +3,0 @@ //#region src/core/overlay-reader.d.ts |
@@ -1,2 +0,2 @@ | ||
| import { t as OverlayReader } from "../overlay-reader-BOb105gS.mjs"; | ||
| import { t as OverlayReader } from "../overlay-reader-DNaVsgiS.mjs"; | ||
| export { OverlayReader }; |
| import "../fs-DLbVB-Ek.mjs"; | ||
| import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles, r as SCAN_IGNORE_DIRS, t as MAX_SCAN_FILES } from "../scan-config-BGUflS8t.mjs"; | ||
| import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles, r as SCAN_IGNORE_DIRS, t as MAX_SCAN_FILES } from "../scan-config-BlNLRCMx.mjs"; | ||
| export { MAX_SCAN_FILES, SCAN_EXTENSIONS, SCAN_IGNORE_DIRS, autoDetectSourceDirs, classifyFile, discoverFiles }; |
| import "../fs-DLbVB-Ek.mjs"; | ||
| import "../scan-config-BGUflS8t.mjs"; | ||
| import { n as scanSummary, t as scanCandidates } from "../scanner-VOwrKLGC.mjs"; | ||
| import "../tsx-parser-B_aI_C2r.mjs"; | ||
| import "../scan-config-BlNLRCMx.mjs"; | ||
| import { n as scanSummary, t as scanCandidates } from "../scanner-CGWhmpDz.mjs"; | ||
| import "../tsx-parser-md1N0Niu.mjs"; | ||
| export { scanCandidates, scanSummary }; |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader } from "../../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader } from "../../index-w8QHThNS.mjs"; | ||
| import { EntryMeta, FieldDef, ModelDefinition, ValidationError, ValidationResult } from "@contentrain/types"; | ||
@@ -3,0 +3,0 @@ |
@@ -5,3 +5,3 @@ import "../../fs-DLbVB-Ek.mjs"; | ||
| import "../../model-manager-DP2CZiMT.mjs"; | ||
| import { i as validateContent, n as validateScheduleFields, r as checkRelationIntegrity, t as validateProject } from "../../validator-D5VncJ8M.mjs"; | ||
| import { i as validateContent, n as validateScheduleFields, r as checkRelationIntegrity, t as validateProject } from "../../validator-ChiYk6ap.mjs"; | ||
| export { checkRelationIntegrity, validateContent, validateProject, validateScheduleFields }; |
@@ -1,2 +0,2 @@ | ||
| import { a as RemoteDeleteResult, c as checkBranchHealth, d as deleteRemoteBranch, f as isRefMerged, i as RemoteBranchList, l as classifyMergedBranches, m as pruneMergedRemoteBranches, n as BranchHealthCheck, o as RemotePruneResult, p as listRemoteCrBranches, r as CleanupResult, s as branchDiff, t as BranchDiffResult, u as cleanupMergedBranches } from "../branch-lifecycle-AwBJIheA.mjs"; | ||
| import { a as RemoteDeleteResult, c as checkBranchHealth, d as deleteRemoteBranch, f as isRefMerged, i as RemoteBranchList, l as classifyMergedBranches, m as pruneMergedRemoteBranches, n as BranchHealthCheck, o as RemotePruneResult, p as listRemoteCrBranches, r as CleanupResult, s as branchDiff, t as BranchDiffResult, u as cleanupMergedBranches } from "../branch-lifecycle-D4W0WIs4.mjs"; | ||
| export { BranchDiffResult, BranchHealthCheck, CleanupResult, RemoteBranchList, RemoteDeleteResult, RemotePruneResult, branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, pruneMergedRemoteBranches }; |
@@ -1,2 +0,2 @@ | ||
| import { a as RemoteDeleteResult } from "../branch-lifecycle-AwBJIheA.mjs"; | ||
| import { a as RemoteDeleteResult } from "../branch-lifecycle-D4W0WIs4.mjs"; | ||
| import { SyncResult, WorkflowMode } from "@contentrain/types"; | ||
@@ -3,0 +3,0 @@ |
+8
-8
@@ -16,10 +16,10 @@ #!/usr/bin/env node | ||
| import "./annotations-D3tlsF38.mjs"; | ||
| import { n as createServer } from "./server-RWxCofdl.mjs"; | ||
| import "./validator-D5VncJ8M.mjs"; | ||
| import "./scan-config-BGUflS8t.mjs"; | ||
| import "./graph-builder-CRUX_8mA.mjs"; | ||
| import "./scanner-VOwrKLGC.mjs"; | ||
| import "./tsx-parser-B_aI_C2r.mjs"; | ||
| import "./apply-manager-B7BrL-ZW.mjs"; | ||
| import "./doctor-pw8EWRFR.mjs"; | ||
| import { n as createServer } from "./server-I6sqKvO8.mjs"; | ||
| import "./validator-ChiYk6ap.mjs"; | ||
| import "./scan-config-BlNLRCMx.mjs"; | ||
| import "./graph-builder-DK4Mh8Tn.mjs"; | ||
| import "./scanner-CGWhmpDz.mjs"; | ||
| import "./tsx-parser-md1N0Niu.mjs"; | ||
| import "./apply-manager-SLCRLHN_.mjs"; | ||
| import "./doctor-BwJ_nmqS.mjs"; | ||
| import { resolve } from "node:path"; | ||
@@ -26,0 +26,0 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-w8QHThNS.mjs"; | ||
| import { Octokit } from "@octokit/rest"; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,3 @@ | ||
| import { n as isNotFoundError, t as resolveRepoPath } from "../../paths-BU6E-oDs.mjs"; | ||
| import { t as isNotFoundError } from "../../errors-e0YdjooK.mjs"; | ||
| import { t as resolveRepoPath } from "../../paths-enT2coeX.mjs"; | ||
| import { CONTENTRAIN_BRANCH } from "@contentrain/types"; | ||
@@ -3,0 +4,0 @@ import { createPrivateKey, createSign } from "node:crypto"; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n// A GitHub Git-tree entry. Writes carry `content` inline (GitHub creates the\n// blob as part of createTree); deletions carry `sha: null`. The two are\n// mutually exclusive — an entry may set `content` OR `sha`, never both.\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Map `input.changes` to tree entries — `content` inline for each write,\n * `sha: null` for each deletion. No per-file blob round trip: GitHub\n * creates the blobs as part of `createTree`, which keeps the write to a\n * fixed 3 mutations (tree + commit + ref) regardless of file count and\n * stays under the mutation-rate secondary limit. Mirrors the GitLab\n * provider, which already inlines content in its commit actions.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = input.changes.map(change => buildTreeEntry(repo, change))\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nfunction buildTreeEntry(repo: RepoRef, change: FileChange): TreeEntry {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n // Inline UTF-8 content — the write path never produces binary/base64, so\n // there is no blob-encoding branch to preserve. GitHub creates the blob\n // when the tree is created.\n return { path, mode: '100644', type: 'blob', content: change.content }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: response.data.sha, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n } catch (error) {\n if (isNotModified(error)) {\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: null, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n }\n throw error\n }\n}\n\n/**\n * Post-merge deletion of the source branch — **opt-in**. `mergeBranch` is a\n * general merge primitive: like `git merge` and GitHub's merge API it leaves\n * the source branch alone by default. A caller that wants the merged branch\n * removed (e.g. cr/* review-branch cleanup) opts in with\n * `removeSourceBranch: true`.\n *\n * Even when opted in, a long-lived branch is NEVER deleted: not the merge\n * target (`into`), not the `contentrain` content branch, and not the repo's\n * default branch. This mirrors the LocalProvider's `deleteRemoteBranch`\n * guard and defends against head/base confusion and `contentrain→main` /\n * `main→contentrain` flows. If the default branch can't be resolved, the\n * delete is skipped (fail safe). Never throws — the merge already succeeded.\n */\nasync function cleanupSourceBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n // Free guards first — no API call for the obvious protected refs.\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, repo)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n // Cannot verify the default branch — refuse to delete rather than risk it.\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, repo, branch)\n return { deleted: true }\n } catch (error) {\n const status = (error as { status?: number }).status\n // 404/422: ref already gone (e.g. a concurrent cleanup) — expected no-op.\n if (status === 404 || status === 422) {\n return { deleted: false, skipped: 'not-found' }\n }\n return {\n deleted: false,\n warning: `Could not delete \"${branch}\": ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n *\n * Pass `{ memoize: true }` to dedupe reads within one operation: a repeated\n * `(path, ref)` returns the in-flight/cached promise instead of a fresh\n * `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only\n * reader — a long-lived reader that outlives a write would serve stale\n * results, so the provider's own reader never enables it. Failed reads are\n * evicted so a transient error is retried, not cached forever.\n */\nexport class GitHubReader implements RepoReader {\n private readonly fileMemo = new Map<string, Promise<string>>()\n private readonly listMemo = new Map<string, Promise<string[]>>()\n private readonly existsMemo = new Map<string, Promise<boolean>>()\n\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n private readonly opts?: { memoize?: boolean },\n ) {}\n\n /**\n * Run `fetch` through the given memo when memoization is enabled, keyed by\n * `(ref, repoPath)`. A rejected promise is evicted so the next call retries.\n */\n private memoized<T>(memo: Map<string, Promise<T>>, repoPath: string, ref: string | undefined, fetch: () => Promise<T>): Promise<T> {\n if (!this.opts?.memoize) return fetch()\n const key = `${ref ?? ''}:${repoPath}`\n const cached = memo.get(key)\n if (cached) return cached\n const promise = fetch()\n memo.set(key, promise)\n promise.catch(() => memo.delete(key))\n return promise\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.fileMemo, repoPath, ref, () => this.readFileUncached(path, repoPath, ref))\n }\n\n private async readFileUncached(path: string, repoPath: string, ref?: string): Promise<string> {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.listMemo, repoPath, ref, async () => {\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n })\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.existsMemo, repoPath, ref, async () => {\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n })\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into, opts)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"} | ||
| {"version":3,"file":"index.mjs","names":["listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/apply-plan.ts","../../../src/providers/github/branch-ops.ts","../../../src/providers/github/reader.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/types.ts"],"sourcesContent":["import { createPrivateKey, createSign } from 'node:crypto'\n\n/**\n * GitHub App authentication helpers.\n *\n * Two entry points:\n *\n * - {@link signAppJwt} — mint a short-lived (10 min) GitHub App JWT from\n * `appId` + `privateKey`. Used to authenticate app-level endpoints\n * (listing installations, creating installation tokens).\n *\n * - {@link exchangeInstallationToken} — exchange an app JWT for a\n * per-installation access token that can be passed straight to\n * `Octokit({ auth: token })`. Installation tokens last ~1 hour and\n * must be refreshed.\n *\n * Both helpers are pure — they never import `@octokit/rest` or\n * `@octokit/auth-app`. Callers that want auto-refresh behaviour should\n * either wire `@octokit/auth-app` as the `authStrategy` when constructing\n * their own Octokit (see the embedding guide), or call\n * `exchangeInstallationToken` on their own schedule.\n */\n\nexport interface AppAuthConfig {\n /** GitHub App ID (numeric, from the app's settings page). */\n appId: number\n /** PEM-encoded private key — the contents of the `.pem` the app issued. */\n privateKey: string\n /** Installation the token should be scoped to. */\n installationId: number\n}\n\nexport interface InstallationTokenResult {\n /** Opaque bearer token — pass to `new Octokit({ auth: token })`. */\n token: string\n /** ISO 8601 expiry. Installation tokens expire after ~1 hour. */\n expiresAt: string\n}\n\n/**\n * Sign a GitHub App JWT.\n *\n * GitHub's spec: RS256 (RSASSA-PKCS1-v1_5), 10-minute max lifetime,\n * `iat` 60 seconds in the past to cover small clock skew, `iss`\n * set to the numeric app ID.\n */\nfunction base64UrlEncode(obj: unknown): string {\n return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url')\n}\n\nexport function signAppJwt(config: Pick<AppAuthConfig, 'appId' | 'privateKey'>): string {\n const now = Math.floor(Date.now() / 1000)\n const header = { alg: 'RS256', typ: 'JWT' }\n const payload = {\n iat: now - 60,\n exp: now + 9 * 60,\n iss: String(config.appId),\n }\n\n const toSign = `${base64UrlEncode(header)}.${base64UrlEncode(payload)}`\n const keyObject = createPrivateKey({ key: config.privateKey, format: 'pem' })\n const signer = createSign('RSA-SHA256')\n signer.update(toSign)\n signer.end()\n const signature = signer.sign(keyObject).toString('base64url')\n\n return `${toSign}.${signature}`\n}\n\n/**\n * Exchange an App JWT for an installation-scoped access token by calling\n * GitHub's `POST /app/installations/{id}/access_tokens` endpoint.\n *\n * Uses `fetch` (Node ≥22 has it native) so the helper stays dependency-\n * free. Throws on non-2xx responses with the GitHub-returned message.\n */\nexport async function exchangeInstallationToken(\n config: AppAuthConfig,\n opts: { baseUrl?: string, fetchImpl?: typeof globalThis.fetch } = {},\n): Promise<InstallationTokenResult> {\n const jwt = signAppJwt(config)\n const baseUrl = opts.baseUrl ?? 'https://api.github.com'\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n const url = `${baseUrl}/app/installations/${config.installationId}/access_tokens`\n const response = await fetchImpl(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${jwt}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2022-11-28',\n },\n })\n\n if (!response.ok) {\n const body = await response.text().catch(() => '')\n throw new Error(\n `GitHub installation-token exchange failed: ${response.status} ${response.statusText}${body ? ` — ${body}` : ''}`,\n )\n }\n\n const data = await response.json() as { token: string, expires_at: string }\n return { token: data.token, expiresAt: data.expires_at }\n}\n","import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitHubProvider.\n *\n * GitHub over the Git Data API has no working tree, so local worktree\n * features, source-file access and AST scans are unavailable. Push /\n * PR operations are free because every commit goes straight to the\n * remote. Branch protection detection uses the Repos API.\n *\n * Tools that require `astScan`, `sourceRead` or `sourceWrite` must\n * gracefully reject (with `capability_required`) when running against\n * a GitHubProvider — this is the mechanism behind phase 6's normalize\n * capability-gate.\n */\nexport const GITHUB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n// A GitHub Git-tree entry. Writes carry `content` inline (GitHub creates the\n// blob as part of createTree); deletions carry `sha: null`. The two are\n// mutually exclusive — an entry may set `content` OR `sha`, never both.\ntype TreeEntry =\n | { path: string, mode: '100644', type: 'blob', content: string }\n | { path: string, mode: '100644', type: 'blob', sha: null }\n\n/**\n * Apply a plan to a GitHub repository as a single atomic commit via the\n * Git Data API. High-level flow:\n *\n * 1. Resolve the base commit SHA — either the current HEAD of the target\n * branch, or the HEAD of `input.base` (or the repo's default branch)\n * when the target branch does not yet exist.\n * 2. Read the base tree SHA from that commit.\n * 3. Map `input.changes` to tree entries — `content` inline for each write,\n * `sha: null` for each deletion. No per-file blob round trip: GitHub\n * creates the blobs as part of `createTree`, which keeps the write to a\n * fixed 3 mutations (tree + commit + ref) regardless of file count and\n * stays under the mutation-rate secondary limit. Mirrors the GitLab\n * provider, which already inlines content in its commit actions.\n * 4. Create a new tree layered on top of the base tree with the collected\n * entries.\n * 5. Create the commit (tree, parents, author).\n * 6. Update an existing branch ref or create a new one pointing at the\n * commit.\n *\n * No working tree, no transaction — the commit is durable as soon as the\n * final ref update returns.\n */\nexport async function applyPlanToGitHub(\n client: GitHubClient,\n repo: RepoRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base)\n\n const baseCommit = await client.rest.git.getCommit({\n owner: repo.owner,\n repo: repo.name,\n commit_sha: baseSha,\n })\n const baseTreeSha = baseCommit.data.tree.sha\n\n const treeEntries = input.changes.map(change => buildTreeEntry(repo, change))\n\n const tree = await client.rest.git.createTree({\n owner: repo.owner,\n repo: repo.name,\n base_tree: baseTreeSha,\n tree: treeEntries,\n })\n\n const timestamp = new Date().toISOString()\n const commit = await client.rest.git.createCommit({\n owner: repo.owner,\n repo: repo.name,\n message: input.message,\n tree: tree.data.sha,\n parents: [baseSha],\n author: {\n name: input.author.name,\n email: input.author.email,\n date: timestamp,\n },\n })\n\n if (branchExists) {\n await client.rest.git.updateRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${input.branch}`,\n sha: commit.data.sha,\n })\n } else {\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${input.branch}`,\n sha: commit.data.sha,\n })\n }\n\n return {\n sha: commit.data.sha,\n message: commit.data.message,\n author: {\n name: commit.data.author?.name ?? input.author.name,\n email: commit.data.author?.email ?? input.author.email,\n },\n timestamp: commit.data.author?.date ?? timestamp,\n }\n}\n\nfunction buildTreeEntry(repo: RepoRef, change: FileChange): TreeEntry {\n const path = resolveRepoPath(repo.contentRoot, change.path)\n if (change.content === null) {\n return { path, mode: '100644', type: 'blob', sha: null }\n }\n // Inline UTF-8 content — the write path never produces binary/base64, so\n // there is no blob-encoding branch to preserve. GitHub creates the blob\n // when the tree is created.\n return { path, mode: '100644', type: 'blob', content: change.content }\n}\n\nasync function resolveBaseSha(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string | undefined,\n): Promise<{ baseSha: string, branchExists: boolean }> {\n try {\n const ref = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${branch}`,\n })\n return { baseSha: ref.data.object.sha, branchExists: true }\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // Invariant: feature branches always fork from the Contentrain\n // content-tracking branch. Callers that genuinely want to bypass this\n // must pass `base` explicitly. The repository's default branch\n // (main / master / trunk) is NOT the fallback — that would create a\n // split-brain where remote writes derive from a different ref than\n // local writes, which the LocalProvider transaction path forbids.\n const baseRefName = base ?? CONTENTRAIN_BRANCH\n\n const baseRef = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${baseRefName}`,\n })\n return { baseSha: baseRef.data.object.sha, branchExists: false }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * Branch/merge/diff helpers backed by the Repos and Git Data APIs.\n *\n * Pure functions so they can be composed into `GitHubProvider` or used\n * standalone. All throw on non-404 errors; 404s collapse to empty-ish\n * results where that matches the `RepoProvider` contract (e.g. a\n * missing branch prefix yields `[]` rather than raising).\n */\n\nexport async function getDefaultBranch(client: GitHubClient, repo: RepoRef): Promise<string> {\n const response = await client.rest.repos.get({ owner: repo.owner, repo: repo.name })\n return response.data.default_branch\n}\n\nexport async function listBranches(\n client: GitHubClient,\n repo: RepoRef,\n prefix?: string,\n): Promise<Branch[]> {\n const branches: Branch[] = []\n const iterator = client.paginate.iterator(client.rest.repos.listBranches, {\n owner: repo.owner,\n repo: repo.name,\n per_page: 100,\n })\n for await (const page of iterator) {\n for (const b of page.data) {\n if (prefix && !b.name.startsWith(prefix)) continue\n branches.push({ name: b.name, sha: b.commit.sha, protected: b.protected })\n }\n }\n return branches\n}\n\nexport async function createBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n const base = await client.rest.git.getRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${fromRef}`,\n })\n await client.rest.git.createRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `refs/heads/${name}`,\n sha: base.data.object.sha,\n })\n}\n\nexport async function deleteBranch(\n client: GitHubClient,\n repo: RepoRef,\n name: string,\n): Promise<void> {\n await client.rest.git.deleteRef({\n owner: repo.owner,\n repo: repo.name,\n ref: `heads/${name}`,\n })\n}\n\nexport async function getBranchDiff(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base,\n head: branch,\n })\n const files = response.data.files ?? []\n return files.map(f => ({\n path: f.filename,\n status: normaliseStatus(f.status),\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n try {\n const response = await client.rest.repos.merge({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: response.data.sha, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n } catch (error) {\n if (isNotModified(error)) {\n const remote = await cleanupSourceBranch(client, repo, branch, into, opts)\n return { merged: true, sha: null, pullRequestUrl: null, ...(remote ? { remote } : {}) }\n }\n throw error\n }\n}\n\n/**\n * Post-merge deletion of the source branch — **opt-in**. `mergeBranch` is a\n * general merge primitive: like `git merge` and GitHub's merge API it leaves\n * the source branch alone by default. A caller that wants the merged branch\n * removed (e.g. cr/* review-branch cleanup) opts in with\n * `removeSourceBranch: true`.\n *\n * Even when opted in, a long-lived branch is NEVER deleted: not the merge\n * target (`into`), not the `contentrain` content branch, and not the repo's\n * default branch. This mirrors the LocalProvider's `deleteRemoteBranch`\n * guard and defends against head/base confusion and `contentrain→main` /\n * `main→contentrain` flows. If the default branch can't be resolved, the\n * delete is skipped (fail safe). Never throws — the merge already succeeded.\n */\nasync function cleanupSourceBranch(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n // Free guards first — no API call for the obvious protected refs.\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, repo)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n // Cannot verify the default branch — refuse to delete rather than risk it.\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, repo, branch)\n return { deleted: true }\n } catch (error) {\n const status = (error as { status?: number }).status\n // 404/422: ref already gone (e.g. a concurrent cleanup) — expected no-op.\n if (status === 404 || status === 422) {\n return { deleted: false, skipped: 'not-found' }\n }\n return {\n deleted: false,\n warning: `Could not delete \"${branch}\": ${error instanceof Error ? error.message : String(error)}`,\n }\n }\n}\n\nexport async function isMerged(\n client: GitHubClient,\n repo: RepoRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n const response = await client.rest.repos.compareCommits({\n owner: repo.owner,\n repo: repo.name,\n base: into,\n head: branch,\n })\n return response.data.ahead_by === 0\n}\n\nfunction normaliseStatus(status: string): FileDiff['status'] {\n if (status === 'added') return 'added'\n if (status === 'removed') return 'removed'\n return 'modified'\n}\n\nfunction isNotModified(error: unknown): boolean {\n return typeof error === 'object' && error !== null && (error as { status?: number }).status === 204\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitHubClient } from './client.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.\n *\n * Reads pass through `repos.getContent`; directories return a list of\n * names, files return decoded UTF-8 text. Files larger than ~1 MB are\n * fetched by blob SHA through `git.getBlob` because `getContent` omits\n * the body in that case.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. When omitted, GitHub resolves to the repository's default\n * branch — which is usually wrong for Contentrain flows, so callers\n * should always pass the explicit `contentrain` tracking branch.\n *\n * Pass `{ memoize: true }` to dedupe reads within one operation: a repeated\n * `(path, ref)` returns the in-flight/cached promise instead of a fresh\n * `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only\n * reader — a long-lived reader that outlives a write would serve stale\n * results, so the provider's own reader never enables it. Failed reads are\n * evicted so a transient error is retried, not cached forever.\n */\nexport class GitHubReader implements RepoReader {\n private readonly fileMemo = new Map<string, Promise<string>>()\n private readonly listMemo = new Map<string, Promise<string[]>>()\n private readonly existsMemo = new Map<string, Promise<boolean>>()\n\n constructor(\n private readonly client: GitHubClient,\n private readonly repo: RepoRef,\n private readonly opts?: { memoize?: boolean },\n ) {}\n\n /**\n * Run `fetch` through the given memo when memoization is enabled, keyed by\n * `(ref, repoPath)`. A rejected promise is evicted so the next call retries.\n */\n private memoized<T>(memo: Map<string, Promise<T>>, repoPath: string, ref: string | undefined, fetch: () => Promise<T>): Promise<T> {\n if (!this.opts?.memoize) return fetch()\n const key = `${ref ?? ''}:${repoPath}`\n const cached = memo.get(key)\n if (cached) return cached\n const promise = fetch()\n memo.set(key, promise)\n promise.catch(() => memo.delete(key))\n return promise\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.fileMemo, repoPath, ref, () => this.readFileUncached(path, repoPath, ref))\n }\n\n private async readFileUncached(path: string, repoPath: string, ref?: string): Promise<string> {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (Array.isArray(data)) {\n throw new Error(`GitHubReader: path \"${path}\" is a directory, not a file`)\n }\n if (data.type !== 'file') {\n throw new Error(`GitHubReader: path \"${path}\" is a ${data.type}, not a file`)\n }\n\n // GitHub omits content for files > 1 MB — fall back to blob fetch.\n if (data.content === '' && data.size > 0) {\n const blob = await this.client.rest.git.getBlob({\n owner: this.repo.owner,\n repo: this.repo.name,\n file_sha: data.sha,\n })\n if (blob.data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected blob encoding \"${blob.data.encoding}\" for ${path}`)\n }\n return Buffer.from(blob.data.content, 'base64').toString('utf-8')\n }\n\n if (data.encoding !== 'base64') {\n throw new Error(`GitHubReader: unexpected encoding \"${data.encoding}\" for ${path}`)\n }\n return Buffer.from(data.content, 'base64').toString('utf-8')\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.listMemo, repoPath, ref, async () => {\n try {\n const response = await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n const data = response.data\n if (!Array.isArray(data)) return []\n return data.map(entry => entry.name)\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n })\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.repo.contentRoot, path)\n return this.memoized(this.existsMemo, repoPath, ref, async () => {\n try {\n await this.client.rest.repos.getContent({\n owner: this.repo.owner,\n repo: this.repo.name,\n path: repoPath,\n ref,\n })\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n })\n }\n}\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitHub } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITHUB_CAPABILITIES } from './capabilities.js'\nimport type { GitHubClient } from './client.js'\nimport { GitHubReader } from './reader.js'\nimport type { RepoRef } from './types.js'\n\n/**\n * GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub\n * REST + Git Data APIs.\n *\n * The provider is transport-agnostic; it only talks to an `Octokit`\n * instance passed into the constructor. The `createGitHubProvider`\n * helper in `factory.ts` wraps the dynamic import so consumers never\n * have to touch Octokit directly.\n */\nexport class GitHubProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITHUB_CAPABILITIES\n private readonly reader: GitHubReader\n\n constructor(\n private readonly client: GitHubClient,\n public readonly repo: RepoRef,\n ) {\n this.reader = new GitHubReader(client, repo)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitHub(this.client, this.repo, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.repo, prefix)\n }\n createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? 'main'\n return createBranchOp(this.client, this.repo, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.repo, name)\n }\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? 'main'\n return getBranchDiffOp(this.client, this.repo, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.repo, branch, into, opts)\n }\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? 'main'\n return isMergedOp(this.client, this.repo, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.repo)\n }\n}\n","import type { GitHubClient } from './client.js'\nimport { exchangeInstallationToken } from './app-auth.js'\nimport { GitHubProvider } from './provider.js'\nimport type { GitHubAuth, RepoRef } from './types.js'\n\n/**\n * Create an Octokit-backed `GitHubClient` from an auth configuration.\n *\n * The `@octokit/rest` module is imported dynamically so it stays a pure\n * optional peer dependency — self-hosted MCP on stdio can run without\n * it. If the module is not installed, the import throws with a helpful\n * hint pointing the operator at the peer dependency.\n *\n * Two auth modes:\n *\n * - `pat` — personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners.\n * - `app` — GitHub App installation auth. The factory mints a short-\n * lived JWT, exchanges it for an installation token via\n * `exchangeInstallationToken`, and instantiates Octokit with the\n * resulting bearer. The returned token expires in ~1 hour; callers\n * that need auto-refresh should instead inject their own Octokit\n * built with `@octokit/auth-app`'s auth strategy and construct\n * `GitHubProvider` directly. See the embedding guide for trade-offs.\n */\nexport async function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient> {\n let OctokitCtor: typeof import('@octokit/rest').Octokit\n try {\n ({ Octokit: OctokitCtor } = await import('@octokit/rest'))\n } catch (error) {\n throw new Error(\n '@octokit/rest is required for the GitHubProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @octokit/rest.',\n { cause: error },\n )\n }\n\n if (auth.type === 'pat') {\n return new OctokitCtor({ auth: auth.token })\n }\n\n if (auth.type === 'app') {\n const { token } = await exchangeInstallationToken({\n appId: auth.appId,\n privateKey: auth.privateKey,\n installationId: auth.installationId,\n })\n return new OctokitCtor({ auth: token })\n }\n\n const unknown = auth as { type: string }\n throw new Error(`GitHub auth type \"${unknown.type}\" is not supported. Use \"pat\" or \"app\".`)\n}\n\n/**\n * Factory for the full provider — instantiates an Octokit client and\n * wraps it in a `GitHubProvider`. Consumers who already hold an Octokit\n * instance (HTTP server injecting shared clients, tests, etc.) should\n * instantiate `GitHubProvider` directly instead.\n */\nexport async function createGitHubProvider(opts: { auth: GitHubAuth, repo: RepoRef }): Promise<GitHubProvider> {\n const client = await createGitHubClient(opts.auth)\n return new GitHubProvider(client, opts.repo)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitHub repository.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds\n * that prefix. All reader/writer paths are joined against it.\n */\nexport interface RepoRef {\n owner: string\n name: string\n contentRoot?: string\n}\n\n/**\n * Authentication options for the GitHub provider.\n *\n * - `pat` — a personal access token or fine-grained PAT. Simplest for\n * self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.\n * - `app` — GitHub App installation auth (JWT + installation token\n * exchange). Planned for Phase 5.2; see `.internal/refactor/\n * 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.\n */\nexport type GitHubAuth =\n | { type: 'pat', token: string }\n | {\n type: 'app'\n appId: number\n privateKey: string\n installationId: number\n }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITHUB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"} |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput } from "../../index-w8QHThNS.mjs"; | ||
| import { Gitlab } from "@gitbeaker/rest"; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,3 @@ | ||
| import { n as isNotFoundError, t as resolveRepoPath } from "../../paths-BU6E-oDs.mjs"; | ||
| import { t as isNotFoundError } from "../../errors-e0YdjooK.mjs"; | ||
| import { t as resolveRepoPath } from "../../paths-enT2coeX.mjs"; | ||
| import { CONTENTRAIN_BRANCH } from "@contentrain/types"; | ||
@@ -3,0 +4,0 @@ //#region src/providers/gitlab/capabilities.ts |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.mjs","names":["listBranchesOp","getDefaultBranchOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp"],"sources":["../../../src/providers/gitlab/capabilities.ts","../../../src/providers/gitlab/apply-plan.ts","../../../src/providers/gitlab/branch-ops.ts","../../../src/providers/gitlab/reader.ts","../../../src/providers/gitlab/provider.ts","../../../src/providers/gitlab/factory.ts","../../../src/providers/gitlab/types.ts"],"sourcesContent":["import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitLabProvider.\n *\n * GitLab over REST has no working tree, so local worktree features,\n * source-file access and AST scans are unavailable. Push, merge and\n * branch-protection detection all work over the API. GitLab enforces\n * merges through merge requests, so `pullRequestFallback` is `true` —\n * `mergeBranch` opens an MR and auto-accepts it to match GitHub's\n * `repos.merge` semantics while still leaving an audit trail.\n */\nexport const GITLAB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitLabClient } from './client.js'\nimport type { ProjectRef } from './types.js'\n\ntype CommitActionType = 'create' | 'update' | 'delete'\n\ninterface CommitAction {\n action: CommitActionType\n filePath: string\n content?: string\n encoding?: 'text' | 'base64'\n}\n\n/**\n * Apply a plan to a GitLab repository as a single atomic commit via the\n * Commits API. High-level flow:\n *\n * 1. Check whether the target branch exists. If it does not, GitLab's\n * `startBranch` option forks a new branch from `input.base` (or the\n * project's default branch) as part of the same commit.\n * 2. For each `FileChange`, figure out the right GitLab action verb\n * (`create` vs `update` vs `delete`) by probing the path against the\n * resolved ref. GitLab validates this server-side and returns a 400\n * for mismatches — we avoid the round trip by getting it right up\n * front.\n * 3. POST `/repository/commits` once with the full action set. No\n * working tree, no multi-call tree-assembly dance — GitLab exposes\n * a richer primitive than the GitHub Git Data API for this case.\n *\n * The commit is durable as soon as the call returns; GitLab's ref\n * update happens inside the same request.\n */\nexport async function applyPlanToGitLab(\n client: GitLabClient,\n project: ProjectRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const branchExists = await branchHasRef(client, project, input.branch)\n // Invariant: fork from the Contentrain content-tracking branch, not\n // the repo's default branch. See ApplyPlanInput.base docstring in\n // @contentrain/types.\n const baseBranch = input.base ?? CONTENTRAIN_BRANCH\n\n // Actions are computed against the HEAD of the feature branch when it\n // exists, otherwise against the fork point (baseBranch). GitLab\n // treats `create` / `update` / `delete` as a strict check against the\n // path's existence at that ref.\n const refForActionResolution = branchExists ? input.branch : baseBranch\n const rawActions = await Promise.all(\n input.changes.map(change => resolveAction(client, project, refForActionResolution, change)),\n )\n const actions = rawActions.filter((a): a is CommitAction => a !== null)\n\n if (actions.length === 0) {\n // Nothing to apply. Callers don't generally build empty plans, but\n // if they do we short-circuit before touching the API.\n throw new Error('applyPlanToGitLab: plan contained no applicable actions')\n }\n\n const options: Record<string, unknown> = {\n authorName: input.author.name,\n authorEmail: input.author.email,\n }\n if (!branchExists) {\n options.startBranch = baseBranch\n }\n\n const response = await client.Commits.create(\n project.projectId,\n input.branch,\n input.message,\n actions,\n options,\n )\n\n // Gitbeaker's generic-heavy response type widens string fields to\n // `string | Camelize<…>` — at runtime they are always strings under\n // the default (non-camelize) response mode we use. Cast narrows the\n // shape we actually touch.\n const commit = response as {\n id: string\n message?: string\n author_name?: string\n author_email?: string\n created_at?: string\n }\n const commitTimestamp = toIsoTimestamp(commit.created_at) ?? new Date().toISOString()\n return {\n sha: commit.id,\n message: commit.message ?? input.message,\n author: {\n name: commit.author_name ?? input.author.name,\n email: commit.author_email ?? input.author.email,\n },\n timestamp: commitTimestamp,\n }\n}\n\nasync function resolveAction(\n client: GitLabClient,\n project: ProjectRef,\n ref: string,\n change: FileChange,\n): Promise<CommitAction | null> {\n const filePath = resolveRepoPath(project.contentRoot, change.path)\n const exists = await fileExistsAtRef(client, project, filePath, ref)\n\n if (change.content === null) {\n // Filter out deletes against non-existent files — GitLab returns\n // 400 otherwise. The plan author asked us to \"make this file not\n // exist\", which is already satisfied.\n if (!exists) return null\n return { action: 'delete', filePath }\n }\n\n return {\n action: exists ? 'update' : 'create',\n filePath,\n content: change.content,\n encoding: 'text',\n }\n}\n\nasync function branchHasRef(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n): Promise<boolean> {\n try {\n await client.Branches.show(project.projectId, branch)\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n}\n\nasync function fileExistsAtRef(\n client: GitLabClient,\n project: ProjectRef,\n filePath: string,\n ref: string,\n): Promise<boolean> {\n try {\n await client.RepositoryFiles.show(project.projectId, filePath, ref)\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n}\n\n\nfunction toIsoTimestamp(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const date = new Date(raw)\n if (Number.isNaN(date.getTime())) return null\n return date.toISOString()\n}\n\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitLabClient } from './client.js'\nimport type { ProjectRef } from './types.js'\n\n/**\n * Branch / merge / diff helpers backed by the GitLab REST API.\n *\n * Pure functions so they can be composed into `GitLabProvider` or used\n * standalone. 404s collapse to empty-ish results where that matches\n * the `RepoProvider` contract (missing branch prefix → `[]`).\n *\n * Merge semantics: GitLab does not expose a direct branch-to-branch\n * merge endpoint. Every merge flows through a merge request, so\n * `mergeBranch` opens an MR and immediately accepts it. The resulting\n * `MergeResult` mirrors GitHub's `repos.merge` return shape — callers\n * see the same `{ merged, sha, pullRequestUrl }` envelope either way,\n * with the MR URL available for audit.\n */\n\nexport async function getDefaultBranch(\n client: GitLabClient,\n project: ProjectRef,\n): Promise<string> {\n const p = await client.Projects.show(project.projectId) as { default_branch?: string }\n return p.default_branch ?? 'main'\n}\n\nexport async function listBranches(\n client: GitLabClient,\n project: ProjectRef,\n prefix?: string,\n): Promise<Branch[]> {\n // Gitbeaker's `all` supports a `search` string (substring match). For\n // Contentrain's `cr/*` naming the substring case is equivalent to a\n // prefix because the slug never appears anywhere except at the start.\n // Server-side filter + client-side prefix enforcement keeps us\n // correct even if the substring coincidence breaks someday.\n const options: Record<string, unknown> = {\n perPage: 100,\n maxPages: 10,\n }\n if (prefix) options.search = prefix\n\n const rawBranches = await client.Branches.all(project.projectId, options)\n const branches = Array.isArray(rawBranches) ? rawBranches : []\n\n return branches\n .filter((b: { name: string }) => !prefix || b.name.startsWith(prefix))\n .map((b: { name: string, commit: { id: string }, protected?: boolean }) => ({\n name: b.name,\n sha: b.commit.id,\n protected: b.protected ?? false,\n }))\n}\n\nexport async function createBranch(\n client: GitLabClient,\n project: ProjectRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n await client.Branches.create(project.projectId, name, fromRef)\n}\n\nexport async function deleteBranch(\n client: GitLabClient,\n project: ProjectRef,\n name: string,\n): Promise<void> {\n await client.Branches.remove(project.projectId, name)\n}\n\nexport async function getBranchDiff(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.Repositories.compare(\n project.projectId,\n base,\n branch,\n { straight: false },\n )\n const diffs = Array.isArray(response.diffs) ? response.diffs : []\n return diffs.map((d: { new_path: string, old_path: string, new_file?: boolean, deleted_file?: boolean }) => ({\n path: d.new_file ? d.new_path : d.old_path,\n status: d.deleted_file ? 'removed' : d.new_file ? 'added' : 'modified',\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n // 1. Open MR — GitLab rejects create when source === target or when\n // an MR is already open for this pair. Let the error propagate in\n // those cases; the caller retries or surfaces the message.\n const mr = await client.MergeRequests.create(\n project.projectId,\n branch,\n into,\n `[contentrain] merge ${branch} → ${into}`,\n { removeSourceBranch: false },\n )\n\n // 2. Accept the MR immediately. `shouldRemoveSourceBranch: false` keeps\n // GitLab's async server-side deletion out of the accept call — the\n // explicit cleanup below owns it, so the outcome is deterministic and\n // reported via `MergeResult.remote`.\n const accepted = await client.MergeRequests.accept(\n project.projectId,\n (mr as { iid: number }).iid,\n { shouldRemoveSourceBranch: false, squash: false },\n )\n\n const mergeSha = (accepted as { merge_commit_sha?: string | null, sha?: string | null }).merge_commit_sha\n ?? (accepted as { sha?: string | null }).sha\n ?? null\n const webUrl = (mr as { web_url?: string }).web_url ?? null\n const merged = mergeSha !== null\n\n // 3. Source-branch cleanup — OPT-IN (`removeSourceBranch: true`). Like git\n // and GitLab's own merge, `mergeBranch` leaves the source alone by\n // default. Even when opted in, a long-lived branch is NEVER deleted:\n // not the merge target (`into`), not the `contentrain` content branch,\n // not the project's default branch. Mirrors the LocalProvider guard;\n // defends against head/base confusion and contentrain↔default flows.\n // Never throws — the merge itself already succeeded.\n const remote = merged ? await cleanupSourceBranch(client, project, branch, into, opts) : undefined\n\n return { merged, sha: mergeSha, pullRequestUrl: webUrl, ...(remote ? { remote } : {}) }\n}\n\n/**\n * Opt-in ({@link mergeBranch} `removeSourceBranch: true`) deletion of the\n * merged source branch, with the same guard as the GitHub provider and the\n * LocalProvider: never delete the merge target, the `contentrain` content\n * branch, or the project default branch — even when opted in. Fail-safe\n * skips the delete if the default branch cannot be resolved. Never throws.\n */\nasync function cleanupSourceBranch(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, project)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, project, branch)\n return { deleted: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return /404|not found/i.test(message)\n ? { deleted: false, skipped: 'not-found' }\n : { deleted: false, warning: `Could not delete \"${branch}\": ${message}` }\n }\n}\n\nexport async function isMerged(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n // compare(from=into, to=branch) — commits list is empty when branch\n // is fully contained in into (i.e. already merged).\n const response = await client.Repositories.compare(\n project.projectId,\n into,\n branch,\n { straight: false },\n )\n const commits = Array.isArray(response.commits) ? response.commits : []\n return commits.length === 0\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitLabClient } from './client.js'\nimport type { ProjectRef } from './types.js'\n\n/**\n * GitLabReader — `RepoReader` backed by the GitLab REST API.\n *\n * File reads go through `RepositoryFiles.showRaw`, which returns the\n * file content as UTF-8 text (or a `Blob` on browser/edge runtimes we\n * decode with `.text()`). Directory listings go through\n * `Repositories.allRepositoryTrees`. `fileExists` tries the file\n * endpoint first and falls back to a tree listing so directories\n * resolve to `true` as well — matching LocalReader / GitHubReader.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. Callers should always pass the explicit `contentrain`\n * tracking branch; GitLab's default resolution is the project's\n * default branch, which is usually wrong for Contentrain flows.\n */\nexport class GitLabReader implements RepoReader {\n constructor(\n private readonly client: GitLabClient,\n private readonly project: ProjectRef,\n ) {}\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.project.contentRoot, path)\n const resolvedRef = ref ?? await this.resolveDefaultRef()\n const raw = await this.client.RepositoryFiles.showRaw(\n this.project.projectId,\n repoPath,\n resolvedRef,\n )\n if (typeof raw === 'string') return raw\n // Browser / edge runtimes return a Blob; decode as UTF-8.\n return (raw as Blob).text()\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.project.contentRoot, path)\n const resolvedRef = ref ?? await this.resolveDefaultRef()\n try {\n const entries = await this.client.Repositories.allRepositoryTrees(\n this.project.projectId,\n {\n path: repoPath,\n ref: resolvedRef,\n perPage: 100,\n recursive: false,\n },\n )\n return Array.isArray(entries) ? entries.map(e => e.name) : []\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.project.contentRoot, path)\n const resolvedRef = ref ?? await this.resolveDefaultRef()\n\n // 1. Try as a file — cheap and most common case for Contentrain.\n try {\n await this.client.RepositoryFiles.show(\n this.project.projectId,\n repoPath,\n resolvedRef,\n )\n return true\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // 2. Fall back to a tree listing — directories and empty dirs show\n // up here. Any non-404 result means the path resolves.\n try {\n const entries = await this.client.Repositories.allRepositoryTrees(\n this.project.projectId,\n { path: repoPath, ref: resolvedRef, perPage: 1 },\n )\n return Array.isArray(entries) && entries.length > 0\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n }\n\n private async resolveDefaultRef(): Promise<string> {\n const project = await this.client.Projects.show(this.project.projectId) as { default_branch?: string }\n return project.default_branch ?? 'main'\n }\n}\n\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitLab } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITLAB_CAPABILITIES } from './capabilities.js'\nimport type { GitLabClient } from './client.js'\nimport { GitLabReader } from './reader.js'\nimport type { ProjectRef } from './types.js'\n\n/**\n * GitLabProvider — `RepoProvider` backed by the gitbeaker-driven GitLab\n * REST API.\n *\n * Transport-agnostic: the provider only talks to a `GitLabClient`\n * (a `@gitbeaker/rest` `Gitlab` instance). `createGitLabProvider` in\n * `factory.ts` wraps the dynamic import so consumers never touch\n * `@gitbeaker/rest` directly unless they want to.\n *\n * Capability gaps versus `GitHubProvider`: none — both providers\n * expose the same set. GitLab's merge flow routes through an MR under\n * the hood, but `mergeBranch` presents the same `MergeResult` shape.\n */\nexport class GitLabProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITLAB_CAPABILITIES\n private readonly reader: GitLabReader\n\n constructor(\n private readonly client: GitLabClient,\n public readonly project: ProjectRef,\n ) {\n this.reader = new GitLabReader(client, project)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitLab(this.client, this.project, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.project, prefix)\n }\n async createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? await getDefaultBranchOp(this.client, this.project)\n await createBranchOp(this.client, this.project, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.project, name)\n }\n async getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? await getDefaultBranchOp(this.client, this.project)\n return getBranchDiffOp(this.client, this.project, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.project, branch, into, opts)\n }\n async isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? await getDefaultBranchOp(this.client, this.project)\n return isMergedOp(this.client, this.project, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.project)\n }\n}\n","import type { GitLabClient } from './client.js'\nimport { GitLabProvider } from './provider.js'\nimport type { GitLabAuth, ProjectRef } from './types.js'\n\n/**\n * Create a gitbeaker-backed `GitLabClient` from an auth configuration.\n *\n * The `@gitbeaker/rest` module is imported dynamically so it stays a\n * pure optional peer dependency — self-hosted MCP on stdio runs fine\n * without it. If the module is not installed, the import throws with\n * a helpful hint pointing the operator at the peer dependency.\n *\n * Supported auth types: `pat`, `oauth`, `job`. All three are thin\n * wrappers over gitbeaker's `token`, `oauthToken`, and `jobToken`\n * constructor options.\n */\nexport async function createGitLabClient(\n auth: GitLabAuth,\n host?: string,\n): Promise<GitLabClient> {\n let GitlabCtor: typeof import('@gitbeaker/rest').Gitlab\n try {\n ({ Gitlab: GitlabCtor } = await import('@gitbeaker/rest'))\n } catch (error) {\n throw new Error(\n '@gitbeaker/rest is required for the GitLabProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @gitbeaker/rest.',\n { cause: error },\n )\n }\n\n const config: Record<string, unknown> = host ? { host } : {}\n switch (auth.type) {\n case 'pat':\n config.token = auth.token\n break\n case 'oauth':\n config.oauthToken = auth.oauthToken\n break\n case 'job':\n config.jobToken = auth.jobToken\n break\n default: {\n const { type } = auth as { type: string }\n throw new Error(`Unsupported GitLab auth type: \"${type}\"`)\n }\n }\n\n return new GitlabCtor(config) as GitLabClient\n}\n\n/**\n * Factory for the full provider — instantiates a gitbeaker client and\n * wraps it in a `GitLabProvider`. Consumers who already hold a\n * gitbeaker instance (HTTP server injecting shared clients, tests,\n * etc.) should instantiate `GitLabProvider` directly instead.\n */\nexport async function createGitLabProvider(\n opts: { auth: GitLabAuth, project: ProjectRef },\n): Promise<GitLabProvider> {\n const client = await createGitLabClient(opts.auth, opts.project.host)\n return new GitLabProvider(client, opts.project)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitLab project.\n *\n * `projectId` accepts the numeric project ID or a URL-encoded path\n * (`namespace/project`). Gitbeaker URL-encodes string paths internally,\n * so either form is fine.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain sits under `apps/web/.contentrain/` it holds that\n * prefix. All reader/writer paths are joined against it.\n *\n * `host` points at a self-hosted GitLab instance. Leave undefined to\n * use `https://gitlab.com` (gitbeaker's default).\n */\nexport interface ProjectRef {\n projectId: string | number\n contentRoot?: string\n host?: string\n}\n\n/**\n * Authentication options for the GitLab provider.\n *\n * - `pat` — personal access token with the `api` scope. Simplest for\n * self-hosted MCP or CI runners.\n * - `oauth` — OAuth2 token. Used when the runner is driven by a GitLab\n * OAuth flow.\n * - `job` — CI job token (`CI_JOB_TOKEN`). Scoped to the running\n * pipeline; useful for pipeline-driven content updates.\n */\nexport type GitLabAuth =\n | { type: 'pat', token: string }\n | { type: 'oauth', oauthToken: string }\n | { type: 'job', jobToken: string }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITLAB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"} | ||
| {"version":3,"file":"index.mjs","names":["listBranchesOp","getDefaultBranchOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp"],"sources":["../../../src/providers/gitlab/capabilities.ts","../../../src/providers/gitlab/apply-plan.ts","../../../src/providers/gitlab/branch-ops.ts","../../../src/providers/gitlab/reader.ts","../../../src/providers/gitlab/provider.ts","../../../src/providers/gitlab/factory.ts","../../../src/providers/gitlab/types.ts"],"sourcesContent":["import type { ProviderCapabilities } from '../../core/contracts/index.js'\n\n/**\n * Capability set for GitLabProvider.\n *\n * GitLab over REST has no working tree, so local worktree features,\n * source-file access and AST scans are unavailable. Push, merge and\n * branch-protection detection all work over the API. GitLab enforces\n * merges through merge requests, so `pullRequestFallback` is `true` —\n * `mergeBranch` opens an MR and auto-accepts it to match GitHub's\n * `repos.merge` semantics while still leaving an audit trail.\n */\nexport const GITLAB_CAPABILITIES: ProviderCapabilities = {\n localWorktree: false,\n sourceRead: false,\n sourceWrite: false,\n pushRemote: true,\n branchProtection: true,\n pullRequestFallback: true,\n astScan: false,\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { ApplyPlanInput, Commit, FileChange } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitLabClient } from './client.js'\nimport type { ProjectRef } from './types.js'\n\ntype CommitActionType = 'create' | 'update' | 'delete'\n\ninterface CommitAction {\n action: CommitActionType\n filePath: string\n content?: string\n encoding?: 'text' | 'base64'\n}\n\n/**\n * Apply a plan to a GitLab repository as a single atomic commit via the\n * Commits API. High-level flow:\n *\n * 1. Check whether the target branch exists. If it does not, GitLab's\n * `startBranch` option forks a new branch from `input.base` (or the\n * project's default branch) as part of the same commit.\n * 2. For each `FileChange`, figure out the right GitLab action verb\n * (`create` vs `update` vs `delete`) by probing the path against the\n * resolved ref. GitLab validates this server-side and returns a 400\n * for mismatches — we avoid the round trip by getting it right up\n * front.\n * 3. POST `/repository/commits` once with the full action set. No\n * working tree, no multi-call tree-assembly dance — GitLab exposes\n * a richer primitive than the GitHub Git Data API for this case.\n *\n * The commit is durable as soon as the call returns; GitLab's ref\n * update happens inside the same request.\n */\nexport async function applyPlanToGitLab(\n client: GitLabClient,\n project: ProjectRef,\n input: ApplyPlanInput,\n): Promise<Commit> {\n const branchExists = await branchHasRef(client, project, input.branch)\n // Invariant: fork from the Contentrain content-tracking branch, not\n // the repo's default branch. See ApplyPlanInput.base docstring in\n // @contentrain/types.\n const baseBranch = input.base ?? CONTENTRAIN_BRANCH\n\n // Actions are computed against the HEAD of the feature branch when it\n // exists, otherwise against the fork point (baseBranch). GitLab\n // treats `create` / `update` / `delete` as a strict check against the\n // path's existence at that ref.\n const refForActionResolution = branchExists ? input.branch : baseBranch\n const rawActions = await Promise.all(\n input.changes.map(change => resolveAction(client, project, refForActionResolution, change)),\n )\n const actions = rawActions.filter((a): a is CommitAction => a !== null)\n\n if (actions.length === 0) {\n // Nothing to apply. Callers don't generally build empty plans, but\n // if they do we short-circuit before touching the API.\n throw new Error('applyPlanToGitLab: plan contained no applicable actions')\n }\n\n const options: Record<string, unknown> = {\n authorName: input.author.name,\n authorEmail: input.author.email,\n }\n if (!branchExists) {\n options.startBranch = baseBranch\n }\n\n const response = await client.Commits.create(\n project.projectId,\n input.branch,\n input.message,\n actions,\n options,\n )\n\n // Gitbeaker's generic-heavy response type widens string fields to\n // `string | Camelize<…>` — at runtime they are always strings under\n // the default (non-camelize) response mode we use. Cast narrows the\n // shape we actually touch.\n const commit = response as {\n id: string\n message?: string\n author_name?: string\n author_email?: string\n created_at?: string\n }\n const commitTimestamp = toIsoTimestamp(commit.created_at) ?? new Date().toISOString()\n return {\n sha: commit.id,\n message: commit.message ?? input.message,\n author: {\n name: commit.author_name ?? input.author.name,\n email: commit.author_email ?? input.author.email,\n },\n timestamp: commitTimestamp,\n }\n}\n\nasync function resolveAction(\n client: GitLabClient,\n project: ProjectRef,\n ref: string,\n change: FileChange,\n): Promise<CommitAction | null> {\n const filePath = resolveRepoPath(project.contentRoot, change.path)\n const exists = await fileExistsAtRef(client, project, filePath, ref)\n\n if (change.content === null) {\n // Filter out deletes against non-existent files — GitLab returns\n // 400 otherwise. The plan author asked us to \"make this file not\n // exist\", which is already satisfied.\n if (!exists) return null\n return { action: 'delete', filePath }\n }\n\n return {\n action: exists ? 'update' : 'create',\n filePath,\n content: change.content,\n encoding: 'text',\n }\n}\n\nasync function branchHasRef(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n): Promise<boolean> {\n try {\n await client.Branches.show(project.projectId, branch)\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n}\n\nasync function fileExistsAtRef(\n client: GitLabClient,\n project: ProjectRef,\n filePath: string,\n ref: string,\n): Promise<boolean> {\n try {\n await client.RepositoryFiles.show(project.projectId, filePath, ref)\n return true\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n}\n\n\nfunction toIsoTimestamp(raw: unknown): string | null {\n if (typeof raw !== 'string') return null\n const date = new Date(raw)\n if (Number.isNaN(date.getTime())) return null\n return date.toISOString()\n}\n\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport type { GitLabClient } from './client.js'\nimport type { ProjectRef } from './types.js'\n\n/**\n * Branch / merge / diff helpers backed by the GitLab REST API.\n *\n * Pure functions so they can be composed into `GitLabProvider` or used\n * standalone. 404s collapse to empty-ish results where that matches\n * the `RepoProvider` contract (missing branch prefix → `[]`).\n *\n * Merge semantics: GitLab does not expose a direct branch-to-branch\n * merge endpoint. Every merge flows through a merge request, so\n * `mergeBranch` opens an MR and immediately accepts it. The resulting\n * `MergeResult` mirrors GitHub's `repos.merge` return shape — callers\n * see the same `{ merged, sha, pullRequestUrl }` envelope either way,\n * with the MR URL available for audit.\n */\n\nexport async function getDefaultBranch(\n client: GitLabClient,\n project: ProjectRef,\n): Promise<string> {\n const p = await client.Projects.show(project.projectId) as { default_branch?: string }\n return p.default_branch ?? 'main'\n}\n\nexport async function listBranches(\n client: GitLabClient,\n project: ProjectRef,\n prefix?: string,\n): Promise<Branch[]> {\n // Gitbeaker's `all` supports a `search` string (substring match). For\n // Contentrain's `cr/*` naming the substring case is equivalent to a\n // prefix because the slug never appears anywhere except at the start.\n // Server-side filter + client-side prefix enforcement keeps us\n // correct even if the substring coincidence breaks someday.\n const options: Record<string, unknown> = {\n perPage: 100,\n maxPages: 10,\n }\n if (prefix) options.search = prefix\n\n const rawBranches = await client.Branches.all(project.projectId, options)\n const branches = Array.isArray(rawBranches) ? rawBranches : []\n\n return branches\n .filter((b: { name: string }) => !prefix || b.name.startsWith(prefix))\n .map((b: { name: string, commit: { id: string }, protected?: boolean }) => ({\n name: b.name,\n sha: b.commit.id,\n protected: b.protected ?? false,\n }))\n}\n\nexport async function createBranch(\n client: GitLabClient,\n project: ProjectRef,\n name: string,\n fromRef: string,\n): Promise<void> {\n await client.Branches.create(project.projectId, name, fromRef)\n}\n\nexport async function deleteBranch(\n client: GitLabClient,\n project: ProjectRef,\n name: string,\n): Promise<void> {\n await client.Branches.remove(project.projectId, name)\n}\n\nexport async function getBranchDiff(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const response = await client.Repositories.compare(\n project.projectId,\n base,\n branch,\n { straight: false },\n )\n const diffs = Array.isArray(response.diffs) ? response.diffs : []\n return diffs.map((d: { new_path: string, old_path: string, new_file?: boolean, deleted_file?: boolean }) => ({\n path: d.new_file ? d.new_path : d.old_path,\n status: d.deleted_file ? 'removed' : d.new_file ? 'added' : 'modified',\n before: null,\n after: null,\n }))\n}\n\nexport async function mergeBranch(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult> {\n // 1. Open MR — GitLab rejects create when source === target or when\n // an MR is already open for this pair. Let the error propagate in\n // those cases; the caller retries or surfaces the message.\n const mr = await client.MergeRequests.create(\n project.projectId,\n branch,\n into,\n `[contentrain] merge ${branch} → ${into}`,\n { removeSourceBranch: false },\n )\n\n // 2. Accept the MR immediately. `shouldRemoveSourceBranch: false` keeps\n // GitLab's async server-side deletion out of the accept call — the\n // explicit cleanup below owns it, so the outcome is deterministic and\n // reported via `MergeResult.remote`.\n const accepted = await client.MergeRequests.accept(\n project.projectId,\n (mr as { iid: number }).iid,\n { shouldRemoveSourceBranch: false, squash: false },\n )\n\n const mergeSha = (accepted as { merge_commit_sha?: string | null, sha?: string | null }).merge_commit_sha\n ?? (accepted as { sha?: string | null }).sha\n ?? null\n const webUrl = (mr as { web_url?: string }).web_url ?? null\n const merged = mergeSha !== null\n\n // 3. Source-branch cleanup — OPT-IN (`removeSourceBranch: true`). Like git\n // and GitLab's own merge, `mergeBranch` leaves the source alone by\n // default. Even when opted in, a long-lived branch is NEVER deleted:\n // not the merge target (`into`), not the `contentrain` content branch,\n // not the project's default branch. Mirrors the LocalProvider guard;\n // defends against head/base confusion and contentrain↔default flows.\n // Never throws — the merge itself already succeeded.\n const remote = merged ? await cleanupSourceBranch(client, project, branch, into, opts) : undefined\n\n return { merged, sha: mergeSha, pullRequestUrl: webUrl, ...(remote ? { remote } : {}) }\n}\n\n/**\n * Opt-in ({@link mergeBranch} `removeSourceBranch: true`) deletion of the\n * merged source branch, with the same guard as the GitHub provider and the\n * LocalProvider: never delete the merge target, the `contentrain` content\n * branch, or the project default branch — even when opted in. Fail-safe\n * skips the delete if the default branch cannot be resolved. Never throws.\n */\nasync function cleanupSourceBranch(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n into: string,\n opts?: { removeSourceBranch?: boolean },\n): Promise<MergeResult['remote'] | undefined> {\n if (opts?.removeSourceBranch !== true) return undefined\n\n if (branch === into || branch === CONTENTRAIN_BRANCH) {\n return { deleted: false, skipped: 'protected' }\n }\n try {\n if (branch === await getDefaultBranch(client, project)) {\n return { deleted: false, skipped: 'protected' }\n }\n } catch {\n return { deleted: false, skipped: 'protected' }\n }\n\n try {\n await deleteBranch(client, project, branch)\n return { deleted: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return /404|not found/i.test(message)\n ? { deleted: false, skipped: 'not-found' }\n : { deleted: false, warning: `Could not delete \"${branch}\": ${message}` }\n }\n}\n\nexport async function isMerged(\n client: GitLabClient,\n project: ProjectRef,\n branch: string,\n into: string,\n): Promise<boolean> {\n // compare(from=into, to=branch) — commits list is empty when branch\n // is fully contained in into (i.e. already merged).\n const response = await client.Repositories.compare(\n project.projectId,\n into,\n branch,\n { straight: false },\n )\n const commits = Array.isArray(response.commits) ? response.commits : []\n return commits.length === 0\n}\n","import type { RepoReader } from '../../core/contracts/index.js'\nimport { isNotFoundError, resolveRepoPath } from '../shared/index.js'\nimport type { GitLabClient } from './client.js'\nimport type { ProjectRef } from './types.js'\n\n/**\n * GitLabReader — `RepoReader` backed by the GitLab REST API.\n *\n * File reads go through `RepositoryFiles.showRaw`, which returns the\n * file content as UTF-8 text (or a `Blob` on browser/edge runtimes we\n * decode with `.text()`). Directory listings go through\n * `Repositories.allRepositoryTrees`. `fileExists` tries the file\n * endpoint first and falls back to a tree listing so directories\n * resolve to `true` as well — matching LocalReader / GitHubReader.\n *\n * `ref` is forwarded verbatim and may be a branch name, tag name or\n * commit SHA. Callers should always pass the explicit `contentrain`\n * tracking branch; GitLab's default resolution is the project's\n * default branch, which is usually wrong for Contentrain flows.\n */\nexport class GitLabReader implements RepoReader {\n constructor(\n private readonly client: GitLabClient,\n private readonly project: ProjectRef,\n ) {}\n\n async readFile(path: string, ref?: string): Promise<string> {\n const repoPath = resolveRepoPath(this.project.contentRoot, path)\n const resolvedRef = ref ?? await this.resolveDefaultRef()\n const raw = await this.client.RepositoryFiles.showRaw(\n this.project.projectId,\n repoPath,\n resolvedRef,\n )\n if (typeof raw === 'string') return raw\n // Browser / edge runtimes return a Blob; decode as UTF-8.\n return (raw as Blob).text()\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const repoPath = resolveRepoPath(this.project.contentRoot, path)\n const resolvedRef = ref ?? await this.resolveDefaultRef()\n try {\n const entries = await this.client.Repositories.allRepositoryTrees(\n this.project.projectId,\n {\n path: repoPath,\n ref: resolvedRef,\n perPage: 100,\n recursive: false,\n },\n )\n return Array.isArray(entries) ? entries.map(e => e.name) : []\n } catch (error) {\n if (isNotFoundError(error)) return []\n throw error\n }\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const repoPath = resolveRepoPath(this.project.contentRoot, path)\n const resolvedRef = ref ?? await this.resolveDefaultRef()\n\n // 1. Try as a file — cheap and most common case for Contentrain.\n try {\n await this.client.RepositoryFiles.show(\n this.project.projectId,\n repoPath,\n resolvedRef,\n )\n return true\n } catch (error) {\n if (!isNotFoundError(error)) throw error\n }\n\n // 2. Fall back to a tree listing — directories and empty dirs show\n // up here. Any non-404 result means the path resolves.\n try {\n const entries = await this.client.Repositories.allRepositoryTrees(\n this.project.projectId,\n { path: repoPath, ref: resolvedRef, perPage: 1 },\n )\n return Array.isArray(entries) && entries.length > 0\n } catch (error) {\n if (isNotFoundError(error)) return false\n throw error\n }\n }\n\n private async resolveDefaultRef(): Promise<string> {\n const project = await this.client.Projects.show(this.project.projectId) as { default_branch?: string }\n return project.default_branch ?? 'main'\n }\n}\n\n","import type {\n ApplyPlanInput,\n Branch,\n Commit,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { applyPlanToGitLab } from './apply-plan.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { GITLAB_CAPABILITIES } from './capabilities.js'\nimport type { GitLabClient } from './client.js'\nimport { GitLabReader } from './reader.js'\nimport type { ProjectRef } from './types.js'\n\n/**\n * GitLabProvider — `RepoProvider` backed by the gitbeaker-driven GitLab\n * REST API.\n *\n * Transport-agnostic: the provider only talks to a `GitLabClient`\n * (a `@gitbeaker/rest` `Gitlab` instance). `createGitLabProvider` in\n * `factory.ts` wraps the dynamic import so consumers never touch\n * `@gitbeaker/rest` directly unless they want to.\n *\n * Capability gaps versus `GitHubProvider`: none — both providers\n * expose the same set. GitLab's merge flow routes through an MR under\n * the hood, but `mergeBranch` presents the same `MergeResult` shape.\n */\nexport class GitLabProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = GITLAB_CAPABILITIES\n private readonly reader: GitLabReader\n\n constructor(\n private readonly client: GitLabClient,\n public readonly project: ProjectRef,\n ) {\n this.reader = new GitLabReader(client, project)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n applyPlan(input: ApplyPlanInput): Promise<Commit> {\n return applyPlanToGitLab(this.client, this.project, input)\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.client, this.project, prefix)\n }\n async createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? await getDefaultBranchOp(this.client, this.project)\n await createBranchOp(this.client, this.project, name, resolved)\n }\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.client, this.project, name)\n }\n async getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? await getDefaultBranchOp(this.client, this.project)\n return getBranchDiffOp(this.client, this.project, branch, resolved)\n }\n mergeBranch(branch: string, into: string, opts?: { removeSourceBranch?: boolean }): Promise<MergeResult> {\n return mergeBranchOp(this.client, this.project, branch, into, opts)\n }\n async isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? await getDefaultBranchOp(this.client, this.project)\n return isMergedOp(this.client, this.project, branch, resolved)\n }\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.client, this.project)\n }\n}\n","import type { GitLabClient } from './client.js'\nimport { GitLabProvider } from './provider.js'\nimport type { GitLabAuth, ProjectRef } from './types.js'\n\n/**\n * Create a gitbeaker-backed `GitLabClient` from an auth configuration.\n *\n * The `@gitbeaker/rest` module is imported dynamically so it stays a\n * pure optional peer dependency — self-hosted MCP on stdio runs fine\n * without it. If the module is not installed, the import throws with\n * a helpful hint pointing the operator at the peer dependency.\n *\n * Supported auth types: `pat`, `oauth`, `job`. All three are thin\n * wrappers over gitbeaker's `token`, `oauthToken`, and `jobToken`\n * constructor options.\n */\nexport async function createGitLabClient(\n auth: GitLabAuth,\n host?: string,\n): Promise<GitLabClient> {\n let GitlabCtor: typeof import('@gitbeaker/rest').Gitlab\n try {\n ({ Gitlab: GitlabCtor } = await import('@gitbeaker/rest'))\n } catch (error) {\n throw new Error(\n '@gitbeaker/rest is required for the GitLabProvider but could not be loaded. '\n + 'Install it as a peer dependency: pnpm add @gitbeaker/rest.',\n { cause: error },\n )\n }\n\n const config: Record<string, unknown> = host ? { host } : {}\n switch (auth.type) {\n case 'pat':\n config.token = auth.token\n break\n case 'oauth':\n config.oauthToken = auth.oauthToken\n break\n case 'job':\n config.jobToken = auth.jobToken\n break\n default: {\n const { type } = auth as { type: string }\n throw new Error(`Unsupported GitLab auth type: \"${type}\"`)\n }\n }\n\n return new GitlabCtor(config) as GitLabClient\n}\n\n/**\n * Factory for the full provider — instantiates a gitbeaker client and\n * wraps it in a `GitLabProvider`. Consumers who already hold a\n * gitbeaker instance (HTTP server injecting shared clients, tests,\n * etc.) should instantiate `GitLabProvider` directly instead.\n */\nexport async function createGitLabProvider(\n opts: { auth: GitLabAuth, project: ProjectRef },\n): Promise<GitLabProvider> {\n const client = await createGitLabClient(opts.auth, opts.project.host)\n return new GitLabProvider(client, opts.project)\n}\n","import type { CommitAuthor } from '../../core/contracts/index.js'\n\n/**\n * A reference to a GitLab project.\n *\n * `projectId` accepts the numeric project ID or a URL-encoded path\n * (`namespace/project`). Gitbeaker URL-encodes string paths internally,\n * so either form is fine.\n *\n * `contentRoot` is the repo-relative directory prefix where Contentrain\n * content lives. For a flat content repo it stays `''`; for a monorepo\n * where Contentrain sits under `apps/web/.contentrain/` it holds that\n * prefix. All reader/writer paths are joined against it.\n *\n * `host` points at a self-hosted GitLab instance. Leave undefined to\n * use `https://gitlab.com` (gitbeaker's default).\n */\nexport interface ProjectRef {\n projectId: string | number\n contentRoot?: string\n host?: string\n}\n\n/**\n * Authentication options for the GitLab provider.\n *\n * - `pat` — personal access token with the `api` scope. Simplest for\n * self-hosted MCP or CI runners.\n * - `oauth` — OAuth2 token. Used when the runner is driven by a GitLab\n * OAuth flow.\n * - `job` — CI job token (`CI_JOB_TOKEN`). Scoped to the running\n * pipeline; useful for pipeline-driven content updates.\n */\nexport type GitLabAuth =\n | { type: 'pat', token: string }\n | { type: 'oauth', oauthToken: string }\n | { type: 'job', jobToken: string }\n\n/** Default author used when a call does not provide one. */\nexport const DEFAULT_GITLAB_AUTHOR: CommitAuthor = {\n name: 'Contentrain',\n email: 'ai@contentrain.io',\n}\n"],"mappings":"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"} |
@@ -1,2 +0,2 @@ | ||
| import { _ as RepoReader, a as FileChange, g as RepoProvider, h as ProviderCapabilities$1, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit } from "../../index-DDX-qYNw.mjs"; | ||
| import { _ as RepoReader, a as FileChange, g as RepoProvider, h as ProviderCapabilities$1, i as CommitAuthor, m as MergeResult, n as Branch, o as FileDiff, r as Commit } from "../../index-w8QHThNS.mjs"; | ||
| import { SyncResult, WorkflowMode } from "@contentrain/types"; | ||
@@ -3,0 +3,0 @@ |
@@ -1,2 +0,2 @@ | ||
| import { i as createServer, n as DEFAULT_INSTRUCTIONS, r as ToolProvider, t as CreateServerOptions } from "./server-vA1tM7DA.mjs"; | ||
| import { i as createServer, n as DEFAULT_INSTRUCTIONS, r as ToolProvider, t as CreateServerOptions } from "./server-DLauBiu2.mjs"; | ||
| export { CreateServerOptions, DEFAULT_INSTRUCTIONS, ToolProvider, createServer }; |
+8
-8
@@ -15,10 +15,10 @@ import "./contracts-DfL0BfrD.mjs"; | ||
| import "./annotations-D3tlsF38.mjs"; | ||
| import { n as createServer, t as DEFAULT_INSTRUCTIONS } from "./server-RWxCofdl.mjs"; | ||
| import "./validator-D5VncJ8M.mjs"; | ||
| import "./scan-config-BGUflS8t.mjs"; | ||
| import "./graph-builder-CRUX_8mA.mjs"; | ||
| import "./scanner-VOwrKLGC.mjs"; | ||
| import "./tsx-parser-B_aI_C2r.mjs"; | ||
| import "./apply-manager-B7BrL-ZW.mjs"; | ||
| import "./doctor-pw8EWRFR.mjs"; | ||
| import { n as createServer, t as DEFAULT_INSTRUCTIONS } from "./server-I6sqKvO8.mjs"; | ||
| import "./validator-ChiYk6ap.mjs"; | ||
| import "./scan-config-BlNLRCMx.mjs"; | ||
| import "./graph-builder-DK4Mh8Tn.mjs"; | ||
| import "./scanner-CGWhmpDz.mjs"; | ||
| import "./tsx-parser-md1N0Niu.mjs"; | ||
| import "./apply-manager-SLCRLHN_.mjs"; | ||
| import "./doctor-BwJ_nmqS.mjs"; | ||
| export { DEFAULT_INSTRUCTIONS, createServer }; |
@@ -1,2 +0,2 @@ | ||
| import { r as ToolProvider } from "../../server-vA1tM7DA.mjs"; | ||
| import { r as ToolProvider } from "../../server-DLauBiu2.mjs"; | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
@@ -3,0 +3,0 @@ import http from "node:http"; |
@@ -15,10 +15,10 @@ import "../../contracts-DfL0BfrD.mjs"; | ||
| import "../../annotations-D3tlsF38.mjs"; | ||
| import { n as createServer } from "../../server-RWxCofdl.mjs"; | ||
| import "../../validator-D5VncJ8M.mjs"; | ||
| import "../../scan-config-BGUflS8t.mjs"; | ||
| import "../../graph-builder-CRUX_8mA.mjs"; | ||
| import "../../scanner-VOwrKLGC.mjs"; | ||
| import "../../tsx-parser-B_aI_C2r.mjs"; | ||
| import "../../apply-manager-B7BrL-ZW.mjs"; | ||
| import "../../doctor-pw8EWRFR.mjs"; | ||
| import { n as createServer } from "../../server-I6sqKvO8.mjs"; | ||
| import "../../validator-ChiYk6ap.mjs"; | ||
| import "../../scan-config-BlNLRCMx.mjs"; | ||
| import "../../graph-builder-DK4Mh8Tn.mjs"; | ||
| import "../../scanner-CGWhmpDz.mjs"; | ||
| import "../../tsx-parser-md1N0Niu.mjs"; | ||
| import "../../apply-manager-SLCRLHN_.mjs"; | ||
| import "../../doctor-BwJ_nmqS.mjs"; | ||
| import { randomUUID } from "node:crypto"; | ||
@@ -25,0 +25,0 @@ import http from "node:http"; |
+1
-1
| { | ||
| "name": "@contentrain/mcp", | ||
| "version": "2.2.0", | ||
| "version": "2.3.0", | ||
| "mcpName": "io.github.Contentrain/contentrain", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
| import { c as writeText, o as readText, r as pathExists } from "./fs-DLbVB-Ek.mjs"; | ||
| import { t as readConfig } from "./config-oxxgznz7.mjs"; | ||
| import { S as writeContent, _ as resolveMdFilePath, c as validateModelDefinition, h as resolveJsonFilePath, l as writeModel, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs"; | ||
| import { r as writeContext } from "./context-DjglDPvj.mjs"; | ||
| import { n as checkBranchHealth } from "./branch-lifecycle-BAfgSQBv.mjs"; | ||
| import { n as createTransaction, t as buildBranchName } from "./transaction-1SPznNt3.mjs"; | ||
| import { extname, join } from "node:path"; | ||
| //#region src/core/apply-manager.ts | ||
| const MAX_PATCHES = 100; | ||
| /** File extensions allowed for patching — scannable source files only */ | ||
| const PATCHABLE_EXTENSIONS = new Set([ | ||
| ".vue", | ||
| ".tsx", | ||
| ".jsx", | ||
| ".ts", | ||
| ".js", | ||
| ".mjs", | ||
| ".astro", | ||
| ".svelte" | ||
| ]); | ||
| /** Directories that must never be patched */ | ||
| const FORBIDDEN_PATH_SEGMENTS = new Set([ | ||
| ".contentrain", | ||
| "node_modules", | ||
| ".git", | ||
| "dist", | ||
| "build", | ||
| ".next", | ||
| ".nuxt" | ||
| ]); | ||
| function detectFileFramework(filePath) { | ||
| switch (extname(filePath).toLowerCase()) { | ||
| case ".vue": return "vue"; | ||
| case ".svelte": return "svelte"; | ||
| case ".tsx": | ||
| case ".jsx": return "jsx"; | ||
| case ".astro": return "astro"; | ||
| case ".ts": | ||
| case ".js": | ||
| case ".mjs": return "script"; | ||
| default: return "script"; | ||
| } | ||
| } | ||
| /** | ||
| * Validate that a replacement expression uses the correct template syntax | ||
| * for the target file's framework. Returns a warning string or null. | ||
| */ | ||
| function validateFrameworkExpression(filePath, newExpression, context) { | ||
| if (context !== "tag_text") return null; | ||
| switch (detectFileFramework(filePath)) { | ||
| case "vue": | ||
| if (!newExpression.includes("{{")) return `Vue file "${filePath}": tag text expression "${newExpression}" does not contain "{{" — expected Vue template syntax like {{ $t('key') }}`; | ||
| break; | ||
| case "jsx": | ||
| if (!newExpression.includes("{")) return `JSX file "${filePath}": tag text expression "${newExpression}" does not contain "{" — expected JSX syntax like {t('key')}`; | ||
| break; | ||
| case "svelte": | ||
| if (!newExpression.includes("{")) return `Svelte file "${filePath}": tag text expression "${newExpression}" does not contain "{" — expected Svelte syntax like {$t('key')}`; | ||
| break; | ||
| case "astro": | ||
| if (!newExpression.includes("{")) return `Astro file "${filePath}": tag text expression "${newExpression}" does not contain "{" — expected Astro syntax like {t('key')}`; | ||
| break; | ||
| case "script": return `Script file "${filePath}": tag text replacement not applicable for .ts/.js files`; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Validate that a patch file path is safe and within allowed scope. | ||
| * Returns an error message or null if valid. | ||
| */ | ||
| function validatePatchPath(filePath) { | ||
| const normalizedPath = filePath.replace(/\\/g, "/"); | ||
| if (normalizedPath.includes("..")) return `Path traversal detected: "${filePath}"`; | ||
| if (normalizedPath.startsWith("/")) return `Absolute path not allowed: "${filePath}"`; | ||
| const segments = normalizedPath.split("/"); | ||
| for (const seg of segments) if (FORBIDDEN_PATH_SEGMENTS.has(seg)) return `Patching files inside "${seg}/" is not allowed: "${filePath}"`; | ||
| const ext = extname(filePath).toLowerCase(); | ||
| if (!PATCHABLE_EXTENSIONS.has(ext)) return `File extension "${ext}" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(", ")}. Path: "${filePath}"`; | ||
| return null; | ||
| } | ||
| /** | ||
| * Perform a basic syntax check on a patched file. | ||
| * Returns an error message or null if syntax appears valid. | ||
| */ | ||
| function checkSyntax(filePath, content) { | ||
| switch (extname(filePath).toLowerCase()) { | ||
| case ".ts": | ||
| case ".tsx": | ||
| case ".js": | ||
| case ".jsx": | ||
| case ".mjs": return checkJsSyntax(content); | ||
| case ".vue": return checkVueSyntax(content); | ||
| case ".svelte": | ||
| case ".astro": return checkTagBalance(content); | ||
| default: return null; | ||
| } | ||
| } | ||
| /** | ||
| * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure. | ||
| * This is intentionally conservative — it catches obvious breakage without | ||
| * requiring a full parser. | ||
| */ | ||
| function checkJsSyntax(content) { | ||
| const stack = []; | ||
| const pairs = { | ||
| ")": "(", | ||
| "]": "[", | ||
| "}": "{" | ||
| }; | ||
| let inString = null; | ||
| let escaped = false; | ||
| let inLineComment = false; | ||
| let inBlockComment = false; | ||
| for (let i = 0; i < content.length; i++) { | ||
| const ch = content[i]; | ||
| const next = content[i + 1]; | ||
| if (escaped) { | ||
| escaped = false; | ||
| continue; | ||
| } | ||
| if (ch === "\\" && inString !== null) { | ||
| escaped = true; | ||
| continue; | ||
| } | ||
| if (!inString && !inBlockComment && ch === "/" && next === "/") { | ||
| inLineComment = true; | ||
| continue; | ||
| } | ||
| if (inLineComment) { | ||
| if (ch === "\n") inLineComment = false; | ||
| continue; | ||
| } | ||
| if (!inString && !inBlockComment && ch === "/" && next === "*") { | ||
| inBlockComment = true; | ||
| i++; | ||
| continue; | ||
| } | ||
| if (inBlockComment) { | ||
| if (ch === "*" && next === "/") { | ||
| inBlockComment = false; | ||
| i++; | ||
| } | ||
| continue; | ||
| } | ||
| if (inString !== null) { | ||
| if (ch === inString) inString = null; | ||
| else if (inString !== "`" && ch === "\n") return `Unterminated string literal near offset ${i}`; | ||
| continue; | ||
| } | ||
| if (ch === "\"" || ch === "'" || ch === "`") { | ||
| inString = ch; | ||
| continue; | ||
| } | ||
| if (ch === "(" || ch === "[" || ch === "{") stack.push(ch); | ||
| else if (ch === ")" || ch === "]" || ch === "}") { | ||
| const expected = pairs[ch]; | ||
| if (stack.length === 0) return `Unmatched closing "${ch}" near offset ${i}`; | ||
| const top = stack.pop(); | ||
| if (top !== expected) return `Mismatched bracket: expected closing for "${top}" but found "${ch}" near offset ${i}`; | ||
| } | ||
| } | ||
| if (inString !== null) return `Unterminated string literal (opened with ${inString})`; | ||
| if (stack.length > 0) return `Unclosed bracket "${stack[stack.length - 1]}" — ${stack.length} unclosed bracket(s)`; | ||
| return null; | ||
| } | ||
| /** | ||
| * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced. | ||
| */ | ||
| function checkVueSyntax(content) { | ||
| const tagBalance = checkTagBalance(content); | ||
| if (tagBalance) return tagBalance; | ||
| for (const tag of ["template", "script"]) { | ||
| const openRe = new RegExp(`<${tag}[\\s>]`, "g"); | ||
| const closeRe = new RegExp(`</${tag}>`, "g"); | ||
| const opens = content.match(openRe)?.length ?? 0; | ||
| const closes = content.match(closeRe)?.length ?? 0; | ||
| if (opens !== closes) return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`; | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Basic tag balance check for HTML-like files. | ||
| * Checks that self-closing tags are handled and major structural tags are balanced. | ||
| */ | ||
| function checkTagBalance(content) { | ||
| for (const tag of [ | ||
| "div", | ||
| "section", | ||
| "main", | ||
| "header", | ||
| "footer", | ||
| "nav", | ||
| "article", | ||
| "aside", | ||
| "ul", | ||
| "ol", | ||
| "table" | ||
| ]) { | ||
| const openRe = new RegExp(`<${tag}[\\s>]`, "g"); | ||
| const closeRe = new RegExp(`</${tag}>`, "g"); | ||
| const opens = content.match(openRe)?.length ?? 0; | ||
| const closes = content.match(closeRe)?.length ?? 0; | ||
| if (opens !== closes) return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`; | ||
| } | ||
| return null; | ||
| } | ||
| async function applyExtract(projectRoot, input) { | ||
| const config = await readConfig(projectRoot); | ||
| if (!config) throw new Error("Project not initialized. Run contentrain_init first."); | ||
| const { extractions, dry_run } = input; | ||
| const existingModels = await listModels(projectRoot); | ||
| const existingIds = new Set(existingModels.map((m) => m.id)); | ||
| const modelsToCreate = []; | ||
| const modelsToUpdate = []; | ||
| const contentFiles = []; | ||
| let totalEntries = 0; | ||
| const validationErrors = []; | ||
| for (const ext of extractions) { | ||
| const modelErrors = validateModelDefinition({ | ||
| id: ext.model, | ||
| kind: ext.kind, | ||
| fields: ext.fields | ||
| }); | ||
| if (modelErrors.errors.length > 0) validationErrors.push(...modelErrors.errors.map((e) => `[${ext.model}] ${e}`)); | ||
| for (const entry of ext.entries) if (ext.kind === "dictionary") { | ||
| if (entry.data["id"] !== void 0 || entry.data["slug"] !== void 0) validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`); | ||
| for (const [key, val] of Object.entries(entry.data)) if (typeof val !== "string") validationErrors.push(`[${ext.model}] Dictionary entry value for key "${key}" must be a string, got ${typeof val}`); | ||
| } else if (ext.kind === "document") { | ||
| if (!entry.slug && !entry.data["slug"]) validationErrors.push(`[${ext.model}] Document entries must have a slug`); | ||
| } else if (ext.kind === "collection") { | ||
| if (entry.slug !== void 0 || entry.data["slug"] !== void 0) validationErrors.push(`[${ext.model}] Collection entries should not have slug`); | ||
| } else if (ext.kind === "singleton") { | ||
| if (entry.data["id"] !== void 0 || entry.slug !== void 0 || entry.data["slug"] !== void 0) validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`); | ||
| } | ||
| if (existingIds.has(ext.model)) modelsToUpdate.push(ext.model); | ||
| else modelsToCreate.push(ext.model); | ||
| totalEntries += ext.entries.length; | ||
| let previewModel; | ||
| if (existingIds.has(ext.model)) { | ||
| const real = await readModel(projectRoot, ext.model); | ||
| if (real) previewModel = real; | ||
| else previewModel = { | ||
| id: ext.model, | ||
| kind: ext.kind, | ||
| domain: ext.domain, | ||
| i18n: ext.i18n ?? true | ||
| }; | ||
| } else previewModel = { | ||
| id: ext.model, | ||
| kind: ext.kind, | ||
| domain: ext.domain, | ||
| i18n: ext.i18n ?? true | ||
| }; | ||
| const cDir = resolveContentDir(projectRoot, previewModel); | ||
| for (const entry of ext.entries) { | ||
| const locale = entry.locale ?? config.locales.default; | ||
| if (ext.kind === "document" && entry.slug) contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug)); | ||
| else contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale)); | ||
| } | ||
| } | ||
| const preview = { | ||
| models_to_create: modelsToCreate, | ||
| models_to_update: modelsToUpdate, | ||
| total_entries: totalEntries, | ||
| content_files: [...new Set(contentFiles)] | ||
| }; | ||
| if (dry_run !== false) return { | ||
| dry_run: true, | ||
| preview, | ||
| ...validationErrors.length > 0 ? { validation_errors: validationErrors } : {}, | ||
| next_steps: [ | ||
| ...validationErrors.length > 0 ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`] : [], | ||
| "Review the preview above", | ||
| "Call contentrain_apply with mode:extract and dry_run:false to execute" | ||
| ] | ||
| }; | ||
| if (validationErrors.length > 0) return { | ||
| dry_run: false, | ||
| error: "Model validation failed — cannot execute extract with invalid model definitions", | ||
| validation_errors: validationErrors, | ||
| next_steps: ["Fix the validation errors and retry"] | ||
| }; | ||
| const health = await checkBranchHealth(projectRoot); | ||
| if (health.blocked) return { | ||
| error: health.message, | ||
| action: "blocked", | ||
| hint: "Merge or delete old contentrain/* branches before executing normalize." | ||
| }; | ||
| const branchName = buildBranchName("normalize", "extract"); | ||
| const tx = await createTransaction(projectRoot, branchName, { workflowOverride: "review" }); | ||
| const sourceMap = []; | ||
| const modelsCreated = []; | ||
| const modelsUpdated = []; | ||
| let entriesWritten = 0; | ||
| try { | ||
| await tx.write(async (wt) => { | ||
| for (const ext of extractions) { | ||
| const existing = await readModel(wt, ext.model); | ||
| if (existing) { | ||
| if (ext.fields) { | ||
| const merged = { | ||
| ...existing.fields, | ||
| ...ext.fields | ||
| }; | ||
| if (Object.keys(ext.fields).filter((k) => !(k in (existing.fields ?? {}))).length > 0) { | ||
| existing.fields = merged; | ||
| await writeModel(wt, existing); | ||
| modelsUpdated.push(ext.model); | ||
| } | ||
| } | ||
| } else { | ||
| await writeModel(wt, { | ||
| id: ext.model, | ||
| name: ext.model.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "), | ||
| kind: ext.kind, | ||
| domain: ext.domain, | ||
| i18n: ext.i18n ?? true, | ||
| fields: ext.fields | ||
| }); | ||
| modelsCreated.push(ext.model); | ||
| } | ||
| const model = await readModel(wt, ext.model); | ||
| const entries = ext.entries.map((e) => ({ | ||
| locale: e.locale, | ||
| slug: e.slug, | ||
| data: e.data | ||
| })); | ||
| await writeContent(wt, model, entries, await readConfig(wt) ?? config); | ||
| entriesWritten += entries.length; | ||
| for (const entry of ext.entries) if (ext.kind === "dictionary" && entry.sources) for (const s of entry.sources) sourceMap.push({ | ||
| model: ext.model, | ||
| locale: entry.locale ?? config.locales.default, | ||
| value: s.value, | ||
| file: s.file, | ||
| line: s.line | ||
| }); | ||
| else if (entry.source) sourceMap.push({ | ||
| model: ext.model, | ||
| locale: entry.locale ?? config.locales.default, | ||
| value: entry.source.value, | ||
| file: entry.source.file, | ||
| line: entry.source.line | ||
| }); | ||
| } | ||
| if (sourceMap.length > 0) { | ||
| const sourcesByModel = {}; | ||
| for (const s of sourceMap) { | ||
| if (!sourcesByModel[s.model]) sourcesByModel[s.model] = { | ||
| source_files: [], | ||
| entry_count: 0 | ||
| }; | ||
| const modelEntry = sourcesByModel[s.model]; | ||
| if (!modelEntry.source_files.includes(s.file)) modelEntry.source_files.push(s.file); | ||
| modelEntry.entry_count++; | ||
| } | ||
| const sourcesJson = JSON.stringify({ | ||
| version: 1, | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString(), | ||
| models: sourcesByModel | ||
| }, null, 2) + "\n"; | ||
| await writeText(join(wt, ".contentrain", "normalize-sources.json"), sourcesJson); | ||
| } | ||
| await writeContext(wt, { | ||
| tool: "contentrain_apply", | ||
| model: extractions.map((e) => e.model).join(","), | ||
| locale: config.locales.default, | ||
| entries: extractions.flatMap((e) => e.entries.map((en) => en.slug ?? "entry")) | ||
| }); | ||
| }); | ||
| const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`; | ||
| await tx.commit(commitMsg); | ||
| const gitResult = { | ||
| branch: branchName, | ||
| action: "pending-review", | ||
| commit: "" | ||
| }; | ||
| try { | ||
| const completed = await tx.complete(); | ||
| gitResult.action = completed.action; | ||
| gitResult.commit = completed.commit; | ||
| if (completed.warning !== void 0) gitResult.warning = completed.warning; | ||
| } catch (error) { | ||
| gitResult.action = "incomplete"; | ||
| gitResult.warning = `Content was committed to "${branchName}" but the transaction could not be completed: ${error instanceof Error ? error.message : String(error)}. The branch is preserved — inspect it with contentrain_branch_list before retrying.`; | ||
| } finally { | ||
| await tx.cleanup(); | ||
| } | ||
| return { | ||
| dry_run: false, | ||
| results: { | ||
| models_created: modelsCreated, | ||
| models_updated: modelsUpdated, | ||
| entries_written: entriesWritten, | ||
| source_map: sourceMap | ||
| }, | ||
| git: gitResult, | ||
| context_updated: true, | ||
| next_steps: [ | ||
| "Run contentrain_validate to check the extracted content", | ||
| "Run contentrain_submit to push the branch for review", | ||
| "For browser-based review: ensure `contentrain serve` is running, direct user to http://localhost:3333/normalize", | ||
| "For terminal workflow: use contentrain_merge to merge the branch locally", | ||
| "After merge, run `npx contentrain generate` to update SDK client", | ||
| "After review, proceed with mode:reuse to patch source files" | ||
| ] | ||
| }; | ||
| } catch (error) { | ||
| await tx.cleanup(); | ||
| throw error; | ||
| } | ||
| } | ||
| async function applyReuse(projectRoot, input) { | ||
| const config = await readConfig(projectRoot); | ||
| if (!config) throw new Error("Project not initialized. Run contentrain_init first."); | ||
| const { scope, patches, dry_run } = input; | ||
| if (!scope.model && !scope.domain) throw new Error("Scope required: provide model or domain. Whole-project patching is not allowed."); | ||
| if (patches.length > MAX_PATCHES) throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`); | ||
| if (scope.model) { | ||
| if (!await readModel(projectRoot, scope.model)) throw new Error(`Model "${scope.model}" not found. Run extract phase first.`); | ||
| } | ||
| const scopeWarnings = []; | ||
| for (const patch of patches) { | ||
| const pathError = validatePatchPath(patch.file); | ||
| if (pathError) throw new Error(`Invalid patch path: ${pathError}`); | ||
| } | ||
| if (scope.model || scope.domain) { | ||
| const models = await listModels(projectRoot); | ||
| const scopeModels = scope.model ? models.filter((m) => m.id === scope.model) : scope.domain ? models.filter((m) => m.domain === scope.domain) : models; | ||
| if (scopeModels.length === 0) throw new Error(`No models found for scope ${scope.model ? `model="${scope.model}"` : `domain="${scope.domain}"`}`); | ||
| const { autoDetectSourceDirs } = await import("./core/scan-config.mjs"); | ||
| const sourceDirs = await autoDetectSourceDirs(projectRoot); | ||
| const allowedPrefixes = sourceDirs.map((d) => d === "." ? "" : d + "/"); | ||
| for (const patch of patches) { | ||
| const normalizedPath = patch.file.replace(/\\/g, "/"); | ||
| if (normalizedPath.startsWith(".contentrain/") || normalizedPath.includes("/.contentrain/")) throw new Error(`Cannot patch content/config files directly: "${patch.file}". Reuse patches source files only.`); | ||
| if (sourceDirs.length > 0 && sourceDirs[0] !== ".") { | ||
| if (!allowedPrefixes.some((prefix) => prefix === "" || normalizedPath.startsWith(prefix))) throw new Error(`Patch file "${patch.file}" is outside detected source directories (${sourceDirs.join(", ")}). Reuse patches must target source files within the project's source tree.`); | ||
| } | ||
| } | ||
| if (scope.model || scope.domain) { | ||
| const sourcesRaw = await readText(join(projectRoot, ".contentrain", "normalize-sources.json")); | ||
| if (!sourcesRaw) if (scopeModels.find((m) => m.id === scope.model)?.kind === "dictionary") scopeWarnings.push("normalize-sources.json not found. Dictionary models do not generate per-file source maps. Scope enforcement is based on source-tree locality only."); | ||
| else if (dry_run !== false) scopeWarnings.push("normalize-sources.json not found. Semantic scope enforcement is unavailable. Merge the extract branch first, then reuse will have full scope protection."); | ||
| else throw new Error("Cannot execute reuse: normalize-sources.json not found on base branch. The extract branch must be merged before reuse can execute. This ensures semantic scope enforcement protects against out-of-scope patching."); | ||
| else { | ||
| const sourcesData = JSON.parse(sourcesRaw); | ||
| let allowedSourceFiles = []; | ||
| let scopeLabel = ""; | ||
| if (scope.model) { | ||
| const modelSources = sourcesData.models?.[scope.model]?.source_files; | ||
| if (modelSources) allowedSourceFiles = modelSources; | ||
| scopeLabel = `model "${scope.model}"`; | ||
| } else if (scope.domain) { | ||
| const domainModelIds = scopeModels.map((m) => m.id); | ||
| for (const modelId of domainModelIds) { | ||
| const modelSources = sourcesData.models?.[modelId]?.source_files; | ||
| if (modelSources) allowedSourceFiles.push(...modelSources); | ||
| } | ||
| allowedSourceFiles = [...new Set(allowedSourceFiles)]; | ||
| scopeLabel = `domain "${scope.domain}"`; | ||
| } | ||
| if (allowedSourceFiles.length > 0) { | ||
| const outOfScopePatches = []; | ||
| for (const patch of patches) { | ||
| const normalizedPath = patch.file.replace(/\\/g, "/"); | ||
| if (!allowedSourceFiles.includes(normalizedPath)) outOfScopePatches.push(patch.file); | ||
| } | ||
| if (outOfScopePatches.length > 0) throw new Error(`Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. Out-of-scope files: ${outOfScopePatches.join(", ")}. Known source files: ${allowedSourceFiles.join(", ")}.`); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| const patchesByFile = /* @__PURE__ */ new Map(); | ||
| for (const patch of patches) { | ||
| if (!patchesByFile.has(patch.file)) patchesByFile.set(patch.file, []); | ||
| patchesByFile.get(patch.file).push(patch); | ||
| } | ||
| const filesToModify = [...patchesByFile.keys()]; | ||
| const importsToAdd = patches.filter((p) => p.import_statement).length; | ||
| if (dry_run !== false) return { | ||
| dry_run: true, | ||
| preview: { | ||
| files_to_modify: filesToModify, | ||
| patches_count: patches.length, | ||
| imports_to_add: importsToAdd | ||
| }, | ||
| ...scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}, | ||
| next_steps: [ | ||
| ...scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : [], | ||
| "Review the files and patches above", | ||
| "Call contentrain_apply with mode:reuse and dry_run:false to execute" | ||
| ] | ||
| }; | ||
| const reuseHealth = await checkBranchHealth(projectRoot); | ||
| if (reuseHealth.blocked) return { | ||
| dry_run: false, | ||
| error: `Branch blocked: ${reuseHealth.message}`, | ||
| next_steps: ["Merge or delete old contentrain/* branches before executing reuse."] | ||
| }; | ||
| const scopeTarget = scope.model ?? scope.domain; | ||
| const branchName = buildBranchName("normalize/reuse", scopeTarget); | ||
| const tx = await createTransaction(projectRoot, branchName, { workflowOverride: "review" }); | ||
| const filesModified = []; | ||
| let patchesApplied = 0; | ||
| let importsAdded = 0; | ||
| const patchesSkipped = []; | ||
| const frameworkWarnings = []; | ||
| const syntaxErrors = []; | ||
| try { | ||
| await tx.write(async (wt) => { | ||
| for (const [relFile, filePatches] of patchesByFile) { | ||
| const absPath = join(wt, relFile); | ||
| if (!await pathExists(absPath)) { | ||
| for (const p of filePatches) patchesSkipped.push({ | ||
| file: relFile, | ||
| line: p.line, | ||
| reason: "file not found" | ||
| }); | ||
| continue; | ||
| } | ||
| const content = await readText(absPath); | ||
| if (content === null) { | ||
| for (const p of filePatches) patchesSkipped.push({ | ||
| file: relFile, | ||
| line: p.line, | ||
| reason: "file unreadable" | ||
| }); | ||
| continue; | ||
| } | ||
| const sorted = [...filePatches].toSorted((a, b) => b.line - a.line); | ||
| const lines = content.split("\n"); | ||
| let fileModified = false; | ||
| for (const patch of sorted) { | ||
| const isTagTextContext = (lines[patch.line - 1] ?? "").includes(`>${patch.old_value}<`); | ||
| const replacementContext = isTagTextContext ? "tag_text" : "other"; | ||
| if (isTagTextContext) { | ||
| const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext); | ||
| if (fwWarning) frameworkWarnings.push({ | ||
| file: relFile, | ||
| warning: fwWarning | ||
| }); | ||
| } | ||
| if (applyPatchToLines(lines, patch)) { | ||
| patchesApplied++; | ||
| fileModified = true; | ||
| } else patchesSkipped.push({ | ||
| file: relFile, | ||
| line: patch.line, | ||
| reason: "old_value not found at or near specified line" | ||
| }); | ||
| } | ||
| const importStatements = new Set(filePatches.filter((p) => p.import_statement).map((p) => p.import_statement)); | ||
| if (importStatements.size > 0) { | ||
| const added = addImportsToLines(lines, importStatements); | ||
| importsAdded += added; | ||
| if (added > 0) fileModified = true; | ||
| } | ||
| if (fileModified) { | ||
| const newContent = lines.join("\n"); | ||
| await writeText(absPath, newContent); | ||
| filesModified.push(relFile); | ||
| const syntaxError = checkSyntax(relFile, newContent); | ||
| if (syntaxError) syntaxErrors.push({ | ||
| file: relFile, | ||
| error: syntaxError | ||
| }); | ||
| } | ||
| } | ||
| await writeContext(wt, { | ||
| tool: "contentrain_apply", | ||
| model: scopeTarget, | ||
| locale: config.locales.default | ||
| }); | ||
| }); | ||
| if (filesModified.length === 0) { | ||
| await tx.cleanup(); | ||
| return { | ||
| dry_run: false, | ||
| results: { | ||
| files_modified: [], | ||
| patches_applied: 0, | ||
| patches_skipped: patchesSkipped, | ||
| imports_added: 0, | ||
| framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : void 0 | ||
| }, | ||
| next_steps: ["No files were modified. Check patch definitions and try again."] | ||
| }; | ||
| } | ||
| const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`; | ||
| await tx.commit(commitMsg); | ||
| const gitResult = { | ||
| branch: branchName, | ||
| action: "pending-review", | ||
| commit: "" | ||
| }; | ||
| try { | ||
| const completed = await tx.complete(); | ||
| gitResult.action = completed.action; | ||
| gitResult.commit = completed.commit; | ||
| if (completed.warning !== void 0) gitResult.warning = completed.warning; | ||
| } catch (error) { | ||
| gitResult.action = "incomplete"; | ||
| gitResult.warning = `Content was committed to "${branchName}" but the transaction could not be completed: ${error instanceof Error ? error.message : String(error)}. The branch is preserved — inspect it with contentrain_branch_list before retrying.`; | ||
| } finally { | ||
| await tx.cleanup(); | ||
| } | ||
| return { | ||
| dry_run: false, | ||
| results: { | ||
| files_modified: filesModified, | ||
| patches_applied: patchesApplied, | ||
| patches_skipped: patchesSkipped, | ||
| imports_added: importsAdded, | ||
| framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : void 0, | ||
| syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : void 0 | ||
| }, | ||
| ...scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}, | ||
| git: gitResult, | ||
| next_steps: [ | ||
| "Run contentrain_validate to verify the patched files", | ||
| patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : "", | ||
| syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : "", | ||
| scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : "", | ||
| "Run contentrain_submit to push the branch for review", | ||
| "For review: direct user to http://localhost:3333/branches or use contentrain_merge", | ||
| "After all reuse phases complete, run `npx contentrain generate` to update SDK types" | ||
| ].filter(Boolean) | ||
| }; | ||
| } catch (error) { | ||
| await tx.cleanup(); | ||
| throw error; | ||
| } | ||
| } | ||
| /** | ||
| * Apply a single patch to a lines array. Mutates lines in place. | ||
| * Uses line hint for proximity matching — searches ±10 lines from hint. | ||
| */ | ||
| function applyPatchToLines(lines, patch) { | ||
| const { line, old_value, new_expression } = patch; | ||
| const lineIdx = line - 1; | ||
| const searchStart = Math.max(0, lineIdx - 10); | ||
| const searchEnd = Math.min(lines.length, lineIdx + 11); | ||
| if (lineIdx >= 0 && lineIdx < lines.length) { | ||
| const replaced = replaceInLine(lines[lineIdx], old_value, new_expression); | ||
| if (replaced !== null) { | ||
| lines[lineIdx] = replaced; | ||
| return true; | ||
| } | ||
| } | ||
| for (let i = searchStart; i < searchEnd; i++) { | ||
| if (i === lineIdx) continue; | ||
| const replaced = replaceInLine(lines[i], old_value, new_expression); | ||
| if (replaced !== null) { | ||
| lines[i] = replaced; | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| /** | ||
| * Replace old_value with new_expression in a single line. | ||
| * Matches the string literal (quoted or unquoted tag text). | ||
| * Returns the modified line, or null if not found. | ||
| * | ||
| * Guardrail #4: Safer patch matching — word boundary awareness and | ||
| * ambiguity rejection for plain text fallback. | ||
| */ | ||
| function replaceInLine(line, oldValue, newExpression) { | ||
| for (const quote of [ | ||
| "\"", | ||
| "'", | ||
| "`" | ||
| ]) { | ||
| const quoted = `${quote}${oldValue}${quote}`; | ||
| if (line.includes(quoted)) return line.replace(quoted, newExpression); | ||
| } | ||
| if (line.includes(`>${oldValue}<`)) return line.replace(`>${oldValue}<`, `>${newExpression}<`); | ||
| if (line.includes(oldValue)) { | ||
| if (countOccurrences(line, oldValue) > 1) return null; | ||
| const idx = line.indexOf(oldValue); | ||
| const charBefore = idx > 0 ? line[idx - 1] : ""; | ||
| const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length] : ""; | ||
| const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]); | ||
| const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]); | ||
| if (oldStartsWithWord && isWordChar(charBefore)) return null; | ||
| if (oldEndsWithWord && isWordChar(charAfter)) return null; | ||
| return line.replace(oldValue, newExpression); | ||
| } | ||
| return null; | ||
| } | ||
| /** Count non-overlapping occurrences of a substring */ | ||
| function countOccurrences(str, sub) { | ||
| let count = 0; | ||
| let pos = 0; | ||
| while (pos <= str.length - sub.length) { | ||
| const idx = str.indexOf(sub, pos); | ||
| if (idx === -1) break; | ||
| count++; | ||
| pos = idx + sub.length; | ||
| } | ||
| return count; | ||
| } | ||
| /** Check if a character is a word character (letter, digit, underscore) */ | ||
| function isWordChar(ch) { | ||
| return /\w/.test(ch); | ||
| } | ||
| /** | ||
| * Add import statements to the top of a file (after existing imports). | ||
| * Deduplicates — won't add if the import already exists. | ||
| * Returns number of imports actually added. | ||
| */ | ||
| function addImportsToLines(lines, imports) { | ||
| let added = 0; | ||
| const existingContent = lines.join("\n"); | ||
| let lastImportIdx = -1; | ||
| let inMultiLineImport = false; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const trimmed = lines[i].trim(); | ||
| if (inMultiLineImport) { | ||
| lastImportIdx = i; | ||
| if (trimmed.includes("}")) inMultiLineImport = false; | ||
| continue; | ||
| } | ||
| if (trimmed.startsWith("import ") || trimmed.startsWith("import{")) { | ||
| lastImportIdx = i; | ||
| if (trimmed.includes("{") && !trimmed.includes("}")) inMultiLineImport = true; | ||
| continue; | ||
| } | ||
| if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith("//") && !trimmed.startsWith("/*") && !trimmed.startsWith("*")) break; | ||
| } | ||
| let insertAt; | ||
| if (lastImportIdx >= 0) insertAt = lastImportIdx + 1; | ||
| else { | ||
| insertAt = 0; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const trimmed = lines[i].trim(); | ||
| if (i === 0 && trimmed.startsWith("#!")) { | ||
| insertAt = i + 1; | ||
| continue; | ||
| } | ||
| if (trimmed === "'use client'" || trimmed === "\"use client\"" || trimmed === "'use server'" || trimmed === "\"use server\"" || trimmed === "'use client';" || trimmed === "\"use client\";" || trimmed === "'use server';" || trimmed === "\"use server\";") { | ||
| insertAt = i + 1; | ||
| continue; | ||
| } | ||
| if (insertAt > 0 && trimmed.length > 0) break; | ||
| if (insertAt === 0 && trimmed.length > 0) break; | ||
| } | ||
| } | ||
| const toInsert = []; | ||
| for (const imp of imports) if (!existingContent.includes(imp)) { | ||
| toInsert.push(imp); | ||
| added++; | ||
| } | ||
| if (toInsert.length > 0) lines.splice(insertAt, 0, ...toInsert); | ||
| return added; | ||
| } | ||
| //#endregion | ||
| export { detectFileFramework as a, validatePatchPath as c, checkSyntax as i, applyExtract as n, replaceInLine as o, applyReuse as r, validateFrameworkExpression as s, PATCHABLE_EXTENSIONS as t }; | ||
| //# sourceMappingURL=apply-manager-B7BrL-ZW.mjs.map |
| {"version":3,"file":"apply-manager-B7BrL-ZW.mjs","names":[],"sources":["../src/core/apply-manager.ts"],"sourcesContent":["import type { ModelDefinition, FieldDef, FileFramework } from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText, writeText, pathExists } from '../util/fs.js'\nimport { readModel, writeModel, listModels, validateModelDefinition } from './model-manager.js'\nimport { writeContent, resolveContentDir, resolveJsonFilePath, resolveMdFilePath, type ContentEntry } from './content-manager.js'\nimport { readConfig } from './config.js'\nimport { writeContext } from './context.js'\nimport { createTransaction, buildBranchName } from '../git/transaction.js'\nimport { checkBranchHealth } from '../git/branch-lifecycle.js'\n\n// ─── Types ───\n\nexport interface ExtractionEntry {\n model: string\n kind: 'singleton' | 'collection' | 'dictionary' | 'document'\n domain: string\n i18n?: boolean\n fields?: Record<string, FieldDef>\n entries: Array<{\n locale?: string\n slug?: string\n data: Record<string, unknown>\n source?: { file: string; line: number; value: string }\n sources?: Array<{ file: string; line: number; key: string; value: string }>\n }>\n}\n\nexport interface ExtractionInput {\n extractions: ExtractionEntry[]\n dry_run?: boolean\n}\n\nexport interface ExtractionPreview {\n models_to_create: string[]\n models_to_update: string[]\n total_entries: number\n content_files: string[]\n}\n\nexport interface ExtractionResult {\n dry_run: boolean\n preview?: ExtractionPreview\n error?: string\n validation_errors?: string[]\n results?: {\n models_created: string[]\n models_updated: string[]\n entries_written: number\n source_map: Array<{ model: string; locale: string; value: string; file: string; line: number }>\n }\n git?: { branch: string; action: string; commit: string; warning?: string }\n context_updated?: boolean\n next_steps: string[]\n}\n\nexport interface PatchEntry {\n file: string\n line: number\n old_value: string\n new_expression: string\n import_statement?: string\n}\n\nexport interface ReuseInput {\n scope: { model?: string; domain?: string }\n patches: PatchEntry[]\n dry_run?: boolean\n}\n\nexport interface SyntaxError {\n file: string\n error: string\n}\n\nexport interface ReuseResult {\n dry_run: boolean\n error?: string\n scope_warnings?: string[]\n preview?: {\n files_to_modify: string[]\n patches_count: number\n imports_to_add: number\n }\n results?: {\n files_modified: string[]\n patches_applied: number\n patches_skipped: Array<{ file: string; line: number; reason: string }>\n imports_added: number\n framework_warnings?: Array<{ file: string; warning: string }>\n syntax_errors?: SyntaxError[]\n }\n git?: { branch: string; action: string; commit: string; warning?: string }\n next_steps: string[]\n}\n\n// ─── Constants ───\n\nconst MAX_PATCHES = 100\n\n/** File extensions allowed for patching — scannable source files only */\nexport const PATCHABLE_EXTENSIONS = new Set([\n '.vue', '.tsx', '.jsx', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directories that must never be patched */\nconst FORBIDDEN_PATH_SEGMENTS = new Set([\n '.contentrain', 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',\n])\n\n// ─── Framework Detection (Guardrail #2) ───\n\nexport type { FileFramework } from '@contentrain/types'\n\nexport function detectFileFramework(filePath: string): FileFramework {\n const ext = extname(filePath).toLowerCase()\n switch (ext) {\n case '.vue': return 'vue'\n case '.svelte': return 'svelte'\n case '.tsx':\n case '.jsx': return 'jsx'\n case '.astro': return 'astro'\n case '.ts':\n case '.js':\n case '.mjs': return 'script'\n default: return 'script'\n }\n}\n\n/**\n * Validate that a replacement expression uses the correct template syntax\n * for the target file's framework. Returns a warning string or null.\n */\nexport function validateFrameworkExpression(\n filePath: string,\n newExpression: string,\n context: 'tag_text' | 'other',\n): string | null {\n if (context !== 'tag_text') return null\n\n const framework = detectFileFramework(filePath)\n\n switch (framework) {\n case 'vue':\n if (!newExpression.includes('{{')) {\n return `Vue file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{{\" — expected Vue template syntax like {{ $t('key') }}`\n }\n break\n case 'jsx':\n if (!newExpression.includes('{')) {\n return `JSX file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected JSX syntax like {t('key')}`\n }\n break\n case 'svelte':\n if (!newExpression.includes('{')) {\n return `Svelte file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Svelte syntax like {$t('key')}`\n }\n break\n case 'astro':\n if (!newExpression.includes('{')) {\n return `Astro file \"${filePath}\": tag text expression \"${newExpression}\" does not contain \"{\" — expected Astro syntax like {t('key')}`\n }\n break\n case 'script':\n // Script files don't have template interpolation — warn if attempting tag text replacement\n return `Script file \"${filePath}\": tag text replacement not applicable for .ts/.js files`\n }\n\n return null\n}\n\n// ─── Scope Validation (Guardrail #1) ───\n\n/**\n * Validate that a patch file path is safe and within allowed scope.\n * Returns an error message or null if valid.\n */\nexport function validatePatchPath(filePath: string): string | null {\n const normalizedPath = filePath.replace(/\\\\/g, '/')\n\n // Reject path traversal\n if (normalizedPath.includes('..')) {\n return `Path traversal detected: \"${filePath}\"`\n }\n\n // Reject absolute paths\n if (normalizedPath.startsWith('/')) {\n return `Absolute path not allowed: \"${filePath}\"`\n }\n\n // Reject forbidden directories\n const segments = normalizedPath.split('/')\n for (const seg of segments) {\n if (FORBIDDEN_PATH_SEGMENTS.has(seg)) {\n return `Patching files inside \"${seg}/\" is not allowed: \"${filePath}\"`\n }\n }\n\n // Reject non-scannable extensions\n const ext = extname(filePath).toLowerCase()\n if (!PATCHABLE_EXTENSIONS.has(ext)) {\n return `File extension \"${ext}\" is not patchable. Allowed: ${[...PATCHABLE_EXTENSIONS].join(', ')}. Path: \"${filePath}\"`\n }\n\n return null\n}\n\n// ─── Syntax Check (Guardrail #5) ───\n\n/**\n * Perform a basic syntax check on a patched file.\n * Returns an error message or null if syntax appears valid.\n */\nexport function checkSyntax(filePath: string, content: string): string | null {\n const ext = extname(filePath).toLowerCase()\n\n switch (ext) {\n case '.ts':\n case '.tsx':\n case '.js':\n case '.jsx':\n case '.mjs':\n return checkJsSyntax(content)\n case '.vue':\n return checkVueSyntax(content)\n case '.svelte':\n case '.astro':\n return checkTagBalance(content)\n default:\n return null\n }\n}\n\n/**\n * Basic JS/TS syntax check: bracket/paren/brace balance + string literal closure.\n * This is intentionally conservative — it catches obvious breakage without\n * requiring a full parser.\n */\nfunction checkJsSyntax(content: string): string | null {\n const stack: string[] = []\n const pairs: Record<string, string> = { ')': '(', ']': '[', '}': '{' }\n let inString: string | null = null\n let escaped = false\n let inLineComment = false\n let inBlockComment = false\n\n for (let i = 0; i < content.length; i++) {\n const ch = content[i]!\n const next = content[i + 1]\n\n // Handle escape sequences inside strings\n if (escaped) {\n escaped = false\n continue\n }\n\n if (ch === '\\\\' && inString !== null) {\n escaped = true\n continue\n }\n\n // Line comment\n if (!inString && !inBlockComment && ch === '/' && next === '/') {\n inLineComment = true\n continue\n }\n if (inLineComment) {\n if (ch === '\\n') inLineComment = false\n continue\n }\n\n // Block comment\n if (!inString && !inBlockComment && ch === '/' && next === '*') {\n inBlockComment = true\n i++ // skip *\n continue\n }\n if (inBlockComment) {\n if (ch === '*' && next === '/') {\n inBlockComment = false\n i++ // skip /\n }\n continue\n }\n\n // String handling\n if (inString !== null) {\n if (ch === inString) {\n // Template literal allows multi-line, others don't\n inString = null\n } else if (inString !== '`' && ch === '\\n') {\n return `Unterminated string literal near offset ${i}`\n }\n continue\n }\n\n if (ch === '\"' || ch === \"'\" || ch === '`') {\n inString = ch\n continue\n }\n\n // Bracket matching\n if (ch === '(' || ch === '[' || ch === '{') {\n stack.push(ch)\n } else if (ch === ')' || ch === ']' || ch === '}') {\n const expected = pairs[ch]!\n if (stack.length === 0) {\n return `Unmatched closing \"${ch}\" near offset ${i}`\n }\n const top = stack.pop()!\n if (top !== expected) {\n return `Mismatched bracket: expected closing for \"${top}\" but found \"${ch}\" near offset ${i}`\n }\n }\n }\n\n if (inString !== null) {\n return `Unterminated string literal (opened with ${inString})`\n }\n\n if (stack.length > 0) {\n return `Unclosed bracket \"${stack[stack.length - 1]}\" — ${stack.length} unclosed bracket(s)`\n }\n\n return null\n}\n\n/**\n * Vue SFC syntax check: ensure <template>, <script>, <style> tags are balanced.\n */\nfunction checkVueSyntax(content: string): string | null {\n const tagBalance = checkTagBalance(content)\n if (tagBalance) return tagBalance\n\n // Vue-specific: check that SFC root tags are present and balanced\n for (const tag of ['template', 'script']) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n/**\n * Basic tag balance check for HTML-like files.\n * Checks that self-closing tags are handled and major structural tags are balanced.\n */\nfunction checkTagBalance(content: string): string | null {\n // Check for common structural tags balance\n const structuralTags = ['div', 'section', 'main', 'header', 'footer', 'nav', 'article', 'aside', 'ul', 'ol', 'table']\n\n for (const tag of structuralTags) {\n const openRe = new RegExp(`<${tag}[\\\\s>]`, 'g')\n const closeRe = new RegExp(`</${tag}>`, 'g')\n const opens = content.match(openRe)?.length ?? 0\n const closes = content.match(closeRe)?.length ?? 0\n if (opens !== closes) {\n return `Unbalanced <${tag}> tag: ${opens} opening vs ${closes} closing`\n }\n }\n\n return null\n}\n\n// ─── Extract Mode ───\n\nexport async function applyExtract(\n projectRoot: string,\n input: ExtractionInput,\n): Promise<ExtractionResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { extractions, dry_run } = input\n\n // Analyze what will happen\n const existingModels = await listModels(projectRoot)\n const existingIds = new Set(existingModels.map(m => m.id))\n\n const modelsToCreate: string[] = []\n const modelsToUpdate: string[] = []\n const contentFiles: string[] = []\n let totalEntries = 0\n\n const validationErrors: string[] = []\n\n for (const ext of extractions) {\n // Validate model definition with same rules as model_save\n const modelErrors = validateModelDefinition({\n id: ext.model,\n kind: ext.kind,\n fields: ext.fields as Record<string, unknown> | undefined,\n })\n if (modelErrors.errors.length > 0) {\n validationErrors.push(...modelErrors.errors.map(e => `[${ext.model}] ${e}`))\n }\n\n for (const entry of ext.entries) {\n if (ext.kind === 'dictionary') {\n if (entry.data['id'] !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Dictionary entries should not have id or slug`)\n }\n for (const [key, val] of Object.entries(entry.data)) {\n if (typeof val !== 'string') {\n validationErrors.push(`[${ext.model}] Dictionary entry value for key \"${key}\" must be a string, got ${typeof val}`)\n }\n }\n } else if (ext.kind === 'document') {\n if (!entry.slug && !entry.data['slug']) {\n validationErrors.push(`[${ext.model}] Document entries must have a slug`)\n }\n } else if (ext.kind === 'collection') {\n if (entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Collection entries should not have slug`)\n }\n } else if (ext.kind === 'singleton') {\n if (entry.data['id'] !== undefined || entry.slug !== undefined || entry.data['slug'] !== undefined) {\n validationErrors.push(`[${ext.model}] Singleton entries should not have id or slug`)\n }\n }\n }\n\n if (existingIds.has(ext.model)) {\n modelsToUpdate.push(ext.model)\n } else {\n modelsToCreate.push(ext.model)\n }\n totalEntries += ext.entries.length\n\n // Guardrail #3: Preview-Execute Parity — use real model metadata if it exists\n let previewModel: ModelDefinition\n if (existingIds.has(ext.model)) {\n const real = await readModel(projectRoot, ext.model)\n if (real) {\n previewModel = real\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n } else {\n previewModel = { id: ext.model, kind: ext.kind, domain: ext.domain, i18n: ext.i18n ?? true } as ModelDefinition\n }\n\n const cDir = resolveContentDir(projectRoot, previewModel)\n for (const entry of ext.entries) {\n const locale = entry.locale ?? config.locales.default\n if (ext.kind === 'document' && entry.slug) {\n contentFiles.push(resolveMdFilePath(cDir, previewModel, locale, entry.slug))\n } else {\n contentFiles.push(resolveJsonFilePath(cDir, previewModel, locale))\n }\n }\n }\n\n const preview: ExtractionPreview = {\n models_to_create: modelsToCreate,\n models_to_update: modelsToUpdate,\n total_entries: totalEntries,\n content_files: [...new Set(contentFiles)],\n }\n\n // Dry run — return preview only (include validation errors if any)\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview,\n ...(validationErrors.length > 0 ? { validation_errors: validationErrors } : {}),\n next_steps: [\n ...(validationErrors.length > 0\n ? [`WARNING: ${validationErrors.length} validation error(s) found — fix before executing`]\n : []),\n 'Review the preview above',\n 'Call contentrain_apply with mode:extract and dry_run:false to execute',\n ],\n }\n }\n\n // Block execute if validation errors exist\n if (validationErrors.length > 0) {\n return {\n dry_run: false,\n error: 'Model validation failed — cannot execute extract with invalid model definitions',\n validation_errors: validationErrors,\n next_steps: ['Fix the validation errors and retry'],\n }\n }\n\n // Branch health gate\n const health = await checkBranchHealth(projectRoot)\n if (health.blocked) {\n return {\n error: health.message,\n action: 'blocked' as const,\n hint: 'Merge or delete old contentrain/* branches before executing normalize.',\n } as unknown as ExtractionResult\n }\n\n // Execute — git transaction (always review mode for normalize)\n const branchName = buildBranchName('normalize', 'extract')\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n const sourceMap: Array<{ model: string; locale: string; value: string; file: string; line: number }> = []\n const modelsCreated: string[] = []\n const modelsUpdated: string[] = []\n let entriesWritten = 0\n\n try {\n await tx.write(async (wt) => {\n for (const ext of extractions) {\n // Create or merge model\n const existing = await readModel(wt, ext.model)\n if (existing) {\n // Merge fields: add new, keep existing\n if (ext.fields) {\n const merged = { ...existing.fields, ...ext.fields }\n // Only overwrite if new fields were actually added\n const newFieldNames = Object.keys(ext.fields).filter(k => !(k in (existing.fields ?? {})))\n if (newFieldNames.length > 0) {\n existing.fields = merged\n await writeModel(wt, existing)\n modelsUpdated.push(ext.model)\n }\n }\n } else {\n // Create new model\n const newModel: ModelDefinition = {\n id: ext.model,\n name: ext.model.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),\n kind: ext.kind,\n domain: ext.domain,\n i18n: ext.i18n ?? true,\n fields: ext.fields,\n }\n await writeModel(wt, newModel)\n modelsCreated.push(ext.model)\n }\n\n // Write content entries\n const model = (await readModel(wt, ext.model))!\n const entries: ContentEntry[] = ext.entries.map(e => ({\n locale: e.locale,\n slug: e.slug,\n data: e.data,\n }))\n\n const wtConfig = await readConfig(wt) ?? config\n await writeContent(wt, model, entries, wtConfig)\n entriesWritten += entries.length\n\n // Track source map\n for (const entry of ext.entries) {\n // For dictionary entries with per-key source tracking\n if (ext.kind === 'dictionary' && entry.sources) {\n for (const s of entry.sources) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: s.value,\n file: s.file,\n line: s.line,\n })\n }\n } else if (entry.source) {\n sourceMap.push({\n model: ext.model,\n locale: entry.locale ?? config.locales.default,\n value: entry.source.value,\n file: entry.source.file,\n line: entry.source.line,\n })\n }\n }\n }\n\n // Write source map for reuse scope enforcement\n if (sourceMap.length > 0) {\n const sourcesByModel: Record<string, { source_files: string[]; entry_count: number }> = {}\n for (const s of sourceMap) {\n if (!sourcesByModel[s.model]) {\n sourcesByModel[s.model] = { source_files: [], entry_count: 0 }\n }\n const modelEntry = sourcesByModel[s.model]!\n if (!modelEntry.source_files.includes(s.file)) {\n modelEntry.source_files.push(s.file)\n }\n modelEntry.entry_count++\n }\n const sourcesJson = JSON.stringify({\n version: 1,\n created_at: new Date().toISOString(),\n models: sourcesByModel,\n }, null, 2) + '\\n'\n // Write to worktree only — merge brings it to main\n // Phase 2 (reuse) must wait for extract branch to be merged first\n await writeText(join(wt, '.contentrain', 'normalize-sources.json'), sourcesJson)\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: extractions.map(e => e.model).join(','),\n locale: config.locales.default,\n entries: extractions.flatMap(e => e.entries.map(en => en.slug ?? 'entry')),\n })\n })\n\n const commitMsg = `[contentrain] normalize: extract ${entriesWritten} entries to ${extractions.length} models`\n await tx.commit(commitMsg)\n\n const gitResult: { branch: string; action: string; commit: string; warning?: string } = {\n branch: branchName,\n action: 'pending-review',\n commit: '',\n }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n if (completed.warning !== undefined) gitResult.warning = completed.warning\n } catch (error) {\n // Never fall through to a value that mimics success. The commit landed\n // but publishing it did not, and 'pending-review' with an empty commit\n // hash is indistinguishable from a healthy review branch.\n gitResult.action = 'incomplete'\n gitResult.warning = `Content was committed to \"${branchName}\" but the transaction could not be completed: `\n + `${error instanceof Error ? error.message : String(error)}. `\n + `The branch is preserved — inspect it with contentrain_branch_list before retrying.`\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n models_created: modelsCreated,\n models_updated: modelsUpdated,\n entries_written: entriesWritten,\n source_map: sourceMap,\n },\n git: gitResult,\n context_updated: true,\n next_steps: [\n 'Run contentrain_validate to check the extracted content',\n 'Run contentrain_submit to push the branch for review',\n 'For browser-based review: ensure `contentrain serve` is running, direct user to http://localhost:3333/normalize',\n 'For terminal workflow: use contentrain_merge to merge the branch locally',\n 'After merge, run `npx contentrain generate` to update SDK client',\n 'After review, proceed with mode:reuse to patch source files',\n ],\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Reuse Mode ───\n\nexport async function applyReuse(\n projectRoot: string,\n input: ReuseInput,\n): Promise<ReuseResult> {\n const config = await readConfig(projectRoot)\n if (!config) throw new Error('Project not initialized. Run contentrain_init first.')\n\n const { scope, patches, dry_run } = input\n\n // Validate scope\n if (!scope.model && !scope.domain) {\n throw new Error('Scope required: provide model or domain. Whole-project patching is not allowed.')\n }\n\n // Validate patch count\n if (patches.length > MAX_PATCHES) {\n throw new Error(`Too many patches (${patches.length}). Maximum ${MAX_PATCHES} per operation. Split into multiple calls.`)\n }\n\n // Check content exists for scope (soft warning)\n if (scope.model) {\n const model = await readModel(projectRoot, scope.model)\n if (!model) {\n throw new Error(`Model \"${scope.model}\" not found. Run extract phase first.`)\n }\n }\n\n const scopeWarnings: string[] = []\n\n // Guardrail #1: Scope Real Enforcement\n // Step 1: Path safety — every patch must target a valid, patchable source file\n for (const patch of patches) {\n const pathError = validatePatchPath(patch.file)\n if (pathError) {\n throw new Error(`Invalid patch path: ${pathError}`)\n }\n }\n\n // Step 2: Semantic scope — verify scope model/domain exists and cross-check patch files\n if (scope.model || scope.domain) {\n const models = await listModels(projectRoot)\n const scopeModels = scope.model\n ? models.filter(m => m.id === scope.model)\n : scope.domain\n ? models.filter(m => m.domain === scope.domain)\n : models\n\n if (scopeModels.length === 0) {\n throw new Error(`No models found for scope ${scope.model ? `model=\"${scope.model}\"` : `domain=\"${scope.domain}\"`}`)\n }\n\n // Step 3: Verify patch files are source files (not content/config/meta files)\n // and belong to detectable source directories (not random locations)\n const { autoDetectSourceDirs } = await import('./scan-config.js')\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n\n // Build allowed file prefixes from source dirs\n const allowedPrefixes = sourceDirs.map(d => d === '.' ? '' : d + '/')\n\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n\n // Reject patches targeting .contentrain/ directory (content files should never be patched by reuse)\n if (normalizedPath.startsWith('.contentrain/') || normalizedPath.includes('/.contentrain/')) {\n throw new Error(`Cannot patch content/config files directly: \"${patch.file}\". Reuse patches source files only.`)\n }\n\n // If source dirs were detected (not just \".\"), verify patch files are within them\n if (sourceDirs.length > 0 && sourceDirs[0] !== '.') {\n const inSourceDir = allowedPrefixes.some(prefix =>\n prefix === '' || normalizedPath.startsWith(prefix),\n )\n if (!inSourceDir) {\n throw new Error(\n `Patch file \"${patch.file}\" is outside detected source directories (${sourceDirs.join(', ')}). ` +\n `Reuse patches must target source files within the project's source tree.`,\n )\n }\n }\n }\n\n // Step 4: Semantic source→model cross-check via normalize-sources.json\n // Source map is written by extract into the review branch worktree.\n // It only exists on base after extract branch is merged.\n // If missing: dry_run proceeds with warning, execute is blocked.\n if (scope.model || scope.domain) {\n const sourcesPath = join(projectRoot, '.contentrain', 'normalize-sources.json')\n const sourcesRaw = await readText(sourcesPath)\n\n if (!sourcesRaw) {\n // Source map not found — check if scoped model is a dictionary\n // Dictionaries store all keys in one entry, so per-file source tracking\n // is not available. Allow reuse with a warning instead of blocking.\n const scopedModel = scopeModels.find(m => m.id === scope.model)\n const isDictionary = scopedModel?.kind === 'dictionary'\n\n if (isDictionary) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Dictionary models do not generate per-file source maps. ' +\n 'Scope enforcement is based on source-tree locality only.',\n )\n } else if (dry_run !== false) {\n scopeWarnings.push(\n 'normalize-sources.json not found. Semantic scope enforcement is unavailable. ' +\n 'Merge the extract branch first, then reuse will have full scope protection.',\n )\n } else {\n throw new Error(\n 'Cannot execute reuse: normalize-sources.json not found on base branch. ' +\n 'The extract branch must be merged before reuse can execute. ' +\n 'This ensures semantic scope enforcement protects against out-of-scope patching.',\n )\n }\n } else {\n const sourcesData = JSON.parse(sourcesRaw) as {\n models?: Record<string, { source_files?: string[] }>\n }\n\n // Collect all allowed source files for the scope\n let allowedSourceFiles: string[] = []\n let scopeLabel = ''\n\n if (scope.model) {\n const modelSources = sourcesData.models?.[scope.model]?.source_files\n if (modelSources) allowedSourceFiles = modelSources\n scopeLabel = `model \"${scope.model}\"`\n } else if (scope.domain) {\n const domainModelIds = scopeModels.map(m => m.id)\n for (const modelId of domainModelIds) {\n const modelSources = sourcesData.models?.[modelId]?.source_files\n if (modelSources) allowedSourceFiles.push(...modelSources)\n }\n allowedSourceFiles = [...new Set(allowedSourceFiles)]\n scopeLabel = `domain \"${scope.domain}\"`\n }\n\n if (allowedSourceFiles.length > 0) {\n const outOfScopePatches: string[] = []\n for (const patch of patches) {\n const normalizedPath = patch.file.replace(/\\\\/g, '/')\n if (!allowedSourceFiles.includes(normalizedPath)) {\n outOfScopePatches.push(patch.file)\n }\n }\n if (outOfScopePatches.length > 0) {\n throw new Error(\n `Scope enforcement: ${outOfScopePatches.length} patch file(s) are not associated with ${scopeLabel}. ` +\n `Out-of-scope files: ${outOfScopePatches.join(', ')}. ` +\n `Known source files: ${allowedSourceFiles.join(', ')}.`,\n )\n }\n }\n }\n }\n }\n\n // Group patches by file\n const patchesByFile = new Map<string, PatchEntry[]>()\n for (const patch of patches) {\n if (!patchesByFile.has(patch.file)) {\n patchesByFile.set(patch.file, [])\n }\n patchesByFile.get(patch.file)!.push(patch)\n }\n\n const filesToModify = [...patchesByFile.keys()]\n const importsToAdd = patches.filter(p => p.import_statement).length\n\n // Dry run — return preview only\n if (dry_run !== false) {\n return {\n dry_run: true,\n preview: {\n files_to_modify: filesToModify,\n patches_count: patches.length,\n imports_to_add: importsToAdd,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n next_steps: [\n ...(scopeWarnings.length > 0 ? [`WARNING: ${scopeWarnings.length} patch file(s) not in extract source map — verify intent`] : []),\n 'Review the files and patches above',\n 'Call contentrain_apply with mode:reuse and dry_run:false to execute',\n ],\n }\n }\n\n // Branch health gate\n const reuseHealth = await checkBranchHealth(projectRoot)\n if (reuseHealth.blocked) {\n return {\n dry_run: false,\n error: `Branch blocked: ${reuseHealth.message}`,\n next_steps: ['Merge or delete old contentrain/* branches before executing reuse.'],\n }\n }\n\n // Execute — git transaction\n const scopeTarget = scope.model ?? scope.domain!\n const branchName = buildBranchName('normalize/reuse', scopeTarget)\n const tx = await createTransaction(projectRoot, branchName, { workflowOverride: 'review' })\n\n const filesModified: string[] = []\n let patchesApplied = 0\n let importsAdded = 0\n const patchesSkipped: Array<{ file: string; line: number; reason: string }> = []\n const frameworkWarnings: Array<{ file: string; warning: string }> = []\n const syntaxErrors: SyntaxError[] = []\n\n try {\n await tx.write(async (wt) => {\n for (const [relFile, filePatches] of patchesByFile) {\n const absPath = join(wt, relFile)\n\n if (!(await pathExists(absPath))) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file not found' })\n }\n continue\n }\n\n const content = await readText(absPath)\n if (content === null) {\n for (const p of filePatches) {\n patchesSkipped.push({ file: relFile, line: p.line, reason: 'file unreadable' })\n }\n continue\n }\n\n // Sort patches by line DESC (bottom-up to avoid line shifts)\n const sorted = [...filePatches].toSorted((a, b) => b.line - a.line)\n\n const lines = content.split('\\n')\n let fileModified = false\n\n for (const patch of sorted) {\n // Determine replacement context before applying\n const targetLine = lines[patch.line - 1] ?? ''\n const isTagTextContext = targetLine.includes(`>${patch.old_value}<`)\n const replacementContext = isTagTextContext ? 'tag_text' as const : 'other' as const\n\n // Guardrail #2: Framework-aware validation — only warn for tag text replacements\n if (isTagTextContext) {\n const fwWarning = validateFrameworkExpression(relFile, patch.new_expression, replacementContext)\n if (fwWarning) {\n frameworkWarnings.push({ file: relFile, warning: fwWarning })\n }\n }\n\n const applied = applyPatchToLines(lines, patch)\n if (applied) {\n patchesApplied++\n fileModified = true\n } else {\n patchesSkipped.push({ file: relFile, line: patch.line, reason: 'old_value not found at or near specified line' })\n }\n }\n\n // Add imports (deduplicate)\n const importStatements = new Set(\n filePatches\n .filter(p => p.import_statement)\n .map(p => p.import_statement!),\n )\n\n if (importStatements.size > 0) {\n const added = addImportsToLines(lines, importStatements)\n importsAdded += added\n if (added > 0) fileModified = true\n }\n\n if (fileModified) {\n const newContent = lines.join('\\n')\n await writeText(absPath, newContent)\n filesModified.push(relFile)\n\n // Guardrail #5: Syntax check after patching\n const syntaxError = checkSyntax(relFile, newContent)\n if (syntaxError) {\n syntaxErrors.push({ file: relFile, error: syntaxError })\n }\n }\n }\n\n // Update context\n await writeContext(wt, {\n tool: 'contentrain_apply',\n model: scopeTarget,\n locale: config.locales.default,\n })\n })\n\n if (filesModified.length === 0) {\n await tx.cleanup()\n return {\n dry_run: false,\n results: {\n files_modified: [],\n patches_applied: 0,\n patches_skipped: patchesSkipped,\n imports_added: 0,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n },\n next_steps: ['No files were modified. Check patch definitions and try again.'],\n }\n }\n\n const commitMsg = `[contentrain] normalize: reuse ${scopeTarget} — patch ${filesModified.length} files (${patchesApplied} replacements)`\n await tx.commit(commitMsg)\n\n const gitResult: { branch: string; action: string; commit: string; warning?: string } = {\n branch: branchName,\n action: 'pending-review',\n commit: '',\n }\n try {\n const completed = await tx.complete()\n gitResult.action = completed.action\n gitResult.commit = completed.commit\n if (completed.warning !== undefined) gitResult.warning = completed.warning\n } catch (error) {\n // Never fall through to a value that mimics success. The commit landed\n // but publishing it did not, and 'pending-review' with an empty commit\n // hash is indistinguishable from a healthy review branch.\n gitResult.action = 'incomplete'\n gitResult.warning = `Content was committed to \"${branchName}\" but the transaction could not be completed: `\n + `${error instanceof Error ? error.message : String(error)}. `\n + `The branch is preserved — inspect it with contentrain_branch_list before retrying.`\n } finally {\n await tx.cleanup()\n }\n\n return {\n dry_run: false,\n results: {\n files_modified: filesModified,\n patches_applied: patchesApplied,\n patches_skipped: patchesSkipped,\n imports_added: importsAdded,\n framework_warnings: frameworkWarnings.length > 0 ? frameworkWarnings : undefined,\n syntax_errors: syntaxErrors.length > 0 ? syntaxErrors : undefined,\n },\n ...(scopeWarnings.length > 0 ? { scope_warnings: scopeWarnings } : {}),\n git: gitResult,\n next_steps: [\n 'Run contentrain_validate to verify the patched files',\n patchesSkipped.length > 0 ? `${patchesSkipped.length} patches were skipped — review and retry if needed` : '',\n syntaxErrors.length > 0 ? `WARNING: ${syntaxErrors.length} file(s) may have syntax errors after patching — review manually` : '',\n scopeWarnings.length > 0 ? `NOTE: ${scopeWarnings.length} patch file(s) not in extract source map` : '',\n 'Run contentrain_submit to push the branch for review',\n 'For review: direct user to http://localhost:3333/branches or use contentrain_merge',\n 'After all reuse phases complete, run `npx contentrain generate` to update SDK types',\n ].filter(Boolean),\n }\n } catch (error) {\n await tx.cleanup()\n throw error\n }\n}\n\n// ─── Patch Helpers ───\n\n/**\n * Apply a single patch to a lines array. Mutates lines in place.\n * Uses line hint for proximity matching — searches ±10 lines from hint.\n */\nfunction applyPatchToLines(lines: string[], patch: PatchEntry): boolean {\n const { line, old_value, new_expression } = patch\n const lineIdx = line - 1 // 0-based\n\n // Search range: ±10 lines from hint\n const searchStart = Math.max(0, lineIdx - 10)\n const searchEnd = Math.min(lines.length, lineIdx + 11)\n\n // First pass: exact line match\n if (lineIdx >= 0 && lineIdx < lines.length) {\n const replaced = replaceInLine(lines[lineIdx]!, old_value, new_expression)\n if (replaced !== null) {\n lines[lineIdx] = replaced\n return true\n }\n }\n\n // Second pass: proximity search\n for (let i = searchStart; i < searchEnd; i++) {\n if (i === lineIdx) continue // already tried\n const replaced = replaceInLine(lines[i]!, old_value, new_expression)\n if (replaced !== null) {\n lines[i] = replaced\n return true\n }\n }\n\n return false\n}\n\n/**\n * Replace old_value with new_expression in a single line.\n * Matches the string literal (quoted or unquoted tag text).\n * Returns the modified line, or null if not found.\n *\n * Guardrail #4: Safer patch matching — word boundary awareness and\n * ambiguity rejection for plain text fallback.\n */\nexport function replaceInLine(line: string, oldValue: string, newExpression: string): string | null {\n // Try exact match of the value in quoted strings\n // Match: \"old_value\", 'old_value', `old_value`\n for (const quote of ['\"', \"'\", '`']) {\n const quoted = `${quote}${oldValue}${quote}`\n if (line.includes(quoted)) {\n return line.replace(quoted, newExpression)\n }\n }\n\n // Try unquoted tag text match: >old_value<\n // The agent provides the complete new_expression with correct framework syntax:\n // JSX: {t('key')}\n // Vue: {{ $t('key') }}\n // Svelte: {$t('key')}\n // So we insert the expression as-is between > and <, without wrapping in braces.\n if (line.includes(`>${oldValue}<`)) {\n return line.replace(`>${oldValue}<`, `>${newExpression}<`)\n }\n\n // Guardrail #4: Safer plain text fallback\n // Only match if old_value appears at a word boundary and is unambiguous\n if (line.includes(oldValue)) {\n // Count occurrences — if multiple, it's ambiguous\n const occurrences = countOccurrences(line, oldValue)\n if (occurrences > 1) {\n return null // ambiguous — let proximity search handle it\n }\n\n // Word boundary check: don't replace \"Submit\" inside \"SubmitButton\"\n // Reject if the old_value is a substring of a larger word — check if\n // adjacent characters are word characters that extend the token.\n const idx = line.indexOf(oldValue)\n const charBefore = idx > 0 ? line[idx - 1]! : ''\n const charAfter = idx + oldValue.length < line.length ? line[idx + oldValue.length]! : ''\n\n const oldStartsWithWord = oldValue.length > 0 && isWordChar(oldValue[0]!)\n const oldEndsWithWord = oldValue.length > 0 && isWordChar(oldValue[oldValue.length - 1]!)\n\n // If old_value starts with a word char and char before is also word char, it's a partial match\n if (oldStartsWithWord && isWordChar(charBefore)) {\n return null\n }\n // If old_value ends with a word char and char after is also word char, it's a partial match\n if (oldEndsWithWord && isWordChar(charAfter)) {\n return null\n }\n\n return line.replace(oldValue, newExpression)\n }\n\n return null\n}\n\n/** Count non-overlapping occurrences of a substring */\nfunction countOccurrences(str: string, sub: string): number {\n let count = 0\n let pos = 0\n while (pos <= str.length - sub.length) {\n const idx = str.indexOf(sub, pos)\n if (idx === -1) break\n count++\n pos = idx + sub.length\n }\n return count\n}\n\n/** Check if a character is a word character (letter, digit, underscore) */\nfunction isWordChar(ch: string): boolean {\n return /\\w/.test(ch)\n}\n\n/**\n * Add import statements to the top of a file (after existing imports).\n * Deduplicates — won't add if the import already exists.\n * Returns number of imports actually added.\n */\nfunction addImportsToLines(lines: string[], imports: Set<string>): number {\n let added = 0\n const existingContent = lines.join('\\n')\n\n // Find the last import line position (handles multi-line imports)\n let lastImportIdx = -1\n let inMultiLineImport = false\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (inMultiLineImport) {\n lastImportIdx = i\n if (trimmed.includes('}')) inMultiLineImport = false\n continue\n }\n if (trimmed.startsWith('import ') || trimmed.startsWith('import{')) {\n lastImportIdx = i\n // Check if this is a multi-line import (has { but no closing })\n if (trimmed.includes('{') && !trimmed.includes('}')) {\n inMultiLineImport = true\n }\n continue\n }\n // Stop searching after a non-import, non-empty, non-comment line following imports\n if (lastImportIdx >= 0 && trimmed.length > 0 && !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*')) {\n break\n }\n }\n\n let insertAt: number\n if (lastImportIdx >= 0) {\n insertAt = lastImportIdx + 1\n } else {\n // No existing imports found — insert after shebang and directive lines\n insertAt = 0\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim()\n if (i === 0 && trimmed.startsWith('#!')) {\n insertAt = i + 1\n continue\n }\n if (trimmed === \"'use client'\" || trimmed === '\"use client\"'\n || trimmed === \"'use server'\" || trimmed === '\"use server\"'\n || trimmed === \"'use client';\" || trimmed === '\"use client\";'\n || trimmed === \"'use server';\" || trimmed === '\"use server\";') {\n insertAt = i + 1\n continue\n }\n if (insertAt > 0 && trimmed.length > 0) break\n if (insertAt === 0 && trimmed.length > 0) break\n }\n }\n const toInsert: string[] = []\n\n for (const imp of imports) {\n // Check if this import already exists (by checking the from clause)\n if (!existingContent.includes(imp)) {\n toInsert.push(imp)\n added++\n }\n }\n\n if (toInsert.length > 0) {\n lines.splice(insertAt, 0, ...toInsert)\n }\n\n return added\n}\n"],"mappings":"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"} |
| //#region src/core/ast-scanner/astro-parser.ts | ||
| let _compiler = null; | ||
| async function loadCompiler() { | ||
| if (_compiler) return _compiler; | ||
| try { | ||
| _compiler = await import("./node-wC-z3NoZ.mjs"); | ||
| return _compiler; | ||
| } catch { | ||
| throw new Error("@astrojs/compiler is required to parse .astro files. Install it with: pnpm add -D @astrojs/compiler"); | ||
| } | ||
| } | ||
| let _tsxParser = null; | ||
| async function loadTsxParser() { | ||
| if (_tsxParser) return _tsxParser; | ||
| try { | ||
| _tsxParser = (await import("./tsx-parser-ChduVwKJ.mjs")).parseTsx; | ||
| return _tsxParser; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| const SURROUNDING_MAX = 120; | ||
| /** Attributes whose values are CSS, not content */ | ||
| const CSS_ATTRIBUTES = new Set([ | ||
| "class", | ||
| "style", | ||
| "className" | ||
| ]); | ||
| /** Event/code attribute prefixes — skip these */ | ||
| const CODE_ATTRIBUTE_PREFIXES = [ | ||
| "on", | ||
| "set:", | ||
| "define:", | ||
| "is:" | ||
| ]; | ||
| function getSurroundingByLine(content, line) { | ||
| const lines = content.split("\n"); | ||
| const idx = line - 1; | ||
| const start = Math.max(0, idx - 1); | ||
| const end = Math.min(lines.length - 1, idx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const l = lines[i]; | ||
| if (l !== void 0) parts.push(l); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| function walkAstroTemplate(node, content, results, parentTag = "") { | ||
| switch (node.type) { | ||
| case "root": { | ||
| const root = node; | ||
| for (const child of root.children) walkAstroTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "text": { | ||
| const textNode = node; | ||
| const trimmed = textNode.value.trim(); | ||
| if (trimmed.length > 0 && /\S/.test(trimmed)) { | ||
| const line = textNode.position?.start.line ?? 1; | ||
| const column = textNode.position?.start.column ?? 1; | ||
| results.push({ | ||
| value: trimmed, | ||
| line, | ||
| column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(content, line) | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| case "element": | ||
| case "component": | ||
| case "custom-element": { | ||
| const el = node; | ||
| const tag = el.name; | ||
| for (const attr of el.attributes) processAttribute(attr, tag, content, results); | ||
| for (const child of el.children) walkAstroTemplate(child, content, results, tag); | ||
| break; | ||
| } | ||
| case "fragment": { | ||
| const fragment = node; | ||
| for (const child of fragment.children) walkAstroTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "expression": break; | ||
| case "frontmatter": | ||
| case "comment": | ||
| case "doctype": break; | ||
| default: { | ||
| const unknownNode = node; | ||
| if (unknownNode.children) for (const child of unknownNode.children) walkAstroTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| function processAttribute(attr, parentTag, content, results) { | ||
| const attrName = attr.name; | ||
| if (attr.type === "expression" || attr.type === "spread" || attr.type === "shorthand") return; | ||
| for (const prefix of CODE_ATTRIBUTE_PREFIXES) if (attrName.startsWith(prefix)) return; | ||
| const attrValue = attr.value; | ||
| if (!attrValue || typeof attrValue === "string" && attrValue.trim().length === 0) return; | ||
| const line = attr.position?.start.line ?? 1; | ||
| const column = attr.position?.start.column ?? 1; | ||
| if (CSS_ATTRIBUTES.has(attrName)) { | ||
| results.push({ | ||
| value: attrValue, | ||
| line, | ||
| column, | ||
| context: "css_class", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, line) | ||
| }); | ||
| return; | ||
| } | ||
| results.push({ | ||
| value: attrValue, | ||
| line, | ||
| column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, line) | ||
| }); | ||
| } | ||
| function findFrontmatter(ast) { | ||
| for (const child of ast.children) if (child.type === "frontmatter") return child; | ||
| return null; | ||
| } | ||
| async function parseAstro(content, fileName) { | ||
| const compiler = await loadCompiler(); | ||
| const results = []; | ||
| let parseResult; | ||
| try { | ||
| parseResult = await compiler.parse(content, { position: true }); | ||
| } catch { | ||
| return []; | ||
| } | ||
| const ast = parseResult.ast; | ||
| const frontmatter = findFrontmatter(ast); | ||
| if (frontmatter && frontmatter.value.trim().length > 0) { | ||
| const tsxParser = await loadTsxParser(); | ||
| if (tsxParser) { | ||
| const frontmatterContent = frontmatter.value; | ||
| const contentStartLine = (frontmatter.position?.start.line ?? 1) + 1; | ||
| const scriptResults = tsxParser(frontmatterContent, fileName.replace(/\.astro$/, ".ts")); | ||
| for (const r of scriptResults) results.push({ | ||
| ...r, | ||
| line: r.line + contentStartLine - 1, | ||
| scope: "script" | ||
| }); | ||
| } | ||
| } | ||
| walkAstroTemplate(ast, content, results); | ||
| return results; | ||
| } | ||
| //#endregion | ||
| export { parseAstro }; | ||
| //# sourceMappingURL=astro-parser-BVoGjA6M.mjs.map |
| {"version":3,"file":"astro-parser-BVoGjA6M.mjs","names":[],"sources":["../src/core/ast-scanner/astro-parser.ts"],"sourcesContent":["// ─── Astro Parser for Scanner v2 ───\n// Parses .astro files using @astrojs/compiler.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\nimport type { ExtractedString } from './types.js'\n\n// ─── Lazy-loaded @astrojs/compiler ───\n\n// Astro AST node types from @astrojs/compiler\ninterface AstroBaseNode {\n type: string\n position?: {\n start: { line: number; column: number; offset: number }\n end?: { line: number; column: number; offset: number }\n }\n}\n\ninterface AstroRoot extends AstroBaseNode {\n type: 'root'\n children: AstroNode[]\n}\n\ninterface AstroElement extends AstroBaseNode {\n type: 'element'\n name: string\n attributes: AstroAttribute[]\n children: AstroNode[]\n}\n\ninterface AstroComponent extends AstroBaseNode {\n type: 'component'\n name: string\n attributes: AstroAttribute[]\n children: AstroNode[]\n}\n\ninterface AstroCustomElement extends AstroBaseNode {\n type: 'custom-element'\n name: string\n attributes: AstroAttribute[]\n children: AstroNode[]\n}\n\ninterface AstroFragment extends AstroBaseNode {\n type: 'fragment'\n children: AstroNode[]\n}\n\ninterface AstroText extends AstroBaseNode {\n type: 'text'\n value: string\n}\n\ninterface AstroExpression extends AstroBaseNode {\n type: 'expression'\n children: AstroNode[]\n}\n\ninterface AstroFrontmatter extends AstroBaseNode {\n type: 'frontmatter'\n value: string\n}\n\ninterface AstroComment extends AstroBaseNode {\n type: 'comment'\n value: string\n}\n\ninterface AstroDoctype extends AstroBaseNode {\n type: 'doctype'\n}\n\ninterface AstroAttribute {\n name: string\n type: 'attribute' | 'expression' | 'spread' | 'shorthand' | 'template-literal'\n kind: string\n value: string\n raw?: string\n position?: {\n start: { line: number; column: number; offset: number }\n end?: { line: number; column: number; offset: number }\n }\n}\n\ntype AstroNode =\n | AstroRoot\n | AstroElement\n | AstroComponent\n | AstroCustomElement\n | AstroFragment\n | AstroText\n | AstroExpression\n | AstroFrontmatter\n | AstroComment\n | AstroDoctype\n | AstroBaseNode\n\ninterface AstroParseResult {\n ast: AstroRoot\n}\n\ninterface AstroCompiler {\n parse: (source: string, options?: { position?: boolean }) => Promise<AstroParseResult>\n}\n\nlet _compiler: AstroCompiler | null = null\n\nasync function loadCompiler(): Promise<AstroCompiler> {\n if (_compiler) return _compiler\n try {\n const mod = await import('@astrojs/compiler')\n _compiler = mod as unknown as AstroCompiler\n return _compiler\n } catch {\n throw new Error(\n '@astrojs/compiler is required to parse .astro files. '\n + 'Install it with: pnpm add -D @astrojs/compiler',\n )\n }\n}\n\n// ─── tsx-parser delegation ───\n\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n return null\n }\n}\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n/** Attributes whose values are CSS, not content */\nconst CSS_ATTRIBUTES = new Set(['class', 'style', 'className'])\n\n/** Event/code attribute prefixes — skip these */\nconst CODE_ATTRIBUTE_PREFIXES = ['on', 'set:', 'define:', 'is:']\n\n// ─── Helpers ───\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n const start = Math.max(0, idx - 1)\n const end = Math.min(lines.length - 1, idx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const l = lines[i]\n if (l !== undefined) {\n parts.push(l)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\nfunction _getLineAndColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let lastNewline = -1\n\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n lastNewline = i\n }\n }\n\n return { line, column: offset - lastNewline }\n}\n\n// ─── Template AST Walker ───\n\nfunction walkAstroTemplate(\n node: AstroNode,\n content: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n const nodeType = node.type\n\n switch (nodeType) {\n case 'root': {\n const root = node as AstroRoot\n for (const child of root.children) {\n walkAstroTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'text': {\n const textNode = node as AstroText\n const trimmed = textNode.value.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n const line = textNode.position?.start.line ?? 1\n const column = textNode.position?.start.column ?? 1\n results.push({\n value: trimmed,\n line,\n column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(content, line),\n })\n }\n break\n }\n\n case 'element':\n case 'component':\n case 'custom-element': {\n const el = node as AstroElement | AstroComponent | AstroCustomElement\n const tag = el.name\n\n // Process attributes\n for (const attr of el.attributes) {\n processAttribute(attr, tag, content, results)\n }\n\n // Recurse into children\n for (const child of el.children) {\n walkAstroTemplate(child, content, results, tag)\n }\n break\n }\n\n case 'fragment': {\n const fragment = node as AstroFragment\n for (const child of fragment.children) {\n walkAstroTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'expression': {\n // JSX expressions like {variable} — code, skip\n // (The agent decides if embedded strings in expressions matter)\n break\n }\n\n case 'frontmatter':\n case 'comment':\n case 'doctype': {\n // Frontmatter is handled separately via tsx-parser delegation\n // Comments and doctype are skipped\n break\n }\n\n default: {\n // For unknown node types, try to walk children\n const unknownNode = node as AstroBaseNode & { children?: AstroNode[] }\n if (unknownNode.children) {\n for (const child of unknownNode.children) {\n walkAstroTemplate(child, content, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processAttribute(\n attr: AstroAttribute,\n parentTag: string,\n content: string,\n results: ExtractedString[],\n): void {\n const attrName = attr.name\n\n // Skip expression attributes (dynamic bindings) and spread attributes\n if (attr.type === 'expression' || attr.type === 'spread' || attr.type === 'shorthand') return\n\n // Skip code-related attributes (event handlers, directives)\n for (const prefix of CODE_ATTRIBUTE_PREFIXES) {\n if (attrName.startsWith(prefix)) return\n }\n\n // Skip boolean attributes (no value)\n const attrValue = attr.value\n if (!attrValue || (typeof attrValue === 'string' && attrValue.trim().length === 0)) return\n\n const line = attr.position?.start.line ?? 1\n const column = attr.position?.start.column ?? 1\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line,\n column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, line),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line,\n column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, line),\n })\n}\n\n// ─── Frontmatter Parsing ───\n\nfunction findFrontmatter(ast: AstroRoot): AstroFrontmatter | null {\n for (const child of ast.children) {\n if (child.type === 'frontmatter') {\n return child as AstroFrontmatter\n }\n }\n return null\n}\n\n// ─── Main Export ───\n\nexport async function parseAstro(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n let parseResult: AstroParseResult\n try {\n parseResult = await compiler.parse(content, { position: true })\n } catch {\n // If Astro parsing fails, return empty — malformed files shouldn't block scanning\n return []\n }\n\n const ast = parseResult.ast\n\n // ─── Frontmatter → tsx-parser ───\n const frontmatter = findFrontmatter(ast)\n if (frontmatter && frontmatter.value.trim().length > 0) {\n const tsxParser = await loadTsxParser()\n if (tsxParser) {\n const frontmatterContent = frontmatter.value\n // Frontmatter starts after the opening ---\n const frontmatterLine = frontmatter.position?.start.line ?? 1\n // The content starts on the line after ---\n const contentStartLine = frontmatterLine + 1\n\n // Astro frontmatter is always TypeScript — resolve filename accordingly\n const resolvedFileName = fileName.replace(/\\.astro$/, '.ts')\n const scriptResults = tsxParser(frontmatterContent, resolvedFileName)\n for (const r of scriptResults) {\n results.push({\n ...r,\n line: r.line + contentStartLine - 1,\n scope: 'script',\n })\n }\n }\n }\n\n // ─── Template (everything outside frontmatter) ───\n walkAstroTemplate(ast, content, results)\n\n return results\n}\n"],"mappings":"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"} |
| import { ContentrainConfig } from "@contentrain/types"; | ||
| import { SimpleGit } from "simple-git"; | ||
| //#region src/git/branch-lifecycle.d.ts | ||
| interface CleanupResult { | ||
| deleted: number; | ||
| remaining: number; | ||
| deletedBranches: string[]; | ||
| } | ||
| interface BranchHealthCheck { | ||
| total: number; | ||
| merged: number; | ||
| unmerged: number; | ||
| warning: boolean; | ||
| blocked: boolean; | ||
| message?: string; | ||
| } | ||
| /** | ||
| * Lists all local contentrain/* branches, deletes those already merged | ||
| * into the base branch, and returns the count of remaining unmerged ones. | ||
| */ | ||
| declare function cleanupMergedBranches(projectRoot: string): Promise<CleanupResult>; | ||
| /** | ||
| * Check branch health: count contentrain/* branches and return warning/blocked status. | ||
| * - 50+ branches: warning | ||
| * - 80+ branches: blocked | ||
| */ | ||
| declare function checkBranchHealth(projectRoot: string): Promise<BranchHealthCheck>; | ||
| interface BranchDiffResult { | ||
| /** The feature branch the diff was computed from. */ | ||
| branch: string; | ||
| /** The base ref the diff was computed against. Defaults to the `contentrain` branch. */ | ||
| base: string; | ||
| /** `git diff --stat` output — human-readable summary. */ | ||
| stat: string; | ||
| /** Raw unified diff. */ | ||
| patch: string; | ||
| /** Number of files touched in the diff. */ | ||
| filesChanged: number; | ||
| } | ||
| /** | ||
| * Compute the diff between a feature branch and its base. | ||
| * | ||
| * Defaults `base` to `CONTENTRAIN_BRANCH` — the singleton content- | ||
| * tracking branch every feature branch forks from. Passing the repo's | ||
| * default branch (e.g. `main`) is almost always a bug: when | ||
| * `contentrain` is ahead of `main`, the diff picks up unrelated | ||
| * historical content changes that the feature branch did not produce. | ||
| * | ||
| * Used by `contentrain serve` (branch detail view), the `contentrain | ||
| * diff` CLI command, and any Studio-side driver that needs to preview | ||
| * a feature branch before approving it. | ||
| */ | ||
| declare function branchDiff(projectRoot: string, opts: { | ||
| branch: string; | ||
| base?: string; | ||
| }): Promise<BranchDiffResult>; | ||
| /** | ||
| * Robust merged check for a single ref: ancestry fast-path, then a bounded | ||
| * `git cherry` (patch-id) fallback that survives base-history rewrites. | ||
| * Returns false when either ref cannot be resolved. | ||
| */ | ||
| declare function isRefMerged(git: SimpleGit, ref: string, into: string, opts?: { | ||
| maxCherryCommits?: number; | ||
| }): Promise<boolean>; | ||
| /** | ||
| * Classify which of the given local branches are merged into `into` | ||
| * (default: the contentrain branch). One `git branch --merged` call covers | ||
| * the ancestry-merged majority; only the remainder pays the patch-id | ||
| * fallback (bounded concurrency, verdicts cached). | ||
| * | ||
| * `opts.fallbackThreshold` skips the patch-id fallback entirely when fewer | ||
| * than that many branches are ancestry-unmerged — the fallback can only | ||
| * LOWER the unmerged count, so callers that merely compare the count | ||
| * against a limit (the hot pre-write gate) pay nothing in the normal case. | ||
| * | ||
| * Throws when `into` does not resolve — callers use this to fall back to | ||
| * the base branch (mirrors the previous `branch --merged` semantics). | ||
| */ | ||
| declare function classifyMergedBranches(projectRoot: string, branches: string[], into?: string, opts?: { | ||
| fallbackThreshold?: number; | ||
| }): Promise<Set<string>>; | ||
| interface RemoteDeleteResult { | ||
| deleted: boolean; | ||
| /** Why nothing was deleted, when that is expected (not a failure). */ | ||
| skipped?: 'disabled' | 'no-remote' | 'not-found' | 'protected'; | ||
| /** A real failure (offline, auth, protected ref) — surfaced, never thrown. */ | ||
| warning?: string; | ||
| } | ||
| /** | ||
| * Best-effort delete of a cr/* branch on the configured remote. Never | ||
| * throws: expected conditions land in `skipped`, real failures in | ||
| * `warning`. Gated by `config.remoteBranchCleanup` (default: on). | ||
| * | ||
| * Pass `opts.config` when the caller already read it (avoids a re-read); | ||
| * `null` means "no config" and applies the default gate. | ||
| */ | ||
| declare function deleteRemoteBranch(projectRoot: string, branch: string, opts?: { | ||
| config?: ContentrainConfig | null; | ||
| timeoutMs?: number; | ||
| }): Promise<RemoteDeleteResult>; | ||
| interface RemoteBranchList { | ||
| remote: string; | ||
| branches: { | ||
| name: string; | ||
| sha: string; | ||
| }[]; | ||
| /** ls-remote failed (offline/timeout) — branches is empty, not authoritative. */ | ||
| error?: string; | ||
| } | ||
| /** | ||
| * Authoritative list of cr/* branches on the configured remote via | ||
| * `ls-remote --heads` (no fetch, no stale remote-tracking refs). Returns | ||
| * null when no remote is configured. Never throws. | ||
| */ | ||
| declare function listRemoteCrBranches(projectRoot: string, opts?: { | ||
| timeoutMs?: number; | ||
| }): Promise<RemoteBranchList | null>; | ||
| interface RemotePruneResult { | ||
| /** Branches removed from the remote (in dryRun mode: the candidates). */ | ||
| deleted: string[]; | ||
| kept: string[]; | ||
| errors: string[]; | ||
| skipped?: 'disabled' | 'no-remote' | 'offline'; | ||
| } | ||
| /** | ||
| * Delete already-merged cr/* branches on the remote in batches. Merged-state | ||
| * uses the same ancestry + patch-id classification as the local cleanup, so | ||
| * branches leaked before a base-history rewrite are still recognised. | ||
| * Ignores `branchRetention` — a merged remote copy only produces phantom | ||
| * reviews. Never throws; gated by `config.remoteBranchCleanup`. | ||
| */ | ||
| declare function pruneMergedRemoteBranches(projectRoot: string, opts?: { | ||
| config?: ContentrainConfig | null; | ||
| max?: number; | ||
| dryRun?: boolean; | ||
| timeoutMs?: number; | ||
| }): Promise<RemotePruneResult>; | ||
| //#endregion | ||
| export { RemoteDeleteResult as a, checkBranchHealth as c, deleteRemoteBranch as d, isRefMerged as f, RemoteBranchList as i, classifyMergedBranches as l, pruneMergedRemoteBranches as m, BranchHealthCheck as n, RemotePruneResult as o, listRemoteCrBranches as p, CleanupResult as r, branchDiff as s, BranchDiffResult as t, cleanupMergedBranches as u }; | ||
| //# sourceMappingURL=branch-lifecycle-AwBJIheA.d.mts.map |
| {"version":3,"file":"branch-lifecycle-AwBJIheA.d.mts","names":[],"sources":["../src/git/branch-lifecycle.ts"],"mappings":";;;;UAKiB,aAAA;EACf,OAAA;EACA,SAAA;EACA,eAAA;AAAA;AAAA,UAGe,iBAAA;EACf,KAAA;EACA,MAAA;EACA,QAAA;EACA,OAAA;EACA,OAAA;EACA,OAAA;AAAA;;;;;iBAOoB,qBAAA,CAAsB,WAAA,WAAsB,OAAA,CAAQ,aAAA;;;;;;iBAoEpD,iBAAA,CAAkB,WAAA,WAAsB,OAAA,CAAQ,iBAAA;AAAA,UAkDrD,gBAAA;;EAEf,MAAA;EAxH0C;EA0H1C,IAAA;EA1HwE;EA4HxE,IAAA;EA5HqF;EA8HrF,KAAA;EA1DqC;EA4DrC,YAAA;AAAA;;;;;;AAVF;;;;;;;;iBA0BsB,UAAA,CACpB,WAAA,UACA,IAAA;EAAQ,MAAA;EAAgB,IAAA;AAAA,IACvB,OAAA,CAAQ,gBAAA;;;;;;iBA0FW,WAAA,CACpB,GAAA,EAAK,SAAA,EACL,GAAA,UACA,IAAA,UACA,IAAA;EAAS,gBAAA;AAAA,IACR,OAAA;;;;AALH;;;;;;;;;;;iBAiCsB,sBAAA,CACpB,WAAA,UACA,QAAA,YACA,IAAA,WACA,IAAA;EAAS,iBAAA;AAAA,IACR,OAAA,CAAQ,GAAA;AAAA,UAkFM,kBAAA;EACf,OAAA;EAnFQ;EAqFR,OAAA;EAxFA;EA0FA,OAAA;AAAA;;;;;;AALF;;;iBAgBsB,kBAAA,CACpB,WAAA,UACA,MAAA,UACA,IAAA;EAAS,MAAA,GAAS,iBAAA;EAA0B,SAAA;AAAA,IAC3C,OAAA,CAAQ,kBAAA;AAAA,UAuBM,gBAAA;EACf,MAAA;EACA,QAAA;IAAY,IAAA;IAAc,GAAA;EAAA;EAzBjB;EA2BT,KAAA;AAAA;;;;;;iBAQoB,oBAAA,CACpB,WAAA,UACA,IAAA;EAAS,SAAA;AAAA,IACR,OAAA,CAAQ,gBAAA;AAAA,UAoBM,iBAAA;EA1DY;EA4D3B,OAAA;EACA,IAAA;EACA,MAAA;EACA,OAAA;AAAA;;;;;;;;iBAYoB,yBAAA,CACpB,WAAA,UACA,IAAA;EAAS,MAAA,GAAS,iBAAA;EAA0B,GAAA;EAAc,MAAA;EAAkB,SAAA;AAAA,IAC3E,OAAA,CAAQ,iBAAA"} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { _ as RepoReader } from "./index-DDX-qYNw.mjs"; | ||
| import { ContentrainConfig, LocaleStrategy, ModelDefinition, Vocabulary, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types"; | ||
| //#region src/core/content-manager.d.ts | ||
| declare function resolveContentDir(projectRoot: string, model: ModelDefinition): string; | ||
| declare function resolveLocaleStrategy(model: ModelDefinition): LocaleStrategy; | ||
| /** Build the file path for a JSON content file (singleton/collection/dictionary) */ | ||
| declare function resolveJsonFilePath(dir: string, model: ModelDefinition, locale: string): string; | ||
| /** Build the file path for a markdown document */ | ||
| declare function resolveMdFilePath(dir: string, model: ModelDefinition, locale: string, slug: string): string; | ||
| interface ContentEntry { | ||
| id?: string; | ||
| slug?: string; | ||
| locale?: string; | ||
| data: Record<string, unknown>; | ||
| } | ||
| interface WriteResult { | ||
| action: 'created' | 'updated'; | ||
| id?: string; | ||
| slug?: string; | ||
| locale: string; | ||
| advisories?: string[]; | ||
| } | ||
| interface DeleteOpts { | ||
| id?: string; | ||
| slug?: string; | ||
| locale?: string; | ||
| keys?: string[]; | ||
| /** `config.locales.default` — places a non-i18n model's single meta record. */ | ||
| defaultLocale: string; | ||
| } | ||
| interface ListOpts { | ||
| locale?: string; | ||
| filter?: Record<string, unknown>; | ||
| resolve?: boolean; | ||
| limit?: number; | ||
| offset?: number; | ||
| } | ||
| declare function writeContent(projectRoot: string, model: ModelDefinition, entries: ContentEntry[], config: ContentrainConfig, vocabulary?: Vocabulary | null): Promise<WriteResult[]>; | ||
| declare function deleteContent(projectRoot: string, model: ModelDefinition, opts: DeleteOpts): Promise<string[]>; | ||
| /** | ||
| * List content entries for a model. Dual signature: | ||
| * | ||
| * - `listContent(projectRoot, model, opts, config)` — legacy local flow, | ||
| * uses direct filesystem reads and supports `opts.resolve` for | ||
| * cross-model relation hydration. | ||
| * - `listContent(reader, model, opts, config)` — reader-backed flow for | ||
| * remote providers. Basic list works across all four model kinds. | ||
| * `opts.resolve: true` is rejected with an error on remote readers | ||
| * because the cross-model walk requires local filesystem access. | ||
| * | ||
| * The reader-based function lives in {@link listContentViaReader}; the | ||
| * projectRoot-based entry point continues to call the legacy body to | ||
| * preserve bit-for-bit behaviour for every existing caller. | ||
| */ | ||
| declare function listContent(projectRoot: string, model: ModelDefinition, opts: ListOpts, config: ContentrainConfig): Promise<unknown>; | ||
| declare function listContent(reader: RepoReader, model: ModelDefinition, opts: ListOpts, config: ContentrainConfig): Promise<unknown>; | ||
| declare function readContent(projectRoot: string, model: ModelDefinition, opts: { | ||
| locale: string; | ||
| entryId?: string; | ||
| slug?: string; | ||
| }): Promise<unknown>; | ||
| //#endregion | ||
| export { writeContent as _, deleteContent as a, readContent as c, resolveLocaleStrategy as d, resolveMdFilePath as f, validateSlug$1 as g, validateLocale$1 as h, WriteResult as i, resolveContentDir as l, validateEntryId$1 as m, DeleteOpts as n, listContent as o, serializeFrontmatter as p, ListOpts as r, parseFrontmatter as s, ContentEntry as t, resolveJsonFilePath as u }; | ||
| //# sourceMappingURL=content-manager-BgxF3dHu.d.mts.map |
| {"version":3,"file":"content-manager-BgxF3dHu.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;iBAgBgB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,eAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,eAAA,GAAkB,cAAA;;iBAK/C,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA;;iBAczD,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,eAAA,EAAiB,MAAA,UAAgB,IAAA;AAAA,UAetE,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;AAAA;AAAA,UAGS,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAhDuE;EAkDvE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBA6IW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;AAzLH;;;;;;;;;;;AAOA;;iBA+TgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"} |
| import { a as readJson, i as readDir, o as readText, r as pathExists, t as contentrainDir } from "./fs-DLbVB-Ek.mjs"; | ||
| import { t as readConfig } from "./config-oxxgznz7.mjs"; | ||
| import { g as resolveLocaleStrategy, h as resolveJsonFilePath, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs"; | ||
| import { n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-BAfgSQBv.mjs"; | ||
| import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-BGUflS8t.mjs"; | ||
| import { join } from "node:path"; | ||
| import { readdir, stat } from "node:fs/promises"; | ||
| import { simpleGit } from "simple-git"; | ||
| //#region src/core/doctor.ts | ||
| async function runDoctor(projectRoot, options = {}) { | ||
| const checks = []; | ||
| try { | ||
| const version = await simpleGit(projectRoot).version(); | ||
| checks.push({ | ||
| name: "Git", | ||
| pass: true, | ||
| detail: `v${version.major}.${version.minor}.${version.patch}` | ||
| }); | ||
| } catch { | ||
| checks.push({ | ||
| name: "Git", | ||
| pass: false, | ||
| detail: "Not installed or not in PATH", | ||
| severity: "error" | ||
| }); | ||
| } | ||
| const hasGit = await pathExists(join(projectRoot, ".git")); | ||
| checks.push({ | ||
| name: "Git repository", | ||
| pass: hasGit, | ||
| detail: hasGit ? projectRoot : "No .git directory found", | ||
| severity: hasGit ? void 0 : "error" | ||
| }); | ||
| const nodeVersion = process.versions.node; | ||
| const [major] = nodeVersion.split(".").map(Number); | ||
| const nodePass = (major ?? 0) >= 22; | ||
| checks.push({ | ||
| name: "Node.js", | ||
| pass: nodePass, | ||
| detail: `v${nodeVersion}${nodePass ? "" : " (requires ≥22)"}`, | ||
| severity: nodePass ? void 0 : "error" | ||
| }); | ||
| const crDir = contentrainDir(projectRoot); | ||
| const hasCrDir = await pathExists(crDir); | ||
| const hasConfig = await pathExists(join(crDir, "config.json")); | ||
| const hasModels = await pathExists(join(crDir, "models")); | ||
| const hasContent = await pathExists(join(crDir, "content")); | ||
| const structurePass = hasCrDir && hasConfig && hasModels && hasContent; | ||
| checks.push({ | ||
| name: ".contentrain/ structure", | ||
| pass: structurePass, | ||
| detail: !hasCrDir ? "Not initialized — run `contentrain init`" : [ | ||
| hasConfig ? null : "missing config.json", | ||
| hasModels ? null : "missing models/", | ||
| hasContent ? null : "missing content/" | ||
| ].filter(Boolean).join(", ") || "OK", | ||
| severity: structurePass ? void 0 : "error" | ||
| }); | ||
| let config = null; | ||
| if (hasConfig) { | ||
| config = await readConfig(projectRoot); | ||
| checks.push({ | ||
| name: "Config", | ||
| pass: config !== null, | ||
| detail: config ? `stack: ${config.stack}, locales: ${config.locales.supported.join(", ")}` : "Failed to parse config.json", | ||
| severity: config ? void 0 : "error" | ||
| }); | ||
| } | ||
| if (hasCrDir) try { | ||
| const models = await listModels(projectRoot); | ||
| const allParseable = (await Promise.all(models.map((m) => readModel(projectRoot, m.id)))).every((r) => r !== null); | ||
| checks.push({ | ||
| name: "Models", | ||
| pass: allParseable, | ||
| detail: `${models.length} model(s)${allParseable ? ", all valid" : ", some failed to parse"}`, | ||
| severity: allParseable ? void 0 : "error" | ||
| }); | ||
| } catch { | ||
| checks.push({ | ||
| name: "Models", | ||
| pass: false, | ||
| detail: "Failed to read models", | ||
| severity: "error" | ||
| }); | ||
| } | ||
| if (hasCrDir) { | ||
| const orphans = await findOrphanContent(projectRoot); | ||
| checks.push({ | ||
| name: "Orphan content", | ||
| pass: orphans.length === 0, | ||
| detail: orphans.length === 0 ? "None" : `Found: ${orphans.join(", ")}`, | ||
| severity: orphans.length === 0 ? void 0 : "warning" | ||
| }); | ||
| } | ||
| if (hasGit) { | ||
| try { | ||
| const health = await checkBranchHealth(projectRoot); | ||
| checks.push({ | ||
| name: "Pending branches", | ||
| pass: !health.blocked && !health.warning, | ||
| detail: health.message ?? (health.unmerged === 0 ? "None" : `${health.unmerged} active cr/* branch(es)`), | ||
| severity: health.blocked ? "error" : health.warning ? "warning" : void 0 | ||
| }); | ||
| } catch { | ||
| checks.push({ | ||
| name: "Pending branches", | ||
| pass: true, | ||
| detail: "Could not check" | ||
| }); | ||
| } | ||
| const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5e3 }); | ||
| if (remoteList) if (remoteList.error) checks.push({ | ||
| name: "Remote branches", | ||
| pass: true, | ||
| detail: `Could not check ${remoteList.remote} (offline?)`, | ||
| severity: "info" | ||
| }); | ||
| else { | ||
| const count = remoteList.branches.length; | ||
| const warn = count >= (config?.branchWarnLimit ?? 50); | ||
| checks.push({ | ||
| name: "Remote branches", | ||
| pass: !warn, | ||
| detail: count === 0 ? `None on ${remoteList.remote}` : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? " — run `contentrain prune` to remove merged leftovers" : ""}`, | ||
| severity: warn ? "warning" : void 0 | ||
| }); | ||
| } | ||
| } | ||
| const clientDir = join(crDir, "client"); | ||
| const modelsDir = join(crDir, "models"); | ||
| if (await pathExists(clientDir) && await pathExists(modelsDir)) try { | ||
| const [clientMtime, modelsMtime] = await Promise.all([newestFileMtime(clientDir), newestFileMtime(modelsDir)]); | ||
| if (clientMtime === null || modelsMtime === null) checks.push({ | ||
| name: "SDK client", | ||
| pass: true, | ||
| detail: "Could not check" | ||
| }); | ||
| else { | ||
| const fresh = clientMtime >= modelsMtime; | ||
| checks.push({ | ||
| name: "SDK client", | ||
| pass: fresh, | ||
| detail: fresh ? "Up to date" : "Stale — run `contentrain generate`", | ||
| severity: fresh ? void 0 : "warning" | ||
| }); | ||
| } | ||
| } catch { | ||
| checks.push({ | ||
| name: "SDK client", | ||
| pass: true, | ||
| detail: "Could not check" | ||
| }); | ||
| } | ||
| let usage; | ||
| if (options.usage && hasCrDir && config) { | ||
| const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([ | ||
| analyzeUnusedKeys(projectRoot, config), | ||
| analyzeDuplicateValues(projectRoot, config), | ||
| analyzeMissingLocaleKeys(projectRoot, config) | ||
| ]); | ||
| usage = { | ||
| unusedKeys, | ||
| duplicateValues, | ||
| missingLocaleKeys | ||
| }; | ||
| checks.push({ | ||
| name: "Unused content keys", | ||
| pass: unusedKeys.length === 0, | ||
| detail: unusedKeys.length === 0 ? "All keys referenced in source" : `${unusedKeys.length} key(s) not referenced in source code`, | ||
| severity: unusedKeys.length === 0 ? void 0 : "warning" | ||
| }); | ||
| checks.push({ | ||
| name: "Duplicate dictionary values", | ||
| pass: duplicateValues.length === 0, | ||
| detail: duplicateValues.length === 0 ? "No duplicate values" : `${duplicateValues.length} value(s) mapped to multiple keys`, | ||
| severity: duplicateValues.length === 0 ? void 0 : "warning" | ||
| }); | ||
| checks.push({ | ||
| name: "Locale key coverage", | ||
| pass: missingLocaleKeys.length === 0, | ||
| detail: missingLocaleKeys.length === 0 ? "All locales have matching keys" : `${missingLocaleKeys.length} key(s) missing in some locales`, | ||
| severity: missingLocaleKeys.length === 0 ? void 0 : "warning" | ||
| }); | ||
| } | ||
| const passed = checks.filter((c) => c.pass).length; | ||
| const failed = checks.length - passed; | ||
| const warnings = checks.filter((c) => !c.pass && c.severity === "warning").length; | ||
| const report = { | ||
| checks, | ||
| summary: { | ||
| total: checks.length, | ||
| passed, | ||
| failed, | ||
| warnings | ||
| } | ||
| }; | ||
| if (usage) report.usage = usage; | ||
| return report; | ||
| } | ||
| /** | ||
| * Newest mtime among the files under `dir`, recursively — null if it holds none. | ||
| * | ||
| * Stat the files, never the directory. A directory's mtime only moves when an | ||
| * entry is added, removed, or renamed inside it: `generate` rewrites the client | ||
| * files in place, so `.contentrain/client` never moves after the first run, | ||
| * while a selective sync recreates model files via `git checkout`, which does | ||
| * move `.contentrain/models`. Comparing the two directories therefore reported | ||
| * "stale" permanently after any model save. | ||
| */ | ||
| async function newestFileMtime(dir) { | ||
| const entries = await readdir(dir, { withFileTypes: true }).catch(() => []); | ||
| const known = (await Promise.all(entries.map(async (entry) => { | ||
| const full = join(dir, entry.name); | ||
| if (entry.isDirectory()) return newestFileMtime(full); | ||
| return stat(full).then((s) => s.mtimeMs, () => null); | ||
| }))).filter((m) => m !== null); | ||
| return known.length > 0 ? Math.max(...known) : null; | ||
| } | ||
| async function findOrphanContent(projectRoot) { | ||
| const crDir = contentrainDir(projectRoot); | ||
| const models = await listModels(projectRoot); | ||
| const orphans = []; | ||
| const knownContentDirs = /* @__PURE__ */ new Set(); | ||
| for (const m of models) { | ||
| const full = await readModel(projectRoot, m.id); | ||
| const modelForPath = full ? { | ||
| ...full, | ||
| content_path: full.content_path ?? m.content_path | ||
| } : { | ||
| id: m.id, | ||
| name: m.id, | ||
| kind: m.kind, | ||
| domain: m.domain, | ||
| i18n: m.i18n, | ||
| fields: {}, | ||
| content_path: m.content_path | ||
| }; | ||
| knownContentDirs.add(resolveContentDir(projectRoot, modelForPath)); | ||
| } | ||
| const contentDir = join(crDir, "content"); | ||
| if (await pathExists(contentDir)) { | ||
| const domains = await readDir(contentDir); | ||
| for (const domain of domains) { | ||
| const domainDir = join(contentDir, domain); | ||
| const entries = await readDir(domainDir); | ||
| for (const entry of entries) { | ||
| if (entry === ".gitkeep") continue; | ||
| const entryDir = join(domainDir, entry); | ||
| if (!knownContentDirs.has(entryDir)) orphans.push(`${domain}/${entry}`); | ||
| } | ||
| } | ||
| } | ||
| for (const dir of knownContentDirs) { | ||
| if (dir.startsWith(contentDir)) continue; | ||
| if (!await pathExists(dir)) { | ||
| orphans.push(`(missing custom path) ${dir}`); | ||
| continue; | ||
| } | ||
| await readDir(dir); | ||
| } | ||
| return orphans; | ||
| } | ||
| async function analyzeUnusedKeys(projectRoot, config) { | ||
| const files = await discoverFiles(projectRoot, { paths: await autoDetectSourceDirs(projectRoot) }); | ||
| if (files.length === 0) return []; | ||
| const allSource = (await Promise.all(files.map(async (relPath) => { | ||
| return await readText(join(projectRoot, relPath)) ?? ""; | ||
| }))).join("\n"); | ||
| const models = await listModels(projectRoot); | ||
| const defaultLocale = config.locales.default; | ||
| const unused = []; | ||
| for (const m of models) { | ||
| const fullModel = await readModel(projectRoot, m.id); | ||
| if (!fullModel) continue; | ||
| const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale); | ||
| for (const key of keys) if (!allSource.includes(key)) unused.push({ | ||
| model: m.id, | ||
| kind: m.kind, | ||
| key, | ||
| locale: defaultLocale | ||
| }); | ||
| } | ||
| return unused; | ||
| } | ||
| async function extractContentKeys(projectRoot, model, locale) { | ||
| const cDir = resolveContentDir(projectRoot, model); | ||
| if (!await pathExists(cDir)) return []; | ||
| switch (model.kind) { | ||
| case "dictionary": { | ||
| const data = await readJson(resolveJsonFilePath(cDir, model, locale)); | ||
| return data ? Object.keys(data) : []; | ||
| } | ||
| case "collection": { | ||
| const data = await readJson(resolveJsonFilePath(cDir, model, locale)); | ||
| return data ? Object.keys(data) : []; | ||
| } | ||
| case "document": { | ||
| const strategy = resolveLocaleStrategy(model); | ||
| const slugs = []; | ||
| if (!model.i18n) { | ||
| const files = await readDir(cDir); | ||
| for (const f of files) if (f.endsWith(".md")) slugs.push(f.replace(".md", "")); | ||
| } else if (strategy === "file") { | ||
| const dirs = await readDir(cDir); | ||
| for (const d of dirs) if (!d.startsWith(".")) slugs.push(d); | ||
| } else if (strategy === "suffix") { | ||
| const files = await readDir(cDir); | ||
| const suffix = `.${locale}.md`; | ||
| for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length)); | ||
| } else if (strategy === "directory") { | ||
| const localeDir = join(cDir, locale); | ||
| if (await pathExists(localeDir)) { | ||
| const files = await readDir(localeDir); | ||
| for (const f of files) if (f.endsWith(".md")) slugs.push(f.replace(".md", "")); | ||
| } | ||
| } else { | ||
| const files = await readDir(cDir); | ||
| for (const f of files) if (f.endsWith(".md")) slugs.push(f.replace(".md", "")); | ||
| } | ||
| return slugs; | ||
| } | ||
| case "singleton": return []; | ||
| default: return []; | ||
| } | ||
| } | ||
| async function analyzeDuplicateValues(projectRoot, config) { | ||
| const models = await listModels(projectRoot); | ||
| const result = []; | ||
| for (const m of models) { | ||
| if (m.kind !== "dictionary") continue; | ||
| const fullModel = await readModel(projectRoot, m.id); | ||
| if (!fullModel) continue; | ||
| const cDir = resolveContentDir(projectRoot, fullModel); | ||
| for (const locale of config.locales.supported) { | ||
| const data = await readJson(resolveJsonFilePath(cDir, fullModel, locale)); | ||
| if (!data) continue; | ||
| const valueToKeys = /* @__PURE__ */ new Map(); | ||
| for (const [key, value] of Object.entries(data)) { | ||
| const arr = valueToKeys.get(value); | ||
| if (arr) arr.push(key); | ||
| else valueToKeys.set(value, [key]); | ||
| } | ||
| for (const [value, keys] of valueToKeys) if (keys.length > 1) result.push({ | ||
| model: m.id, | ||
| locale, | ||
| value, | ||
| keys | ||
| }); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| async function analyzeMissingLocaleKeys(projectRoot, config) { | ||
| if (config.locales.supported.length < 2) return []; | ||
| const models = await listModels(projectRoot); | ||
| const result = []; | ||
| const defaultLocale = config.locales.default; | ||
| const otherLocales = config.locales.supported.filter((l) => l !== defaultLocale); | ||
| for (const m of models) { | ||
| if (m.kind !== "dictionary" && m.kind !== "collection") continue; | ||
| if (!m.i18n) continue; | ||
| const fullModel = await readModel(projectRoot, m.id); | ||
| if (!fullModel) continue; | ||
| const cDir = resolveContentDir(projectRoot, fullModel); | ||
| const defaultData = await readJson(resolveJsonFilePath(cDir, fullModel, defaultLocale)); | ||
| if (!defaultData) continue; | ||
| const defaultKeys = new Set(Object.keys(defaultData)); | ||
| for (const locale of otherLocales) { | ||
| const localeData = await readJson(resolveJsonFilePath(cDir, fullModel, locale)); | ||
| const localeKeys = localeData ? new Set(Object.keys(localeData)) : /* @__PURE__ */ new Set(); | ||
| for (const key of defaultKeys) if (!localeKeys.has(key)) result.push({ | ||
| model: m.id, | ||
| key, | ||
| missingIn: locale | ||
| }); | ||
| } | ||
| } | ||
| return result; | ||
| } | ||
| //#endregion | ||
| export { runDoctor as t }; | ||
| //# sourceMappingURL=doctor-pw8EWRFR.mjs.map |
| {"version":3,"file":"doctor-pw8EWRFR.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport { simpleGit } from 'simple-git'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } from './model-manager.js'\nimport { resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy } from './content-manager.js'\nimport { autoDetectSourceDirs, discoverFiles } from './scan-config.js'\nimport { checkBranchHealth, listRemoteCrBranches } from '../git/branch-lifecycle.js'\nimport { contentrainDir, pathExists, readDir, readJson, readText } from '../util/fs.js'\n\n/**\n * Doctor — project health report.\n *\n * The public entry point is `runDoctor(projectRoot, { usage? })`. It is\n * inherently local-filesystem work (Node version, git install, file\n * mtimes, orphan directory detection), so the MCP tool surface gates\n * it behind the `localWorktree` capability — same pattern as\n * `contentrain_setup` and normalize.\n *\n * The report is structured JSON so three consumers can share it:\n *\n * - The `contentrain doctor` CLI command pretty-prints the checks.\n * - The Serve UI `/api/doctor` route returns the report to the\n * Dashboard's Doctor panel.\n * - Automation (CI, Studio) gets a deterministic JSON shape it can\n * assert against.\n *\n * Usage analysis (`--usage`) is a heavier, opt-in branch — it scans\n * every source file in the repo for content-key references. Kept\n * behind the flag so the default doctor run stays fast.\n */\n\nexport type CheckSeverity = 'error' | 'warning' | 'info'\n\nexport interface DoctorCheck {\n name: string\n pass: boolean\n detail: string\n /**\n * `error` — default for failing checks. Blocks a clean bill of health.\n * `warning` — failing-but-not-blocking (e.g. pending branches above\n * threshold, stale SDK client).\n * `info` — passed check; pure informational.\n */\n severity?: CheckSeverity\n}\n\nexport interface UnusedKeyEntry {\n model: string\n kind: string\n key: string\n locale: string\n}\n\nexport interface DuplicateValueEntry {\n model: string\n locale: string\n value: string\n keys: string[]\n}\n\nexport interface MissingLocaleEntry {\n model: string\n key: string\n missingIn: string\n}\n\nexport interface DoctorUsageAnalysis {\n unusedKeys: UnusedKeyEntry[]\n duplicateValues: DuplicateValueEntry[]\n missingLocaleKeys: MissingLocaleEntry[]\n}\n\nexport interface DoctorReport {\n checks: DoctorCheck[]\n summary: {\n total: number\n passed: number\n failed: number\n warnings: number\n }\n /** Present only when `options.usage === true`. */\n usage?: DoctorUsageAnalysis\n}\n\nexport interface RunDoctorOptions {\n /** Run heavier `--usage` analysis (unused keys, duplicates, locale gaps). */\n usage?: boolean\n}\n\nexport async function runDoctor(\n projectRoot: string,\n options: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n const checks: DoctorCheck[] = []\n\n // ─── 1. Git installed ───\n try {\n const git = simpleGit(projectRoot)\n const version = await git.version()\n checks.push({\n name: 'Git',\n pass: true,\n detail: `v${version.major}.${version.minor}.${version.patch}`,\n })\n } catch {\n checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' })\n }\n\n // ─── 2. Git repo initialized ───\n const hasGit = await pathExists(join(projectRoot, '.git'))\n checks.push({\n name: 'Git repository',\n pass: hasGit,\n detail: hasGit ? projectRoot : 'No .git directory found',\n severity: hasGit ? undefined : 'error',\n })\n\n // ─── 3. Node version ───\n const nodeVersion = process.versions.node\n const [major] = nodeVersion.split('.').map(Number)\n const nodePass = (major ?? 0) >= 22\n checks.push({\n name: 'Node.js',\n pass: nodePass,\n detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`,\n severity: nodePass ? undefined : 'error',\n })\n\n // ─── 4. .contentrain/ structure ───\n const crDir = contentrainDir(projectRoot)\n const hasCrDir = await pathExists(crDir)\n const hasConfig = await pathExists(join(crDir, 'config.json'))\n const hasModels = await pathExists(join(crDir, 'models'))\n const hasContent = await pathExists(join(crDir, 'content'))\n const structurePass = hasCrDir && hasConfig && hasModels && hasContent\n\n checks.push({\n name: '.contentrain/ structure',\n pass: structurePass,\n detail: !hasCrDir\n ? 'Not initialized — run `contentrain init`'\n : [\n hasConfig ? null : 'missing config.json',\n hasModels ? null : 'missing models/',\n hasContent ? null : 'missing content/',\n ].filter(Boolean).join(', ') || 'OK',\n severity: structurePass ? undefined : 'error',\n })\n\n // ─── 5. Config parseable ───\n let config: ContentrainConfig | null = null\n if (hasConfig) {\n config = await readConfig(projectRoot)\n checks.push({\n name: 'Config',\n pass: config !== null,\n detail: config\n ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}`\n : 'Failed to parse config.json',\n severity: config ? undefined : 'error',\n })\n }\n\n // ─── 6. Models all parseable ───\n if (hasCrDir) {\n try {\n const models = await listModels(projectRoot)\n const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const allParseable = parseResults.every(r => r !== null)\n checks.push({\n name: 'Models',\n pass: allParseable,\n detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`,\n severity: allParseable ? undefined : 'error',\n })\n } catch {\n checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' })\n }\n }\n\n // ─── 7. Orphan content ───\n if (hasCrDir) {\n const orphans = await findOrphanContent(projectRoot)\n checks.push({\n name: 'Orphan content',\n pass: orphans.length === 0,\n detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`,\n severity: orphans.length === 0 ? undefined : 'warning',\n })\n }\n\n // ─── 8. Stale contentrain branches ───\n if (hasGit) {\n try {\n const health = await checkBranchHealth(projectRoot)\n checks.push({\n name: 'Pending branches',\n pass: !health.blocked && !health.warning,\n detail: health.message\n ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`),\n severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined,\n })\n } catch {\n checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' })\n }\n\n // ─── 8b. Remote cr/* branches ───\n // Authoritative ls-remote count (local branch pressure cannot see the\n // remote pile). Best-effort: skipped entirely without a remote, and an\n // unreachable remote is informational — doctor never fails offline.\n const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5000 })\n if (remoteList) {\n if (remoteList.error) {\n checks.push({\n name: 'Remote branches',\n pass: true,\n detail: `Could not check ${remoteList.remote} (offline?)`,\n severity: 'info',\n })\n } else {\n const count = remoteList.branches.length\n const warnLimit = config?.branchWarnLimit ?? 50\n const warn = count >= warnLimit\n checks.push({\n name: 'Remote branches',\n pass: !warn,\n detail: count === 0\n ? `None on ${remoteList.remote}`\n : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? ' — run `contentrain prune` to remove merged leftovers' : ''}`,\n severity: warn ? 'warning' : undefined,\n })\n }\n }\n }\n\n // ─── 9. SDK client freshness ───\n const clientDir = join(crDir, 'client')\n const modelsDir = join(crDir, 'models')\n if (await pathExists(clientDir) && await pathExists(modelsDir)) {\n try {\n const [clientMtime, modelsMtime] = await Promise.all([\n newestFileMtime(clientDir),\n newestFileMtime(modelsDir),\n ])\n if (clientMtime === null || modelsMtime === null) {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n } else {\n const fresh = clientMtime >= modelsMtime\n checks.push({\n name: 'SDK client',\n pass: fresh,\n detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`',\n severity: fresh ? undefined : 'warning',\n })\n }\n } catch {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 10–12. Usage analysis (optional) ───\n let usage: DoctorUsageAnalysis | undefined\n if (options.usage && hasCrDir && config) {\n const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([\n analyzeUnusedKeys(projectRoot, config),\n analyzeDuplicateValues(projectRoot, config),\n analyzeMissingLocaleKeys(projectRoot, config),\n ])\n usage = { unusedKeys, duplicateValues, missingLocaleKeys }\n\n checks.push({\n name: 'Unused content keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All keys referenced in source'\n : `${unusedKeys.length} key(s) not referenced in source code`,\n severity: unusedKeys.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Duplicate dictionary values',\n pass: duplicateValues.length === 0,\n detail: duplicateValues.length === 0\n ? 'No duplicate values'\n : `${duplicateValues.length} value(s) mapped to multiple keys`,\n severity: duplicateValues.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Locale key coverage',\n pass: missingLocaleKeys.length === 0,\n detail: missingLocaleKeys.length === 0\n ? 'All locales have matching keys'\n : `${missingLocaleKeys.length} key(s) missing in some locales`,\n severity: missingLocaleKeys.length === 0 ? undefined : 'warning',\n })\n }\n\n const passed = checks.filter(c => c.pass).length\n const failed = checks.length - passed\n const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length\n\n const report: DoctorReport = {\n checks,\n summary: { total: checks.length, passed, failed, warnings },\n }\n if (usage) report.usage = usage\n return report\n}\n\n/**\n * Newest mtime among the files under `dir`, recursively — null if it holds none.\n *\n * Stat the files, never the directory. A directory's mtime only moves when an\n * entry is added, removed, or renamed inside it: `generate` rewrites the client\n * files in place, so `.contentrain/client` never moves after the first run,\n * while a selective sync recreates model files via `git checkout`, which does\n * move `.contentrain/models`. Comparing the two directories therefore reported\n * \"stale\" permanently after any model save.\n */\nasync function newestFileMtime(dir: string): Promise<number | null> {\n const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [])\n const mtimes = await Promise.all(entries.map(async (entry) => {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) return newestFileMtime(full)\n return stat(full).then(s => s.mtimeMs, () => null)\n }))\n const known = mtimes.filter((m): m is number => m !== null)\n return known.length > 0 ? Math.max(...known) : null\n}\n\nasync function findOrphanContent(projectRoot: string): Promise<string[]> {\n const crDir = contentrainDir(projectRoot)\n const models = await listModels(projectRoot)\n const orphans: string[] = []\n\n const knownContentDirs = new Set<string>()\n for (const m of models) {\n const full = await readModel(projectRoot, m.id)\n const modelForPath = full\n ? {\n ...full,\n content_path: full.content_path ?? (m as { content_path?: string }).content_path,\n }\n : {\n id: m.id,\n name: m.id,\n kind: m.kind,\n domain: m.domain,\n i18n: m.i18n,\n fields: {},\n content_path: (m as { content_path?: string }).content_path,\n }\n knownContentDirs.add(resolveContentDir(projectRoot, modelForPath))\n }\n\n const contentDir = join(crDir, 'content')\n if (await pathExists(contentDir)) {\n const domains = await readDir(contentDir)\n for (const domain of domains) {\n const domainDir = join(contentDir, domain)\n const entries = await readDir(domainDir)\n for (const entry of entries) {\n if (entry === '.gitkeep') continue\n const entryDir = join(domainDir, entry)\n if (!knownContentDirs.has(entryDir)) {\n orphans.push(`${domain}/${entry}`)\n }\n }\n }\n }\n\n for (const dir of knownContentDirs) {\n if (dir.startsWith(contentDir)) continue\n if (!await pathExists(dir)) {\n orphans.push(`(missing custom path) ${dir}`)\n continue\n }\n await readDir(dir)\n }\n\n return orphans\n}\n\nasync function analyzeUnusedKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<UnusedKeyEntry[]> {\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, { paths: sourceDirs })\n if (files.length === 0) return []\n\n const chunks = await Promise.all(\n files.map(async (relPath) => {\n const content = await readText(join(projectRoot, relPath))\n return content ?? ''\n }),\n )\n const allSource = chunks.join('\\n')\n\n const models = await listModels(projectRoot)\n const defaultLocale = config.locales.default\n const unused: UnusedKeyEntry[] = []\n\n for (const m of models) {\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale)\n for (const key of keys) {\n if (!allSource.includes(key)) {\n unused.push({ model: m.id, kind: m.kind, key, locale: defaultLocale })\n }\n }\n }\n\n return unused\n}\n\nasync function extractContentKeys(\n projectRoot: string,\n model: ModelDefinition,\n locale: string,\n): Promise<string[]> {\n const cDir = resolveContentDir(projectRoot, model)\n if (!await pathExists(cDir)) return []\n\n switch (model.kind) {\n case 'dictionary': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'collection': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'document': {\n const strategy = resolveLocaleStrategy(model)\n const slugs: string[] = []\n if (!model.i18n) {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n } else if (strategy === 'file') {\n const dirs = await readDir(cDir)\n for (const d of dirs) if (!d.startsWith('.')) slugs.push(d)\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length))\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n if (await pathExists(localeDir)) {\n const files = await readDir(localeDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n } else {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n return slugs\n }\n case 'singleton':\n return []\n default:\n return []\n }\n}\n\nasync function analyzeDuplicateValues(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<DuplicateValueEntry[]> {\n const models = await listModels(projectRoot)\n const result: DuplicateValueEntry[] = []\n\n for (const m of models) {\n if (m.kind !== 'dictionary') continue\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, fullModel, locale)\n const data = await readJson<Record<string, string>>(filePath)\n if (!data) continue\n\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n\n for (const [value, keys] of valueToKeys) {\n if (keys.length > 1) {\n result.push({ model: m.id, locale, value, keys })\n }\n }\n }\n }\n\n return result\n}\n\nasync function analyzeMissingLocaleKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<MissingLocaleEntry[]> {\n if (config.locales.supported.length < 2) return []\n\n const models = await listModels(projectRoot)\n const result: MissingLocaleEntry[] = []\n const defaultLocale = config.locales.default\n const otherLocales = config.locales.supported.filter(l => l !== defaultLocale)\n\n for (const m of models) {\n if (m.kind !== 'dictionary' && m.kind !== 'collection') continue\n if (!m.i18n) continue\n\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n const defaultPath = resolveJsonFilePath(cDir, fullModel, defaultLocale)\n const defaultData = await readJson<Record<string, unknown>>(defaultPath)\n if (!defaultData) continue\n const defaultKeys = new Set(Object.keys(defaultData))\n\n for (const locale of otherLocales) {\n const localePath = resolveJsonFilePath(cDir, fullModel, locale)\n const localeData = await readJson<Record<string, unknown>>(localePath)\n const localeKeys = localeData ? new Set(Object.keys(localeData)) : new Set<string>()\n\n for (const key of defaultKeys) {\n if (!localeKeys.has(key)) {\n result.push({ model: m.id, key, missingIn: locale })\n }\n }\n }\n }\n\n return result\n}\n"],"mappings":"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"} |
| import { o as readText, r as pathExists } from "./fs-DLbVB-Ek.mjs"; | ||
| import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles } from "./scan-config-BGUflS8t.mjs"; | ||
| import { dirname, extname, join, relative } from "node:path"; | ||
| //#region src/core/graph-builder.ts | ||
| const MAX_ORPHANS = 10; | ||
| const IMPORT_RE = /(?:import|export)\s+.*?from\s+['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]\s*\)/g; | ||
| function extractImportPaths(content) { | ||
| const paths = []; | ||
| let m; | ||
| IMPORT_RE.lastIndex = 0; | ||
| while ((m = IMPORT_RE.exec(content)) !== null) { | ||
| const p = m[1] ?? m[2]; | ||
| if (p && isLocalImport(p)) paths.push(p); | ||
| } | ||
| return paths; | ||
| } | ||
| function isLocalImport(p) { | ||
| return p.startsWith(".") || p.startsWith("/"); | ||
| } | ||
| const NAMED_IMPORT_RE = /import\s+(?:type\s+)?(?:(\w+)(?:\s*,\s*)?)?(?:\{([^}]*)\})?\s+from\s+['"][^'"]+['"]/g; | ||
| const DEFAULT_IMPORT_RE = /import\s+(?:type\s+)?(\w+)\s+from\s+['"][^'"]+['"]/g; | ||
| function isPascalCase(name) { | ||
| return /^[A-Z][a-zA-Z0-9]*$/.test(name); | ||
| } | ||
| function extractComponentNames(content) { | ||
| const names = /* @__PURE__ */ new Set(); | ||
| DEFAULT_IMPORT_RE.lastIndex = 0; | ||
| let m; | ||
| while ((m = DEFAULT_IMPORT_RE.exec(content)) !== null) { | ||
| const name = m[1]; | ||
| if (name && isPascalCase(name)) names.add(name); | ||
| } | ||
| NAMED_IMPORT_RE.lastIndex = 0; | ||
| while ((m = NAMED_IMPORT_RE.exec(content)) !== null) { | ||
| const defaultName = m[1]; | ||
| if (defaultName && isPascalCase(defaultName)) names.add(defaultName); | ||
| const namedPart = m[2]; | ||
| if (namedPart) for (const segment of namedPart.split(",")) { | ||
| const parts = segment.trim().split(/\s+as\s+/); | ||
| const localName = (parts[1] ?? parts[0])?.trim(); | ||
| if (localName && isPascalCase(localName)) names.add(localName); | ||
| } | ||
| } | ||
| return [...names]; | ||
| } | ||
| const STRING_SINGLE_RE = /'[^'\\]*(?:\\.[^'\\]*)*'/g; | ||
| const STRING_DOUBLE_RE = /"[^"\\]*(?:\\.[^"\\]*)*"/g; | ||
| const STRING_TEMPLATE_RE = /`[^`\\]*(?:\\.[^`\\]*)*`/g; | ||
| function countStrings(content) { | ||
| const stripped = content.replace(/^(?:import|export)\s+.*$/gm, "").replace(/\brequire\s*\([^)]*\)/g, ""); | ||
| const singles = stripped.match(STRING_SINGLE_RE)?.length ?? 0; | ||
| const doubles = stripped.match(STRING_DOUBLE_RE)?.length ?? 0; | ||
| const templates = stripped.match(STRING_TEMPLATE_RE)?.length ?? 0; | ||
| return singles + doubles + templates; | ||
| } | ||
| const RESOLVE_EXTENSIONS = [...SCAN_EXTENSIONS]; | ||
| const RESOLVE_INDEX_FILES = [ | ||
| "index.ts", | ||
| "index.tsx", | ||
| "index.js" | ||
| ]; | ||
| async function resolveImportPath(importPath, importerDir, projectRoot) { | ||
| const base = importPath.startsWith("/") ? join(projectRoot, importPath) : join(importerDir, importPath); | ||
| if (extname(base)) { | ||
| if (await pathExists(base)) return base; | ||
| return null; | ||
| } | ||
| for (const ext of RESOLVE_EXTENSIONS) { | ||
| const candidate = base + ext; | ||
| if (await pathExists(candidate)) return candidate; | ||
| } | ||
| for (const idx of RESOLVE_INDEX_FILES) { | ||
| const candidate = join(base, idx); | ||
| if (await pathExists(candidate)) return candidate; | ||
| } | ||
| return null; | ||
| } | ||
| async function buildGraph(projectRoot, options) { | ||
| const filePaths = await discoverFiles(projectRoot, { | ||
| paths: options?.paths ?? await autoDetectSourceDirs(projectRoot), | ||
| include: options?.include, | ||
| exclude: options?.exclude | ||
| }); | ||
| const fileMap = /* @__PURE__ */ new Map(); | ||
| const parsePromises = filePaths.map(async (relPath) => { | ||
| const content = await readText(join(projectRoot, relPath)); | ||
| if (content === null) return; | ||
| const imports = extractImportPaths(content); | ||
| const components = extractComponentNames(content); | ||
| const strings = countStrings(content); | ||
| const category = classifyFile(relPath); | ||
| fileMap.set(relPath, { | ||
| relPath, | ||
| imports, | ||
| components, | ||
| strings, | ||
| category | ||
| }); | ||
| }); | ||
| await Promise.all(parsePromises); | ||
| const usedByMap = /* @__PURE__ */ new Map(); | ||
| for (const relPath of fileMap.keys()) usedByMap.set(relPath, /* @__PURE__ */ new Set()); | ||
| const resolvePromises = []; | ||
| for (const [relPath, info] of fileMap) { | ||
| const importerAbsDir = dirname(join(projectRoot, relPath)); | ||
| for (const rawImport of info.imports) resolvePromises.push(resolveImportPath(rawImport, importerAbsDir, projectRoot).then((resolved) => { | ||
| if (!resolved) return; | ||
| const resolvedRel = relative(projectRoot, resolved); | ||
| const targetSet = usedByMap.get(resolvedRel); | ||
| if (targetSet) targetSet.add(relPath); | ||
| })); | ||
| } | ||
| await Promise.all(resolvePromises); | ||
| const pages = []; | ||
| const components = []; | ||
| const layouts = []; | ||
| const orphanCandidates = []; | ||
| let totalStrings = 0; | ||
| for (const [relPath, info] of fileMap) { | ||
| const usedBy = [...usedByMap.get(relPath) ?? []].toSorted((a, b) => a.localeCompare(b)); | ||
| totalStrings += info.strings; | ||
| if (info.strings === 0 && info.category !== "other") continue; | ||
| const node = { | ||
| file: relPath, | ||
| category: info.category, | ||
| imports: info.imports, | ||
| used_by: usedBy, | ||
| strings: info.strings | ||
| }; | ||
| if (info.category === "page" && info.components.length > 0) node.components = info.components; | ||
| switch (info.category) { | ||
| case "page": | ||
| pages.push(node); | ||
| break; | ||
| case "component": | ||
| components.push(node); | ||
| break; | ||
| case "layout": | ||
| layouts.push(node); | ||
| break; | ||
| default: | ||
| if (usedBy.length === 0 && info.imports.length === 0) orphanCandidates.push(relPath); | ||
| break; | ||
| } | ||
| } | ||
| return { | ||
| pages: pages.toSorted((a, b) => a.file.localeCompare(b.file)), | ||
| components: components.toSorted((a, b) => a.file.localeCompare(b.file)), | ||
| layouts: layouts.toSorted((a, b) => a.file.localeCompare(b.file)), | ||
| orphan_files: orphanCandidates.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_ORPHANS), | ||
| stats: { | ||
| total_files: fileMap.size, | ||
| total_components: components.length, | ||
| total_pages: pages.length, | ||
| total_strings_estimate: totalStrings | ||
| } | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { buildGraph as t }; | ||
| //# sourceMappingURL=graph-builder-CRUX_8mA.mjs.map |
| {"version":3,"file":"graph-builder-CRUX_8mA.mjs","names":[],"sources":["../src/core/graph-builder.ts"],"sourcesContent":["import type { FileCategory, GraphNode, ProjectGraph } from '@contentrain/types'\nimport { join, relative, dirname, extname } from 'node:path'\nimport { readText, pathExists } from '../util/fs.js'\nimport {\n SCAN_EXTENSIONS,\n classifyFile,\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\n\nexport interface BuildGraphOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\nconst MAX_ORPHANS = 10\n\n// ---------------------------------------------------------------------------\n// Import extraction\n// ---------------------------------------------------------------------------\n\nconst IMPORT_RE = /(?:import|export)\\s+.*?from\\s+['\"]([^'\"]+)['\"]|(?:import|require)\\s*\\(\\s*['\"]([^'\"]+)['\"]\\s*\\)/g\n\nfunction extractImportPaths(content: string): string[] {\n const paths: string[] = []\n let m: RegExpExecArray | null\n // Reset lastIndex before use since we reuse the regex\n IMPORT_RE.lastIndex = 0\n while ((m = IMPORT_RE.exec(content)) !== null) {\n const p = m[1] ?? m[2]\n if (p && isLocalImport(p)) {\n paths.push(p)\n }\n }\n return paths\n}\n\nfunction isLocalImport(p: string): boolean {\n return p.startsWith('.') || p.startsWith('/')\n}\n\n// ---------------------------------------------------------------------------\n// Component name extraction from imports\n// ---------------------------------------------------------------------------\n\nconst NAMED_IMPORT_RE = /import\\s+(?:type\\s+)?(?:(\\w+)(?:\\s*,\\s*)?)?(?:\\{([^}]*)\\})?\\s+from\\s+['\"][^'\"]+['\"]/g\nconst DEFAULT_IMPORT_RE = /import\\s+(?:type\\s+)?(\\w+)\\s+from\\s+['\"][^'\"]+['\"]/g\n\nfunction isPascalCase(name: string): boolean {\n return /^[A-Z][a-zA-Z0-9]*$/.test(name)\n}\n\nfunction extractComponentNames(content: string): string[] {\n const names = new Set<string>()\n\n // Default imports: `import Button from '...'`\n DEFAULT_IMPORT_RE.lastIndex = 0\n let m: RegExpExecArray | null\n while ((m = DEFAULT_IMPORT_RE.exec(content)) !== null) {\n const name = m[1]\n if (name && isPascalCase(name)) {\n names.add(name)\n }\n }\n\n // Named imports: `import { Button, useHook } from '...'`\n NAMED_IMPORT_RE.lastIndex = 0\n while ((m = NAMED_IMPORT_RE.exec(content)) !== null) {\n // Default part before destructuring\n const defaultName = m[1]\n if (defaultName && isPascalCase(defaultName)) {\n names.add(defaultName)\n }\n // Destructured names\n const namedPart = m[2]\n if (namedPart) {\n for (const segment of namedPart.split(',')) {\n // Handle `Foo as Bar` — take the local name (Bar)\n const parts = segment.trim().split(/\\s+as\\s+/)\n const localName = (parts[1] ?? parts[0])?.trim()\n if (localName && isPascalCase(localName)) {\n names.add(localName)\n }\n }\n }\n }\n\n return [...names]\n}\n\n// ---------------------------------------------------------------------------\n// String counting (estimate)\n// ---------------------------------------------------------------------------\n\nconst STRING_SINGLE_RE = /'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'/g\nconst STRING_DOUBLE_RE = /\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"/g\nconst STRING_TEMPLATE_RE = /`[^`\\\\]*(?:\\\\.[^`\\\\]*)*`/g\n\nfunction countStrings(content: string): number {\n // Strip import/export/require lines first so their string literals don't count\n const stripped = content.replace(/^(?:import|export)\\s+.*$/gm, '')\n .replace(/\\brequire\\s*\\([^)]*\\)/g, '')\n\n const singles = stripped.match(STRING_SINGLE_RE)?.length ?? 0\n const doubles = stripped.match(STRING_DOUBLE_RE)?.length ?? 0\n const templates = stripped.match(STRING_TEMPLATE_RE)?.length ?? 0\n\n return singles + doubles + templates\n}\n\n// ---------------------------------------------------------------------------\n// Import path resolution\n// ---------------------------------------------------------------------------\n\nconst RESOLVE_EXTENSIONS = [...SCAN_EXTENSIONS]\nconst RESOLVE_INDEX_FILES = ['index.ts', 'index.tsx', 'index.js']\n\nasync function resolveImportPath(\n importPath: string,\n importerDir: string,\n projectRoot: string,\n): Promise<string | null> {\n const base = importPath.startsWith('/')\n ? join(projectRoot, importPath)\n : join(importerDir, importPath)\n\n // If it already has an extension, check directly\n if (extname(base)) {\n if (await pathExists(base)) {\n return base\n }\n return null\n }\n\n // Try adding extensions\n for (const ext of RESOLVE_EXTENSIONS) {\n const candidate = base + ext\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n // Try index files inside directory\n for (const idx of RESOLVE_INDEX_FILES) {\n const candidate = join(base, idx)\n if (await pathExists(candidate)) {\n return candidate\n }\n }\n\n return null\n}\n\n// ---------------------------------------------------------------------------\n// Main\n// ---------------------------------------------------------------------------\n\ninterface FileInfo {\n relPath: string\n imports: string[]\n components: string[]\n strings: number\n category: FileCategory\n}\n\nexport async function buildGraph(\n projectRoot: string,\n options?: BuildGraphOptions,\n): Promise<ProjectGraph> {\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n // ---- File discovery (shared with scanner) ----\n const filePaths = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // ---- Pass 1: Parse each file ----\n const fileMap = new Map<string, FileInfo>()\n\n const parsePromises = filePaths.map(async (relPath) => {\n const absPath = join(projectRoot, relPath)\n const content = await readText(absPath)\n if (content === null) return\n\n const imports = extractImportPaths(content)\n const components = extractComponentNames(content)\n const strings = countStrings(content)\n const category = classifyFile(relPath)\n\n fileMap.set(relPath, { relPath, imports, components, strings, category })\n })\n\n await Promise.all(parsePromises)\n\n // ---- Pass 2: Resolve imports and build reverse lookup ----\n const usedByMap = new Map<string, Set<string>>()\n\n // Initialize usedBy sets\n for (const relPath of fileMap.keys()) {\n usedByMap.set(relPath, new Set())\n }\n\n const resolvePromises: Promise<void>[] = []\n\n for (const [relPath, info] of fileMap) {\n const importerAbsDir = dirname(join(projectRoot, relPath))\n\n for (const rawImport of info.imports) {\n resolvePromises.push(\n resolveImportPath(rawImport, importerAbsDir, projectRoot).then((resolved) => {\n if (!resolved) return\n const resolvedRel = relative(projectRoot, resolved)\n const targetSet = usedByMap.get(resolvedRel)\n if (targetSet) {\n targetSet.add(relPath)\n }\n }),\n )\n }\n }\n\n await Promise.all(resolvePromises)\n\n // ---- Build nodes ----\n const pages: GraphNode[] = []\n const components: GraphNode[] = []\n const layouts: GraphNode[] = []\n const orphanCandidates: string[] = []\n\n let totalStrings = 0\n\n for (const [relPath, info] of fileMap) {\n const usedBy = [...(usedByMap.get(relPath) ?? [])].toSorted((a, b) => a.localeCompare(b))\n totalStrings += info.strings\n\n // Filter out nodes with 0 strings — not relevant for content extraction\n if (info.strings === 0 && info.category !== 'other') continue\n\n const node: GraphNode = {\n file: relPath,\n category: info.category,\n imports: info.imports,\n used_by: usedBy,\n strings: info.strings,\n }\n\n // Only attach components list for pages\n if (info.category === 'page' && info.components.length > 0) {\n node.components = info.components\n }\n\n switch (info.category) {\n case 'page':\n pages.push(node)\n break\n case 'component':\n components.push(node)\n break\n case 'layout':\n layouts.push(node)\n break\n default:\n // Orphan = \"other\" category with no inbound references and no outbound imports\n if (usedBy.length === 0 && info.imports.length === 0) {\n orphanCandidates.push(relPath)\n }\n break\n }\n }\n\n return {\n pages: pages.toSorted((a, b) => a.file.localeCompare(b.file)),\n components: components.toSorted((a, b) => a.file.localeCompare(b.file)),\n layouts: layouts.toSorted((a, b) => a.file.localeCompare(b.file)),\n orphan_files: orphanCandidates.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_ORPHANS),\n stats: {\n total_files: fileMap.size,\n total_components: components.length,\n total_pages: pages.length,\n total_strings_estimate: totalStrings,\n },\n }\n}\n"],"mappings":"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"} |
| import { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider, RepoReader, RepoWriter } from "@contentrain/types"; | ||
| export { RepoReader as _, FileChange as a, MediaAsset as c, MediaListResult as d, MediaProvider as f, RepoProvider as g, ProviderCapabilities$1 as h, CommitAuthor as i, MediaIngestInput as l, MergeResult as m, Branch as n, FileDiff as o, MediaUpdateInput as p, Commit as r, LOCAL_CAPABILITIES as s, ApplyPlanInput as t, MediaListOptions as u, RepoWriter as v }; |
| import g$1 from "crypto"; | ||
| import _, { promises } from "fs"; | ||
| import { TextDecoder, TextEncoder } from "util"; | ||
| import { fileURLToPath } from "url"; | ||
| //#region ../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/chunk-W5DTLHV4.js | ||
| globalThis.fs || Object.defineProperty(globalThis, "fs", { value: _ }); | ||
| globalThis.process || Object.defineProperties(globalThis, "process", { value: process }); | ||
| globalThis.crypto || Object.defineProperty(globalThis, "crypto", { value: g$1.webcrypto ? g$1.webcrypto : { getRandomValues(m) { | ||
| return g$1.randomFillSync(m); | ||
| } } }); | ||
| globalThis.performance || Object.defineProperty(globalThis, "performance", { value: { now() { | ||
| let [m, o] = process.hrtime(); | ||
| return m * 1e3 + o / 1e6; | ||
| } } }); | ||
| var y$1 = new TextEncoder("utf-8"), w$1 = new TextDecoder("utf-8"); | ||
| var d$1 = class { | ||
| constructor() { | ||
| this.argv = ["js"], this.env = {}, this.exit = (t) => { | ||
| t !== 0 && console.warn("exit code:", t); | ||
| }, this._exitPromise = new Promise((t) => { | ||
| this._resolveExitPromise = t; | ||
| }), this._pendingEvent = null, this._scheduledTimeouts = /* @__PURE__ */ new Map(), this._nextCallbackTimeoutID = 1; | ||
| let o = (t, e) => { | ||
| this.mem.setUint32(t + 0, e, !0), this.mem.setUint32(t + 4, Math.floor(e / 4294967296), !0); | ||
| }, n = (t) => { | ||
| return this.mem.getUint32(t + 0, !0) + this.mem.getInt32(t + 4, !0) * 4294967296; | ||
| }, r = (t) => { | ||
| let e = this.mem.getFloat64(t, !0); | ||
| if (e === 0) return; | ||
| if (!isNaN(e)) return e; | ||
| let s = this.mem.getUint32(t, !0); | ||
| return this._values[s]; | ||
| }, l = (t, e) => { | ||
| if (typeof e == "number" && e !== 0) { | ||
| if (isNaN(e)) { | ||
| this.mem.setUint32(t + 4, 2146959360, !0), this.mem.setUint32(t, 0, !0); | ||
| return; | ||
| } | ||
| this.mem.setFloat64(t, e, !0); | ||
| return; | ||
| } | ||
| if (e === void 0) { | ||
| this.mem.setFloat64(t, 0, !0); | ||
| return; | ||
| } | ||
| let i = this._ids.get(e); | ||
| i === void 0 && (i = this._idPool.pop(), i === void 0 && (i = this._values.length), this._values[i] = e, this._goRefCounts[i] = 0, this._ids.set(e, i)), this._goRefCounts[i]++; | ||
| let a = 0; | ||
| switch (typeof e) { | ||
| case "object": | ||
| e !== null && (a = 1); | ||
| break; | ||
| case "string": | ||
| a = 2; | ||
| break; | ||
| case "symbol": | ||
| a = 3; | ||
| break; | ||
| case "function": | ||
| a = 4; | ||
| break; | ||
| } | ||
| this.mem.setUint32(t + 4, 2146959360 | a, !0), this.mem.setUint32(t, i, !0); | ||
| }, c = (t) => { | ||
| let e = n(t + 0), s = n(t + 8); | ||
| return new Uint8Array(this._inst.exports.mem.buffer, e, s); | ||
| }, f = (t) => { | ||
| let e = n(t + 0), s = n(t + 8), i = new Array(s); | ||
| for (let a = 0; a < s; a++) i[a] = r(e + a * 8); | ||
| return i; | ||
| }, u = (t) => { | ||
| let e = n(t + 0), s = n(t + 8); | ||
| return w$1.decode(new DataView(this._inst.exports.mem.buffer, e, s)); | ||
| }, h = Date.now() - performance.now(); | ||
| this.importObject = { gojs: { | ||
| "runtime.wasmExit": (t) => { | ||
| t >>>= 0; | ||
| let e = this.mem.getInt32(t + 8, !0); | ||
| this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(e); | ||
| }, | ||
| "runtime.wasmWrite": (t) => { | ||
| t >>>= 0; | ||
| let e = n(t + 8), s = n(t + 16), i = this.mem.getInt32(t + 24, !0); | ||
| _.writeSync(e, new Uint8Array(this._inst.exports.mem.buffer, s, i)); | ||
| }, | ||
| "runtime.resetMemoryDataView": (t) => { | ||
| t >>>= 0, this.mem = new DataView(this._inst.exports.mem.buffer); | ||
| }, | ||
| "runtime.nanotime1": (t) => { | ||
| t >>>= 0, o(t + 8, (h + performance.now()) * 1e6); | ||
| }, | ||
| "runtime.walltime": (t) => { | ||
| t >>>= 0; | ||
| let e = (/* @__PURE__ */ new Date()).getTime(); | ||
| o(t + 8, e / 1e3), this.mem.setInt32(t + 16, e % 1e3 * 1e6, !0); | ||
| }, | ||
| "runtime.scheduleTimeoutEvent": (t) => { | ||
| t >>>= 0; | ||
| let e = this._nextCallbackTimeoutID; | ||
| this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(e, setTimeout(() => { | ||
| for (this._resume(); this._scheduledTimeouts.has(e);) console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume(); | ||
| }, n(t + 8) + 1)), this.mem.setInt32(t + 16, e, !0); | ||
| }, | ||
| "runtime.clearTimeoutEvent": (t) => { | ||
| t >>>= 0; | ||
| let e = this.mem.getInt32(t + 8, !0); | ||
| clearTimeout(this._scheduledTimeouts.get(e)), this._scheduledTimeouts.delete(e); | ||
| }, | ||
| "runtime.getRandomData": (t) => { | ||
| t >>>= 0, globalThis.crypto.getRandomValues(c(t + 8)); | ||
| }, | ||
| "syscall/js.finalizeRef": (t) => { | ||
| t >>>= 0; | ||
| let e = this.mem.getUint32(t + 8, !0); | ||
| if (this._goRefCounts[e]--, this._goRefCounts[e] === 0) { | ||
| let s = this._values[e]; | ||
| this._values[e] = null, this._ids.delete(s), this._idPool.push(e); | ||
| } | ||
| }, | ||
| "syscall/js.stringVal": (t) => { | ||
| t >>>= 0, l(t + 24, u(t + 8)); | ||
| }, | ||
| "syscall/js.valueGet": (t) => { | ||
| t >>>= 0; | ||
| let e = Reflect.get(r(t + 8), u(t + 16)); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 32, e); | ||
| }, | ||
| "syscall/js.valueSet": (t) => { | ||
| t >>>= 0, Reflect.set(r(t + 8), u(t + 16), r(t + 32)); | ||
| }, | ||
| "syscall/js.valueDelete": (t) => { | ||
| t >>>= 0, Reflect.deleteProperty(r(t + 8), u(t + 16)); | ||
| }, | ||
| "syscall/js.valueIndex": (t) => { | ||
| t >>>= 0, l(t + 24, Reflect.get(r(t + 8), n(t + 16))); | ||
| }, | ||
| "syscall/js.valueSetIndex": (t) => { | ||
| t >>>= 0, Reflect.set(r(t + 8), n(t + 16), r(t + 24)); | ||
| }, | ||
| "syscall/js.valueCall": (t) => { | ||
| t >>>= 0; | ||
| try { | ||
| let e = r(t + 8), s = Reflect.get(e, u(t + 16)), i = f(t + 32), a = Reflect.apply(s, e, i); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 56, a), this.mem.setUint8(t + 64, 1); | ||
| } catch (e) { | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 56, e), this.mem.setUint8(t + 64, 0); | ||
| } | ||
| }, | ||
| "syscall/js.valueInvoke": (t) => { | ||
| t >>>= 0; | ||
| try { | ||
| let e = r(t + 8), s = f(t + 16), i = Reflect.apply(e, void 0, s); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, i), this.mem.setUint8(t + 48, 1); | ||
| } catch (e) { | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, e), this.mem.setUint8(t + 48, 0); | ||
| } | ||
| }, | ||
| "syscall/js.valueNew": (t) => { | ||
| t >>>= 0; | ||
| try { | ||
| let e = r(t + 8), s = f(t + 16), i = Reflect.construct(e, s); | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, i), this.mem.setUint8(t + 48, 1); | ||
| } catch (e) { | ||
| t = this._inst.exports.getsp() >>> 0, l(t + 40, e), this.mem.setUint8(t + 48, 0); | ||
| } | ||
| }, | ||
| "syscall/js.valueLength": (t) => { | ||
| t >>>= 0, o(t + 16, Number.parseInt(r(t + 8).length)); | ||
| }, | ||
| "syscall/js.valuePrepareString": (t) => { | ||
| t >>>= 0; | ||
| let e = y$1.encode(String(r(t + 8))); | ||
| l(t + 16, e), o(t + 24, e.length); | ||
| }, | ||
| "syscall/js.valueLoadString": (t) => { | ||
| t >>>= 0; | ||
| let e = r(t + 8); | ||
| c(t + 16).set(e); | ||
| }, | ||
| "syscall/js.valueInstanceOf": (t) => { | ||
| t >>>= 0, this.mem.setUint8(t + 24, r(t + 8) instanceof r(t + 16) ? 1 : 0); | ||
| }, | ||
| "syscall/js.copyBytesToGo": (t) => { | ||
| t >>>= 0; | ||
| let e = c(t + 8), s = r(t + 32); | ||
| if (!(s instanceof Uint8Array || s instanceof Uint8ClampedArray)) { | ||
| this.mem.setUint8(t + 48, 0); | ||
| return; | ||
| } | ||
| let i = s.subarray(0, e.length); | ||
| e.set(i), o(t + 40, i.length), this.mem.setUint8(t + 48, 1); | ||
| }, | ||
| "syscall/js.copyBytesToJS": (t) => { | ||
| t >>>= 0; | ||
| let e = r(t + 8), s = c(t + 16); | ||
| if (!(e instanceof Uint8Array || e instanceof Uint8ClampedArray)) { | ||
| this.mem.setUint8(t + 48, 0); | ||
| return; | ||
| } | ||
| let i = s.subarray(0, e.length); | ||
| e.set(i), o(t + 40, i.length), this.mem.setUint8(t + 48, 1); | ||
| }, | ||
| debug: (t) => { | ||
| console.log(t); | ||
| } | ||
| } }; | ||
| } | ||
| async run(o) { | ||
| if (!(o instanceof WebAssembly.Instance)) throw new Error("Go.run: WebAssembly.Instance expected"); | ||
| this._inst = o, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [ | ||
| NaN, | ||
| 0, | ||
| null, | ||
| !0, | ||
| !1, | ||
| globalThis, | ||
| this | ||
| ], this._goRefCounts = new Array(this._values.length).fill(Number.POSITIVE_INFINITY), this._ids = new Map([ | ||
| [0, 1], | ||
| [null, 2], | ||
| [!0, 3], | ||
| [!1, 4], | ||
| [globalThis, 5], | ||
| [this, 6] | ||
| ]), this._idPool = [], this.exited = !1; | ||
| let n = 4096, r = (h) => { | ||
| let t = n, e = y$1.encode(`${h}\0`); | ||
| return new Uint8Array(this.mem.buffer, n, e.length).set(e), n += e.length, n % 8 !== 0 && (n += 8 - n % 8), t; | ||
| }, l = this.argv.length, c = []; | ||
| this.argv.forEach((h) => { | ||
| c.push(r(h)); | ||
| }), c.push(0), Object.keys(this.env).sort().forEach((h) => { | ||
| c.push(r(`${h}=${this.env[h]}`)); | ||
| }), c.push(0); | ||
| let u = n; | ||
| c.forEach((h) => { | ||
| this.mem.setUint32(n, h, !0), this.mem.setUint32(n + 4, 0, !0), n += 8; | ||
| }), this._inst.exports.run(l, u), this.exited && this._resolveExitPromise(), await this._exitPromise; | ||
| } | ||
| _resume() { | ||
| if (this.exited) throw new Error("Go program has already exited"); | ||
| this._inst.exports.resume(), this.exited && this._resolveExitPromise(); | ||
| } | ||
| _makeFuncWrapper(o) { | ||
| let n = this; | ||
| return function() { | ||
| let r = { | ||
| id: o, | ||
| this: this, | ||
| args: arguments | ||
| }; | ||
| return n._pendingEvent = r, n._resume(), r.result; | ||
| }; | ||
| } | ||
| }; | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/node/index.js | ||
| var w = async (t, s) => i().then((r) => r.transform(t, s)), l = async (t, s) => i().then((r) => r.parse(t, s)), b = async (t, s) => i().then((r) => r.convertToTSX(t, s)), P = async (t) => { | ||
| let { default: s } = await import(`data:text/javascript;charset=utf-8;base64,${Buffer.from(t).toString("base64")}`); | ||
| return s; | ||
| }, n, g = () => { | ||
| n = void 0, globalThis["@astrojs/compiler"] = void 0; | ||
| }, i = () => (n || (n = d().catch((t) => { | ||
| throw n = void 0, t; | ||
| })), n), y = async (t, s) => { | ||
| let r; | ||
| return r = await (async () => { | ||
| let o = await promises.readFile(t).then((e) => e.buffer); | ||
| return WebAssembly.instantiate(new Uint8Array(o), s); | ||
| })(), r; | ||
| }, d = async () => { | ||
| let t = new d$1(), s = await y(fileURLToPath(new URL("../astro.wasm", import.meta.url)), t.importObject); | ||
| t.run(s.instance); | ||
| let r = globalThis["@astrojs/compiler"]; | ||
| return { | ||
| transform: (a, o) => new Promise((e) => { | ||
| try { | ||
| e(r.transform(a, o || {})); | ||
| } catch (p) { | ||
| throw n = void 0, p; | ||
| } | ||
| }), | ||
| parse: (a, o) => new Promise((e) => e(r.parse(a, o || {}))).catch((e) => { | ||
| throw n = void 0, e; | ||
| }).then((e) => ({ | ||
| ...e, | ||
| ast: JSON.parse(e.ast) | ||
| })), | ||
| convertToTSX: (a, o) => new Promise((e) => e(r.convertToTSX(a, o || {}))).catch((e) => { | ||
| throw n = void 0, e; | ||
| }).then((e) => ({ | ||
| ...e, | ||
| map: JSON.parse(e.map) | ||
| })) | ||
| }; | ||
| }; | ||
| //#endregion | ||
| export { P as compile, b as convertToTSX, l as parse, g as teardown, w as transform }; | ||
| //# sourceMappingURL=node-wC-z3NoZ.mjs.map |
| {"version":3,"file":"node-wC-z3NoZ.mjs","names":["g","y","v","w","b","d","m","c","f"],"sources":["../../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/chunk-W5DTLHV4.js","../../../node_modules/.pnpm/@astrojs+compiler@2.13.1/node_modules/@astrojs/compiler/dist/node/index.js"],"sourcesContent":["import g from\"crypto\";import _ from\"fs\";import{TextDecoder as b,TextEncoder as v}from\"util\";globalThis.fs||Object.defineProperty(globalThis,\"fs\",{value:_});globalThis.process||Object.defineProperties(globalThis,\"process\",{value:process});globalThis.crypto||Object.defineProperty(globalThis,\"crypto\",{value:g.webcrypto?g.webcrypto:{getRandomValues(m){return g.randomFillSync(m)}}});globalThis.performance||Object.defineProperty(globalThis,\"performance\",{value:{now(){let[m,o]=process.hrtime();return m*1e3+o/1e6}}});var y=new v(\"utf-8\"),w=new b(\"utf-8\");var d=class{constructor(){this.argv=[\"js\"],this.env={},this.exit=t=>{t!==0&&console.warn(\"exit code:\",t)},this._exitPromise=new Promise(t=>{this._resolveExitPromise=t}),this._pendingEvent=null,this._scheduledTimeouts=new Map,this._nextCallbackTimeoutID=1;let o=(t,e)=>{this.mem.setUint32(t+0,e,!0),this.mem.setUint32(t+4,Math.floor(e/4294967296),!0)},n=t=>{let e=this.mem.getUint32(t+0,!0),s=this.mem.getInt32(t+4,!0);return e+s*4294967296},r=t=>{let e=this.mem.getFloat64(t,!0);if(e===0)return;if(!isNaN(e))return e;let s=this.mem.getUint32(t,!0);return this._values[s]},l=(t,e)=>{if(typeof e==\"number\"&&e!==0){if(isNaN(e)){this.mem.setUint32(t+4,2146959360,!0),this.mem.setUint32(t,0,!0);return}this.mem.setFloat64(t,e,!0);return}if(e===void 0){this.mem.setFloat64(t,0,!0);return}let i=this._ids.get(e);i===void 0&&(i=this._idPool.pop(),i===void 0&&(i=this._values.length),this._values[i]=e,this._goRefCounts[i]=0,this._ids.set(e,i)),this._goRefCounts[i]++;let a=0;switch(typeof e){case\"object\":e!==null&&(a=1);break;case\"string\":a=2;break;case\"symbol\":a=3;break;case\"function\":a=4;break}this.mem.setUint32(t+4,2146959360|a,!0),this.mem.setUint32(t,i,!0)},c=t=>{let e=n(t+0),s=n(t+8);return new Uint8Array(this._inst.exports.mem.buffer,e,s)},f=t=>{let e=n(t+0),s=n(t+8),i=new Array(s);for(let a=0;a<s;a++)i[a]=r(e+a*8);return i},u=t=>{let e=n(t+0),s=n(t+8);return w.decode(new DataView(this._inst.exports.mem.buffer,e,s))},h=Date.now()-performance.now();this.importObject={gojs:{\"runtime.wasmExit\":t=>{t>>>=0;let e=this.mem.getInt32(t+8,!0);this.exited=!0,delete this._inst,delete this._values,delete this._goRefCounts,delete this._ids,delete this._idPool,this.exit(e)},\"runtime.wasmWrite\":t=>{t>>>=0;let e=n(t+8),s=n(t+16),i=this.mem.getInt32(t+24,!0);_.writeSync(e,new Uint8Array(this._inst.exports.mem.buffer,s,i))},\"runtime.resetMemoryDataView\":t=>{t>>>=0,this.mem=new DataView(this._inst.exports.mem.buffer)},\"runtime.nanotime1\":t=>{t>>>=0,o(t+8,(h+performance.now())*1e6)},\"runtime.walltime\":t=>{t>>>=0;let e=new Date().getTime();o(t+8,e/1e3),this.mem.setInt32(t+16,e%1e3*1e6,!0)},\"runtime.scheduleTimeoutEvent\":t=>{t>>>=0;let e=this._nextCallbackTimeoutID;this._nextCallbackTimeoutID++,this._scheduledTimeouts.set(e,setTimeout(()=>{for(this._resume();this._scheduledTimeouts.has(e);)console.warn(\"scheduleTimeoutEvent: missed timeout event\"),this._resume()},n(t+8)+1)),this.mem.setInt32(t+16,e,!0)},\"runtime.clearTimeoutEvent\":t=>{t>>>=0;let e=this.mem.getInt32(t+8,!0);clearTimeout(this._scheduledTimeouts.get(e)),this._scheduledTimeouts.delete(e)},\"runtime.getRandomData\":t=>{t>>>=0,globalThis.crypto.getRandomValues(c(t+8))},\"syscall/js.finalizeRef\":t=>{t>>>=0;let e=this.mem.getUint32(t+8,!0);if(this._goRefCounts[e]--,this._goRefCounts[e]===0){let s=this._values[e];this._values[e]=null,this._ids.delete(s),this._idPool.push(e)}},\"syscall/js.stringVal\":t=>{t>>>=0,l(t+24,u(t+8))},\"syscall/js.valueGet\":t=>{t>>>=0;let e=Reflect.get(r(t+8),u(t+16));t=this._inst.exports.getsp()>>>0,l(t+32,e)},\"syscall/js.valueSet\":t=>{t>>>=0,Reflect.set(r(t+8),u(t+16),r(t+32))},\"syscall/js.valueDelete\":t=>{t>>>=0,Reflect.deleteProperty(r(t+8),u(t+16))},\"syscall/js.valueIndex\":t=>{t>>>=0,l(t+24,Reflect.get(r(t+8),n(t+16)))},\"syscall/js.valueSetIndex\":t=>{t>>>=0,Reflect.set(r(t+8),n(t+16),r(t+24))},\"syscall/js.valueCall\":t=>{t>>>=0;try{let e=r(t+8),s=Reflect.get(e,u(t+16)),i=f(t+32),a=Reflect.apply(s,e,i);t=this._inst.exports.getsp()>>>0,l(t+56,a),this.mem.setUint8(t+64,1)}catch(e){t=this._inst.exports.getsp()>>>0,l(t+56,e),this.mem.setUint8(t+64,0)}},\"syscall/js.valueInvoke\":t=>{t>>>=0;try{let e=r(t+8),s=f(t+16),i=Reflect.apply(e,void 0,s);t=this._inst.exports.getsp()>>>0,l(t+40,i),this.mem.setUint8(t+48,1)}catch(e){t=this._inst.exports.getsp()>>>0,l(t+40,e),this.mem.setUint8(t+48,0)}},\"syscall/js.valueNew\":t=>{t>>>=0;try{let e=r(t+8),s=f(t+16),i=Reflect.construct(e,s);t=this._inst.exports.getsp()>>>0,l(t+40,i),this.mem.setUint8(t+48,1)}catch(e){t=this._inst.exports.getsp()>>>0,l(t+40,e),this.mem.setUint8(t+48,0)}},\"syscall/js.valueLength\":t=>{t>>>=0,o(t+16,Number.parseInt(r(t+8).length))},\"syscall/js.valuePrepareString\":t=>{t>>>=0;let e=y.encode(String(r(t+8)));l(t+16,e),o(t+24,e.length)},\"syscall/js.valueLoadString\":t=>{t>>>=0;let e=r(t+8);c(t+16).set(e)},\"syscall/js.valueInstanceOf\":t=>{t>>>=0,this.mem.setUint8(t+24,r(t+8)instanceof r(t+16)?1:0)},\"syscall/js.copyBytesToGo\":t=>{t>>>=0;let e=c(t+8),s=r(t+32);if(!(s instanceof Uint8Array||s instanceof Uint8ClampedArray)){this.mem.setUint8(t+48,0);return}let i=s.subarray(0,e.length);e.set(i),o(t+40,i.length),this.mem.setUint8(t+48,1)},\"syscall/js.copyBytesToJS\":t=>{t>>>=0;let e=r(t+8),s=c(t+16);if(!(e instanceof Uint8Array||e instanceof Uint8ClampedArray)){this.mem.setUint8(t+48,0);return}let i=s.subarray(0,e.length);e.set(i),o(t+40,i.length),this.mem.setUint8(t+48,1)},debug:t=>{console.log(t)}}}}async run(o){if(!(o instanceof WebAssembly.Instance))throw new Error(\"Go.run: WebAssembly.Instance expected\");this._inst=o,this.mem=new DataView(this._inst.exports.mem.buffer),this._values=[Number.NaN,0,null,!0,!1,globalThis,this],this._goRefCounts=new Array(this._values.length).fill(Number.POSITIVE_INFINITY),this._ids=new Map([[0,1],[null,2],[!0,3],[!1,4],[globalThis,5],[this,6]]),this._idPool=[],this.exited=!1;let n=4096,r=h=>{let t=n,e=y.encode(`${h}\\0`);return new Uint8Array(this.mem.buffer,n,e.length).set(e),n+=e.length,n%8!==0&&(n+=8-n%8),t},l=this.argv.length,c=[];this.argv.forEach(h=>{c.push(r(h))}),c.push(0),Object.keys(this.env).sort().forEach(h=>{c.push(r(`${h}=${this.env[h]}`))}),c.push(0);let u=n;c.forEach(h=>{this.mem.setUint32(n,h,!0),this.mem.setUint32(n+4,0,!0),n+=8}),this._inst.exports.run(l,u),this.exited&&this._resolveExitPromise(),await this._exitPromise}_resume(){if(this.exited)throw new Error(\"Go program has already exited\");this._inst.exports.resume(),this.exited&&this._resolveExitPromise()}_makeFuncWrapper(o){let n=this;return function(){let r={id:o,this:this,args:arguments};return n._pendingEvent=r,n._resume(),r.result}}};export{d as a};\n","import{a as c}from\"../chunk-W5DTLHV4.js\";import{promises as m}from\"fs\";import{fileURLToPath as f}from\"url\";var w=async(t,s)=>i().then(r=>r.transform(t,s)),l=async(t,s)=>i().then(r=>r.parse(t,s)),b=async(t,s)=>i().then(r=>r.convertToTSX(t,s)),P=async t=>{let{default:s}=await import(`data:text/javascript;charset=utf-8;base64,${Buffer.from(t).toString(\"base64\")}`);return s},n,g=()=>{n=void 0,globalThis[\"@astrojs/compiler\"]=void 0},i=()=>(n||(n=d().catch(t=>{throw n=void 0,t})),n),y=async(t,s)=>{let r;return r=await(async()=>{let o=await m.readFile(t).then(e=>e.buffer);return WebAssembly.instantiate(new Uint8Array(o),s)})(),r},d=async()=>{let t=new c,s=await y(f(new URL(\"../astro.wasm\",import.meta.url)),t.importObject);t.run(s.instance);let r=globalThis[\"@astrojs/compiler\"];return{transform:(a,o)=>new Promise(e=>{try{e(r.transform(a,o||{}))}catch(p){throw n=void 0,p}}),parse:(a,o)=>new Promise(e=>e(r.parse(a,o||{}))).catch(e=>{throw n=void 0,e}).then(e=>({...e,ast:JSON.parse(e.ast)})),convertToTSX:(a,o)=>new Promise(e=>e(r.convertToTSX(a,o||{}))).catch(e=>{throw n=void 0,e}).then(e=>({...e,map:JSON.parse(e.map)}))}};export{P as compile,b as convertToTSX,l as parse,g as teardown,w as transform};\n"],"x_google_ignoreList":[0,1],"mappings":"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"} |
| //#region src/core/overlay-reader.ts | ||
| /** | ||
| * A RepoReader that overlays a set of pending FileChanges on top of an | ||
| * underlying reader. Used in the remote-provider write path so helpers | ||
| * like {@link import('./context.js').buildContextChange} and | ||
| * {@link import('./validator/project.js').validateProject} see the | ||
| * post-change state — the state the pending commit is about to produce | ||
| * — rather than the pre-change base branch. | ||
| * | ||
| * Semantics: | ||
| * | ||
| * - `readFile(path)` — returns pending `content` when the overlay maps | ||
| * the path; falls back to the base reader otherwise. A pending delete | ||
| * (`content: null`) surfaces as "missing" (throws, matching | ||
| * `RepoReader.readFile`'s missing-file contract). | ||
| * | ||
| * - `listDirectory(path)` — merges the base directory listing with | ||
| * pending additions that live directly in the same folder, removes | ||
| * entries whose pending change is a delete, and de-duplicates the | ||
| * result. Pending paths in nested subdirectories surface only at | ||
| * their own listings. | ||
| * | ||
| * - `fileExists(path)` — pending adds → `true`, pending deletes → | ||
| * `false`, otherwise delegates. | ||
| * | ||
| * The overlay keys are canonicalised to match the FileChange contract: | ||
| * forward slashes, no leading `/`, no `..` segments (FileChanges are | ||
| * required to respect these invariants). | ||
| */ | ||
| var OverlayReader = class { | ||
| overlay; | ||
| constructor(base, pendingChanges) { | ||
| this.base = base; | ||
| this.overlay = /* @__PURE__ */ new Map(); | ||
| for (const change of pendingChanges) this.overlay.set(normalise(change.path), change); | ||
| } | ||
| async readFile(path, ref) { | ||
| const key = normalise(path); | ||
| const pending = this.overlay.get(key); | ||
| if (pending) { | ||
| if (pending.content === null) throw new Error(`OverlayReader: "${path}" is marked for deletion`); | ||
| return pending.content; | ||
| } | ||
| return this.base.readFile(path, ref); | ||
| } | ||
| async listDirectory(path, ref) { | ||
| const baseEntries = await this.base.listDirectory(path, ref); | ||
| const dirKey = normalise(path); | ||
| const prefix = dirKey === "" ? "" : `${dirKey}/`; | ||
| const direct = [...this.overlay.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, change]) => ({ | ||
| name: key.slice(prefix.length).split("/")[0] ?? "", | ||
| isNested: key.slice(prefix.length).includes("/"), | ||
| deleted: change.content === null | ||
| })).filter((entry) => entry.name.length > 0); | ||
| const deleted = new Set(direct.filter((e) => e.deleted && !e.isNested).map((e) => e.name)); | ||
| const added = direct.filter((e) => !e.deleted).map((e) => e.name); | ||
| const result = baseEntries.filter((n) => !deleted.has(n)); | ||
| for (const name of added) if (!result.includes(name)) result.push(name); | ||
| return result; | ||
| } | ||
| async fileExists(path, ref) { | ||
| const key = normalise(path); | ||
| const pending = this.overlay.get(key); | ||
| if (pending) return pending.content !== null; | ||
| return this.base.fileExists(path, ref); | ||
| } | ||
| }; | ||
| function normalise(path) { | ||
| return path.replace(/^\/+/, ""); | ||
| } | ||
| //#endregion | ||
| export { OverlayReader as t }; | ||
| //# sourceMappingURL=overlay-reader-BOb105gS.mjs.map |
| {"version":3,"file":"overlay-reader-BOb105gS.mjs","names":[],"sources":["../src/core/overlay-reader.ts"],"sourcesContent":["import type { FileChange, RepoReader } from './contracts/index.js'\n\n/**\n * A RepoReader that overlays a set of pending FileChanges on top of an\n * underlying reader. Used in the remote-provider write path so helpers\n * like {@link import('./context.js').buildContextChange} and\n * {@link import('./validator/project.js').validateProject} see the\n * post-change state — the state the pending commit is about to produce\n * — rather than the pre-change base branch.\n *\n * Semantics:\n *\n * - `readFile(path)` — returns pending `content` when the overlay maps\n * the path; falls back to the base reader otherwise. A pending delete\n * (`content: null`) surfaces as \"missing\" (throws, matching\n * `RepoReader.readFile`'s missing-file contract).\n *\n * - `listDirectory(path)` — merges the base directory listing with\n * pending additions that live directly in the same folder, removes\n * entries whose pending change is a delete, and de-duplicates the\n * result. Pending paths in nested subdirectories surface only at\n * their own listings.\n *\n * - `fileExists(path)` — pending adds → `true`, pending deletes →\n * `false`, otherwise delegates.\n *\n * The overlay keys are canonicalised to match the FileChange contract:\n * forward slashes, no leading `/`, no `..` segments (FileChanges are\n * required to respect these invariants).\n */\nexport class OverlayReader implements RepoReader {\n private readonly overlay: Map<string, FileChange>\n\n constructor(\n private readonly base: RepoReader,\n pendingChanges: FileChange[],\n ) {\n this.overlay = new Map()\n for (const change of pendingChanges) {\n this.overlay.set(normalise(change.path), change)\n }\n }\n\n async readFile(path: string, ref?: string): Promise<string> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) {\n if (pending.content === null) {\n throw new Error(`OverlayReader: \"${path}\" is marked for deletion`)\n }\n return pending.content\n }\n return this.base.readFile(path, ref)\n }\n\n async listDirectory(path: string, ref?: string): Promise<string[]> {\n const baseEntries = await this.base.listDirectory(path, ref)\n const dirKey = normalise(path)\n const prefix = dirKey === '' ? '' : `${dirKey}/`\n\n const direct = [...this.overlay.entries()]\n .filter(([key]) => key.startsWith(prefix))\n .map(([key, change]) => ({\n name: key.slice(prefix.length).split('/')[0] ?? '',\n isNested: key.slice(prefix.length).includes('/'),\n deleted: change.content === null,\n }))\n .filter(entry => entry.name.length > 0)\n\n // For nested pending paths (e.g. overlay at `dir/sub/a.json` when\n // listing `dir`), the immediate child directory `sub` must surface\n // even though no pending change targets it directly.\n const deleted = new Set(\n direct.filter(e => e.deleted && !e.isNested).map(e => e.name),\n )\n const added = direct\n .filter(e => !e.deleted)\n .map(e => e.name)\n\n const result = baseEntries.filter(n => !deleted.has(n))\n for (const name of added) {\n if (!result.includes(name)) result.push(name)\n }\n return result\n }\n\n async fileExists(path: string, ref?: string): Promise<boolean> {\n const key = normalise(path)\n const pending = this.overlay.get(key)\n if (pending) return pending.content !== null\n return this.base.fileExists(path, ref)\n }\n}\n\nfunction normalise(path: string): string {\n return path.replace(/^\\/+/, '')\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAAiD;CAC/C;CAEA,YACE,MACA,gBACA;AAFiB,OAAA,OAAA;AAGjB,OAAK,0BAAU,IAAI,KAAK;AACxB,OAAK,MAAM,UAAU,eACnB,MAAK,QAAQ,IAAI,UAAU,OAAO,KAAK,EAAE,OAAO;;CAIpD,MAAM,SAAS,MAAc,KAA+B;EAC1D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,SAAS;AACX,OAAI,QAAQ,YAAY,KACtB,OAAM,IAAI,MAAM,mBAAmB,KAAK,0BAA0B;AAEpE,UAAO,QAAQ;;AAEjB,SAAO,KAAK,KAAK,SAAS,MAAM,IAAI;;CAGtC,MAAM,cAAc,MAAc,KAAiC;EACjE,MAAM,cAAc,MAAM,KAAK,KAAK,cAAc,MAAM,IAAI;EAC5D,MAAM,SAAS,UAAU,KAAK;EAC9B,MAAM,SAAS,WAAW,KAAK,KAAK,GAAG,OAAO;EAE9C,MAAM,SAAS,CAAC,GAAG,KAAK,QAAQ,SAAS,CAAC,CACvC,QAAQ,CAAC,SAAS,IAAI,WAAW,OAAO,CAAC,CACzC,KAAK,CAAC,KAAK,aAAa;GACvB,MAAM,IAAI,MAAM,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,MAAM;GAChD,UAAU,IAAI,MAAM,OAAO,OAAO,CAAC,SAAS,IAAI;GAChD,SAAS,OAAO,YAAY;GAC7B,EAAE,CACF,QAAO,UAAS,MAAM,KAAK,SAAS,EAAE;EAKzC,MAAM,UAAU,IAAI,IAClB,OAAO,QAAO,MAAK,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,KAAI,MAAK,EAAE,KAAK,CAC9D;EACD,MAAM,QAAQ,OACX,QAAO,MAAK,CAAC,EAAE,QAAQ,CACvB,KAAI,MAAK,EAAE,KAAK;EAEnB,MAAM,SAAS,YAAY,QAAO,MAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;AACvD,OAAK,MAAM,QAAQ,MACjB,KAAI,CAAC,OAAO,SAAS,KAAK,CAAE,QAAO,KAAK,KAAK;AAE/C,SAAO;;CAGT,MAAM,WAAW,MAAc,KAAgC;EAC7D,MAAM,MAAM,UAAU,KAAK;EAC3B,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI;AACrC,MAAI,QAAS,QAAO,QAAQ,YAAY;AACxC,SAAO,KAAK,KAAK,WAAW,MAAM,IAAI;;;AAI1C,SAAS,UAAU,MAAsB;AACvC,QAAO,KAAK,QAAQ,QAAQ,GAAG"} |
| //#region src/providers/shared/errors.ts | ||
| /** | ||
| * Unified "is this a 404?" helper for API-backed providers. | ||
| * | ||
| * Two common error shapes in the provider SDKs we use: | ||
| * | ||
| * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a | ||
| * top-level `.status` number set to the HTTP status code. | ||
| * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose | ||
| * `.cause` includes `{ response: { status } }`. | ||
| * | ||
| * Both forms converge on `404` meaning "resource missing", so we check | ||
| * both shapes strictly. We deliberately do NOT fall back to substring | ||
| * matching on the error message — that leniency can silently mask other | ||
| * 404-like errors (forbidden repo, deleted project, rate limits) and | ||
| * produce the wrong answer. If either SDK ever stops populating the | ||
| * status field on a legitimate 404, the regression will surface in tests | ||
| * rather than being papered over at the reader layer. | ||
| */ | ||
| function isNotFoundError(error) { | ||
| if (typeof error !== "object" || error === null) return false; | ||
| if (error.status === 404) return true; | ||
| return error.cause?.response?.status === 404; | ||
| } | ||
| //#endregion | ||
| //#region src/providers/shared/paths.ts | ||
| /** | ||
| * Normalise an optional contentRoot — strip leading/trailing slashes, | ||
| * treat `''`, `/` and `undefined` as "no prefix". Used by API-backed | ||
| * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative | ||
| * paths against a repo subdirectory when Contentrain lives under a | ||
| * monorepo path like `apps/web/.contentrain/`. | ||
| */ | ||
| function normaliseContentRoot(raw) { | ||
| if (!raw || raw === "/" || raw === "") return ""; | ||
| return raw.replace(/^\/+|\/+$/g, ""); | ||
| } | ||
| /** | ||
| * Resolve a content-root-relative path to a repo-relative path. The result | ||
| * always uses forward slashes and has no leading slash — the form every | ||
| * REST git API consumes for `file_path` / `path` query parameters and the | ||
| * Git Data API tree entries. | ||
| */ | ||
| function resolveRepoPath(contentRoot, relativePath) { | ||
| const prefix = normaliseContentRoot(contentRoot); | ||
| const cleanPath = relativePath.replace(/^\/+/, ""); | ||
| return prefix ? `${prefix}/${cleanPath}` : cleanPath; | ||
| } | ||
| //#endregion | ||
| export { isNotFoundError as n, resolveRepoPath as t }; | ||
| //# sourceMappingURL=paths-BU6E-oDs.mjs.map |
| {"version":3,"file":"paths-BU6E-oDs.mjs","names":[],"sources":["../src/providers/shared/errors.ts","../src/providers/shared/paths.ts"],"sourcesContent":["/**\n * Unified \"is this a 404?\" helper for API-backed providers.\n *\n * Two common error shapes in the provider SDKs we use:\n *\n * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a\n * top-level `.status` number set to the HTTP status code.\n * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose\n * `.cause` includes `{ response: { status } }`.\n *\n * Both forms converge on `404` meaning \"resource missing\", so we check\n * both shapes strictly. We deliberately do NOT fall back to substring\n * matching on the error message — that leniency can silently mask other\n * 404-like errors (forbidden repo, deleted project, rate limits) and\n * produce the wrong answer. If either SDK ever stops populating the\n * status field on a legitimate 404, the regression will surface in tests\n * rather than being papered over at the reader layer.\n */\nexport function isNotFoundError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false\n const direct = (error as { status?: number }).status\n if (direct === 404) return true\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return nested === 404\n}\n","/**\n * Normalise an optional contentRoot — strip leading/trailing slashes,\n * treat `''`, `/` and `undefined` as \"no prefix\". Used by API-backed\n * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative\n * paths against a repo subdirectory when Contentrain lives under a\n * monorepo path like `apps/web/.contentrain/`.\n */\nexport function normaliseContentRoot(raw?: string): string {\n if (!raw || raw === '/' || raw === '') return ''\n return raw.replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Resolve a content-root-relative path to a repo-relative path. The result\n * always uses forward slashes and has no leading slash — the form every\n * REST git API consumes for `file_path` / `path` query parameters and the\n * Git Data API tree entries.\n */\nexport function resolveRepoPath(contentRoot: string | undefined, relativePath: string): string {\n const prefix = normaliseContentRoot(contentRoot)\n const cleanPath = relativePath.replace(/^\\/+/, '')\n return prefix ? `${prefix}/${cleanPath}` : cleanPath\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,gBAAgB,OAAyB;AACvD,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AAExD,KADgB,MAA8B,WAC/B,IAAK,QAAO;AAE3B,QADgB,MAAyD,OAAO,UAAU,WACxE;;;;;;;;;;;AChBpB,SAAgB,qBAAqB,KAAsB;AACzD,KAAI,CAAC,OAAO,QAAQ,OAAO,QAAQ,GAAI,QAAO;AAC9C,QAAO,IAAI,QAAQ,cAAc,GAAG;;;;;;;;AAStC,SAAgB,gBAAgB,aAAiC,cAA8B;CAC7F,MAAM,SAAS,qBAAqB,YAAY;CAChD,MAAM,YAAY,aAAa,QAAQ,QAAQ,GAAG;AAClD,QAAO,SAAS,GAAG,OAAO,GAAG,cAAc"} |
| import { r as pathExists } from "./fs-DLbVB-Ek.mjs"; | ||
| import { basename, extname, join } from "node:path"; | ||
| import { readdir } from "node:fs/promises"; | ||
| //#region src/core/scan-config.ts | ||
| /** File extensions to scan across all JS/TS ecosystem projects */ | ||
| const SCAN_EXTENSIONS = new Set([ | ||
| ".tsx", | ||
| ".jsx", | ||
| ".vue", | ||
| ".ts", | ||
| ".js", | ||
| ".mjs", | ||
| ".astro", | ||
| ".svelte" | ||
| ]); | ||
| /** Directory names to always exclude from scanning */ | ||
| const SCAN_IGNORE_DIRS = new Set([ | ||
| "node_modules", | ||
| ".pnpm", | ||
| "dist", | ||
| "build", | ||
| "out", | ||
| ".output", | ||
| ".nuxt", | ||
| ".next", | ||
| ".svelte-kit", | ||
| ".expo", | ||
| ".turbo", | ||
| ".parcel-cache", | ||
| ".vercel", | ||
| ".netlify", | ||
| "coverage", | ||
| "__tests__", | ||
| "__mocks__", | ||
| ".git", | ||
| ".vscode", | ||
| ".idea", | ||
| ".contentrain" | ||
| ]); | ||
| /** Max files per scan operation */ | ||
| const MAX_SCAN_FILES = 500; | ||
| /** File patterns to skip regardless of extension */ | ||
| const SKIP_FILE_RE = /\.(test|spec)\.[^.]+$|\.d\.ts$|\.min\.[^.]+$/; | ||
| /** Directories that represent entry points / pages / screens */ | ||
| const PAGE_DIR_NAMES = new Set([ | ||
| "pages", | ||
| "routes", | ||
| "screens", | ||
| "views", | ||
| "controllers", | ||
| "handlers", | ||
| "resolvers" | ||
| ]); | ||
| /** Directories that represent reusable components / modules */ | ||
| const COMPONENT_DIR_NAMES = new Set([ | ||
| "components", | ||
| "ui", | ||
| "widgets", | ||
| "elements", | ||
| "features", | ||
| "modules", | ||
| "services", | ||
| "providers", | ||
| "shared", | ||
| "common" | ||
| ]); | ||
| /** Directories that represent layouts / templates */ | ||
| const LAYOUT_DIR_NAMES = new Set(["layouts", "templates"]); | ||
| /** Next.js App Router special files that are page-like */ | ||
| const NEXTJS_PAGE_FILES = new Set([ | ||
| "page.tsx", | ||
| "page.jsx", | ||
| "page.ts", | ||
| "page.js", | ||
| "layout.tsx", | ||
| "layout.jsx", | ||
| "layout.ts", | ||
| "layout.js", | ||
| "error.tsx", | ||
| "error.jsx", | ||
| "loading.tsx", | ||
| "loading.jsx", | ||
| "not-found.tsx", | ||
| "not-found.jsx" | ||
| ]); | ||
| /** Classify a file into page/component/layout/other based on path heuristics */ | ||
| function classifyFile(relPath) { | ||
| const parts = relPath.split("/"); | ||
| const fileName = parts[parts.length - 1] ?? ""; | ||
| if (parts.includes("app") && NEXTJS_PAGE_FILES.has(fileName)) return fileName.startsWith("layout") ? "layout" : "page"; | ||
| for (const part of parts) { | ||
| const lower = part.toLowerCase(); | ||
| if (LAYOUT_DIR_NAMES.has(lower)) return "layout"; | ||
| if (PAGE_DIR_NAMES.has(lower)) return "page"; | ||
| if (COMPONENT_DIR_NAMES.has(lower)) return "component"; | ||
| } | ||
| return "other"; | ||
| } | ||
| /** Common source directories across all JS/TS project types */ | ||
| const AUTO_DETECT_DIRS = [ | ||
| "src", | ||
| "app", | ||
| "lib", | ||
| "pages", | ||
| "components", | ||
| "layouts", | ||
| "views", | ||
| "screens", | ||
| "modules", | ||
| "routes", | ||
| "controllers", | ||
| "services", | ||
| "features", | ||
| "shared", | ||
| "common", | ||
| "hooks", | ||
| "composables", | ||
| "stores" | ||
| ]; | ||
| /** Auto-detect which source directories exist in the project */ | ||
| async function autoDetectSourceDirs(projectRoot) { | ||
| const found = []; | ||
| for (const dir of AUTO_DETECT_DIRS) if (await pathExists(join(projectRoot, dir))) found.push(dir); | ||
| return found.length > 0 ? found : ["."]; | ||
| } | ||
| /** | ||
| * Discover source files matching scan criteria. | ||
| * Returns relative paths (relative to projectRoot). | ||
| */ | ||
| async function discoverFiles(projectRoot, options) { | ||
| const extensions = options?.include ? new Set(options.include.map((e) => e.startsWith(".") ? e : `.${e}`)) : SCAN_EXTENSIONS; | ||
| const extraExcludes = new Set(options?.exclude ?? []); | ||
| const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot); | ||
| const files = []; | ||
| for (const dir of scanDirs) { | ||
| const absDir = join(projectRoot, dir); | ||
| if (!await pathExists(absDir)) continue; | ||
| let entries; | ||
| try { | ||
| entries = await readdir(absDir, { recursive: true }); | ||
| } catch { | ||
| continue; | ||
| } | ||
| for (const entry of entries) { | ||
| if (files.length >= 500) break; | ||
| const fileName = basename(entry); | ||
| if (entry.split("/").some((seg) => SCAN_IGNORE_DIRS.has(seg) || extraExcludes.has(seg))) continue; | ||
| if (!extensions.has(extname(fileName))) continue; | ||
| if (SKIP_FILE_RE.test(fileName)) continue; | ||
| files.push(join(dir, entry)); | ||
| } | ||
| if (files.length >= 500) break; | ||
| } | ||
| return files.toSorted((a, b) => a.localeCompare(b)).slice(0, 500); | ||
| } | ||
| //#endregion | ||
| export { classifyFile as a, autoDetectSourceDirs as i, SCAN_EXTENSIONS as n, discoverFiles as o, SCAN_IGNORE_DIRS as r, MAX_SCAN_FILES as t }; | ||
| //# sourceMappingURL=scan-config-BGUflS8t.mjs.map |
| {"version":3,"file":"scan-config-BGUflS8t.mjs","names":[],"sources":["../src/core/scan-config.ts"],"sourcesContent":["import { readdir } from 'node:fs/promises'\nimport { join, extname, basename } from 'node:path'\nimport { pathExists } from '../util/fs.js'\n\n// ─── Shared Scan Constants ───\n\n/** File extensions to scan across all JS/TS ecosystem projects */\nexport const SCAN_EXTENSIONS = new Set([\n '.tsx', '.jsx', '.vue', '.ts', '.js', '.mjs', '.astro', '.svelte',\n])\n\n/** Directory names to always exclude from scanning */\nexport const SCAN_IGNORE_DIRS = new Set([\n // Package managers / deps\n 'node_modules', '.pnpm',\n // Build outputs\n 'dist', 'build', 'out', '.output',\n // Framework caches\n '.nuxt', '.next', '.svelte-kit', '.expo', '.turbo', '.parcel-cache', '.vercel', '.netlify',\n // Test / coverage\n 'coverage', '__tests__', '__mocks__',\n // VCS / IDE\n '.git', '.vscode', '.idea',\n // Contentrain\n '.contentrain',\n])\n\n/** Max files per scan operation */\nexport const MAX_SCAN_FILES = 500\n\n/** File patterns to skip regardless of extension */\nconst SKIP_FILE_RE = /\\.(test|spec)\\.[^.]+$|\\.d\\.ts$|\\.min\\.[^.]+$/\n\n// ─── File Classification ───\n// Covers: React, Next.js, Nuxt, Vue, Astro, SvelteKit, Remix,\n// React Native/Expo, NestJS, Express, Fastify, Koa, Hapi\n\n/** Directories that represent entry points / pages / screens */\nconst PAGE_DIR_NAMES = new Set([\n // Frontend routing\n 'pages', 'routes', 'screens', 'views',\n // Backend entry points\n 'controllers', 'handlers', 'resolvers',\n])\n\n/** Directories that represent reusable components / modules */\nconst COMPONENT_DIR_NAMES = new Set([\n // UI components\n 'components', 'ui', 'widgets', 'elements',\n // Feature modules\n 'features', 'modules',\n // Backend services\n 'services', 'providers',\n // Shared / common\n 'shared', 'common',\n])\n\n/** Directories that represent layouts / templates */\nconst LAYOUT_DIR_NAMES = new Set([\n 'layouts', 'templates',\n])\n\n/** Next.js App Router special files that are page-like */\nconst NEXTJS_PAGE_FILES = new Set([\n 'page.tsx', 'page.jsx', 'page.ts', 'page.js',\n 'layout.tsx', 'layout.jsx', 'layout.ts', 'layout.js',\n 'error.tsx', 'error.jsx',\n 'loading.tsx', 'loading.jsx',\n 'not-found.tsx', 'not-found.jsx',\n])\n\n/** Classify a file into page/component/layout/other based on path heuristics */\nexport function classifyFile(relPath: string): 'page' | 'component' | 'layout' | 'other' {\n const parts = relPath.split('/')\n const fileName = parts[parts.length - 1] ?? ''\n\n // Next.js App Router: files in app/ with special names are pages\n if (parts.includes('app') && NEXTJS_PAGE_FILES.has(fileName)) {\n return fileName.startsWith('layout') ? 'layout' : 'page'\n }\n\n // Check directory names in path (check layout first — more specific)\n for (const part of parts) {\n const lower = part.toLowerCase()\n if (LAYOUT_DIR_NAMES.has(lower)) return 'layout'\n if (PAGE_DIR_NAMES.has(lower)) return 'page'\n if (COMPONENT_DIR_NAMES.has(lower)) return 'component'\n }\n\n return 'other'\n}\n\n// ─── Source Directory Detection ───\n\n/** Common source directories across all JS/TS project types */\nconst AUTO_DETECT_DIRS = [\n // Standard source\n 'src', 'app', 'lib',\n // Frontend specific\n 'pages', 'components', 'layouts', 'views',\n // Mobile\n 'screens',\n // Backend\n 'modules', 'routes', 'controllers', 'services',\n // Shared\n 'features', 'shared', 'common',\n // Hooks / composables\n 'hooks', 'composables',\n // Stores\n 'stores',\n]\n\n/** Auto-detect which source directories exist in the project */\nexport async function autoDetectSourceDirs(projectRoot: string): Promise<string[]> {\n const found: string[] = []\n for (const dir of AUTO_DETECT_DIRS) {\n if (await pathExists(join(projectRoot, dir))) {\n found.push(dir)\n }\n }\n return found.length > 0 ? found : ['.']\n}\n\n// ─── File Discovery ───\n\nexport interface DiscoverFilesOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n}\n\n/**\n * Discover source files matching scan criteria.\n * Returns relative paths (relative to projectRoot).\n */\nexport async function discoverFiles(\n projectRoot: string,\n options?: DiscoverFilesOptions,\n): Promise<string[]> {\n const extensions = options?.include\n ? new Set(options.include.map(e => e.startsWith('.') ? e : `.${e}`))\n : SCAN_EXTENSIONS\n const extraExcludes = new Set(options?.exclude ?? [])\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n\n const files: string[] = []\n\n for (const dir of scanDirs) {\n const absDir = join(projectRoot, dir)\n if (!(await pathExists(absDir))) continue\n\n let entries: string[]\n try {\n entries = await readdir(absDir, { recursive: true }) as unknown as string[]\n } catch {\n continue\n }\n\n for (const entry of entries) {\n if (files.length >= MAX_SCAN_FILES) break\n\n const fileName = basename(entry)\n\n // Check if any path segment is excluded\n const pathSegments = entry.split('/')\n if (pathSegments.some(seg => SCAN_IGNORE_DIRS.has(seg) || extraExcludes.has(seg))) continue\n\n // Check extension\n if (!extensions.has(extname(fileName))) continue\n\n // Skip test/spec/declaration/minified files\n if (SKIP_FILE_RE.test(fileName)) continue\n\n files.push(join(dir, entry))\n }\n\n if (files.length >= MAX_SCAN_FILES) break\n }\n\n return files.toSorted((a, b) => a.localeCompare(b)).slice(0, MAX_SCAN_FILES)\n}\n"],"mappings":"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"} |
| import { o as readText } from "./fs-DLbVB-Ek.mjs"; | ||
| import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-BGUflS8t.mjs"; | ||
| import { t as parseTsx } from "./tsx-parser-B_aI_C2r.mjs"; | ||
| import { extname, join } from "node:path"; | ||
| //#region src/core/ast-scanner/pre-filter.ts | ||
| const PURE_NUMBER_RE = /^-?\d+(\.\d+)?$/; | ||
| const HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i; | ||
| const FILE_EXT_RE = /\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i; | ||
| const SVG_PATH_DATA_RE = /^[Mm][\d\s.,LHVCSQTAZlhvcsqtazmMzZ-]+$/; | ||
| const SVG_VIEWBOX_RE = /^\d+(\.\d+)?\s+\d+(\.\d+)?\s+\d+(\.\d+)?\s+\d+(\.\d+)?$/; | ||
| const I18N_KEY_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/; | ||
| const TECHNICAL_IDENTIFIER_RE = /^[_a-z][a-z0-9_-]*$/; | ||
| const ERROR_CODE_RE = /^[A-Z][A-Z0-9_]+$/; | ||
| const PLACEHOLDER_RE = /^\{\d+\}$|^\.{2,}$/; | ||
| const CAMEL_CASE_RE = /^[a-z]+[A-Z]/; | ||
| const LOCALE_CODE_RE = /^[a-z]{2}[-_][A-Z]{2}$/; | ||
| const DIMENSION_RE = /^\d+[x×]\d+$/; | ||
| const REPEAT_CHAR_RE = /^(.)\1{3,}$/; | ||
| const MIME_TYPE_RE = /^(application|text|image|audio|video|multipart|font)\/[\w.+-]+$/; | ||
| const PASCAL_CASE_RE = /^[A-Z][a-z]+[A-Z]/; | ||
| const HTML_TARGETS = new Set([ | ||
| "_blank", | ||
| "_self", | ||
| "_parent", | ||
| "_top" | ||
| ]); | ||
| function isURLLike(str) { | ||
| if (/^(https?|ftp|file|mailto|data):/.test(str)) return true; | ||
| if (/^(\.\.?\/|\/|[A-Za-z]:\\)/.test(str)) return true; | ||
| if (/^['"]?[@a-z][\w-]*/.test(str.toLowerCase()) && !str.includes(" ") && (str.includes("/") || str.includes("."))) return true; | ||
| return false; | ||
| } | ||
| const TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/; | ||
| function isCssClassList(value) { | ||
| const segments = value.trim().split(/\s+/); | ||
| if (segments.length < 2) return false; | ||
| let matched = 0; | ||
| for (const seg of segments) if (TAILWIND_SEGMENT_RE.test(seg)) matched++; | ||
| return matched / segments.length >= .5; | ||
| } | ||
| function isSingleCssUtility(value) { | ||
| const trimmed = value.trim(); | ||
| if (trimmed.includes(" ")) return false; | ||
| return TAILWIND_SEGMENT_RE.test(trimmed); | ||
| } | ||
| const SVG_TECHNICAL_ATTRIBUTES = new Set([ | ||
| "d", | ||
| "viewBox", | ||
| "points", | ||
| "transform", | ||
| "pathLength", | ||
| "xmlns", | ||
| "preserveAspectRatio", | ||
| "stroke-linecap", | ||
| "stroke-linejoin", | ||
| "stroke-width", | ||
| "stroke-dasharray", | ||
| "stroke-dashoffset", | ||
| "stroke-miterlimit", | ||
| "fill-rule", | ||
| "clip-rule" | ||
| ]); | ||
| const SVG_GRAPHIC_ELEMENTS = new Set([ | ||
| "svg", | ||
| "path", | ||
| "circle", | ||
| "rect", | ||
| "line", | ||
| "polyline", | ||
| "polygon", | ||
| "ellipse", | ||
| "g", | ||
| "defs", | ||
| "use", | ||
| "symbol", | ||
| "clipPath", | ||
| "mask", | ||
| "pattern", | ||
| "linearGradient", | ||
| "radialGradient", | ||
| "stop", | ||
| "marker", | ||
| "animate", | ||
| "animateTransform", | ||
| "image" | ||
| ]); | ||
| const I18N_FUNCTIONS = new Set([ | ||
| "t", | ||
| "$t", | ||
| "i18n", | ||
| "translate", | ||
| "formatMessage", | ||
| "msg" | ||
| ]); | ||
| const EMIT_FUNCTIONS = new Set(["emit", "$emit"]); | ||
| const TRANSLATABLE_ATTRIBUTES = new Set([ | ||
| "title", | ||
| "alt", | ||
| "placeholder", | ||
| "label", | ||
| "summary", | ||
| "caption", | ||
| "abbr", | ||
| "accesskey", | ||
| "content", | ||
| "description", | ||
| "aria-label", | ||
| "aria-description", | ||
| "aria-placeholder", | ||
| "aria-roledescription", | ||
| "aria-valuetext", | ||
| "accessibilityLabel", | ||
| "accessibilityHint", | ||
| "accessibilityValue", | ||
| "heading", | ||
| "subheading", | ||
| "message", | ||
| "hint", | ||
| "tooltip", | ||
| "helper-text", | ||
| "error-message", | ||
| "success-message", | ||
| "confirm-text", | ||
| "cancel-text", | ||
| "empty-text", | ||
| "loading-text", | ||
| "no-data-text", | ||
| "no-results-text" | ||
| ]); | ||
| const TRANSLATABLE_PROPERTIES = new Set([ | ||
| "label", | ||
| "title", | ||
| "description", | ||
| "text", | ||
| "message", | ||
| "placeholder", | ||
| "caption", | ||
| "summary", | ||
| "heading", | ||
| "subheading", | ||
| "subtitle", | ||
| "tooltip", | ||
| "hint", | ||
| "helpText", | ||
| "errorMessage", | ||
| "successMessage", | ||
| "name" | ||
| ]); | ||
| /** | ||
| * Determines if a string is definitely NOT user-visible content. | ||
| * Returns skip reason if it should be filtered, null if it should proceed to scoring. | ||
| * | ||
| * Conservative for template_text/jsx_text (tag-between text is almost always content). | ||
| * Aggressive for everything else (technical tokens, config values, framework artifacts). | ||
| */ | ||
| function shouldSkip(str) { | ||
| if (str.context === "import_path") return "import_path"; | ||
| if (str.context === "type_annotation") return "type_annotation"; | ||
| if (str.context === "css_class") return "css_class"; | ||
| if (str.context === "css_utility_call") return "css_utility_call"; | ||
| if (str.context === "console_call") return "console_call"; | ||
| if (str.context === "test_assertion") return "test_assertion"; | ||
| if (str.context === "switch_case") return "switch_case"; | ||
| const v = str.value; | ||
| if (v.length <= 1) return "single_char"; | ||
| if (/^\s+$/.test(v)) return "whitespace"; | ||
| if (PURE_NUMBER_RE.test(v)) return "pure_number"; | ||
| if (HEX_COLOR_RE.test(v)) return "hex_color"; | ||
| if (FILE_EXT_RE.test(v)) return "file_extension"; | ||
| if (v.startsWith("--")) return "cli_flag"; | ||
| if (I18N_KEY_RE.test(v)) return "i18n_key"; | ||
| if (MIME_TYPE_RE.test(v)) return "mime_type"; | ||
| if (isURLLike(v)) return "url_path"; | ||
| if (isCssClassList(v)) return "css_class_list"; | ||
| if (isSingleCssUtility(v)) return "css_utility_token"; | ||
| if (v.length > 3 && SVG_PATH_DATA_RE.test(v)) return "svg_path_data"; | ||
| if (SVG_VIEWBOX_RE.test(v)) return "svg_viewbox"; | ||
| if (str.parentProperty !== void 0 && SVG_TECHNICAL_ATTRIBUTES.has(str.parentProperty)) return "svg_technical_attr"; | ||
| if (str.context === "template_attribute" && SVG_GRAPHIC_ELEMENTS.has(str.parent)) return "svg_element_attr"; | ||
| if (v.startsWith("update:")) return "vue_emit_event"; | ||
| if (PLACEHOLDER_RE.test(v)) return "placeholder"; | ||
| if (LOCALE_CODE_RE.test(v)) return "locale_code"; | ||
| if (DIMENSION_RE.test(v)) return "dimension"; | ||
| if (REPEAT_CHAR_RE.test(v)) return "repeat_chars"; | ||
| if (HTML_TARGETS.has(v)) return "html_target"; | ||
| if (str.context === "function_argument" && I18N_FUNCTIONS.has(str.parent)) { | ||
| if (/^[a-z][a-z0-9_.-]*$/.test(v)) return "i18n_function_arg"; | ||
| } | ||
| if (str.context === "function_argument" && EMIT_FUNCTIONS.has(str.parent)) return "emit_event_arg"; | ||
| if (str.context !== "template_text" && str.context !== "jsx_text") { | ||
| if (TECHNICAL_IDENTIFIER_RE.test(v) && v.length < 30) return "technical_identifier"; | ||
| } | ||
| if (ERROR_CODE_RE.test(v) && v.includes("_") && v.length > 3) return "error_code"; | ||
| return null; | ||
| } | ||
| /** | ||
| * Calculates a content confidence score (0-1) for a string that passed shouldSkip. | ||
| * Uses AST context metadata (our advantage over offset-based tools) combined with | ||
| * value-based signals proven by i18next-cli. | ||
| * | ||
| * Base score: 0.5. Boosted/penalized by context and value characteristics. | ||
| */ | ||
| function calculateContentScore(str) { | ||
| let score = .5; | ||
| if (str.context === "template_text" || str.context === "jsx_text") score += .3; | ||
| if (str.context === "template_attribute" || str.context === "jsx_attribute") if (str.parentProperty && TRANSLATABLE_ATTRIBUTES.has(str.parentProperty)) score += .2; | ||
| else score -= .2; | ||
| if (str.context === "object_property") { | ||
| if (str.parentProperty && TRANSLATABLE_PROPERTIES.has(str.parentProperty)) score += .25; | ||
| } | ||
| const wordCount = str.value.split(/\s+/).length; | ||
| if (wordCount >= 3) score += .2; | ||
| else if (wordCount === 2) score += .1; | ||
| if (/[.!?:;]$/.test(str.value)) score += .1; | ||
| if (/[\u0080-\uFFFF]/.test(str.value)) score += .15; | ||
| if (/^[A-Z]/.test(str.value) && /[a-z]/.test(str.value)) score += .1; | ||
| if (CAMEL_CASE_RE.test(str.value)) score -= .3; | ||
| if (PASCAL_CASE_RE.test(str.value) && !str.value.includes(" ")) score -= .25; | ||
| if (/^[A-Z]{2,5}$/.test(str.value)) score -= .15; | ||
| if (str.value.includes("/") && !str.value.includes(" ")) score -= .2; | ||
| return Math.max(0, Math.min(1, score)); | ||
| } | ||
| /** | ||
| * Two-phase pre-filter: | ||
| * 1. shouldSkip(): Binary removal of definite non-content | ||
| * 2. calculateContentScore(): 0-1 confidence scoring for ambiguous strings | ||
| * | ||
| * Returns candidates that passed both phases, with content scores attached. | ||
| */ | ||
| function applyPreFilter(strings, minScore = .4) { | ||
| const candidates = []; | ||
| const skipReasons = {}; | ||
| let skipped = 0; | ||
| let lowConfidence = 0; | ||
| for (const str of strings) { | ||
| const skipReason = shouldSkip(str); | ||
| if (skipReason) { | ||
| skipped++; | ||
| skipReasons[skipReason] = (skipReasons[skipReason] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| const contentScore = calculateContentScore(str); | ||
| if (contentScore < minScore) { | ||
| lowConfidence++; | ||
| skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| str.contentScore = contentScore; | ||
| candidates.push(str); | ||
| } | ||
| return { | ||
| candidates, | ||
| skipped, | ||
| lowConfidence, | ||
| skipReasons | ||
| }; | ||
| } | ||
| //#endregion | ||
| //#region src/core/ast-scanner/index.ts | ||
| const TSX_EXTENSIONS = new Set([ | ||
| ".tsx", | ||
| ".jsx", | ||
| ".ts", | ||
| ".js", | ||
| ".mjs" | ||
| ]); | ||
| const VUE_EXTENSIONS = new Set([".vue"]); | ||
| const SVELTE_EXTENSIONS = new Set([".svelte"]); | ||
| const ASTRO_EXTENSIONS = new Set([".astro"]); | ||
| /** | ||
| * Lazily import vue-parser. Returns undefined if not available | ||
| * (@vue/compiler-sfc is an optional dependency). | ||
| */ | ||
| async function loadVueParser() { | ||
| try { | ||
| return (await import("./vue-parser-COig4Y1a.mjs")).parseVue; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| /** | ||
| * Lazily import svelte-parser. Returns undefined if not available | ||
| * (svelte is an optional dependency). | ||
| */ | ||
| async function loadSvelteParser() { | ||
| try { | ||
| return (await import("./svelte-parser-Bll-1SK0.mjs")).parseSvelte; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| /** | ||
| * Lazily import astro-parser. Returns undefined if not available | ||
| * (@astrojs/compiler is an optional dependency). | ||
| */ | ||
| async function loadAstroParser() { | ||
| try { | ||
| return (await import("./astro-parser-BVoGjA6M.mjs")).parseAstro; | ||
| } catch { | ||
| return; | ||
| } | ||
| } | ||
| const SURROUNDING_MAX = 120; | ||
| /** | ||
| * Minimal regex-based extractor for file types without AST parsers. | ||
| * Extracts quoted strings and tag text — conservative, low accuracy. | ||
| * Used as fallback when dedicated parsers are unavailable. | ||
| */ | ||
| function extractWithRegex(content, _filePath) { | ||
| const lines = content.split("\n"); | ||
| const results = []; | ||
| const stringRe = /(['"`])(?:(?!\1|\\).|\\.)*?\1/g; | ||
| for (let i = 0; i < lines.length; i++) { | ||
| const line = lines[i]; | ||
| const trimmed = line.trim(); | ||
| if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) continue; | ||
| if (/^\s*(import|export)\s/.test(line)) continue; | ||
| let match; | ||
| stringRe.lastIndex = 0; | ||
| while ((match = stringRe.exec(line)) !== null) { | ||
| const value = match[0].slice(1, -1); | ||
| if (value.length === 0) continue; | ||
| const surrounding = buildSurrounding(lines, i); | ||
| results.push({ | ||
| value, | ||
| line: i + 1, | ||
| column: match.index + 1, | ||
| context: "other", | ||
| scope: "script", | ||
| parent: "", | ||
| surrounding | ||
| }); | ||
| } | ||
| const tagTextRe = />([^<>{]+)</g; | ||
| let tagMatch; | ||
| tagTextRe.lastIndex = 0; | ||
| while ((tagMatch = tagTextRe.exec(line)) !== null) { | ||
| const text = tagMatch[1].trim(); | ||
| if (text.length === 0) continue; | ||
| if (/^[\s\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue; | ||
| results.push({ | ||
| value: text, | ||
| line: i + 1, | ||
| column: tagMatch.index + 1, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: "", | ||
| surrounding: buildSurrounding(lines, i) | ||
| }); | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| function buildSurrounding(lines, lineIdx) { | ||
| const start = Math.max(0, lineIdx - 1); | ||
| const end = Math.min(lines.length - 1, lineIdx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const line = lines[i]; | ||
| if (line !== void 0) parts.push(line); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| /** | ||
| * Extract strings from a source file with structural context. | ||
| * | ||
| * Routes to the correct parser based on file extension: | ||
| * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based) | ||
| * - .vue -> vue-parser (lazy-loaded, AST-based) | ||
| * - .svelte -> svelte-parser (lazy-loaded, AST-based) | ||
| * - .astro -> astro-parser (lazy-loaded, AST-based) | ||
| * - unknown -> empty array | ||
| * | ||
| * Falls back to regex extraction when the dedicated parser's | ||
| * optional dependency is not installed. | ||
| * | ||
| * Applies structural pre-filter to remove 100% non-content strings. | ||
| * Returns only candidates that should be sent to the agent. | ||
| */ | ||
| async function extractStrings(filePath, content, ext) { | ||
| const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`; | ||
| let rawStrings; | ||
| if (TSX_EXTENSIONS.has(normalizedExt)) rawStrings = parseTsx(content, filePath); | ||
| else if (VUE_EXTENSIONS.has(normalizedExt)) { | ||
| const parseVue = await loadVueParser(); | ||
| if (parseVue) rawStrings = await parseVue(content, filePath); | ||
| else rawStrings = extractWithRegex(content, filePath); | ||
| } else if (SVELTE_EXTENSIONS.has(normalizedExt)) { | ||
| const parseSvelte = await loadSvelteParser(); | ||
| if (parseSvelte) try { | ||
| rawStrings = await parseSvelte(content, filePath); | ||
| } catch { | ||
| rawStrings = extractWithRegex(content, filePath); | ||
| } | ||
| else rawStrings = extractWithRegex(content, filePath); | ||
| } else if (ASTRO_EXTENSIONS.has(normalizedExt)) { | ||
| const parseAstro = await loadAstroParser(); | ||
| if (parseAstro) try { | ||
| rawStrings = await parseAstro(content, filePath); | ||
| } catch { | ||
| rawStrings = extractWithRegex(content, filePath); | ||
| } | ||
| else rawStrings = extractWithRegex(content, filePath); | ||
| } else return []; | ||
| const { candidates } = applyPreFilter(rawStrings); | ||
| return candidates; | ||
| } | ||
| //#endregion | ||
| //#region src/core/scanner.ts | ||
| const DEFAULT_LIMIT = 50; | ||
| const DEFAULT_OFFSET = 0; | ||
| const DEFAULT_MIN_LENGTH = 2; | ||
| const DEFAULT_MAX_LENGTH = 500; | ||
| const DEFAULT_MIN_SCORE = .4; | ||
| const SUMMARY_SAMPLE_SIZE = 10; | ||
| const CONTEXT_MAP = { | ||
| "template_text": "template_text", | ||
| "template_attribute": "template_attribute", | ||
| "jsx_text": "jsx_text", | ||
| "jsx_attribute": "jsx_attribute", | ||
| "variable_assignment": "variable_assignment", | ||
| "object_property": "object_value", | ||
| "function_argument": "function_argument", | ||
| "array_element": "other", | ||
| "enum_value": "other", | ||
| "template_literal": "other", | ||
| "switch_case": "other", | ||
| "other": "other", | ||
| "import_path": "other", | ||
| "type_annotation": "other", | ||
| "css_class": "other", | ||
| "css_utility_call": "other", | ||
| "console_call": "other", | ||
| "test_assertion": "other" | ||
| }; | ||
| async function scanCandidates(projectRoot, options) { | ||
| const limit = options?.limit ?? DEFAULT_LIMIT; | ||
| const offset = options?.offset ?? DEFAULT_OFFSET; | ||
| const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH; | ||
| const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH; | ||
| const minScore = options?.min_score ?? DEFAULT_MIN_SCORE; | ||
| const files = await discoverFiles(projectRoot, { | ||
| paths: options?.paths ?? await autoDetectSourceDirs(projectRoot), | ||
| include: options?.include, | ||
| exclude: options?.exclude | ||
| }); | ||
| const filePromises = files.map(async (relPath) => { | ||
| const filePath = join(projectRoot, relPath); | ||
| const content = await readText(filePath); | ||
| if (!content) return { | ||
| relPath, | ||
| extractions: [] | ||
| }; | ||
| return { | ||
| relPath, | ||
| extractions: await extractStrings(filePath, content, extname(filePath)) | ||
| }; | ||
| }); | ||
| const fileResults = await Promise.all(filePromises); | ||
| let rawStringsFound = 0; | ||
| let skippedCount = 0; | ||
| let lowConfidenceCount = 0; | ||
| const skipReasons = {}; | ||
| const uniqueMap = /* @__PURE__ */ new Map(); | ||
| const dupeMap = /* @__PURE__ */ new Map(); | ||
| for (const { relPath, extractions } of fileResults) { | ||
| rawStringsFound += extractions.length; | ||
| for (const extraction of extractions) { | ||
| if (extraction.value.length < minLength || extraction.value.length > maxLength) { | ||
| skippedCount++; | ||
| skipReasons["length_filter"] = (skipReasons["length_filter"] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| const contentScore = extraction.contentScore ?? calculateContentScore(extraction); | ||
| if (contentScore < minScore) { | ||
| lowConfidenceCount++; | ||
| skipReasons["low_confidence"] = (skipReasons["low_confidence"] ?? 0) + 1; | ||
| continue; | ||
| } | ||
| const mappedContext = CONTEXT_MAP[extraction.context]; | ||
| const loc = { | ||
| file: relPath, | ||
| line: extraction.line | ||
| }; | ||
| if (!dupeMap.has(extraction.value)) dupeMap.set(extraction.value, []); | ||
| dupeMap.get(extraction.value).push(loc); | ||
| if (!uniqueMap.has(extraction.value)) uniqueMap.set(extraction.value, { | ||
| candidate: { | ||
| file: relPath, | ||
| line: extraction.line, | ||
| column: extraction.column, | ||
| value: extraction.value, | ||
| context: mappedContext, | ||
| surrounding: extraction.surrounding, | ||
| contentScore, | ||
| occurrences: [loc] | ||
| }, | ||
| maxScore: contentScore | ||
| }); | ||
| else { | ||
| const entry = uniqueMap.get(extraction.value); | ||
| entry.candidate.occurrences.push(loc); | ||
| if (contentScore > entry.maxScore) { | ||
| entry.maxScore = contentScore; | ||
| entry.candidate.contentScore = contentScore; | ||
| entry.candidate.file = relPath; | ||
| entry.candidate.line = extraction.line; | ||
| entry.candidate.column = extraction.column; | ||
| entry.candidate.context = mappedContext; | ||
| entry.candidate.surrounding = extraction.surrounding; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| const allUniqueCandidates = [...uniqueMap.values()].map((e) => e.candidate).toSorted((a, b) => b.contentScore - a.contentScore); | ||
| const duplicates = [...dupeMap.entries()].filter(([, occurrences]) => occurrences.length >= 2).map(([value, occurrences]) => ({ | ||
| value, | ||
| count: occurrences.length, | ||
| occurrences | ||
| })).toSorted((a, b) => b.count - a.count); | ||
| const uniqueCount = allUniqueCandidates.length; | ||
| const paginated = allUniqueCandidates.slice(offset, offset + limit); | ||
| const hasMore = uniqueCount > offset + limit; | ||
| return { | ||
| candidates: paginated, | ||
| duplicates, | ||
| stats: { | ||
| files_scanned: files.length, | ||
| raw_strings_found: rawStringsFound, | ||
| skipped: skippedCount, | ||
| low_confidence: lowConfidenceCount, | ||
| unique_candidates: uniqueCount, | ||
| candidates_returned: paginated.length, | ||
| has_more: hasMore, | ||
| skip_reasons: skipReasons | ||
| } | ||
| }; | ||
| } | ||
| async function scanSummary(projectRoot, options) { | ||
| const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH; | ||
| const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH; | ||
| const minScore = options?.min_score ?? DEFAULT_MIN_SCORE; | ||
| const files = await discoverFiles(projectRoot, { | ||
| paths: options?.paths ?? await autoDetectSourceDirs(projectRoot), | ||
| include: options?.include, | ||
| exclude: options?.exclude | ||
| }); | ||
| const dirFiles = /* @__PURE__ */ new Map(); | ||
| const fileTypes = {}; | ||
| for (const relPath of files) { | ||
| const dir = relPath.split("/").slice(0, -1).join("/") || "."; | ||
| const ext = extname(relPath); | ||
| if (!dirFiles.has(dir)) dirFiles.set(dir, []); | ||
| dirFiles.get(dir).push(relPath); | ||
| fileTypes[ext] = (fileTypes[ext] ?? 0) + 1; | ||
| } | ||
| const byDirectory = {}; | ||
| const freqMap = /* @__PURE__ */ new Map(); | ||
| let totalCandidatesEstimate = 0; | ||
| for (const [dir, dirFileList] of dirFiles) { | ||
| const totalInDir = dirFileList.length; | ||
| const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE); | ||
| let sampleCandidates = 0; | ||
| const samplePromises = sampleFiles.map(async (relPath) => { | ||
| const filePath = join(projectRoot, relPath); | ||
| const content = await readText(filePath); | ||
| if (!content) return []; | ||
| return extractStrings(filePath, content, extname(filePath)); | ||
| }); | ||
| const sampleResults = await Promise.all(samplePromises); | ||
| for (const extractions of sampleResults) for (const extraction of extractions) { | ||
| if (extraction.value.length < minLength || extraction.value.length > maxLength) continue; | ||
| if ((extraction.contentScore ?? calculateContentScore(extraction)) < minScore) continue; | ||
| sampleCandidates++; | ||
| const prev = freqMap.get(extraction.value) ?? 0; | ||
| freqMap.set(extraction.value, prev + 1); | ||
| } | ||
| const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0; | ||
| const estimatedCandidates = Math.round(avgPerFile * totalInDir); | ||
| byDirectory[dir] = { | ||
| files: totalInDir, | ||
| candidates: estimatedCandidates | ||
| }; | ||
| totalCandidatesEstimate += estimatedCandidates; | ||
| } | ||
| const topRepeated = [...freqMap.entries()].filter(([, count]) => count >= 2).toSorted((a, b) => b[1] - a[1]).slice(0, 20).map(([value, count]) => ({ | ||
| value, | ||
| count | ||
| })); | ||
| return { | ||
| total_files: files.length, | ||
| total_candidates_estimate: totalCandidatesEstimate, | ||
| by_directory: byDirectory, | ||
| top_repeated: topRepeated, | ||
| sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`, | ||
| file_types: fileTypes | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { scanSummary as n, scanCandidates as t }; | ||
| //# sourceMappingURL=scanner-VOwrKLGC.mjs.map |
| {"version":3,"file":"scanner-VOwrKLGC.mjs","names":[],"sources":["../src/core/ast-scanner/pre-filter.ts","../src/core/ast-scanner/index.ts","../src/core/scanner.ts"],"sourcesContent":["import type { ExtractedString } from './types.js'\n\n// ─── Pre-filter Result ───\n\nexport interface PreFilterResult {\n /** Strings that passed the pre-filter (candidates for agent) */\n candidates: ExtractedString[]\n /** Total number of strings removed by shouldSkip */\n skipped: number\n /** Total number of strings removed by low content score */\n lowConfidence: number\n /** Breakdown: skip reason → count */\n skipReasons: Record<string, number>\n}\n\n// ─── Value-based regexes ───\n\nconst PURE_NUMBER_RE = /^-?\\d+(\\.\\d+)?$/\nconst HEX_COLOR_RE = /^#[0-9a-f]{3,8}$/i\nconst FILE_EXT_RE = /\\.(png|jpg|jpeg|gif|svg|webp|ico|css|scss|less|js|ts|tsx|jsx|json|md|html|xml|yaml|yml|woff|woff2|ttf|eot|mp4|webm|mp3|wav|pdf)$/i\nconst SVG_PATH_DATA_RE = /^[Mm][\\d\\s.,LHVCSQTAZlhvcsqtazmMzZ-]+$/\nconst SVG_VIEWBOX_RE = /^\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?\\s+\\d+(\\.\\d+)?$/\nconst I18N_KEY_RE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/\nconst TECHNICAL_IDENTIFIER_RE = /^[_a-z][a-z0-9_-]*$/\nconst ERROR_CODE_RE = /^[A-Z][A-Z0-9_]+$/\nconst PLACEHOLDER_RE = /^\\{\\d+\\}$|^\\.{2,}$/\nconst CAMEL_CASE_RE = /^[a-z]+[A-Z]/\nconst LOCALE_CODE_RE = /^[a-z]{2}[-_][A-Z]{2}$/\nconst DIMENSION_RE = /^\\d+[x×]\\d+$/\nconst REPEAT_CHAR_RE = /^(.)\\1{3,}$/\nconst MIME_TYPE_RE = /^(application|text|image|audio|video|multipart|font)\\/[\\w.+-]+$/\nconst PASCAL_CASE_RE = /^[A-Z][a-z]+[A-Z]/\n\nconst HTML_TARGETS = new Set(['_blank', '_self', '_parent', '_top'])\n\n// ─── URL / path detection (consolidated from legacy isNonContent) ───\n\nfunction isURLLike(str: string): boolean {\n if (/^(https?|ftp|file|mailto|data):/.test(str)) return true\n if (/^(\\.\\.?\\/|\\/|[A-Za-z]:\\\\)/.test(str)) return true\n if (/^['\"]?[@a-z][\\w-]*/.test(str.toLowerCase()) && !str.includes(' ') && (str.includes('/') || str.includes('.'))) {\n return true\n }\n return false\n}\n\n// ─── CSS / Tailwind detection ───\n\nconst TAILWIND_SEGMENT_RE = /^(?:bg-|text-|border-|flex|grid|p-|px-|py-|pt-|pb-|pl-|pr-|m-|mx-|my-|mt-|mb-|ml-|mr-|rounded|shadow|w-|h-|min-|max-|gap-|space-|items-|justify-|self-|overflow-|z-|opacity-|transition|duration-|ease-|animate-|font-|leading-|tracking-|decoration-|underline|line-through|uppercase|lowercase|capitalize|truncate|whitespace-|break-|sr-only|not-sr-only|hover:|focus:|active:|disabled:|dark:|sm:|md:|lg:|xl:|2xl:|group-|peer-|ring-|outline-|divide-|table-|col-|row-|aspect-|object-|inset-|top-|right-|bottom-|left-|translate-|rotate-|scale-|skew-|origin-|cursor-|select-|resize-|fill-|stroke-|block|inline|absolute|relative|fixed|sticky|static|float-|clear-|isolate|visible|invisible|grow|shrink|basis-|order-|place-)/\n\nfunction isCssClassList(value: string): boolean {\n const segments = value.trim().split(/\\s+/)\n if (segments.length < 2) return false\n let matched = 0\n for (const seg of segments) {\n if (TAILWIND_SEGMENT_RE.test(seg)) matched++\n }\n return matched / segments.length >= 0.5\n}\n\nfunction isSingleCssUtility(value: string): boolean {\n const trimmed = value.trim()\n if (trimmed.includes(' ')) return false\n return TAILWIND_SEGMENT_RE.test(trimmed)\n}\n\n// ─── SVG technical attributes ───\n\nconst SVG_TECHNICAL_ATTRIBUTES = new Set([\n 'd', 'viewBox', 'points', 'transform', 'pathLength',\n 'xmlns', 'preserveAspectRatio',\n 'stroke-linecap', 'stroke-linejoin', 'stroke-width',\n 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit',\n 'fill-rule', 'clip-rule',\n])\n\nconst SVG_GRAPHIC_ELEMENTS = new Set([\n 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon',\n 'ellipse', 'g', 'defs', 'use', 'symbol', 'clipPath', 'mask',\n 'pattern', 'linearGradient', 'radialGradient', 'stop',\n 'marker', 'animate', 'animateTransform', 'image',\n])\n\n// ─── Known function names ───\n\nconst I18N_FUNCTIONS = new Set([\n 't', '$t', 'i18n', 'translate', 'formatMessage', 'msg',\n])\n\nconst EMIT_FUNCTIONS = new Set([\n 'emit', '$emit',\n])\n\n// ─── Translatable attribute whitelist (i18next-cli compatible + extended) ───\n\nconst TRANSLATABLE_ATTRIBUTES = new Set([\n // Standard HTML content attributes\n 'title', 'alt', 'placeholder', 'label', 'summary', 'caption',\n 'abbr', 'accesskey', 'content', 'description',\n // ARIA content\n 'aria-label', 'aria-description', 'aria-placeholder',\n 'aria-roledescription', 'aria-valuetext',\n // React Native accessibility (equivalent to aria-label)\n 'accessibilityLabel', 'accessibilityHint', 'accessibilityValue',\n // Common component content props\n 'heading', 'subheading', 'message', 'hint', 'tooltip',\n 'helper-text', 'error-message', 'success-message',\n 'confirm-text', 'cancel-text', 'empty-text', 'loading-text',\n 'no-data-text', 'no-results-text',\n])\n\n// ─── Translatable object property whitelist (i18next-cli compatible) ───\n\nconst TRANSLATABLE_PROPERTIES = new Set([\n 'label', 'title', 'description', 'text', 'message', 'placeholder',\n 'caption', 'summary', 'heading', 'subheading', 'subtitle', 'tooltip',\n 'hint', 'helpText', 'errorMessage', 'successMessage', 'name',\n])\n\n// ─── shouldSkip: Binary non-content detection ───\n\n/**\n * Determines if a string is definitely NOT user-visible content.\n * Returns skip reason if it should be filtered, null if it should proceed to scoring.\n *\n * Conservative for template_text/jsx_text (tag-between text is almost always content).\n * Aggressive for everything else (technical tokens, config values, framework artifacts).\n */\nexport function shouldSkip(str: ExtractedString): string | null {\n // ── Context-based rules (AST-determined, 100% accurate) ──\n\n if (str.context === 'import_path') return 'import_path'\n if (str.context === 'type_annotation') return 'type_annotation'\n if (str.context === 'css_class') return 'css_class'\n if (str.context === 'css_utility_call') return 'css_utility_call'\n if (str.context === 'console_call') return 'console_call'\n if (str.context === 'test_assertion') return 'test_assertion'\n if (str.context === 'switch_case') return 'switch_case'\n\n const v = str.value\n\n // ── Value-based rules (structural patterns) ──\n\n if (v.length <= 1) return 'single_char'\n if (/^\\s+$/.test(v)) return 'whitespace'\n if (PURE_NUMBER_RE.test(v)) return 'pure_number'\n if (HEX_COLOR_RE.test(v)) return 'hex_color'\n if (FILE_EXT_RE.test(v)) return 'file_extension'\n if (v.startsWith('--')) return 'cli_flag'\n\n // ── i18n key paths (checked before URL — both contain dots, but i18n keys are more specific) ──\n\n if (I18N_KEY_RE.test(v)) return 'i18n_key'\n\n // ── MIME types (checked before URL — both contain slash, MIME is more specific) ──\n\n if (MIME_TYPE_RE.test(v)) return 'mime_type'\n\n // ── URL/path patterns ──\n\n if (isURLLike(v)) return 'url_path'\n\n // ── CSS patterns ──\n\n if (isCssClassList(v)) return 'css_class_list'\n if (isSingleCssUtility(v)) return 'css_utility_token'\n\n // ── SVG patterns ──\n\n if (v.length > 3 && SVG_PATH_DATA_RE.test(v)) return 'svg_path_data'\n if (SVG_VIEWBOX_RE.test(v)) return 'svg_viewbox'\n if (str.parentProperty !== undefined && SVG_TECHNICAL_ATTRIBUTES.has(str.parentProperty)) return 'svg_technical_attr'\n if (str.context === 'template_attribute' && SVG_GRAPHIC_ELEMENTS.has(str.parent)) return 'svg_element_attr'\n\n // ── Framework event patterns ──\n\n if (v.startsWith('update:')) return 'vue_emit_event'\n\n // ── Placeholder / interpolation ──\n\n if (PLACEHOLDER_RE.test(v)) return 'placeholder'\n\n // ── Structural value patterns (100% non-content) ──\n\n if (LOCALE_CODE_RE.test(v)) return 'locale_code'\n if (DIMENSION_RE.test(v)) return 'dimension'\n if (REPEAT_CHAR_RE.test(v)) return 'repeat_chars'\n if (HTML_TARGETS.has(v)) return 'html_target'\n\n // ── Known function argument detection ──\n\n if (str.context === 'function_argument' && I18N_FUNCTIONS.has(str.parent)) {\n // i18n function args: filter lowercase identifiers (namespace/key), keep sentences\n if (/^[a-z][a-z0-9_.-]*$/.test(v)) return 'i18n_function_arg'\n }\n\n if (str.context === 'function_argument' && EMIT_FUNCTIONS.has(str.parent)) {\n return 'emit_event_arg'\n }\n\n // ── CRITICAL: Technical identifier detection (i18next-cli proven pattern) ──\n // Single lowercase ASCII word/kebab-case/snake_case < 30 chars → technical token\n // EXEMPT: template_text and jsx_text (tag-between text IS content, even lowercase)\n\n if (str.context !== 'template_text' && str.context !== 'jsx_text') {\n if (TECHNICAL_IDENTIFIER_RE.test(v) && v.length < 30) {\n return 'technical_identifier'\n }\n }\n\n // ── Error codes (SCREAMING_SNAKE_CASE with underscores) ──\n\n if (ERROR_CODE_RE.test(v) && v.includes('_') && v.length > 3) {\n return 'error_code'\n }\n\n return null\n}\n\n// ─── calculateContentScore: 0-1 confidence scoring ───\n\n/**\n * Calculates a content confidence score (0-1) for a string that passed shouldSkip.\n * Uses AST context metadata (our advantage over offset-based tools) combined with\n * value-based signals proven by i18next-cli.\n *\n * Base score: 0.5. Boosted/penalized by context and value characteristics.\n */\nexport function calculateContentScore(str: ExtractedString): number {\n let score = 0.5\n\n // ── Context signals (AST metadata advantage) ──\n\n // Template/JSX text = almost certainly user-visible content\n if (str.context === 'template_text' || str.context === 'jsx_text') {\n score += 0.3\n }\n\n // Content-bearing attribute (title, alt, placeholder, aria-label, etc.)\n if (str.context === 'template_attribute' || str.context === 'jsx_attribute') {\n if (str.parentProperty && TRANSLATABLE_ATTRIBUTES.has(str.parentProperty)) {\n score += 0.2\n } else {\n score -= 0.2 // Unknown/technical attribute\n }\n }\n\n // Content-bearing object property (message, label, description, etc.)\n if (str.context === 'object_property') {\n if (str.parentProperty && TRANSLATABLE_PROPERTIES.has(str.parentProperty)) {\n score += 0.25\n }\n }\n\n // ── Value signals (i18next-cli proven heuristics) ──\n\n // Multi-word strings are more likely content\n const wordCount = str.value.split(/\\s+/).length\n if (wordCount >= 3) score += 0.2\n else if (wordCount === 2) score += 0.1\n\n // Terminal punctuation suggests a sentence\n if (/[.!?:;]$/.test(str.value)) score += 0.1\n\n // Non-ASCII characters (Turkish, Chinese, Arabic, etc.) → almost certainly content\n if (/[\\u0080-\\uFFFF]/.test(str.value)) score += 0.15\n\n // Capitalized first letter with lowercase body (Dashboard, Kaydet, Settings)\n if (/^[A-Z]/.test(str.value) && /[a-z]/.test(str.value)) score += 0.1\n\n // camelCase → probably a technical identifier\n if (CAMEL_CASE_RE.test(str.value)) score -= 0.3\n\n // PascalCase with internal uppercase (PhGameController, GameCard) → likely component/icon name\n // Does NOT match single-uppercase words (Dashboard, Karadeniz, Settings)\n if (PASCAL_CASE_RE.test(str.value) && !str.value.includes(' ')) score -= 0.25\n\n // Short ALL-CAPS (TRY, GET, USD) → likely code/abbreviation, not content\n // In template_text the +0.3 context boost keeps real labels like \"FAQ\" above threshold\n if (/^[A-Z]{2,5}$/.test(str.value)) score -= 0.15\n\n // Contains slash without spaces → path-like\n if (str.value.includes('/') && !str.value.includes(' ')) score -= 0.2\n\n return Math.max(0, Math.min(1, score))\n}\n\n// ─── Public API ───\n\n/**\n * Two-phase pre-filter:\n * 1. shouldSkip(): Binary removal of definite non-content\n * 2. calculateContentScore(): 0-1 confidence scoring for ambiguous strings\n *\n * Returns candidates that passed both phases, with content scores attached.\n */\nexport function applyPreFilter(\n strings: ExtractedString[],\n minScore: number = 0.4,\n): PreFilterResult {\n const candidates: ExtractedString[] = []\n const skipReasons: Record<string, number> = {}\n let skipped = 0\n let lowConfidence = 0\n\n for (const str of strings) {\n // Phase 1: Binary skip\n const skipReason = shouldSkip(str)\n if (skipReason) {\n skipped++\n skipReasons[skipReason] = (skipReasons[skipReason] ?? 0) + 1\n continue\n }\n\n // Phase 2: Content scoring\n const contentScore = calculateContentScore(str)\n if (contentScore < minScore) {\n lowConfidence++\n skipReasons['low_confidence'] = (skipReasons['low_confidence'] ?? 0) + 1\n continue\n }\n\n // Attach score to the extraction for downstream use\n ;(str as ExtractedString & { contentScore: number }).contentScore = contentScore\n candidates.push(str)\n }\n\n return { candidates, skipped, lowConfidence, skipReasons }\n}\n","import type { ExtractedString } from './types.js'\nimport { applyPreFilter } from './pre-filter.js'\nimport { parseTsx } from './tsx-parser.js'\n\n// ─── Extension sets ───\n\nconst TSX_EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs'])\nconst VUE_EXTENSIONS = new Set(['.vue'])\nconst SVELTE_EXTENSIONS = new Set(['.svelte'])\nconst ASTRO_EXTENSIONS = new Set(['.astro'])\n\n// ─── Lazy-loaded parsers ───\n\n/**\n * Lazily import vue-parser. Returns undefined if not available\n * (@vue/compiler-sfc is an optional dependency).\n */\nasync function loadVueParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]> | ExtractedString[]) | undefined> {\n try {\n const mod = await import('./vue-parser.js')\n return mod.parseVue\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import svelte-parser. Returns undefined if not available\n * (svelte is an optional dependency).\n */\nasync function loadSvelteParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./svelte-parser.js')\n return mod.parseSvelte\n } catch {\n return undefined\n }\n}\n\n/**\n * Lazily import astro-parser. Returns undefined if not available\n * (@astrojs/compiler is an optional dependency).\n */\nasync function loadAstroParser(): Promise<((content: string, fileName: string) => Promise<ExtractedString[]>) | undefined> {\n try {\n const mod = await import('./astro-parser.js')\n return mod.parseAstro\n } catch {\n return undefined\n }\n}\n\n// ─── Regex fallback for unknown extensions ───\n\nconst SURROUNDING_MAX = 120\n\n/**\n * Minimal regex-based extractor for file types without AST parsers.\n * Extracts quoted strings and tag text — conservative, low accuracy.\n * Used as fallback when dedicated parsers are unavailable.\n */\nfunction extractWithRegex(content: string, _filePath: string): ExtractedString[] {\n const lines = content.split('\\n')\n const results: ExtractedString[] = []\n\n // Simple quoted string extraction\n const stringRe = /(['\"`])(?:(?!\\1|\\\\).|\\\\.)*?\\1/g\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!\n const trimmed = line.trim()\n\n // Skip comments, imports\n if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) continue\n if (/^\\s*(import|export)\\s/.test(line)) continue\n\n let match: RegExpExecArray | null\n stringRe.lastIndex = 0\n\n while ((match = stringRe.exec(line)) !== null) {\n // Remove surrounding quotes\n const raw = match[0]\n const value = raw.slice(1, -1)\n if (value.length === 0) continue\n\n const surrounding = buildSurrounding(lines, i)\n\n results.push({\n value,\n line: i + 1,\n column: match.index + 1,\n context: 'other',\n scope: 'script',\n parent: '',\n surrounding,\n })\n }\n\n // Tag text extraction: >text<\n const tagTextRe = />([^<>{]+)</g\n let tagMatch: RegExpExecArray | null\n tagTextRe.lastIndex = 0\n\n while ((tagMatch = tagTextRe.exec(line)) !== null) {\n const text = tagMatch[1]!.trim()\n if (text.length === 0) continue\n if (/^[\\s\\W]*$/.test(text) && !/[a-zA-Z]/.test(text)) continue\n\n results.push({\n value: text,\n line: i + 1,\n column: tagMatch.index + 1,\n context: 'template_text',\n scope: 'template',\n parent: '',\n surrounding: buildSurrounding(lines, i),\n })\n }\n }\n\n return results\n}\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Public API ───\n\n/**\n * Extract strings from a source file with structural context.\n *\n * Routes to the correct parser based on file extension:\n * - .tsx/.jsx/.ts/.js/.mjs -> tsx-parser (AST-based)\n * - .vue -> vue-parser (lazy-loaded, AST-based)\n * - .svelte -> svelte-parser (lazy-loaded, AST-based)\n * - .astro -> astro-parser (lazy-loaded, AST-based)\n * - unknown -> empty array\n *\n * Falls back to regex extraction when the dedicated parser's\n * optional dependency is not installed.\n *\n * Applies structural pre-filter to remove 100% non-content strings.\n * Returns only candidates that should be sent to the agent.\n */\nexport async function extractStrings(\n filePath: string,\n content: string,\n ext: string,\n): Promise<ExtractedString[]> {\n const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`\n let rawStrings: ExtractedString[]\n\n if (TSX_EXTENSIONS.has(normalizedExt)) {\n rawStrings = parseTsx(content, filePath)\n } else if (VUE_EXTENSIONS.has(normalizedExt)) {\n const parseVue = await loadVueParser()\n if (parseVue) {\n rawStrings = await parseVue(content, filePath)\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (SVELTE_EXTENSIONS.has(normalizedExt)) {\n const parseSvelte = await loadSvelteParser()\n if (parseSvelte) {\n try {\n rawStrings = await parseSvelte(content, filePath)\n } catch {\n // svelte/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else if (ASTRO_EXTENSIONS.has(normalizedExt)) {\n const parseAstro = await loadAstroParser()\n if (parseAstro) {\n try {\n rawStrings = await parseAstro(content, filePath)\n } catch {\n // @astrojs/compiler not installed — fall back to regex\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n rawStrings = extractWithRegex(content, filePath)\n }\n } else {\n return []\n }\n\n // Apply structural pre-filter\n const { candidates } = applyPreFilter(rawStrings)\n return candidates\n}\n\n// Re-export types for convenience\nexport type { ExtractedString, StructuralContext, PreFilterRule } from './types.js'\nexport { applyPreFilter, shouldSkip, calculateContentScore } from './pre-filter.js'\nexport type { PreFilterResult } from './pre-filter.js'\n","import type {\n StringContext,\n ScanCandidate,\n DuplicateGroup,\n ScanCandidatesResult,\n ScanSummaryResult,\n} from '@contentrain/types'\nimport { join, extname } from 'node:path'\nimport { readText } from '../util/fs.js'\nimport {\n autoDetectSourceDirs,\n discoverFiles,\n} from './scan-config.js'\nimport { extractStrings } from './ast-scanner/index.js'\nimport { calculateContentScore } from './ast-scanner/pre-filter.js'\nimport type { StructuralContext } from './ast-scanner/types.js'\n\n// ─── Options ───\n\nexport interface ScanOptions {\n paths?: string[]\n include?: string[]\n exclude?: string[]\n limit?: number\n offset?: number\n min_length?: number\n max_length?: number\n /** Minimum content confidence score (0-1). Default: 0.4 */\n min_score?: number\n}\n\n// ─── Constants ───\n\nconst DEFAULT_LIMIT = 50\nconst DEFAULT_OFFSET = 0\nconst DEFAULT_MIN_LENGTH = 2\nconst DEFAULT_MAX_LENGTH = 500\nconst DEFAULT_MIN_SCORE = 0.4\nconst SUMMARY_SAMPLE_SIZE = 10\n\n// ─── Context mapping: StructuralContext → StringContext ───\n\nconst CONTEXT_MAP: Record<StructuralContext, StringContext> = {\n 'template_text': 'template_text',\n 'template_attribute': 'template_attribute',\n 'jsx_text': 'jsx_text',\n 'jsx_attribute': 'jsx_attribute',\n 'variable_assignment': 'variable_assignment',\n 'object_property': 'object_value',\n 'function_argument': 'function_argument',\n 'array_element': 'other',\n 'enum_value': 'other',\n 'template_literal': 'other',\n 'switch_case': 'other',\n 'other': 'other',\n // Pre-filtered contexts should not reach here, but map them just in case\n 'import_path': 'other',\n 'type_annotation': 'other',\n 'css_class': 'other',\n 'css_utility_call': 'other',\n 'console_call': 'other',\n 'test_assertion': 'other',\n}\n\n// ─── Main: scanCandidates ───\n\nexport async function scanCandidates(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanCandidatesResult> {\n const limit = options?.limit ?? DEFAULT_LIMIT\n const offset = options?.offset ?? DEFAULT_OFFSET\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n const minScore = options?.min_score ?? DEFAULT_MIN_SCORE\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // ─── Phase 1: Extract strings from all files (pre-filter applied inside extractStrings) ───\n\n const filePromises = files.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return { relPath, extractions: [] }\n\n const ext = extname(filePath)\n const extractions = await extractStrings(filePath, content, ext)\n return { relPath, extractions }\n })\n\n const fileResults = await Promise.all(filePromises)\n\n // ─── Phase 2: Score + length filter + deduplicate ───\n\n let rawStringsFound = 0\n let skippedCount = 0\n let lowConfidenceCount = 0\n const skipReasons: Record<string, number> = {}\n\n // Deduplication map: value → first candidate + all occurrences\n const uniqueMap = new Map<string, {\n candidate: ScanCandidate\n maxScore: number\n }>()\n\n // Also track duplicates for backward compatibility\n const dupeMap = new Map<string, Array<{ file: string; line: number }>>()\n\n for (const { relPath, extractions } of fileResults) {\n rawStringsFound += extractions.length\n\n for (const extraction of extractions) {\n // Length filter\n if (extraction.value.length < minLength || extraction.value.length > maxLength) {\n skippedCount++\n skipReasons['length_filter'] = (skipReasons['length_filter'] ?? 0) + 1\n continue\n }\n\n // shouldSkip was already applied inside extractStrings (via applyPreFilter).\n // But applyPreFilter uses default minScore. Here we apply the user-configured minScore\n // on the contentScore that was attached during pre-filtering.\n\n // Get the contentScore attached by applyPreFilter\n const contentScore: number = (extraction as unknown as { contentScore?: number }).contentScore ?? calculateContentScore(extraction)\n\n if (contentScore < minScore) {\n lowConfidenceCount++\n skipReasons['low_confidence'] = (skipReasons['low_confidence'] ?? 0) + 1\n continue\n }\n\n const mappedContext = CONTEXT_MAP[extraction.context]\n const loc = { file: relPath, line: extraction.line }\n\n // Track all occurrences for duplicates section\n if (!dupeMap.has(extraction.value)) {\n dupeMap.set(extraction.value, [])\n }\n dupeMap.get(extraction.value)!.push(loc)\n\n // Deduplication: keep first occurrence, accumulate locations\n if (!uniqueMap.has(extraction.value)) {\n uniqueMap.set(extraction.value, {\n candidate: {\n file: relPath,\n line: extraction.line,\n column: extraction.column,\n value: extraction.value,\n context: mappedContext,\n surrounding: extraction.surrounding,\n contentScore,\n occurrences: [loc],\n },\n maxScore: contentScore,\n })\n } else {\n const entry = uniqueMap.get(extraction.value)!\n entry.candidate.occurrences.push(loc)\n // Keep the highest score across occurrences\n if (contentScore > entry.maxScore) {\n entry.maxScore = contentScore\n entry.candidate.contentScore = contentScore\n entry.candidate.file = relPath\n entry.candidate.line = extraction.line\n entry.candidate.column = extraction.column\n entry.candidate.context = mappedContext\n entry.candidate.surrounding = extraction.surrounding\n }\n }\n }\n }\n\n // Build sorted unique candidates (highest score first)\n const allUniqueCandidates = [...uniqueMap.values()]\n .map(e => e.candidate)\n .toSorted((a, b) => b.contentScore - a.contentScore)\n\n // Build duplicate groups (only count >= 2), sorted by count descending\n const duplicates: DuplicateGroup[] = [...dupeMap.entries()]\n .filter(([, occurrences]) => occurrences.length >= 2)\n .map(([value, occurrences]) => ({ value, count: occurrences.length, occurrences }))\n .toSorted((a, b) => b.count - a.count)\n\n // Pagination on unique candidates\n const uniqueCount = allUniqueCandidates.length\n const paginated = allUniqueCandidates.slice(offset, offset + limit)\n const hasMore = uniqueCount > offset + limit\n\n return {\n candidates: paginated,\n duplicates,\n stats: {\n files_scanned: files.length,\n raw_strings_found: rawStringsFound,\n skipped: skippedCount,\n low_confidence: lowConfidenceCount,\n unique_candidates: uniqueCount,\n candidates_returned: paginated.length,\n has_more: hasMore,\n skip_reasons: skipReasons,\n },\n }\n}\n\n// ─── Main: scanSummary ───\n\nexport async function scanSummary(\n projectRoot: string,\n options?: ScanOptions,\n): Promise<ScanSummaryResult> {\n const minLength = options?.min_length ?? DEFAULT_MIN_LENGTH\n const maxLength = options?.max_length ?? DEFAULT_MAX_LENGTH\n const minScore = options?.min_score ?? DEFAULT_MIN_SCORE\n\n const scanDirs = options?.paths ?? await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, {\n paths: scanDirs,\n include: options?.include,\n exclude: options?.exclude,\n })\n\n // Group files by directory\n const dirFiles = new Map<string, string[]>()\n const fileTypes: Record<string, number> = {}\n\n for (const relPath of files) {\n const parts = relPath.split('/')\n const dir = parts.slice(0, -1).join('/') || '.'\n const ext = extname(relPath)\n\n if (!dirFiles.has(dir)) dirFiles.set(dir, [])\n dirFiles.get(dir)!.push(relPath)\n\n fileTypes[ext] = (fileTypes[ext] ?? 0) + 1\n }\n\n // Sample files per directory and count candidates\n const byDirectory: Record<string, { files: number; candidates: number }> = {}\n const freqMap = new Map<string, number>()\n let totalCandidatesEstimate = 0\n\n for (const [dir, dirFileList] of dirFiles) {\n const totalInDir = dirFileList.length\n const sampleFiles = dirFileList.slice(0, SUMMARY_SAMPLE_SIZE)\n let sampleCandidates = 0\n\n const samplePromises = sampleFiles.map(async (relPath) => {\n const filePath = join(projectRoot, relPath)\n const content = await readText(filePath)\n if (!content) return []\n\n const ext = extname(filePath)\n return extractStrings(filePath, content, ext)\n })\n\n const sampleResults = await Promise.all(samplePromises)\n\n for (const extractions of sampleResults) {\n for (const extraction of extractions) {\n // Apply same filters as scanCandidates\n if (extraction.value.length < minLength || extraction.value.length > maxLength) continue\n\n const contentScore: number = (extraction as unknown as { contentScore?: number }).contentScore ?? calculateContentScore(extraction)\n if (contentScore < minScore) continue\n\n sampleCandidates++\n\n // Track frequencies for top_repeated\n const prev = freqMap.get(extraction.value) ?? 0\n freqMap.set(extraction.value, prev + 1)\n }\n }\n\n // Estimate for full directory\n const avgPerFile = sampleFiles.length > 0 ? sampleCandidates / sampleFiles.length : 0\n const estimatedCandidates = Math.round(avgPerFile * totalInDir)\n\n byDirectory[dir] = {\n files: totalInDir,\n candidates: estimatedCandidates,\n }\n\n totalCandidatesEstimate += estimatedCandidates\n }\n\n // Top repeated strings\n const topRepeated = [...freqMap.entries()]\n .filter(([, count]) => count >= 2)\n .toSorted((a, b) => b[1] - a[1])\n .slice(0, 20)\n .map(([value, count]) => ({ value, count }))\n\n return {\n total_files: files.length,\n total_candidates_estimate: totalCandidatesEstimate,\n by_directory: byDirectory,\n top_repeated: topRepeated,\n sampling_note: `Based on first ${SUMMARY_SAMPLE_SIZE} files per directory. Counts are from sampled subset, not project-wide.`,\n file_types: fileTypes,\n }\n}\n"],"mappings":"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"} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { g as RepoProvider } from "./index-DDX-qYNw.mjs"; | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| //#region src/server.d.ts | ||
| /** | ||
| * The provider shape tool handlers consume. Now that every provider | ||
| * (Local, GitHub, GitLab) implements the full `RepoProvider`, tools can | ||
| * depend on the shared surface directly — no private alias required. | ||
| * Kept as a re-export so callers that already import `ToolProvider` do | ||
| * not need to migrate. | ||
| */ | ||
| type ToolProvider = RepoProvider; | ||
| /** | ||
| * Default MCP `instructions` surfaced to clients at initialize time. | ||
| * Deliberately kept under 512 characters — directory listings and client | ||
| * UIs truncate longer strings. Override via `CreateServerOptions.instructions`. | ||
| */ | ||
| declare const DEFAULT_INSTRUCTIONS: string; | ||
| interface CreateServerOptions { | ||
| /** | ||
| * Content provider — drives reads (and, in later phases, writes) through | ||
| * a reader surface. Required when `projectRoot` is omitted. Accepts the | ||
| * narrow `ToolProvider` shape so either `LocalProvider` or | ||
| * `GitHubProvider` satisfies the contract. | ||
| */ | ||
| provider?: ToolProvider; | ||
| /** | ||
| * Local project root. When the provider is a `LocalProvider`, its own | ||
| * `projectRoot` is used as the fallback. Tools that require local disk | ||
| * (normalize, setup, git submit/merge) are not registered when no | ||
| * projectRoot is available. | ||
| */ | ||
| projectRoot?: string; | ||
| /** | ||
| * MCP `instructions` string sent to clients in the `initialize` response. | ||
| * Defaults to `DEFAULT_INSTRUCTIONS`; pass an empty string to omit | ||
| * instructions entirely. | ||
| */ | ||
| instructions?: string; | ||
| } | ||
| /** | ||
| * Create an MCP server instance with every *available* Contentrain tool | ||
| * registered. | ||
| * | ||
| * Two signatures: | ||
| * | ||
| * - `createServer('/path/to/project')` — legacy stdio flow. A `LocalProvider` | ||
| * is constructed under the hood; every tool keeps behaving exactly as it | ||
| * did before phase 5.3. | ||
| * - `createServer({ provider, projectRoot? })` — phase 5.3 flow. Any | ||
| * `RepoProvider` (including `GitHubProvider`) drives reads and writes. If | ||
| * the provider is a `LocalProvider` and `projectRoot` is omitted, the | ||
| * provider's own `projectRoot` is used. | ||
| * | ||
| * Tool listing is capability-aware: tools whose requirements | ||
| * (`TOOL_REQUIREMENTS`) cannot be met by the resolved provider + | ||
| * projectRoot pair are not registered, so `tools/list` only advertises | ||
| * tools that can actually succeed. With a `LocalProvider` (stdio and CLI | ||
| * flows) all 19 tools remain registered — behavior there is unchanged. | ||
| */ | ||
| declare function createServer(projectRoot: string): McpServer; | ||
| declare function createServer(opts: CreateServerOptions): McpServer; | ||
| //#endregion | ||
| export { createServer as i, DEFAULT_INSTRUCTIONS as n, ToolProvider as r, CreateServerOptions as t }; | ||
| //# sourceMappingURL=server-vA1tM7DA.d.mts.map |
| {"version":3,"file":"server-vA1tM7DA.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;AAYA;;;;;KAAY,YAAA,GAAe,YAAA;;;;;AA2B3B;cAVa,oBAAA;AAAA,UAUI,mBAAA;EAOQ;;;;;;EAAvB,QAAA,GAAW,YAAA;EAiEG;;;;;AAChB;EA3DE,WAAA;;;;;;EAMA,YAAA;AAAA;;;;;;;;;;;;;;;;;;;;;iBAoDc,YAAA,CAAa,WAAA,WAAsB,SAAA;AAAA,iBACnC,YAAA,CAAa,IAAA,EAAM,mBAAA,GAAsB,SAAA"} |
| import { t as __commonJSMin } from "./chunk-BEJ448es.mjs"; | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64.js | ||
| var require_base64 = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var intToCharMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split(""); | ||
| /** | ||
| * Encode an integer in the range of 0 to 63 to a single base 64 digit. | ||
| */ | ||
| exports.encode = function(number) { | ||
| if (0 <= number && number < intToCharMap.length) return intToCharMap[number]; | ||
| throw new TypeError("Must be between 0 and 63: " + number); | ||
| }; | ||
| /** | ||
| * Decode a single base 64 character code digit to an integer. Returns -1 on | ||
| * failure. | ||
| */ | ||
| exports.decode = function(charCode) { | ||
| var bigA = 65; | ||
| var bigZ = 90; | ||
| var littleA = 97; | ||
| var littleZ = 122; | ||
| var zero = 48; | ||
| var nine = 57; | ||
| var plus = 43; | ||
| var slash = 47; | ||
| var littleOffset = 26; | ||
| var numberOffset = 52; | ||
| if (bigA <= charCode && charCode <= bigZ) return charCode - bigA; | ||
| if (littleA <= charCode && charCode <= littleZ) return charCode - littleA + littleOffset; | ||
| if (zero <= charCode && charCode <= nine) return charCode - zero + numberOffset; | ||
| if (charCode == plus) return 62; | ||
| if (charCode == slash) return 63; | ||
| return -1; | ||
| }; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64-vlq.js | ||
| var require_base64_vlq = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var base64 = require_base64(); | ||
| var VLQ_BASE_SHIFT = 5; | ||
| var VLQ_BASE = 1 << VLQ_BASE_SHIFT; | ||
| var VLQ_BASE_MASK = VLQ_BASE - 1; | ||
| var VLQ_CONTINUATION_BIT = VLQ_BASE; | ||
| /** | ||
| * Converts from a two-complement value to a value where the sign bit is | ||
| * placed in the least significant bit. For example, as decimals: | ||
| * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) | ||
| * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) | ||
| */ | ||
| function toVLQSigned(aValue) { | ||
| return aValue < 0 ? (-aValue << 1) + 1 : (aValue << 1) + 0; | ||
| } | ||
| /** | ||
| * Converts to a two-complement value from a value where the sign bit is | ||
| * placed in the least significant bit. For example, as decimals: | ||
| * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 | ||
| * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 | ||
| */ | ||
| function fromVLQSigned(aValue) { | ||
| var isNegative = (aValue & 1) === 1; | ||
| var shifted = aValue >> 1; | ||
| return isNegative ? -shifted : shifted; | ||
| } | ||
| /** | ||
| * Returns the base 64 VLQ encoded value. | ||
| */ | ||
| exports.encode = function base64VLQ_encode(aValue) { | ||
| var encoded = ""; | ||
| var digit; | ||
| var vlq = toVLQSigned(aValue); | ||
| do { | ||
| digit = vlq & VLQ_BASE_MASK; | ||
| vlq >>>= VLQ_BASE_SHIFT; | ||
| if (vlq > 0) digit |= VLQ_CONTINUATION_BIT; | ||
| encoded += base64.encode(digit); | ||
| } while (vlq > 0); | ||
| return encoded; | ||
| }; | ||
| /** | ||
| * Decodes the next base 64 VLQ value from the given string and returns the | ||
| * value and the rest of the string via the out parameter. | ||
| */ | ||
| exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { | ||
| var strLen = aStr.length; | ||
| var result = 0; | ||
| var shift = 0; | ||
| var continuation, digit; | ||
| do { | ||
| if (aIndex >= strLen) throw new Error("Expected more digits in base 64 VLQ value."); | ||
| digit = base64.decode(aStr.charCodeAt(aIndex++)); | ||
| if (digit === -1) throw new Error("Invalid base64 digit: " + aStr.charAt(aIndex - 1)); | ||
| continuation = !!(digit & VLQ_CONTINUATION_BIT); | ||
| digit &= VLQ_BASE_MASK; | ||
| result = result + (digit << shift); | ||
| shift += VLQ_BASE_SHIFT; | ||
| } while (continuation); | ||
| aOutParam.value = fromVLQSigned(result); | ||
| aOutParam.rest = aIndex; | ||
| }; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/util.js | ||
| var require_util = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| /** | ||
| * This is a helper function for getting values from parameter/options | ||
| * objects. | ||
| * | ||
| * @param args The object we are extracting values from | ||
| * @param name The name of the property we are getting. | ||
| * @param defaultValue An optional value to return if the property is missing | ||
| * from the object. If this is not specified and the property is missing, an | ||
| * error will be thrown. | ||
| */ | ||
| function getArg(aArgs, aName, aDefaultValue) { | ||
| if (aName in aArgs) return aArgs[aName]; | ||
| else if (arguments.length === 3) return aDefaultValue; | ||
| else throw new Error("\"" + aName + "\" is a required argument."); | ||
| } | ||
| exports.getArg = getArg; | ||
| var urlRegexp = /^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/; | ||
| var dataUrlRegexp = /^data:.+\,.+$/; | ||
| function urlParse(aUrl) { | ||
| var match = aUrl.match(urlRegexp); | ||
| if (!match) return null; | ||
| return { | ||
| scheme: match[1], | ||
| auth: match[2], | ||
| host: match[3], | ||
| port: match[4], | ||
| path: match[5] | ||
| }; | ||
| } | ||
| exports.urlParse = urlParse; | ||
| function urlGenerate(aParsedUrl) { | ||
| var url = ""; | ||
| if (aParsedUrl.scheme) url += aParsedUrl.scheme + ":"; | ||
| url += "//"; | ||
| if (aParsedUrl.auth) url += aParsedUrl.auth + "@"; | ||
| if (aParsedUrl.host) url += aParsedUrl.host; | ||
| if (aParsedUrl.port) url += ":" + aParsedUrl.port; | ||
| if (aParsedUrl.path) url += aParsedUrl.path; | ||
| return url; | ||
| } | ||
| exports.urlGenerate = urlGenerate; | ||
| var MAX_CACHED_INPUTS = 32; | ||
| /** | ||
| * Takes some function `f(input) -> result` and returns a memoized version of | ||
| * `f`. | ||
| * | ||
| * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The | ||
| * memoization is a dumb-simple, linear least-recently-used cache. | ||
| */ | ||
| function lruMemoize(f) { | ||
| var cache = []; | ||
| return function(input) { | ||
| for (var i = 0; i < cache.length; i++) if (cache[i].input === input) { | ||
| var temp = cache[0]; | ||
| cache[0] = cache[i]; | ||
| cache[i] = temp; | ||
| return cache[0].result; | ||
| } | ||
| var result = f(input); | ||
| cache.unshift({ | ||
| input, | ||
| result | ||
| }); | ||
| if (cache.length > MAX_CACHED_INPUTS) cache.pop(); | ||
| return result; | ||
| }; | ||
| } | ||
| /** | ||
| * Normalizes a path, or the path portion of a URL: | ||
| * | ||
| * - Replaces consecutive slashes with one slash. | ||
| * - Removes unnecessary '.' parts. | ||
| * - Removes unnecessary '<dir>/..' parts. | ||
| * | ||
| * Based on code in the Node.js 'path' core module. | ||
| * | ||
| * @param aPath The path or url to normalize. | ||
| */ | ||
| var normalize = lruMemoize(function normalize(aPath) { | ||
| var path = aPath; | ||
| var url = urlParse(aPath); | ||
| if (url) { | ||
| if (!url.path) return aPath; | ||
| path = url.path; | ||
| } | ||
| var isAbsolute = exports.isAbsolute(path); | ||
| var parts = []; | ||
| var start = 0; | ||
| var i = 0; | ||
| while (true) { | ||
| start = i; | ||
| i = path.indexOf("/", start); | ||
| if (i === -1) { | ||
| parts.push(path.slice(start)); | ||
| break; | ||
| } else { | ||
| parts.push(path.slice(start, i)); | ||
| while (i < path.length && path[i] === "/") i++; | ||
| } | ||
| } | ||
| for (var part, up = 0, i = parts.length - 1; i >= 0; i--) { | ||
| part = parts[i]; | ||
| if (part === ".") parts.splice(i, 1); | ||
| else if (part === "..") up++; | ||
| else if (up > 0) if (part === "") { | ||
| parts.splice(i + 1, up); | ||
| up = 0; | ||
| } else { | ||
| parts.splice(i, 2); | ||
| up--; | ||
| } | ||
| } | ||
| path = parts.join("/"); | ||
| if (path === "") path = isAbsolute ? "/" : "."; | ||
| if (url) { | ||
| url.path = path; | ||
| return urlGenerate(url); | ||
| } | ||
| return path; | ||
| }); | ||
| exports.normalize = normalize; | ||
| /** | ||
| * Joins two paths/URLs. | ||
| * | ||
| * @param aRoot The root path or URL. | ||
| * @param aPath The path or URL to be joined with the root. | ||
| * | ||
| * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a | ||
| * scheme-relative URL: Then the scheme of aRoot, if any, is prepended | ||
| * first. | ||
| * - Otherwise aPath is a path. If aRoot is a URL, then its path portion | ||
| * is updated with the result and aRoot is returned. Otherwise the result | ||
| * is returned. | ||
| * - If aPath is absolute, the result is aPath. | ||
| * - Otherwise the two paths are joined with a slash. | ||
| * - Joining for example 'http://' and 'www.example.com' is also supported. | ||
| */ | ||
| function join(aRoot, aPath) { | ||
| if (aRoot === "") aRoot = "."; | ||
| if (aPath === "") aPath = "."; | ||
| var aPathUrl = urlParse(aPath); | ||
| var aRootUrl = urlParse(aRoot); | ||
| if (aRootUrl) aRoot = aRootUrl.path || "/"; | ||
| if (aPathUrl && !aPathUrl.scheme) { | ||
| if (aRootUrl) aPathUrl.scheme = aRootUrl.scheme; | ||
| return urlGenerate(aPathUrl); | ||
| } | ||
| if (aPathUrl || aPath.match(dataUrlRegexp)) return aPath; | ||
| if (aRootUrl && !aRootUrl.host && !aRootUrl.path) { | ||
| aRootUrl.host = aPath; | ||
| return urlGenerate(aRootUrl); | ||
| } | ||
| var joined = aPath.charAt(0) === "/" ? aPath : normalize(aRoot.replace(/\/+$/, "") + "/" + aPath); | ||
| if (aRootUrl) { | ||
| aRootUrl.path = joined; | ||
| return urlGenerate(aRootUrl); | ||
| } | ||
| return joined; | ||
| } | ||
| exports.join = join; | ||
| exports.isAbsolute = function(aPath) { | ||
| return aPath.charAt(0) === "/" || urlRegexp.test(aPath); | ||
| }; | ||
| /** | ||
| * Make a path relative to a URL or another path. | ||
| * | ||
| * @param aRoot The root path or URL. | ||
| * @param aPath The path or URL to be made relative to aRoot. | ||
| */ | ||
| function relative(aRoot, aPath) { | ||
| if (aRoot === "") aRoot = "."; | ||
| aRoot = aRoot.replace(/\/$/, ""); | ||
| var level = 0; | ||
| while (aPath.indexOf(aRoot + "/") !== 0) { | ||
| var index = aRoot.lastIndexOf("/"); | ||
| if (index < 0) return aPath; | ||
| aRoot = aRoot.slice(0, index); | ||
| if (aRoot.match(/^([^\/]+:\/)?\/*$/)) return aPath; | ||
| ++level; | ||
| } | ||
| return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1); | ||
| } | ||
| exports.relative = relative; | ||
| var supportsNullProto = function() { | ||
| return !("__proto__" in Object.create(null)); | ||
| }(); | ||
| function identity(s) { | ||
| return s; | ||
| } | ||
| /** | ||
| * Because behavior goes wacky when you set `__proto__` on objects, we | ||
| * have to prefix all the strings in our set with an arbitrary character. | ||
| * | ||
| * See https://github.com/mozilla/source-map/pull/31 and | ||
| * https://github.com/mozilla/source-map/issues/30 | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| function toSetString(aStr) { | ||
| if (isProtoString(aStr)) return "$" + aStr; | ||
| return aStr; | ||
| } | ||
| exports.toSetString = supportsNullProto ? identity : toSetString; | ||
| function fromSetString(aStr) { | ||
| if (isProtoString(aStr)) return aStr.slice(1); | ||
| return aStr; | ||
| } | ||
| exports.fromSetString = supportsNullProto ? identity : fromSetString; | ||
| function isProtoString(s) { | ||
| if (!s) return false; | ||
| var length = s.length; | ||
| if (length < 9) return false; | ||
| if (s.charCodeAt(length - 1) !== 95 || s.charCodeAt(length - 2) !== 95 || s.charCodeAt(length - 3) !== 111 || s.charCodeAt(length - 4) !== 116 || s.charCodeAt(length - 5) !== 111 || s.charCodeAt(length - 6) !== 114 || s.charCodeAt(length - 7) !== 112 || s.charCodeAt(length - 8) !== 95 || s.charCodeAt(length - 9) !== 95) return false; | ||
| for (var i = length - 10; i >= 0; i--) if (s.charCodeAt(i) !== 36) return false; | ||
| return true; | ||
| } | ||
| /** | ||
| * Comparator between two mappings where the original positions are compared. | ||
| * | ||
| * Optionally pass in `true` as `onlyCompareGenerated` to consider two | ||
| * mappings with the same original source/line/column, but different generated | ||
| * line and column the same. Useful when searching for a mapping with a | ||
| * stubbed out mapping. | ||
| */ | ||
| function compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) { | ||
| var cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0 || onlyCompareOriginal) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByOriginalPositions = compareByOriginalPositions; | ||
| function compareByOriginalPositionsNoSource(mappingA, mappingB, onlyCompareOriginal) { | ||
| var cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0 || onlyCompareOriginal) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByOriginalPositionsNoSource = compareByOriginalPositionsNoSource; | ||
| /** | ||
| * Comparator between two mappings with deflated source and name indices where | ||
| * the generated positions are compared. | ||
| * | ||
| * Optionally pass in `true` as `onlyCompareGenerated` to consider two | ||
| * mappings with the same generated line and column, but different | ||
| * source/name/original line and column the same. Useful when searching for a | ||
| * mapping with a stubbed out mapping. | ||
| */ | ||
| function compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) { | ||
| var cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0 || onlyCompareGenerated) return cmp; | ||
| cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated; | ||
| function compareByGeneratedPositionsDeflatedNoLine(mappingA, mappingB, onlyCompareGenerated) { | ||
| var cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0 || onlyCompareGenerated) return cmp; | ||
| cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByGeneratedPositionsDeflatedNoLine = compareByGeneratedPositionsDeflatedNoLine; | ||
| function strcmp(aStr1, aStr2) { | ||
| if (aStr1 === aStr2) return 0; | ||
| if (aStr1 === null) return 1; | ||
| if (aStr2 === null) return -1; | ||
| if (aStr1 > aStr2) return 1; | ||
| return -1; | ||
| } | ||
| /** | ||
| * Comparator between two mappings with inflated source and name strings where | ||
| * the generated positions are compared. | ||
| */ | ||
| function compareByGeneratedPositionsInflated(mappingA, mappingB) { | ||
| var cmp = mappingA.generatedLine - mappingB.generatedLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.generatedColumn - mappingB.generatedColumn; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = strcmp(mappingA.source, mappingB.source); | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalLine - mappingB.originalLine; | ||
| if (cmp !== 0) return cmp; | ||
| cmp = mappingA.originalColumn - mappingB.originalColumn; | ||
| if (cmp !== 0) return cmp; | ||
| return strcmp(mappingA.name, mappingB.name); | ||
| } | ||
| exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated; | ||
| /** | ||
| * Strip any JSON XSSI avoidance prefix from the string (as documented | ||
| * in the source maps specification), and then parse the string as | ||
| * JSON. | ||
| */ | ||
| function parseSourceMapInput(str) { | ||
| return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, "")); | ||
| } | ||
| exports.parseSourceMapInput = parseSourceMapInput; | ||
| /** | ||
| * Compute the URL of a source given the the source root, the source's | ||
| * URL, and the source map's URL. | ||
| */ | ||
| function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { | ||
| sourceURL = sourceURL || ""; | ||
| if (sourceRoot) { | ||
| if (sourceRoot[sourceRoot.length - 1] !== "/" && sourceURL[0] !== "/") sourceRoot += "/"; | ||
| sourceURL = sourceRoot + sourceURL; | ||
| } | ||
| if (sourceMapURL) { | ||
| var parsed = urlParse(sourceMapURL); | ||
| if (!parsed) throw new Error("sourceMapURL could not be parsed"); | ||
| if (parsed.path) { | ||
| var index = parsed.path.lastIndexOf("/"); | ||
| if (index >= 0) parsed.path = parsed.path.substring(0, index + 1); | ||
| } | ||
| sourceURL = join(urlGenerate(parsed), sourceURL); | ||
| } | ||
| return normalize(sourceURL); | ||
| } | ||
| exports.computeSourceURL = computeSourceURL; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/array-set.js | ||
| var require_array_set = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var util = require_util(); | ||
| var has = Object.prototype.hasOwnProperty; | ||
| var hasNativeMap = typeof Map !== "undefined"; | ||
| /** | ||
| * A data structure which is a combination of an array and a set. Adding a new | ||
| * member is O(1), testing for membership is O(1), and finding the index of an | ||
| * element is O(1). Removing elements from the set is not supported. Only | ||
| * strings are supported for membership. | ||
| */ | ||
| function ArraySet() { | ||
| this._array = []; | ||
| this._set = hasNativeMap ? /* @__PURE__ */ new Map() : Object.create(null); | ||
| } | ||
| /** | ||
| * Static method for creating ArraySet instances from an existing array. | ||
| */ | ||
| ArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) { | ||
| var set = new ArraySet(); | ||
| for (var i = 0, len = aArray.length; i < len; i++) set.add(aArray[i], aAllowDuplicates); | ||
| return set; | ||
| }; | ||
| /** | ||
| * Return how many unique items are in this ArraySet. If duplicates have been | ||
| * added, than those do not count towards the size. | ||
| * | ||
| * @returns Number | ||
| */ | ||
| ArraySet.prototype.size = function ArraySet_size() { | ||
| return hasNativeMap ? this._set.size : Object.getOwnPropertyNames(this._set).length; | ||
| }; | ||
| /** | ||
| * Add the given string to this set. | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| ArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) { | ||
| var sStr = hasNativeMap ? aStr : util.toSetString(aStr); | ||
| var isDuplicate = hasNativeMap ? this.has(aStr) : has.call(this._set, sStr); | ||
| var idx = this._array.length; | ||
| if (!isDuplicate || aAllowDuplicates) this._array.push(aStr); | ||
| if (!isDuplicate) if (hasNativeMap) this._set.set(aStr, idx); | ||
| else this._set[sStr] = idx; | ||
| }; | ||
| /** | ||
| * Is the given string a member of this set? | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| ArraySet.prototype.has = function ArraySet_has(aStr) { | ||
| if (hasNativeMap) return this._set.has(aStr); | ||
| else { | ||
| var sStr = util.toSetString(aStr); | ||
| return has.call(this._set, sStr); | ||
| } | ||
| }; | ||
| /** | ||
| * What is the index of the given string in the array? | ||
| * | ||
| * @param String aStr | ||
| */ | ||
| ArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) { | ||
| if (hasNativeMap) { | ||
| var idx = this._set.get(aStr); | ||
| if (idx >= 0) return idx; | ||
| } else { | ||
| var sStr = util.toSetString(aStr); | ||
| if (has.call(this._set, sStr)) return this._set[sStr]; | ||
| } | ||
| throw new Error("\"" + aStr + "\" is not in the set."); | ||
| }; | ||
| /** | ||
| * What is the element at the given index? | ||
| * | ||
| * @param Number aIdx | ||
| */ | ||
| ArraySet.prototype.at = function ArraySet_at(aIdx) { | ||
| if (aIdx >= 0 && aIdx < this._array.length) return this._array[aIdx]; | ||
| throw new Error("No element indexed by " + aIdx); | ||
| }; | ||
| /** | ||
| * Returns the array representation of this set (which has the proper indices | ||
| * indicated by indexOf). Note that this is a copy of the internal array used | ||
| * for storing the members so that no one can mess with internal state. | ||
| */ | ||
| ArraySet.prototype.toArray = function ArraySet_toArray() { | ||
| return this._array.slice(); | ||
| }; | ||
| exports.ArraySet = ArraySet; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/mapping-list.js | ||
| var require_mapping_list = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var util = require_util(); | ||
| /** | ||
| * Determine whether mappingB is after mappingA with respect to generated | ||
| * position. | ||
| */ | ||
| function generatedPositionAfter(mappingA, mappingB) { | ||
| var lineA = mappingA.generatedLine; | ||
| var lineB = mappingB.generatedLine; | ||
| var columnA = mappingA.generatedColumn; | ||
| var columnB = mappingB.generatedColumn; | ||
| return lineB > lineA || lineB == lineA && columnB >= columnA || util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0; | ||
| } | ||
| /** | ||
| * A data structure to provide a sorted view of accumulated mappings in a | ||
| * performance conscious manner. It trades a neglibable overhead in general | ||
| * case for a large speedup in case of mappings being added in order. | ||
| */ | ||
| function MappingList() { | ||
| this._array = []; | ||
| this._sorted = true; | ||
| this._last = { | ||
| generatedLine: -1, | ||
| generatedColumn: 0 | ||
| }; | ||
| } | ||
| /** | ||
| * Iterate through internal items. This method takes the same arguments that | ||
| * `Array.prototype.forEach` takes. | ||
| * | ||
| * NOTE: The order of the mappings is NOT guaranteed. | ||
| */ | ||
| MappingList.prototype.unsortedForEach = function MappingList_forEach(aCallback, aThisArg) { | ||
| this._array.forEach(aCallback, aThisArg); | ||
| }; | ||
| /** | ||
| * Add the given source mapping. | ||
| * | ||
| * @param Object aMapping | ||
| */ | ||
| MappingList.prototype.add = function MappingList_add(aMapping) { | ||
| if (generatedPositionAfter(this._last, aMapping)) { | ||
| this._last = aMapping; | ||
| this._array.push(aMapping); | ||
| } else { | ||
| this._sorted = false; | ||
| this._array.push(aMapping); | ||
| } | ||
| }; | ||
| /** | ||
| * Returns the flat, sorted array of mappings. The mappings are sorted by | ||
| * generated position. | ||
| * | ||
| * WARNING: This method returns internal data without copying, for | ||
| * performance. The return value must NOT be mutated, and should be treated as | ||
| * an immutable borrow. If you want to take ownership, you must make your own | ||
| * copy. | ||
| */ | ||
| MappingList.prototype.toArray = function MappingList_toArray() { | ||
| if (!this._sorted) { | ||
| this._array.sort(util.compareByGeneratedPositionsInflated); | ||
| this._sorted = true; | ||
| } | ||
| return this._array; | ||
| }; | ||
| exports.MappingList = MappingList; | ||
| })); | ||
| //#endregion | ||
| //#region ../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/source-map-generator.js | ||
| var require_source_map_generator = /* @__PURE__ */ __commonJSMin(((exports) => { | ||
| var base64VLQ = require_base64_vlq(); | ||
| var util = require_util(); | ||
| var ArraySet = require_array_set().ArraySet; | ||
| var MappingList = require_mapping_list().MappingList; | ||
| /** | ||
| * An instance of the SourceMapGenerator represents a source map which is | ||
| * being built incrementally. You may pass an object with the following | ||
| * properties: | ||
| * | ||
| * - file: The filename of the generated source. | ||
| * - sourceRoot: A root for all relative URLs in this source map. | ||
| */ | ||
| function SourceMapGenerator(aArgs) { | ||
| if (!aArgs) aArgs = {}; | ||
| this._file = util.getArg(aArgs, "file", null); | ||
| this._sourceRoot = util.getArg(aArgs, "sourceRoot", null); | ||
| this._skipValidation = util.getArg(aArgs, "skipValidation", false); | ||
| this._ignoreInvalidMapping = util.getArg(aArgs, "ignoreInvalidMapping", false); | ||
| this._sources = new ArraySet(); | ||
| this._names = new ArraySet(); | ||
| this._mappings = new MappingList(); | ||
| this._sourcesContents = null; | ||
| } | ||
| SourceMapGenerator.prototype._version = 3; | ||
| /** | ||
| * Creates a new SourceMapGenerator based on a SourceMapConsumer | ||
| * | ||
| * @param aSourceMapConsumer The SourceMap. | ||
| */ | ||
| SourceMapGenerator.fromSourceMap = function SourceMapGenerator_fromSourceMap(aSourceMapConsumer, generatorOps) { | ||
| var sourceRoot = aSourceMapConsumer.sourceRoot; | ||
| var generator = new SourceMapGenerator(Object.assign(generatorOps || {}, { | ||
| file: aSourceMapConsumer.file, | ||
| sourceRoot | ||
| })); | ||
| aSourceMapConsumer.eachMapping(function(mapping) { | ||
| var newMapping = { generated: { | ||
| line: mapping.generatedLine, | ||
| column: mapping.generatedColumn | ||
| } }; | ||
| if (mapping.source != null) { | ||
| newMapping.source = mapping.source; | ||
| if (sourceRoot != null) newMapping.source = util.relative(sourceRoot, newMapping.source); | ||
| newMapping.original = { | ||
| line: mapping.originalLine, | ||
| column: mapping.originalColumn | ||
| }; | ||
| if (mapping.name != null) newMapping.name = mapping.name; | ||
| } | ||
| generator.addMapping(newMapping); | ||
| }); | ||
| aSourceMapConsumer.sources.forEach(function(sourceFile) { | ||
| var sourceRelative = sourceFile; | ||
| if (sourceRoot !== null) sourceRelative = util.relative(sourceRoot, sourceFile); | ||
| if (!generator._sources.has(sourceRelative)) generator._sources.add(sourceRelative); | ||
| var content = aSourceMapConsumer.sourceContentFor(sourceFile); | ||
| if (content != null) generator.setSourceContent(sourceFile, content); | ||
| }); | ||
| return generator; | ||
| }; | ||
| /** | ||
| * Add a single mapping from original source line and column to the generated | ||
| * source's line and column for this source map being created. The mapping | ||
| * object should have the following properties: | ||
| * | ||
| * - generated: An object with the generated line and column positions. | ||
| * - original: An object with the original line and column positions. | ||
| * - source: The original source file (relative to the sourceRoot). | ||
| * - name: An optional original token name for this mapping. | ||
| */ | ||
| SourceMapGenerator.prototype.addMapping = function SourceMapGenerator_addMapping(aArgs) { | ||
| var generated = util.getArg(aArgs, "generated"); | ||
| var original = util.getArg(aArgs, "original", null); | ||
| var source = util.getArg(aArgs, "source", null); | ||
| var name = util.getArg(aArgs, "name", null); | ||
| if (!this._skipValidation) { | ||
| if (this._validateMapping(generated, original, source, name) === false) return; | ||
| } | ||
| if (source != null) { | ||
| source = String(source); | ||
| if (!this._sources.has(source)) this._sources.add(source); | ||
| } | ||
| if (name != null) { | ||
| name = String(name); | ||
| if (!this._names.has(name)) this._names.add(name); | ||
| } | ||
| this._mappings.add({ | ||
| generatedLine: generated.line, | ||
| generatedColumn: generated.column, | ||
| originalLine: original != null && original.line, | ||
| originalColumn: original != null && original.column, | ||
| source, | ||
| name | ||
| }); | ||
| }; | ||
| /** | ||
| * Set the source content for a source file. | ||
| */ | ||
| SourceMapGenerator.prototype.setSourceContent = function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) { | ||
| var source = aSourceFile; | ||
| if (this._sourceRoot != null) source = util.relative(this._sourceRoot, source); | ||
| if (aSourceContent != null) { | ||
| if (!this._sourcesContents) this._sourcesContents = Object.create(null); | ||
| this._sourcesContents[util.toSetString(source)] = aSourceContent; | ||
| } else if (this._sourcesContents) { | ||
| delete this._sourcesContents[util.toSetString(source)]; | ||
| if (Object.keys(this._sourcesContents).length === 0) this._sourcesContents = null; | ||
| } | ||
| }; | ||
| /** | ||
| * Applies the mappings of a sub-source-map for a specific source file to the | ||
| * source map being generated. Each mapping to the supplied source file is | ||
| * rewritten using the supplied source map. Note: The resolution for the | ||
| * resulting mappings is the minimium of this map and the supplied map. | ||
| * | ||
| * @param aSourceMapConsumer The source map to be applied. | ||
| * @param aSourceFile Optional. The filename of the source file. | ||
| * If omitted, SourceMapConsumer's file property will be used. | ||
| * @param aSourceMapPath Optional. The dirname of the path to the source map | ||
| * to be applied. If relative, it is relative to the SourceMapConsumer. | ||
| * This parameter is needed when the two source maps aren't in the same | ||
| * directory, and the source map to be applied contains relative source | ||
| * paths. If so, those relative source paths need to be rewritten | ||
| * relative to the SourceMapGenerator. | ||
| */ | ||
| SourceMapGenerator.prototype.applySourceMap = function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) { | ||
| var sourceFile = aSourceFile; | ||
| if (aSourceFile == null) { | ||
| if (aSourceMapConsumer.file == null) throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, or the source map's \"file\" property. Both were omitted."); | ||
| sourceFile = aSourceMapConsumer.file; | ||
| } | ||
| var sourceRoot = this._sourceRoot; | ||
| if (sourceRoot != null) sourceFile = util.relative(sourceRoot, sourceFile); | ||
| var newSources = new ArraySet(); | ||
| var newNames = new ArraySet(); | ||
| this._mappings.unsortedForEach(function(mapping) { | ||
| if (mapping.source === sourceFile && mapping.originalLine != null) { | ||
| var original = aSourceMapConsumer.originalPositionFor({ | ||
| line: mapping.originalLine, | ||
| column: mapping.originalColumn | ||
| }); | ||
| if (original.source != null) { | ||
| mapping.source = original.source; | ||
| if (aSourceMapPath != null) mapping.source = util.join(aSourceMapPath, mapping.source); | ||
| if (sourceRoot != null) mapping.source = util.relative(sourceRoot, mapping.source); | ||
| mapping.originalLine = original.line; | ||
| mapping.originalColumn = original.column; | ||
| if (original.name != null) mapping.name = original.name; | ||
| } | ||
| } | ||
| var source = mapping.source; | ||
| if (source != null && !newSources.has(source)) newSources.add(source); | ||
| var name = mapping.name; | ||
| if (name != null && !newNames.has(name)) newNames.add(name); | ||
| }, this); | ||
| this._sources = newSources; | ||
| this._names = newNames; | ||
| aSourceMapConsumer.sources.forEach(function(sourceFile) { | ||
| var content = aSourceMapConsumer.sourceContentFor(sourceFile); | ||
| if (content != null) { | ||
| if (aSourceMapPath != null) sourceFile = util.join(aSourceMapPath, sourceFile); | ||
| if (sourceRoot != null) sourceFile = util.relative(sourceRoot, sourceFile); | ||
| this.setSourceContent(sourceFile, content); | ||
| } | ||
| }, this); | ||
| }; | ||
| /** | ||
| * A mapping can have one of the three levels of data: | ||
| * | ||
| * 1. Just the generated position. | ||
| * 2. The Generated position, original position, and original source. | ||
| * 3. Generated and original position, original source, as well as a name | ||
| * token. | ||
| * | ||
| * To maintain consistency, we validate that any new mapping being added falls | ||
| * in to one of these categories. | ||
| */ | ||
| SourceMapGenerator.prototype._validateMapping = function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource, aName) { | ||
| if (aOriginal && typeof aOriginal.line !== "number" && typeof aOriginal.column !== "number") { | ||
| var message = "original.line and original.column are not numbers -- you probably meant to omit the original mapping entirely and only map the generated position. If so, pass null for the original mapping instead of an object with empty or null values."; | ||
| if (this._ignoreInvalidMapping) { | ||
| if (typeof console !== "undefined" && console.warn) console.warn(message); | ||
| return false; | ||
| } else throw new Error(message); | ||
| } | ||
| if (aGenerated && "line" in aGenerated && "column" in aGenerated && aGenerated.line > 0 && aGenerated.column >= 0 && !aOriginal && !aSource && !aName) return; | ||
| else if (aGenerated && "line" in aGenerated && "column" in aGenerated && aOriginal && "line" in aOriginal && "column" in aOriginal && aGenerated.line > 0 && aGenerated.column >= 0 && aOriginal.line > 0 && aOriginal.column >= 0 && aSource) return; | ||
| else { | ||
| var message = "Invalid mapping: " + JSON.stringify({ | ||
| generated: aGenerated, | ||
| source: aSource, | ||
| original: aOriginal, | ||
| name: aName | ||
| }); | ||
| if (this._ignoreInvalidMapping) { | ||
| if (typeof console !== "undefined" && console.warn) console.warn(message); | ||
| return false; | ||
| } else throw new Error(message); | ||
| } | ||
| }; | ||
| /** | ||
| * Serialize the accumulated mappings in to the stream of base 64 VLQs | ||
| * specified by the source map format. | ||
| */ | ||
| SourceMapGenerator.prototype._serializeMappings = function SourceMapGenerator_serializeMappings() { | ||
| var previousGeneratedColumn = 0; | ||
| var previousGeneratedLine = 1; | ||
| var previousOriginalColumn = 0; | ||
| var previousOriginalLine = 0; | ||
| var previousName = 0; | ||
| var previousSource = 0; | ||
| var result = ""; | ||
| var next; | ||
| var mapping; | ||
| var nameIdx; | ||
| var sourceIdx; | ||
| var mappings = this._mappings.toArray(); | ||
| for (var i = 0, len = mappings.length; i < len; i++) { | ||
| mapping = mappings[i]; | ||
| next = ""; | ||
| if (mapping.generatedLine !== previousGeneratedLine) { | ||
| previousGeneratedColumn = 0; | ||
| while (mapping.generatedLine !== previousGeneratedLine) { | ||
| next += ";"; | ||
| previousGeneratedLine++; | ||
| } | ||
| } else if (i > 0) { | ||
| if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) continue; | ||
| next += ","; | ||
| } | ||
| next += base64VLQ.encode(mapping.generatedColumn - previousGeneratedColumn); | ||
| previousGeneratedColumn = mapping.generatedColumn; | ||
| if (mapping.source != null) { | ||
| sourceIdx = this._sources.indexOf(mapping.source); | ||
| next += base64VLQ.encode(sourceIdx - previousSource); | ||
| previousSource = sourceIdx; | ||
| next += base64VLQ.encode(mapping.originalLine - 1 - previousOriginalLine); | ||
| previousOriginalLine = mapping.originalLine - 1; | ||
| next += base64VLQ.encode(mapping.originalColumn - previousOriginalColumn); | ||
| previousOriginalColumn = mapping.originalColumn; | ||
| if (mapping.name != null) { | ||
| nameIdx = this._names.indexOf(mapping.name); | ||
| next += base64VLQ.encode(nameIdx - previousName); | ||
| previousName = nameIdx; | ||
| } | ||
| } | ||
| result += next; | ||
| } | ||
| return result; | ||
| }; | ||
| SourceMapGenerator.prototype._generateSourcesContent = function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) { | ||
| return aSources.map(function(source) { | ||
| if (!this._sourcesContents) return null; | ||
| if (aSourceRoot != null) source = util.relative(aSourceRoot, source); | ||
| var key = util.toSetString(source); | ||
| return Object.prototype.hasOwnProperty.call(this._sourcesContents, key) ? this._sourcesContents[key] : null; | ||
| }, this); | ||
| }; | ||
| /** | ||
| * Externalize the source map. | ||
| */ | ||
| SourceMapGenerator.prototype.toJSON = function SourceMapGenerator_toJSON() { | ||
| var map = { | ||
| version: this._version, | ||
| sources: this._sources.toArray(), | ||
| names: this._names.toArray(), | ||
| mappings: this._serializeMappings() | ||
| }; | ||
| if (this._file != null) map.file = this._file; | ||
| if (this._sourceRoot != null) map.sourceRoot = this._sourceRoot; | ||
| if (this._sourcesContents) map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot); | ||
| return map; | ||
| }; | ||
| /** | ||
| * Render the source map being generated to a string. | ||
| */ | ||
| SourceMapGenerator.prototype.toString = function SourceMapGenerator_toString() { | ||
| return JSON.stringify(this.toJSON()); | ||
| }; | ||
| exports.SourceMapGenerator = SourceMapGenerator; | ||
| })); | ||
| //#endregion | ||
| export { require_base64_vlq as i, require_array_set as n, require_util as r, require_source_map_generator as t }; | ||
| //# sourceMappingURL=source-map-generator-BIEiz3If.mjs.map |
| {"version":3,"file":"source-map-generator-BIEiz3If.mjs","names":[],"sources":["../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/base64-vlq.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/util.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/array-set.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/mapping-list.js","../../../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/lib/source-map-generator.js"],"sourcesContent":["/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar intToCharMap = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('');\n\n/**\n * Encode an integer in the range of 0 to 63 to a single base 64 digit.\n */\nexports.encode = function (number) {\n if (0 <= number && number < intToCharMap.length) {\n return intToCharMap[number];\n }\n throw new TypeError(\"Must be between 0 and 63: \" + number);\n};\n\n/**\n * Decode a single base 64 character code digit to an integer. Returns -1 on\n * failure.\n */\nexports.decode = function (charCode) {\n var bigA = 65; // 'A'\n var bigZ = 90; // 'Z'\n\n var littleA = 97; // 'a'\n var littleZ = 122; // 'z'\n\n var zero = 48; // '0'\n var nine = 57; // '9'\n\n var plus = 43; // '+'\n var slash = 47; // '/'\n\n var littleOffset = 26;\n var numberOffset = 52;\n\n // 0 - 25: ABCDEFGHIJKLMNOPQRSTUVWXYZ\n if (bigA <= charCode && charCode <= bigZ) {\n return (charCode - bigA);\n }\n\n // 26 - 51: abcdefghijklmnopqrstuvwxyz\n if (littleA <= charCode && charCode <= littleZ) {\n return (charCode - littleA + littleOffset);\n }\n\n // 52 - 61: 0123456789\n if (zero <= charCode && charCode <= nine) {\n return (charCode - zero + numberOffset);\n }\n\n // 62: +\n if (charCode == plus) {\n return 62;\n }\n\n // 63: /\n if (charCode == slash) {\n return 63;\n }\n\n // Invalid base64 digit.\n return -1;\n};\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n *\n * Based on the Base 64 VLQ implementation in Closure Compiler:\n * https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java\n *\n * Copyright 2011 The Closure Compiler Authors. All rights reserved.\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above\n * copyright notice, this list of conditions and the following\n * disclaimer in the documentation and/or other materials provided\n * with the distribution.\n * * Neither the name of Google Inc. nor the names of its\n * contributors may be used to endorse or promote products derived\n * from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\nvar base64 = require('./base64');\n\n// A single base 64 digit can contain 6 bits of data. For the base 64 variable\n// length quantities we use in the source map spec, the first bit is the sign,\n// the next four bits are the actual value, and the 6th bit is the\n// continuation bit. The continuation bit tells us whether there are more\n// digits in this value following this digit.\n//\n// Continuation\n// | Sign\n// | |\n// V V\n// 101011\n\nvar VLQ_BASE_SHIFT = 5;\n\n// binary: 100000\nvar VLQ_BASE = 1 << VLQ_BASE_SHIFT;\n\n// binary: 011111\nvar VLQ_BASE_MASK = VLQ_BASE - 1;\n\n// binary: 100000\nvar VLQ_CONTINUATION_BIT = VLQ_BASE;\n\n/**\n * Converts from a two-complement value to a value where the sign bit is\n * placed in the least significant bit. For example, as decimals:\n * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)\n * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)\n */\nfunction toVLQSigned(aValue) {\n return aValue < 0\n ? ((-aValue) << 1) + 1\n : (aValue << 1) + 0;\n}\n\n/**\n * Converts to a two-complement value from a value where the sign bit is\n * placed in the least significant bit. For example, as decimals:\n * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1\n * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2\n */\nfunction fromVLQSigned(aValue) {\n var isNegative = (aValue & 1) === 1;\n var shifted = aValue >> 1;\n return isNegative\n ? -shifted\n : shifted;\n}\n\n/**\n * Returns the base 64 VLQ encoded value.\n */\nexports.encode = function base64VLQ_encode(aValue) {\n var encoded = \"\";\n var digit;\n\n var vlq = toVLQSigned(aValue);\n\n do {\n digit = vlq & VLQ_BASE_MASK;\n vlq >>>= VLQ_BASE_SHIFT;\n if (vlq > 0) {\n // There are still more digits in this value, so we must make sure the\n // continuation bit is marked.\n digit |= VLQ_CONTINUATION_BIT;\n }\n encoded += base64.encode(digit);\n } while (vlq > 0);\n\n return encoded;\n};\n\n/**\n * Decodes the next base 64 VLQ value from the given string and returns the\n * value and the rest of the string via the out parameter.\n */\nexports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) {\n var strLen = aStr.length;\n var result = 0;\n var shift = 0;\n var continuation, digit;\n\n do {\n if (aIndex >= strLen) {\n throw new Error(\"Expected more digits in base 64 VLQ value.\");\n }\n\n digit = base64.decode(aStr.charCodeAt(aIndex++));\n if (digit === -1) {\n throw new Error(\"Invalid base64 digit: \" + aStr.charAt(aIndex - 1));\n }\n\n continuation = !!(digit & VLQ_CONTINUATION_BIT);\n digit &= VLQ_BASE_MASK;\n result = result + (digit << shift);\n shift += VLQ_BASE_SHIFT;\n } while (continuation);\n\n aOutParam.value = fromVLQSigned(result);\n aOutParam.rest = aIndex;\n};\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\n/**\n * This is a helper function for getting values from parameter/options\n * objects.\n *\n * @param args The object we are extracting values from\n * @param name The name of the property we are getting.\n * @param defaultValue An optional value to return if the property is missing\n * from the object. If this is not specified and the property is missing, an\n * error will be thrown.\n */\nfunction getArg(aArgs, aName, aDefaultValue) {\n if (aName in aArgs) {\n return aArgs[aName];\n } else if (arguments.length === 3) {\n return aDefaultValue;\n } else {\n throw new Error('\"' + aName + '\" is a required argument.');\n }\n}\nexports.getArg = getArg;\n\nvar urlRegexp = /^(?:([\\w+\\-.]+):)?\\/\\/(?:(\\w+:\\w+)@)?([\\w.-]*)(?::(\\d+))?(.*)$/;\nvar dataUrlRegexp = /^data:.+\\,.+$/;\n\nfunction urlParse(aUrl) {\n var match = aUrl.match(urlRegexp);\n if (!match) {\n return null;\n }\n return {\n scheme: match[1],\n auth: match[2],\n host: match[3],\n port: match[4],\n path: match[5]\n };\n}\nexports.urlParse = urlParse;\n\nfunction urlGenerate(aParsedUrl) {\n var url = '';\n if (aParsedUrl.scheme) {\n url += aParsedUrl.scheme + ':';\n }\n url += '//';\n if (aParsedUrl.auth) {\n url += aParsedUrl.auth + '@';\n }\n if (aParsedUrl.host) {\n url += aParsedUrl.host;\n }\n if (aParsedUrl.port) {\n url += \":\" + aParsedUrl.port\n }\n if (aParsedUrl.path) {\n url += aParsedUrl.path;\n }\n return url;\n}\nexports.urlGenerate = urlGenerate;\n\nvar MAX_CACHED_INPUTS = 32;\n\n/**\n * Takes some function `f(input) -> result` and returns a memoized version of\n * `f`.\n *\n * We keep at most `MAX_CACHED_INPUTS` memoized results of `f` alive. The\n * memoization is a dumb-simple, linear least-recently-used cache.\n */\nfunction lruMemoize(f) {\n var cache = [];\n\n return function(input) {\n for (var i = 0; i < cache.length; i++) {\n if (cache[i].input === input) {\n var temp = cache[0];\n cache[0] = cache[i];\n cache[i] = temp;\n return cache[0].result;\n }\n }\n\n var result = f(input);\n\n cache.unshift({\n input,\n result,\n });\n\n if (cache.length > MAX_CACHED_INPUTS) {\n cache.pop();\n }\n\n return result;\n };\n}\n\n/**\n * Normalizes a path, or the path portion of a URL:\n *\n * - Replaces consecutive slashes with one slash.\n * - Removes unnecessary '.' parts.\n * - Removes unnecessary '<dir>/..' parts.\n *\n * Based on code in the Node.js 'path' core module.\n *\n * @param aPath The path or url to normalize.\n */\nvar normalize = lruMemoize(function normalize(aPath) {\n var path = aPath;\n var url = urlParse(aPath);\n if (url) {\n if (!url.path) {\n return aPath;\n }\n path = url.path;\n }\n var isAbsolute = exports.isAbsolute(path);\n // Split the path into parts between `/` characters. This is much faster than\n // using `.split(/\\/+/g)`.\n var parts = [];\n var start = 0;\n var i = 0;\n while (true) {\n start = i;\n i = path.indexOf(\"/\", start);\n if (i === -1) {\n parts.push(path.slice(start));\n break;\n } else {\n parts.push(path.slice(start, i));\n while (i < path.length && path[i] === \"/\") {\n i++;\n }\n }\n }\n\n for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {\n part = parts[i];\n if (part === '.') {\n parts.splice(i, 1);\n } else if (part === '..') {\n up++;\n } else if (up > 0) {\n if (part === '') {\n // The first part is blank if the path is absolute. Trying to go\n // above the root is a no-op. Therefore we can remove all '..' parts\n // directly after the root.\n parts.splice(i + 1, up);\n up = 0;\n } else {\n parts.splice(i, 2);\n up--;\n }\n }\n }\n path = parts.join('/');\n\n if (path === '') {\n path = isAbsolute ? '/' : '.';\n }\n\n if (url) {\n url.path = path;\n return urlGenerate(url);\n }\n return path;\n});\nexports.normalize = normalize;\n\n/**\n * Joins two paths/URLs.\n *\n * @param aRoot The root path or URL.\n * @param aPath The path or URL to be joined with the root.\n *\n * - If aPath is a URL or a data URI, aPath is returned, unless aPath is a\n * scheme-relative URL: Then the scheme of aRoot, if any, is prepended\n * first.\n * - Otherwise aPath is a path. If aRoot is a URL, then its path portion\n * is updated with the result and aRoot is returned. Otherwise the result\n * is returned.\n * - If aPath is absolute, the result is aPath.\n * - Otherwise the two paths are joined with a slash.\n * - Joining for example 'http://' and 'www.example.com' is also supported.\n */\nfunction join(aRoot, aPath) {\n if (aRoot === \"\") {\n aRoot = \".\";\n }\n if (aPath === \"\") {\n aPath = \".\";\n }\n var aPathUrl = urlParse(aPath);\n var aRootUrl = urlParse(aRoot);\n if (aRootUrl) {\n aRoot = aRootUrl.path || '/';\n }\n\n // `join(foo, '//www.example.org')`\n if (aPathUrl && !aPathUrl.scheme) {\n if (aRootUrl) {\n aPathUrl.scheme = aRootUrl.scheme;\n }\n return urlGenerate(aPathUrl);\n }\n\n if (aPathUrl || aPath.match(dataUrlRegexp)) {\n return aPath;\n }\n\n // `join('http://', 'www.example.com')`\n if (aRootUrl && !aRootUrl.host && !aRootUrl.path) {\n aRootUrl.host = aPath;\n return urlGenerate(aRootUrl);\n }\n\n var joined = aPath.charAt(0) === '/'\n ? aPath\n : normalize(aRoot.replace(/\\/+$/, '') + '/' + aPath);\n\n if (aRootUrl) {\n aRootUrl.path = joined;\n return urlGenerate(aRootUrl);\n }\n return joined;\n}\nexports.join = join;\n\nexports.isAbsolute = function (aPath) {\n return aPath.charAt(0) === '/' || urlRegexp.test(aPath);\n};\n\n/**\n * Make a path relative to a URL or another path.\n *\n * @param aRoot The root path or URL.\n * @param aPath The path or URL to be made relative to aRoot.\n */\nfunction relative(aRoot, aPath) {\n if (aRoot === \"\") {\n aRoot = \".\";\n }\n\n aRoot = aRoot.replace(/\\/$/, '');\n\n // It is possible for the path to be above the root. In this case, simply\n // checking whether the root is a prefix of the path won't work. Instead, we\n // need to remove components from the root one by one, until either we find\n // a prefix that fits, or we run out of components to remove.\n var level = 0;\n while (aPath.indexOf(aRoot + '/') !== 0) {\n var index = aRoot.lastIndexOf(\"/\");\n if (index < 0) {\n return aPath;\n }\n\n // If the only part of the root that is left is the scheme (i.e. http://,\n // file:///, etc.), one or more slashes (/), or simply nothing at all, we\n // have exhausted all components, so the path is not relative to the root.\n aRoot = aRoot.slice(0, index);\n if (aRoot.match(/^([^\\/]+:\\/)?\\/*$/)) {\n return aPath;\n }\n\n ++level;\n }\n\n // Make sure we add a \"../\" for each component we removed from the root.\n return Array(level + 1).join(\"../\") + aPath.substr(aRoot.length + 1);\n}\nexports.relative = relative;\n\nvar supportsNullProto = (function () {\n var obj = Object.create(null);\n return !('__proto__' in obj);\n}());\n\nfunction identity (s) {\n return s;\n}\n\n/**\n * Because behavior goes wacky when you set `__proto__` on objects, we\n * have to prefix all the strings in our set with an arbitrary character.\n *\n * See https://github.com/mozilla/source-map/pull/31 and\n * https://github.com/mozilla/source-map/issues/30\n *\n * @param String aStr\n */\nfunction toSetString(aStr) {\n if (isProtoString(aStr)) {\n return '$' + aStr;\n }\n\n return aStr;\n}\nexports.toSetString = supportsNullProto ? identity : toSetString;\n\nfunction fromSetString(aStr) {\n if (isProtoString(aStr)) {\n return aStr.slice(1);\n }\n\n return aStr;\n}\nexports.fromSetString = supportsNullProto ? identity : fromSetString;\n\nfunction isProtoString(s) {\n if (!s) {\n return false;\n }\n\n var length = s.length;\n\n if (length < 9 /* \"__proto__\".length */) {\n return false;\n }\n\n if (s.charCodeAt(length - 1) !== 95 /* '_' */ ||\n s.charCodeAt(length - 2) !== 95 /* '_' */ ||\n s.charCodeAt(length - 3) !== 111 /* 'o' */ ||\n s.charCodeAt(length - 4) !== 116 /* 't' */ ||\n s.charCodeAt(length - 5) !== 111 /* 'o' */ ||\n s.charCodeAt(length - 6) !== 114 /* 'r' */ ||\n s.charCodeAt(length - 7) !== 112 /* 'p' */ ||\n s.charCodeAt(length - 8) !== 95 /* '_' */ ||\n s.charCodeAt(length - 9) !== 95 /* '_' */) {\n return false;\n }\n\n for (var i = length - 10; i >= 0; i--) {\n if (s.charCodeAt(i) !== 36 /* '$' */) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Comparator between two mappings where the original positions are compared.\n *\n * Optionally pass in `true` as `onlyCompareGenerated` to consider two\n * mappings with the same original source/line/column, but different generated\n * line and column the same. Useful when searching for a mapping with a\n * stubbed out mapping.\n */\nfunction compareByOriginalPositions(mappingA, mappingB, onlyCompareOriginal) {\n var cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0 || onlyCompareOriginal) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByOriginalPositions = compareByOriginalPositions;\n\nfunction compareByOriginalPositionsNoSource(mappingA, mappingB, onlyCompareOriginal) {\n var cmp\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0 || onlyCompareOriginal) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByOriginalPositionsNoSource = compareByOriginalPositionsNoSource;\n\n/**\n * Comparator between two mappings with deflated source and name indices where\n * the generated positions are compared.\n *\n * Optionally pass in `true` as `onlyCompareGenerated` to consider two\n * mappings with the same generated line and column, but different\n * source/name/original line and column the same. Useful when searching for a\n * mapping with a stubbed out mapping.\n */\nfunction compareByGeneratedPositionsDeflated(mappingA, mappingB, onlyCompareGenerated) {\n var cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0 || onlyCompareGenerated) {\n return cmp;\n }\n\n cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByGeneratedPositionsDeflated = compareByGeneratedPositionsDeflated;\n\nfunction compareByGeneratedPositionsDeflatedNoLine(mappingA, mappingB, onlyCompareGenerated) {\n var cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0 || onlyCompareGenerated) {\n return cmp;\n }\n\n cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByGeneratedPositionsDeflatedNoLine = compareByGeneratedPositionsDeflatedNoLine;\n\nfunction strcmp(aStr1, aStr2) {\n if (aStr1 === aStr2) {\n return 0;\n }\n\n if (aStr1 === null) {\n return 1; // aStr2 !== null\n }\n\n if (aStr2 === null) {\n return -1; // aStr1 !== null\n }\n\n if (aStr1 > aStr2) {\n return 1;\n }\n\n return -1;\n}\n\n/**\n * Comparator between two mappings with inflated source and name strings where\n * the generated positions are compared.\n */\nfunction compareByGeneratedPositionsInflated(mappingA, mappingB) {\n var cmp = mappingA.generatedLine - mappingB.generatedLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.generatedColumn - mappingB.generatedColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = strcmp(mappingA.source, mappingB.source);\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalLine - mappingB.originalLine;\n if (cmp !== 0) {\n return cmp;\n }\n\n cmp = mappingA.originalColumn - mappingB.originalColumn;\n if (cmp !== 0) {\n return cmp;\n }\n\n return strcmp(mappingA.name, mappingB.name);\n}\nexports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflated;\n\n/**\n * Strip any JSON XSSI avoidance prefix from the string (as documented\n * in the source maps specification), and then parse the string as\n * JSON.\n */\nfunction parseSourceMapInput(str) {\n return JSON.parse(str.replace(/^\\)]}'[^\\n]*\\n/, ''));\n}\nexports.parseSourceMapInput = parseSourceMapInput;\n\n/**\n * Compute the URL of a source given the the source root, the source's\n * URL, and the source map's URL.\n */\nfunction computeSourceURL(sourceRoot, sourceURL, sourceMapURL) {\n sourceURL = sourceURL || '';\n\n if (sourceRoot) {\n // This follows what Chrome does.\n if (sourceRoot[sourceRoot.length - 1] !== '/' && sourceURL[0] !== '/') {\n sourceRoot += '/';\n }\n // The spec says:\n // Line 4: An optional source root, useful for relocating source\n // files on a server or removing repeated values in the\n // “sources” entry. This value is prepended to the individual\n // entries in the “source” field.\n sourceURL = sourceRoot + sourceURL;\n }\n\n // Historically, SourceMapConsumer did not take the sourceMapURL as\n // a parameter. This mode is still somewhat supported, which is why\n // this code block is conditional. However, it's preferable to pass\n // the source map URL to SourceMapConsumer, so that this function\n // can implement the source URL resolution algorithm as outlined in\n // the spec. This block is basically the equivalent of:\n // new URL(sourceURL, sourceMapURL).toString()\n // ... except it avoids using URL, which wasn't available in the\n // older releases of node still supported by this library.\n //\n // The spec says:\n // If the sources are not absolute URLs after prepending of the\n // “sourceRoot”, the sources are resolved relative to the\n // SourceMap (like resolving script src in a html document).\n if (sourceMapURL) {\n var parsed = urlParse(sourceMapURL);\n if (!parsed) {\n throw new Error(\"sourceMapURL could not be parsed\");\n }\n if (parsed.path) {\n // Strip the last path component, but keep the \"/\".\n var index = parsed.path.lastIndexOf('/');\n if (index >= 0) {\n parsed.path = parsed.path.substring(0, index + 1);\n }\n }\n sourceURL = join(urlGenerate(parsed), sourceURL);\n }\n\n return normalize(sourceURL);\n}\nexports.computeSourceURL = computeSourceURL;\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar util = require('./util');\nvar has = Object.prototype.hasOwnProperty;\nvar hasNativeMap = typeof Map !== \"undefined\";\n\n/**\n * A data structure which is a combination of an array and a set. Adding a new\n * member is O(1), testing for membership is O(1), and finding the index of an\n * element is O(1). Removing elements from the set is not supported. Only\n * strings are supported for membership.\n */\nfunction ArraySet() {\n this._array = [];\n this._set = hasNativeMap ? new Map() : Object.create(null);\n}\n\n/**\n * Static method for creating ArraySet instances from an existing array.\n */\nArraySet.fromArray = function ArraySet_fromArray(aArray, aAllowDuplicates) {\n var set = new ArraySet();\n for (var i = 0, len = aArray.length; i < len; i++) {\n set.add(aArray[i], aAllowDuplicates);\n }\n return set;\n};\n\n/**\n * Return how many unique items are in this ArraySet. If duplicates have been\n * added, than those do not count towards the size.\n *\n * @returns Number\n */\nArraySet.prototype.size = function ArraySet_size() {\n return hasNativeMap ? this._set.size : Object.getOwnPropertyNames(this._set).length;\n};\n\n/**\n * Add the given string to this set.\n *\n * @param String aStr\n */\nArraySet.prototype.add = function ArraySet_add(aStr, aAllowDuplicates) {\n var sStr = hasNativeMap ? aStr : util.toSetString(aStr);\n var isDuplicate = hasNativeMap ? this.has(aStr) : has.call(this._set, sStr);\n var idx = this._array.length;\n if (!isDuplicate || aAllowDuplicates) {\n this._array.push(aStr);\n }\n if (!isDuplicate) {\n if (hasNativeMap) {\n this._set.set(aStr, idx);\n } else {\n this._set[sStr] = idx;\n }\n }\n};\n\n/**\n * Is the given string a member of this set?\n *\n * @param String aStr\n */\nArraySet.prototype.has = function ArraySet_has(aStr) {\n if (hasNativeMap) {\n return this._set.has(aStr);\n } else {\n var sStr = util.toSetString(aStr);\n return has.call(this._set, sStr);\n }\n};\n\n/**\n * What is the index of the given string in the array?\n *\n * @param String aStr\n */\nArraySet.prototype.indexOf = function ArraySet_indexOf(aStr) {\n if (hasNativeMap) {\n var idx = this._set.get(aStr);\n if (idx >= 0) {\n return idx;\n }\n } else {\n var sStr = util.toSetString(aStr);\n if (has.call(this._set, sStr)) {\n return this._set[sStr];\n }\n }\n\n throw new Error('\"' + aStr + '\" is not in the set.');\n};\n\n/**\n * What is the element at the given index?\n *\n * @param Number aIdx\n */\nArraySet.prototype.at = function ArraySet_at(aIdx) {\n if (aIdx >= 0 && aIdx < this._array.length) {\n return this._array[aIdx];\n }\n throw new Error('No element indexed by ' + aIdx);\n};\n\n/**\n * Returns the array representation of this set (which has the proper indices\n * indicated by indexOf). Note that this is a copy of the internal array used\n * for storing the members so that no one can mess with internal state.\n */\nArraySet.prototype.toArray = function ArraySet_toArray() {\n return this._array.slice();\n};\n\nexports.ArraySet = ArraySet;\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2014 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar util = require('./util');\n\n/**\n * Determine whether mappingB is after mappingA with respect to generated\n * position.\n */\nfunction generatedPositionAfter(mappingA, mappingB) {\n // Optimized for most common case\n var lineA = mappingA.generatedLine;\n var lineB = mappingB.generatedLine;\n var columnA = mappingA.generatedColumn;\n var columnB = mappingB.generatedColumn;\n return lineB > lineA || lineB == lineA && columnB >= columnA ||\n util.compareByGeneratedPositionsInflated(mappingA, mappingB) <= 0;\n}\n\n/**\n * A data structure to provide a sorted view of accumulated mappings in a\n * performance conscious manner. It trades a neglibable overhead in general\n * case for a large speedup in case of mappings being added in order.\n */\nfunction MappingList() {\n this._array = [];\n this._sorted = true;\n // Serves as infimum\n this._last = {generatedLine: -1, generatedColumn: 0};\n}\n\n/**\n * Iterate through internal items. This method takes the same arguments that\n * `Array.prototype.forEach` takes.\n *\n * NOTE: The order of the mappings is NOT guaranteed.\n */\nMappingList.prototype.unsortedForEach =\n function MappingList_forEach(aCallback, aThisArg) {\n this._array.forEach(aCallback, aThisArg);\n };\n\n/**\n * Add the given source mapping.\n *\n * @param Object aMapping\n */\nMappingList.prototype.add = function MappingList_add(aMapping) {\n if (generatedPositionAfter(this._last, aMapping)) {\n this._last = aMapping;\n this._array.push(aMapping);\n } else {\n this._sorted = false;\n this._array.push(aMapping);\n }\n};\n\n/**\n * Returns the flat, sorted array of mappings. The mappings are sorted by\n * generated position.\n *\n * WARNING: This method returns internal data without copying, for\n * performance. The return value must NOT be mutated, and should be treated as\n * an immutable borrow. If you want to take ownership, you must make your own\n * copy.\n */\nMappingList.prototype.toArray = function MappingList_toArray() {\n if (!this._sorted) {\n this._array.sort(util.compareByGeneratedPositionsInflated);\n this._sorted = true;\n }\n return this._array;\n};\n\nexports.MappingList = MappingList;\n","/* -*- Mode: js; js-indent-level: 2; -*- */\n/*\n * Copyright 2011 Mozilla Foundation and contributors\n * Licensed under the New BSD license. See LICENSE or:\n * http://opensource.org/licenses/BSD-3-Clause\n */\n\nvar base64VLQ = require('./base64-vlq');\nvar util = require('./util');\nvar ArraySet = require('./array-set').ArraySet;\nvar MappingList = require('./mapping-list').MappingList;\n\n/**\n * An instance of the SourceMapGenerator represents a source map which is\n * being built incrementally. You may pass an object with the following\n * properties:\n *\n * - file: The filename of the generated source.\n * - sourceRoot: A root for all relative URLs in this source map.\n */\nfunction SourceMapGenerator(aArgs) {\n if (!aArgs) {\n aArgs = {};\n }\n this._file = util.getArg(aArgs, 'file', null);\n this._sourceRoot = util.getArg(aArgs, 'sourceRoot', null);\n this._skipValidation = util.getArg(aArgs, 'skipValidation', false);\n this._ignoreInvalidMapping = util.getArg(aArgs, 'ignoreInvalidMapping', false);\n this._sources = new ArraySet();\n this._names = new ArraySet();\n this._mappings = new MappingList();\n this._sourcesContents = null;\n}\n\nSourceMapGenerator.prototype._version = 3;\n\n/**\n * Creates a new SourceMapGenerator based on a SourceMapConsumer\n *\n * @param aSourceMapConsumer The SourceMap.\n */\nSourceMapGenerator.fromSourceMap =\n function SourceMapGenerator_fromSourceMap(aSourceMapConsumer, generatorOps) {\n var sourceRoot = aSourceMapConsumer.sourceRoot;\n var generator = new SourceMapGenerator(Object.assign(generatorOps || {}, {\n file: aSourceMapConsumer.file,\n sourceRoot: sourceRoot\n }));\n aSourceMapConsumer.eachMapping(function (mapping) {\n var newMapping = {\n generated: {\n line: mapping.generatedLine,\n column: mapping.generatedColumn\n }\n };\n\n if (mapping.source != null) {\n newMapping.source = mapping.source;\n if (sourceRoot != null) {\n newMapping.source = util.relative(sourceRoot, newMapping.source);\n }\n\n newMapping.original = {\n line: mapping.originalLine,\n column: mapping.originalColumn\n };\n\n if (mapping.name != null) {\n newMapping.name = mapping.name;\n }\n }\n\n generator.addMapping(newMapping);\n });\n aSourceMapConsumer.sources.forEach(function (sourceFile) {\n var sourceRelative = sourceFile;\n if (sourceRoot !== null) {\n sourceRelative = util.relative(sourceRoot, sourceFile);\n }\n\n if (!generator._sources.has(sourceRelative)) {\n generator._sources.add(sourceRelative);\n }\n\n var content = aSourceMapConsumer.sourceContentFor(sourceFile);\n if (content != null) {\n generator.setSourceContent(sourceFile, content);\n }\n });\n return generator;\n };\n\n/**\n * Add a single mapping from original source line and column to the generated\n * source's line and column for this source map being created. The mapping\n * object should have the following properties:\n *\n * - generated: An object with the generated line and column positions.\n * - original: An object with the original line and column positions.\n * - source: The original source file (relative to the sourceRoot).\n * - name: An optional original token name for this mapping.\n */\nSourceMapGenerator.prototype.addMapping =\n function SourceMapGenerator_addMapping(aArgs) {\n var generated = util.getArg(aArgs, 'generated');\n var original = util.getArg(aArgs, 'original', null);\n var source = util.getArg(aArgs, 'source', null);\n var name = util.getArg(aArgs, 'name', null);\n\n if (!this._skipValidation) {\n if (this._validateMapping(generated, original, source, name) === false) {\n return;\n }\n }\n\n if (source != null) {\n source = String(source);\n if (!this._sources.has(source)) {\n this._sources.add(source);\n }\n }\n\n if (name != null) {\n name = String(name);\n if (!this._names.has(name)) {\n this._names.add(name);\n }\n }\n\n this._mappings.add({\n generatedLine: generated.line,\n generatedColumn: generated.column,\n originalLine: original != null && original.line,\n originalColumn: original != null && original.column,\n source: source,\n name: name\n });\n };\n\n/**\n * Set the source content for a source file.\n */\nSourceMapGenerator.prototype.setSourceContent =\n function SourceMapGenerator_setSourceContent(aSourceFile, aSourceContent) {\n var source = aSourceFile;\n if (this._sourceRoot != null) {\n source = util.relative(this._sourceRoot, source);\n }\n\n if (aSourceContent != null) {\n // Add the source content to the _sourcesContents map.\n // Create a new _sourcesContents map if the property is null.\n if (!this._sourcesContents) {\n this._sourcesContents = Object.create(null);\n }\n this._sourcesContents[util.toSetString(source)] = aSourceContent;\n } else if (this._sourcesContents) {\n // Remove the source file from the _sourcesContents map.\n // If the _sourcesContents map is empty, set the property to null.\n delete this._sourcesContents[util.toSetString(source)];\n if (Object.keys(this._sourcesContents).length === 0) {\n this._sourcesContents = null;\n }\n }\n };\n\n/**\n * Applies the mappings of a sub-source-map for a specific source file to the\n * source map being generated. Each mapping to the supplied source file is\n * rewritten using the supplied source map. Note: The resolution for the\n * resulting mappings is the minimium of this map and the supplied map.\n *\n * @param aSourceMapConsumer The source map to be applied.\n * @param aSourceFile Optional. The filename of the source file.\n * If omitted, SourceMapConsumer's file property will be used.\n * @param aSourceMapPath Optional. The dirname of the path to the source map\n * to be applied. If relative, it is relative to the SourceMapConsumer.\n * This parameter is needed when the two source maps aren't in the same\n * directory, and the source map to be applied contains relative source\n * paths. If so, those relative source paths need to be rewritten\n * relative to the SourceMapGenerator.\n */\nSourceMapGenerator.prototype.applySourceMap =\n function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile, aSourceMapPath) {\n var sourceFile = aSourceFile;\n // If aSourceFile is omitted, we will use the file property of the SourceMap\n if (aSourceFile == null) {\n if (aSourceMapConsumer.file == null) {\n throw new Error(\n 'SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, ' +\n 'or the source map\\'s \"file\" property. Both were omitted.'\n );\n }\n sourceFile = aSourceMapConsumer.file;\n }\n var sourceRoot = this._sourceRoot;\n // Make \"sourceFile\" relative if an absolute Url is passed.\n if (sourceRoot != null) {\n sourceFile = util.relative(sourceRoot, sourceFile);\n }\n // Applying the SourceMap can add and remove items from the sources and\n // the names array.\n var newSources = new ArraySet();\n var newNames = new ArraySet();\n\n // Find mappings for the \"sourceFile\"\n this._mappings.unsortedForEach(function (mapping) {\n if (mapping.source === sourceFile && mapping.originalLine != null) {\n // Check if it can be mapped by the source map, then update the mapping.\n var original = aSourceMapConsumer.originalPositionFor({\n line: mapping.originalLine,\n column: mapping.originalColumn\n });\n if (original.source != null) {\n // Copy mapping\n mapping.source = original.source;\n if (aSourceMapPath != null) {\n mapping.source = util.join(aSourceMapPath, mapping.source)\n }\n if (sourceRoot != null) {\n mapping.source = util.relative(sourceRoot, mapping.source);\n }\n mapping.originalLine = original.line;\n mapping.originalColumn = original.column;\n if (original.name != null) {\n mapping.name = original.name;\n }\n }\n }\n\n var source = mapping.source;\n if (source != null && !newSources.has(source)) {\n newSources.add(source);\n }\n\n var name = mapping.name;\n if (name != null && !newNames.has(name)) {\n newNames.add(name);\n }\n\n }, this);\n this._sources = newSources;\n this._names = newNames;\n\n // Copy sourcesContents of applied map.\n aSourceMapConsumer.sources.forEach(function (sourceFile) {\n var content = aSourceMapConsumer.sourceContentFor(sourceFile);\n if (content != null) {\n if (aSourceMapPath != null) {\n sourceFile = util.join(aSourceMapPath, sourceFile);\n }\n if (sourceRoot != null) {\n sourceFile = util.relative(sourceRoot, sourceFile);\n }\n this.setSourceContent(sourceFile, content);\n }\n }, this);\n };\n\n/**\n * A mapping can have one of the three levels of data:\n *\n * 1. Just the generated position.\n * 2. The Generated position, original position, and original source.\n * 3. Generated and original position, original source, as well as a name\n * token.\n *\n * To maintain consistency, we validate that any new mapping being added falls\n * in to one of these categories.\n */\nSourceMapGenerator.prototype._validateMapping =\n function SourceMapGenerator_validateMapping(aGenerated, aOriginal, aSource,\n aName) {\n // When aOriginal is truthy but has empty values for .line and .column,\n // it is most likely a programmer error. In this case we throw a very\n // specific error message to try to guide them the right way.\n // For example: https://github.com/Polymer/polymer-bundler/pull/519\n if (aOriginal && typeof aOriginal.line !== 'number' && typeof aOriginal.column !== 'number') {\n var message = 'original.line and original.column are not numbers -- you probably meant to omit ' +\n 'the original mapping entirely and only map the generated position. If so, pass ' +\n 'null for the original mapping instead of an object with empty or null values.'\n\n if (this._ignoreInvalidMapping) {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(message);\n }\n return false;\n } else {\n throw new Error(message);\n }\n }\n\n if (aGenerated && 'line' in aGenerated && 'column' in aGenerated\n && aGenerated.line > 0 && aGenerated.column >= 0\n && !aOriginal && !aSource && !aName) {\n // Case 1.\n return;\n }\n else if (aGenerated && 'line' in aGenerated && 'column' in aGenerated\n && aOriginal && 'line' in aOriginal && 'column' in aOriginal\n && aGenerated.line > 0 && aGenerated.column >= 0\n && aOriginal.line > 0 && aOriginal.column >= 0\n && aSource) {\n // Cases 2 and 3.\n return;\n }\n else {\n var message = 'Invalid mapping: ' + JSON.stringify({\n generated: aGenerated,\n source: aSource,\n original: aOriginal,\n name: aName\n });\n\n if (this._ignoreInvalidMapping) {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(message);\n }\n return false;\n } else {\n throw new Error(message)\n }\n }\n };\n\n/**\n * Serialize the accumulated mappings in to the stream of base 64 VLQs\n * specified by the source map format.\n */\nSourceMapGenerator.prototype._serializeMappings =\n function SourceMapGenerator_serializeMappings() {\n var previousGeneratedColumn = 0;\n var previousGeneratedLine = 1;\n var previousOriginalColumn = 0;\n var previousOriginalLine = 0;\n var previousName = 0;\n var previousSource = 0;\n var result = '';\n var next;\n var mapping;\n var nameIdx;\n var sourceIdx;\n\n var mappings = this._mappings.toArray();\n for (var i = 0, len = mappings.length; i < len; i++) {\n mapping = mappings[i];\n next = ''\n\n if (mapping.generatedLine !== previousGeneratedLine) {\n previousGeneratedColumn = 0;\n while (mapping.generatedLine !== previousGeneratedLine) {\n next += ';';\n previousGeneratedLine++;\n }\n }\n else {\n if (i > 0) {\n if (!util.compareByGeneratedPositionsInflated(mapping, mappings[i - 1])) {\n continue;\n }\n next += ',';\n }\n }\n\n next += base64VLQ.encode(mapping.generatedColumn\n - previousGeneratedColumn);\n previousGeneratedColumn = mapping.generatedColumn;\n\n if (mapping.source != null) {\n sourceIdx = this._sources.indexOf(mapping.source);\n next += base64VLQ.encode(sourceIdx - previousSource);\n previousSource = sourceIdx;\n\n // lines are stored 0-based in SourceMap spec version 3\n next += base64VLQ.encode(mapping.originalLine - 1\n - previousOriginalLine);\n previousOriginalLine = mapping.originalLine - 1;\n\n next += base64VLQ.encode(mapping.originalColumn\n - previousOriginalColumn);\n previousOriginalColumn = mapping.originalColumn;\n\n if (mapping.name != null) {\n nameIdx = this._names.indexOf(mapping.name);\n next += base64VLQ.encode(nameIdx - previousName);\n previousName = nameIdx;\n }\n }\n\n result += next;\n }\n\n return result;\n };\n\nSourceMapGenerator.prototype._generateSourcesContent =\n function SourceMapGenerator_generateSourcesContent(aSources, aSourceRoot) {\n return aSources.map(function (source) {\n if (!this._sourcesContents) {\n return null;\n }\n if (aSourceRoot != null) {\n source = util.relative(aSourceRoot, source);\n }\n var key = util.toSetString(source);\n return Object.prototype.hasOwnProperty.call(this._sourcesContents, key)\n ? this._sourcesContents[key]\n : null;\n }, this);\n };\n\n/**\n * Externalize the source map.\n */\nSourceMapGenerator.prototype.toJSON =\n function SourceMapGenerator_toJSON() {\n var map = {\n version: this._version,\n sources: this._sources.toArray(),\n names: this._names.toArray(),\n mappings: this._serializeMappings()\n };\n if (this._file != null) {\n map.file = this._file;\n }\n if (this._sourceRoot != null) {\n map.sourceRoot = this._sourceRoot;\n }\n if (this._sourcesContents) {\n map.sourcesContent = this._generateSourcesContent(map.sources, map.sourceRoot);\n }\n\n return map;\n };\n\n/**\n * Render the source map being generated to a string.\n */\nSourceMapGenerator.prototype.toString =\n function SourceMapGenerator_toString() {\n return JSON.stringify(this.toJSON());\n };\n\nexports.SourceMapGenerator = SourceMapGenerator;\n"],"x_google_ignoreList":[0,1,2,3,4,5],"mappings":"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"} |
| //#region src/core/ast-scanner/svelte-parser.ts | ||
| let _compiler = null; | ||
| async function loadCompiler() { | ||
| if (_compiler) return _compiler; | ||
| try { | ||
| _compiler = await import("./compiler-BZXAN4Ir.mjs"); | ||
| return _compiler; | ||
| } catch { | ||
| throw new Error("svelte is required to parse .svelte files. Install it with: pnpm add -D svelte"); | ||
| } | ||
| } | ||
| let _tsxParser = null; | ||
| async function loadTsxParser() { | ||
| if (_tsxParser) return _tsxParser; | ||
| try { | ||
| _tsxParser = (await import("./tsx-parser-ChduVwKJ.mjs")).parseTsx; | ||
| return _tsxParser; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| const SURROUNDING_MAX = 120; | ||
| /** Attributes whose values are CSS, not content */ | ||
| const CSS_ATTRIBUTES = new Set(["class", "style"]); | ||
| /** Directive types that contain code, not content */ | ||
| const CODE_DIRECTIVE_TYPES = new Set([ | ||
| "EventHandler", | ||
| "Binding", | ||
| "Action", | ||
| "Class", | ||
| "StyleDirective", | ||
| "Transition", | ||
| "Animation", | ||
| "Let", | ||
| "Ref" | ||
| ]); | ||
| function getLineAndColumn(content, offset) { | ||
| let line = 1; | ||
| let lastNewline = -1; | ||
| for (let i = 0; i < offset && i < content.length; i++) if (content[i] === "\n") { | ||
| line++; | ||
| lastNewline = i; | ||
| } | ||
| return { | ||
| line, | ||
| column: offset - lastNewline | ||
| }; | ||
| } | ||
| function getSurroundingByLine(content, line) { | ||
| const lines = content.split("\n"); | ||
| const idx = line - 1; | ||
| const start = Math.max(0, idx - 1); | ||
| const end = Math.min(lines.length - 1, idx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const l = lines[i]; | ||
| if (l !== void 0) parts.push(l); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| function walkTemplate(node, content, results, parentTag = "") { | ||
| switch (node.type) { | ||
| case "Fragment": { | ||
| const fragment = node; | ||
| for (const child of fragment.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "Text": { | ||
| const textNode = node; | ||
| const trimmed = textNode.data.trim(); | ||
| if (trimmed.length > 0 && /\S/.test(trimmed)) { | ||
| const pos = getLineAndColumn(content, textNode.start); | ||
| results.push({ | ||
| value: trimmed, | ||
| line: pos.line, | ||
| column: pos.column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(content, pos.line) | ||
| }); | ||
| } | ||
| break; | ||
| } | ||
| case "Element": | ||
| case "InlineComponent": | ||
| case "SlotTemplate": | ||
| case "Slot": | ||
| case "Head": | ||
| case "Title": | ||
| case "Window": | ||
| case "Document": | ||
| case "Body": { | ||
| const el = node; | ||
| const tag = el.name; | ||
| for (const attr of el.attributes) processAttribute(attr, tag, content, results); | ||
| for (const child of el.children) walkTemplate(child, content, results, tag); | ||
| break; | ||
| } | ||
| case "IfBlock": { | ||
| const ifBlock = node; | ||
| for (const child of ifBlock.children) walkTemplate(child, content, results, parentTag); | ||
| if (ifBlock.else) walkTemplate(ifBlock.else, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "ElseBlock": { | ||
| const elseBlock = node; | ||
| for (const child of elseBlock.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "EachBlock": { | ||
| const eachBlock = node; | ||
| for (const child of eachBlock.children) walkTemplate(child, content, results, parentTag); | ||
| if (eachBlock.else) walkTemplate(eachBlock.else, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "AwaitBlock": { | ||
| const awaitBlock = node; | ||
| if (awaitBlock.pending) walkTemplate(awaitBlock.pending, content, results, parentTag); | ||
| if (awaitBlock.then) walkTemplate(awaitBlock.then, content, results, parentTag); | ||
| if (awaitBlock.catch) walkTemplate(awaitBlock.catch, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "KeyBlock": { | ||
| const keyBlock = node; | ||
| for (const child of keyBlock.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| case "MustacheTag": | ||
| case "RawMustacheTag": break; | ||
| default: { | ||
| const unknownNode = node; | ||
| if (unknownNode.children) for (const child of unknownNode.children) walkTemplate(child, content, results, parentTag); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| function processAttribute(attr, parentTag, content, results) { | ||
| if (CODE_DIRECTIVE_TYPES.has(attr.type)) return; | ||
| if (attr.type !== "Attribute") return; | ||
| const attribute = attr; | ||
| const attrName = attribute.name; | ||
| if (!attribute.value || attribute.value.length === 0) return; | ||
| for (const valuePart of attribute.value) { | ||
| if (valuePart.type !== "Text") continue; | ||
| const textValue = valuePart.data; | ||
| if (!textValue || textValue.trim().length === 0) continue; | ||
| const pos = getLineAndColumn(content, valuePart.start); | ||
| if (CSS_ATTRIBUTES.has(attrName)) { | ||
| results.push({ | ||
| value: textValue, | ||
| line: pos.line, | ||
| column: pos.column, | ||
| context: "css_class", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, pos.line) | ||
| }); | ||
| continue; | ||
| } | ||
| results.push({ | ||
| value: textValue, | ||
| line: pos.line, | ||
| column: pos.column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(content, pos.line) | ||
| }); | ||
| } | ||
| } | ||
| /** | ||
| * Resolve script filename with correct extension for TypeScript parser. | ||
| * Svelte files with <script lang="ts"> need ScriptKind.TS, not ScriptKind.JS. | ||
| */ | ||
| function resolveScriptFileName(svelteFileName, lang) { | ||
| if (lang === "ts" || lang === "typescript") return svelteFileName.replace(/\.svelte$/, ".ts"); | ||
| return svelteFileName.replace(/\.svelte$/, ".js"); | ||
| } | ||
| function parseScriptBlock(scriptContent, scriptStartOffset, fullContent, fileName, parseTsx, lang) { | ||
| const scriptResults = parseTsx(scriptContent, resolveScriptFileName(fileName, lang)); | ||
| const scriptStartLine = getLineAndColumn(fullContent, scriptStartOffset).line; | ||
| return scriptResults.map((r) => { | ||
| r.line = r.line + scriptStartLine - 1; | ||
| r.scope = "script"; | ||
| return r; | ||
| }); | ||
| } | ||
| async function parseSvelte(content, fileName) { | ||
| const compiler = await loadCompiler(); | ||
| const results = []; | ||
| let ast; | ||
| try { | ||
| ast = compiler.parse(content, { filename: fileName }); | ||
| } catch { | ||
| return []; | ||
| } | ||
| if (ast.html) walkTemplate(ast.html, content, results); | ||
| const tsxParser = await loadTsxParser(); | ||
| if (tsxParser) { | ||
| if (ast.instance) { | ||
| const scriptStart = ast.instance.start; | ||
| const scriptEnd = ast.instance.end; | ||
| const scriptSource = content.slice(scriptStart, scriptEnd); | ||
| const scriptContentMatch = scriptSource.match(/<script[^>]*>([\s\S]*?)<\/script>/); | ||
| if (scriptContentMatch?.[1]) { | ||
| const scriptContentStr = scriptContentMatch[1]; | ||
| const scriptLang = scriptSource.match(/<script[^>]*\slang=["'](\w+)["']/)?.[1]; | ||
| const scriptResults = parseScriptBlock(scriptContentStr, scriptStart + (scriptSource.indexOf(">") + 1), content, fileName, tsxParser, scriptLang); | ||
| results.push(...scriptResults); | ||
| } | ||
| } | ||
| if (ast.module) { | ||
| const moduleStart = ast.module.start; | ||
| const moduleEnd = ast.module.end; | ||
| const moduleSource = content.slice(moduleStart, moduleEnd); | ||
| const moduleContentMatch = moduleSource.match(/<script[^>]*>([\s\S]*?)<\/script>/); | ||
| if (moduleContentMatch?.[1]) { | ||
| const moduleContentStr = moduleContentMatch[1]; | ||
| const moduleLang = moduleSource.match(/<script[^>]*\slang=["'](\w+)["']/)?.[1]; | ||
| const moduleResults = parseScriptBlock(moduleContentStr, moduleStart + (moduleSource.indexOf(">") + 1), content, fileName, tsxParser, moduleLang); | ||
| results.push(...moduleResults); | ||
| } | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| //#endregion | ||
| export { parseSvelte }; | ||
| //# sourceMappingURL=svelte-parser-Bll-1SK0.mjs.map |
| {"version":3,"file":"svelte-parser-Bll-1SK0.mjs","names":[],"sources":["../src/core/ast-scanner/svelte-parser.ts"],"sourcesContent":["// ─── Svelte SFC Parser for Scanner v2 ───\n// Parses .svelte Single File Components using svelte/compiler.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\nimport type { ExtractedString } from './types.js'\n\n// ─── Lazy-loaded svelte/compiler ───\n\ninterface _SvelteLoc {\n start: number\n end: number\n line: number\n column: number\n}\n\n// Svelte AST node types from svelte/compiler parse()\ninterface SvelteBaseNode {\n type: string\n start: number\n end: number\n}\n\ninterface SvelteFragment extends SvelteBaseNode {\n type: 'Fragment'\n children: SvelteNode[]\n}\n\ninterface SvelteElement extends SvelteBaseNode {\n type: 'Element' | 'InlineComponent' | 'SlotTemplate' | 'Slot' | 'Head' | 'Title' | 'Window' | 'Document' | 'Body'\n name: string\n attributes: SvelteAttribute[]\n children: SvelteNode[]\n}\n\ninterface SvelteText extends SvelteBaseNode {\n type: 'Text'\n data: string\n raw: string\n}\n\ninterface SvelteAttribute extends SvelteBaseNode {\n type: 'Attribute'\n name: string\n value: SvelteAttributeValue[]\n}\n\ninterface SvelteAttributeText extends SvelteBaseNode {\n type: 'Text'\n data: string\n raw: string\n}\n\ntype SvelteAttributeValue = SvelteAttributeText | SvelteMustacheTag | SvelteBaseNode\n\ninterface _SvelteSpread extends SvelteBaseNode {\n type: 'Spread'\n}\n\ninterface SvelteMustacheTag extends SvelteBaseNode {\n type: 'MustacheTag'\n expression: SvelteBaseNode\n}\n\ninterface SvelteIfBlock extends SvelteBaseNode {\n type: 'IfBlock'\n expression: SvelteBaseNode\n children: SvelteNode[]\n else?: SvelteElseBlock\n}\n\ninterface SvelteElseBlock extends SvelteBaseNode {\n type: 'ElseBlock'\n children: SvelteNode[]\n}\n\ninterface SvelteEachBlock extends SvelteBaseNode {\n type: 'EachBlock'\n expression: SvelteBaseNode\n children: SvelteNode[]\n else?: SvelteElseBlock\n}\n\ninterface SvelteAwaitBlock extends SvelteBaseNode {\n type: 'AwaitBlock'\n pending: SvelteFragment | null\n then: SvelteFragment | null\n catch: SvelteFragment | null\n}\n\ninterface SvelteKeyBlock extends SvelteBaseNode {\n type: 'KeyBlock'\n children: SvelteNode[]\n}\n\ninterface SvelteRawMustacheTag extends SvelteBaseNode {\n type: 'RawMustacheTag'\n expression: SvelteBaseNode\n}\n\ninterface SvelteDirective extends SvelteBaseNode {\n type: 'EventHandler' | 'Binding' | 'Action' | 'Class' | 'StyleDirective' | 'Transition' | 'Animation' | 'Let' | 'Ref'\n name: string\n}\n\ninterface SvelteScript extends SvelteBaseNode {\n type: 'Script'\n content: string\n context?: string // \"module\" for <script context=\"module\">\n}\n\ninterface SvelteStyle extends SvelteBaseNode {\n type: 'Style'\n}\n\ntype SvelteNode =\n | SvelteFragment\n | SvelteElement\n | SvelteText\n | SvelteMustacheTag\n | SvelteIfBlock\n | SvelteEachBlock\n | SvelteAwaitBlock\n | SvelteKeyBlock\n | SvelteRawMustacheTag\n | SvelteBaseNode\n\ninterface SvelteAst {\n html: SvelteFragment\n instance?: SvelteScript\n module?: SvelteScript\n css?: SvelteStyle\n}\n\ninterface SvelteCompiler {\n parse: (source: string, options?: { filename?: string }) => SvelteAst\n}\n\nlet _compiler: SvelteCompiler | null = null\n\nasync function loadCompiler(): Promise<SvelteCompiler> {\n if (_compiler) return _compiler\n try {\n const mod = await import('svelte/compiler')\n _compiler = mod as unknown as SvelteCompiler\n return _compiler\n } catch {\n throw new Error(\n 'svelte is required to parse .svelte files. '\n + 'Install it with: pnpm add -D svelte',\n )\n }\n}\n\n// ─── tsx-parser delegation ───\n\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n return null\n }\n}\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n/** Attributes whose values are CSS, not content */\nconst CSS_ATTRIBUTES = new Set(['class', 'style'])\n\n/** Directive types that contain code, not content */\nconst CODE_DIRECTIVE_TYPES = new Set([\n 'EventHandler', // on:click\n 'Binding', // bind:value\n 'Action', // use:action\n 'Class', // class:name\n 'StyleDirective', // style:color\n 'Transition', // transition:fade\n 'Animation', // animate:flip\n 'Let', // let:item\n 'Ref', // ref (legacy)\n])\n\n// ─── Helpers ───\n\nfunction getLineAndColumn(content: string, offset: number): { line: number; column: number } {\n let line = 1\n let lastNewline = -1\n\n for (let i = 0; i < offset && i < content.length; i++) {\n if (content[i] === '\\n') {\n line++\n lastNewline = i\n }\n }\n\n return { line, column: offset - lastNewline }\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n const start = Math.max(0, idx - 1)\n const end = Math.min(lines.length - 1, idx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const l = lines[i]\n if (l !== undefined) {\n parts.push(l)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: SvelteNode,\n content: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n const nodeType = node.type\n\n switch (nodeType) {\n case 'Fragment': {\n const fragment = node as SvelteFragment\n for (const child of fragment.children) {\n walkTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'Text': {\n const textNode = node as SvelteText\n const trimmed = textNode.data.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n const pos = getLineAndColumn(content, textNode.start)\n results.push({\n value: trimmed,\n line: pos.line,\n column: pos.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(content, pos.line),\n })\n }\n break\n }\n\n case 'Element':\n case 'InlineComponent':\n case 'SlotTemplate':\n case 'Slot':\n case 'Head':\n case 'Title':\n case 'Window':\n case 'Document':\n case 'Body': {\n const el = node as SvelteElement\n const tag = el.name\n\n // Process attributes\n for (const attr of el.attributes) {\n processAttribute(attr, tag, content, results)\n }\n\n // Recurse into children\n for (const child of el.children) {\n walkTemplate(child, content, results, tag)\n }\n break\n }\n\n case 'IfBlock': {\n const ifBlock = node as SvelteIfBlock\n // Skip the expression (code) — walk children for content\n for (const child of ifBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n // Walk else branch\n if (ifBlock.else) {\n walkTemplate(ifBlock.else, content, results, parentTag)\n }\n break\n }\n\n case 'ElseBlock': {\n const elseBlock = node as SvelteElseBlock\n for (const child of elseBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'EachBlock': {\n const eachBlock = node as SvelteEachBlock\n // Skip the expression — walk children for content\n for (const child of eachBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n if (eachBlock.else) {\n walkTemplate(eachBlock.else, content, results, parentTag)\n }\n break\n }\n\n case 'AwaitBlock': {\n const awaitBlock = node as SvelteAwaitBlock\n if (awaitBlock.pending) {\n walkTemplate(awaitBlock.pending, content, results, parentTag)\n }\n if (awaitBlock.then) {\n walkTemplate(awaitBlock.then, content, results, parentTag)\n }\n if (awaitBlock.catch) {\n walkTemplate(awaitBlock.catch, content, results, parentTag)\n }\n break\n }\n\n case 'KeyBlock': {\n const keyBlock = node as SvelteKeyBlock\n for (const child of keyBlock.children) {\n walkTemplate(child, content, results, parentTag)\n }\n break\n }\n\n case 'MustacheTag':\n case 'RawMustacheTag': {\n // {expression} or {@html expression} — code expressions, skip\n break\n }\n\n default: {\n // For unknown node types, try to walk children\n const unknownNode = node as SvelteBaseNode & { children?: SvelteNode[] }\n if (unknownNode.children) {\n for (const child of unknownNode.children) {\n walkTemplate(child, content, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processAttribute(\n attr: SvelteAttribute | SvelteDirective | SvelteBaseNode,\n parentTag: string,\n content: string,\n results: ExtractedString[],\n): void {\n // Skip directive types (EventHandler, Binding, etc.) — they contain code\n if (CODE_DIRECTIVE_TYPES.has(attr.type)) return\n\n // Only process regular Attribute nodes\n if (attr.type !== 'Attribute') return\n\n const attribute = attr as SvelteAttribute\n const attrName = attribute.name\n\n // Skip if no value or empty value array\n if (!attribute.value || attribute.value.length === 0) return\n\n // Process each value segment (attribute values can be arrays in Svelte)\n for (const valuePart of attribute.value) {\n if (valuePart.type !== 'Text') continue\n\n const textValue = (valuePart as SvelteAttributeText).data\n if (!textValue || textValue.trim().length === 0) continue\n\n const pos = getLineAndColumn(content, valuePart.start)\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: textValue,\n line: pos.line,\n column: pos.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, pos.line),\n })\n continue\n }\n\n results.push({\n value: textValue,\n line: pos.line,\n column: pos.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(content, pos.line),\n })\n }\n}\n\n// ─── Script Block Parsing ───\n\n/**\n * Resolve script filename with correct extension for TypeScript parser.\n * Svelte files with <script lang=\"ts\"> need ScriptKind.TS, not ScriptKind.JS.\n */\nfunction resolveScriptFileName(svelteFileName: string, lang?: string): string {\n if (lang === 'ts' || lang === 'typescript') return svelteFileName.replace(/\\.svelte$/, '.ts')\n return svelteFileName.replace(/\\.svelte$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartOffset: number,\n fullContent: string,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n const scriptStartPos = getLineAndColumn(fullContent, scriptStartOffset)\n const scriptStartLine = scriptStartPos.line\n\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseSvelte(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n let ast: SvelteAst\n try {\n ast = compiler.parse(content, { filename: fileName })\n } catch {\n // If Svelte parsing fails, return empty — malformed files shouldn't block scanning\n return []\n }\n\n // ─── Template (html) ───\n if (ast.html) {\n walkTemplate(ast.html, content, results)\n }\n\n // ─── Script Block ───\n const tsxParser = await loadTsxParser()\n if (tsxParser) {\n if (ast.instance) {\n // <script> block — extract its content from source\n const scriptStart = ast.instance.start\n const scriptEnd = ast.instance.end\n\n // Find the content between <script> tags\n const scriptSource = content.slice(scriptStart, scriptEnd)\n const scriptContentMatch = scriptSource.match(/<script[^>]*>([\\s\\S]*?)<\\/script>/)\n if (scriptContentMatch?.[1]) {\n const scriptContentStr = scriptContentMatch[1]\n // Extract lang attribute from <script lang=\"ts\">\n const langMatch = scriptSource.match(/<script[^>]*\\slang=[\"'](\\w+)[\"']/)\n const scriptLang = langMatch?.[1]\n // Calculate offset of script content within the file\n const scriptTagEnd = scriptSource.indexOf('>') + 1\n const contentOffset = scriptStart + scriptTagEnd\n const scriptResults = parseScriptBlock(\n scriptContentStr,\n contentOffset,\n content,\n fileName,\n tsxParser,\n scriptLang,\n )\n results.push(...scriptResults)\n }\n }\n\n if (ast.module) {\n // <script context=\"module\"> block\n const moduleStart = ast.module.start\n const moduleEnd = ast.module.end\n const moduleSource = content.slice(moduleStart, moduleEnd)\n const moduleContentMatch = moduleSource.match(/<script[^>]*>([\\s\\S]*?)<\\/script>/)\n if (moduleContentMatch?.[1]) {\n const moduleContentStr = moduleContentMatch[1]\n const moduleLangMatch = moduleSource.match(/<script[^>]*\\slang=[\"'](\\w+)[\"']/)\n const moduleLang = moduleLangMatch?.[1]\n const scriptTagEnd = moduleSource.indexOf('>') + 1\n const contentOffset = moduleStart + scriptTagEnd\n const moduleResults = parseScriptBlock(\n moduleContentStr,\n contentOffset,\n content,\n fileName,\n tsxParser,\n moduleLang,\n )\n results.push(...moduleResults)\n }\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"} |
| import ts from "typescript"; | ||
| //#region src/core/ast-scanner/tsx-parser.ts | ||
| const SURROUNDING_MAX = 120; | ||
| /** | ||
| * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context. | ||
| * | ||
| * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed). | ||
| * Walks the AST and classifies each string by its parent chain. | ||
| */ | ||
| function parseTsx(content, fileName) { | ||
| const scriptKind = getScriptKind(fileName); | ||
| const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, scriptKind); | ||
| const results = []; | ||
| visit(sourceFile, sourceFile, content.split("\n"), results); | ||
| return results; | ||
| } | ||
| function getScriptKind(fileName) { | ||
| const lower = fileName.toLowerCase(); | ||
| if (lower.endsWith(".tsx")) return ts.ScriptKind.TSX; | ||
| if (lower.endsWith(".jsx")) return ts.ScriptKind.JSX; | ||
| if (lower.endsWith(".ts")) return ts.ScriptKind.TS; | ||
| return ts.ScriptKind.JS; | ||
| } | ||
| function visit(node, sourceFile, lines, results) { | ||
| if (ts.isJsxText(node)) { | ||
| const text = node.text.trim(); | ||
| if (text.length > 0) { | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); | ||
| const parent = getJsxParentTag(node); | ||
| results.push({ | ||
| value: text, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: "jsx_text", | ||
| scope: "script", | ||
| parent, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| } | ||
| return; | ||
| } | ||
| if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { | ||
| const value = node.text; | ||
| if (value.length === 0) return; | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); | ||
| const classification = classifyByParentChain(node, sourceFile); | ||
| results.push({ | ||
| value, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: classification.context, | ||
| scope: "script", | ||
| parent: classification.parent, | ||
| parentProperty: classification.parentProperty, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| return; | ||
| } | ||
| if (ts.isTemplateExpression(node)) { | ||
| extractTemplateParts(node, sourceFile, lines, results); | ||
| return; | ||
| } | ||
| ts.forEachChild(node, (child) => { | ||
| visit(child, sourceFile, lines, results); | ||
| }); | ||
| } | ||
| function extractTemplateParts(node, sourceFile, lines, results) { | ||
| const classification = classifyByParentChain(node, sourceFile); | ||
| const headText = node.head.text; | ||
| if (headText.length > 0) { | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile)); | ||
| results.push({ | ||
| value: headText, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: classification.context, | ||
| scope: "script", | ||
| parent: classification.parent, | ||
| parentProperty: classification.parentProperty, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| } | ||
| for (const span of node.templateSpans) { | ||
| const spanText = span.literal.text; | ||
| if (spanText.length > 0) { | ||
| const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile)); | ||
| results.push({ | ||
| value: spanText, | ||
| line: lineIdx + 1, | ||
| column: character + 1, | ||
| context: classification.context, | ||
| scope: "script", | ||
| parent: classification.parent, | ||
| parentProperty: classification.parentProperty, | ||
| surrounding: buildSurrounding(lines, lineIdx) | ||
| }); | ||
| } | ||
| visit(span.expression, sourceFile, lines, results); | ||
| } | ||
| } | ||
| function classifyByParentChain(node, sourceFile) { | ||
| let current = node.parent; | ||
| while (current) { | ||
| if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) return { | ||
| context: "import_path", | ||
| parent: "import" | ||
| }; | ||
| if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) return { | ||
| context: "import_path", | ||
| parent: "import" | ||
| }; | ||
| if (ts.isCallExpression(current) && ts.isIdentifier(current.expression) && current.expression.text === "require") return { | ||
| context: "import_path", | ||
| parent: "require" | ||
| }; | ||
| if (ts.isJsxAttribute(current)) { | ||
| const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile); | ||
| if (attrName === "className" || attrName === "class" || attrName === "style") return { | ||
| context: "css_class", | ||
| parent: attrName | ||
| }; | ||
| return { | ||
| context: "jsx_attribute", | ||
| parent: attrName, | ||
| parentProperty: attrName | ||
| }; | ||
| } | ||
| if (ts.isVariableDeclaration(current)) return { | ||
| context: "variable_assignment", | ||
| parent: ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile) | ||
| }; | ||
| if (ts.isPropertyAssignment(current)) { | ||
| const key = ts.isIdentifier(current.name) ? current.name.text : ts.isStringLiteral(current.name) ? current.name.text : current.name.getText(sourceFile); | ||
| return { | ||
| context: "object_property", | ||
| parent: key, | ||
| parentProperty: key | ||
| }; | ||
| } | ||
| if (ts.isEnumMember(current)) return { | ||
| context: "enum_value", | ||
| parent: ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile) | ||
| }; | ||
| if (ts.isCallExpression(current)) { | ||
| const callee = getCalleeName(current.expression, sourceFile); | ||
| if ([ | ||
| "cn", | ||
| "clsx", | ||
| "classNames", | ||
| "twMerge", | ||
| "twJoin", | ||
| "cva", | ||
| "cx" | ||
| ].includes(callee)) return { | ||
| context: "css_utility_call", | ||
| parent: callee | ||
| }; | ||
| if (callee.startsWith("console.")) return { | ||
| context: "console_call", | ||
| parent: callee | ||
| }; | ||
| if ([ | ||
| "describe", | ||
| "it", | ||
| "test", | ||
| "expect", | ||
| "beforeEach", | ||
| "afterEach", | ||
| "beforeAll", | ||
| "afterAll" | ||
| ].includes(callee)) return { | ||
| context: "test_assertion", | ||
| parent: callee | ||
| }; | ||
| return { | ||
| context: "function_argument", | ||
| parent: callee | ||
| }; | ||
| } | ||
| if (ts.isArrayLiteralExpression(current)) return { | ||
| context: "array_element", | ||
| parent: "array" | ||
| }; | ||
| if (ts.isCaseClause(current)) return { | ||
| context: "switch_case", | ||
| parent: "case" | ||
| }; | ||
| if (ts.isTypeAliasDeclaration(current) || ts.isTypeReferenceNode(current) || ts.isInterfaceDeclaration(current) || ts.isTypeLiteralNode(current) || ts.isLiteralTypeNode(current) || ts.isUnionTypeNode(current) || ts.isIntersectionTypeNode(current)) return { | ||
| context: "type_annotation", | ||
| parent: "type" | ||
| }; | ||
| if (ts.isPropertySignature(current)) return { | ||
| context: "type_annotation", | ||
| parent: "type" | ||
| }; | ||
| current = current.parent; | ||
| } | ||
| return { | ||
| context: "other", | ||
| parent: "" | ||
| }; | ||
| } | ||
| function getCalleeName(expr, sourceFile) { | ||
| if (ts.isIdentifier(expr)) return expr.text; | ||
| if (ts.isPropertyAccessExpression(expr)) { | ||
| const obj = getCalleeName(expr.expression, sourceFile); | ||
| return obj ? `${obj}.${expr.name.text}` : expr.name.text; | ||
| } | ||
| return expr.getText(sourceFile); | ||
| } | ||
| function getJsxParentTag(node) { | ||
| let current = node.parent; | ||
| while (current) { | ||
| if (ts.isJsxElement(current)) return current.openingElement.tagName.getText(); | ||
| if (ts.isJsxFragment(current)) return "Fragment"; | ||
| current = current.parent; | ||
| } | ||
| return ""; | ||
| } | ||
| function buildSurrounding(lines, lineIdx) { | ||
| const start = Math.max(0, lineIdx - 1); | ||
| const end = Math.min(lines.length - 1, lineIdx + 1); | ||
| const parts = []; | ||
| for (let i = start; i <= end; i++) { | ||
| const line = lines[i]; | ||
| if (line !== void 0) parts.push(line); | ||
| } | ||
| const joined = parts.join("\n"); | ||
| if (joined.length > SURROUNDING_MAX) return joined.slice(0, SURROUNDING_MAX); | ||
| return joined; | ||
| } | ||
| //#endregion | ||
| export { parseTsx as t }; | ||
| //# sourceMappingURL=tsx-parser-B_aI_C2r.mjs.map |
| {"version":3,"file":"tsx-parser-B_aI_C2r.mjs","names":[],"sources":["../src/core/ast-scanner/tsx-parser.ts"],"sourcesContent":["import ts from 'typescript'\nimport type { ExtractedString, StructuralContext } from './types.js'\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n// ─── Public API ───\n\n/**\n * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context.\n *\n * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed).\n * Walks the AST and classifies each string by its parent chain.\n */\nexport function parseTsx(content: string, fileName: string): ExtractedString[] {\n const scriptKind = getScriptKind(fileName)\n const sourceFile = ts.createSourceFile(\n fileName,\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n scriptKind,\n )\n\n const results: ExtractedString[] = []\n const lines = content.split('\\n')\n\n visit(sourceFile, sourceFile, lines, results)\n\n return results\n}\n\n// ─── Script kind detection ───\n\nfunction getScriptKind(fileName: string): ts.ScriptKind {\n const lower = fileName.toLowerCase()\n if (lower.endsWith('.tsx')) return ts.ScriptKind.TSX\n if (lower.endsWith('.jsx')) return ts.ScriptKind.JSX\n if (lower.endsWith('.ts')) return ts.ScriptKind.TS\n return ts.ScriptKind.JS\n}\n\n// ─── AST Walker ───\n\nfunction visit(\n node: ts.Node,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n // Handle JsxText nodes\n if (ts.isJsxText(node)) {\n const text = node.text.trim()\n // Skip whitespace-only or empty JsxText\n if (text.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const parent = getJsxParentTag(node)\n results.push({\n value: text,\n line: lineIdx + 1,\n column: character + 1,\n context: 'jsx_text',\n scope: 'script',\n parent,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n return\n }\n\n // Handle string literals\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {\n const value = node.text\n // Skip empty strings\n if (value.length === 0) return\n\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const classification = classifyByParentChain(node, sourceFile)\n\n results.push({\n value,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n return\n }\n\n // Handle template literals with expressions — extract static head/spans\n if (ts.isTemplateExpression(node)) {\n extractTemplateParts(node, sourceFile, lines, results)\n return\n }\n\n ts.forEachChild(node, (child) => {\n visit(child, sourceFile, lines, results)\n })\n}\n\n// ─── Template literal parts extraction ───\n\nfunction extractTemplateParts(\n node: ts.TemplateExpression,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n const classification = classifyByParentChain(node, sourceFile)\n\n // Head: text before first ${...}\n const headText = node.head.text\n if (headText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile))\n results.push({\n value: headText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Template spans: text after each ${...} expression\n for (const span of node.templateSpans) {\n const spanText = span.literal.text\n if (spanText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile))\n results.push({\n value: spanText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Visit the expression inside ${...} for nested strings\n visit(span.expression, sourceFile, lines, results)\n }\n}\n\n// ─── Parent chain classification ───\n\ninterface Classification {\n context: StructuralContext\n parent: string\n parentProperty?: string\n}\n\nfunction classifyByParentChain(node: ts.Node, sourceFile: ts.SourceFile): Classification {\n let current = node.parent\n\n while (current) {\n // Import / Export declaration → import_path\n if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // Import specifier module path (dynamic import)\n if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // require() calls\n if (\n ts.isCallExpression(current)\n && ts.isIdentifier(current.expression)\n && current.expression.text === 'require'\n ) {\n return { context: 'import_path', parent: 'require' }\n }\n\n // JSX attribute → jsx_attribute or css_class\n if (ts.isJsxAttribute(current)) {\n const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n // className / class / style → CSS, not content\n if (attrName === 'className' || attrName === 'class' || attrName === 'style') {\n return { context: 'css_class', parent: attrName }\n }\n return { context: 'jsx_attribute', parent: attrName, parentProperty: attrName }\n }\n\n // Variable declaration → variable_assignment\n if (ts.isVariableDeclaration(current)) {\n const varName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'variable_assignment', parent: varName }\n }\n\n // Property assignment → object_property\n if (ts.isPropertyAssignment(current)) {\n const key = ts.isIdentifier(current.name)\n ? current.name.text\n : ts.isStringLiteral(current.name)\n ? current.name.text\n : current.name.getText(sourceFile)\n return { context: 'object_property', parent: key, parentProperty: key }\n }\n\n // Enum member → enum_value\n if (ts.isEnumMember(current)) {\n const enumName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'enum_value', parent: enumName }\n }\n\n // Call expression → function_argument or css_utility_call or console_call or test_assertion\n if (ts.isCallExpression(current)) {\n const callee = getCalleeName(current.expression, sourceFile)\n // CSS utility functions\n if (['cn', 'clsx', 'classNames', 'twMerge', 'twJoin', 'cva', 'cx'].includes(callee)) {\n return { context: 'css_utility_call', parent: callee }\n }\n // Console calls\n if (callee.startsWith('console.')) {\n return { context: 'console_call', parent: callee }\n }\n // Test assertions\n if (['describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(callee)) {\n return { context: 'test_assertion', parent: callee }\n }\n return { context: 'function_argument', parent: callee }\n }\n\n // Array literal → array_element\n if (ts.isArrayLiteralExpression(current)) {\n return { context: 'array_element', parent: 'array' }\n }\n\n // Case clause → switch_case\n if (ts.isCaseClause(current)) {\n return { context: 'switch_case', parent: 'case' }\n }\n\n // Type contexts → type_annotation\n if (\n ts.isTypeAliasDeclaration(current)\n || ts.isTypeReferenceNode(current)\n || ts.isInterfaceDeclaration(current)\n || ts.isTypeLiteralNode(current)\n || ts.isLiteralTypeNode(current)\n || ts.isUnionTypeNode(current)\n || ts.isIntersectionTypeNode(current)\n ) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n // Property declaration with type context (e.g., `as const` typed properties)\n // Check if we're inside a type annotation specifically\n if (ts.isPropertySignature(current)) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n current = current.parent\n }\n\n return { context: 'other', parent: '' }\n}\n\n// ─── Callee name extraction ───\n\nfunction getCalleeName(expr: ts.Expression, sourceFile: ts.SourceFile): string {\n if (ts.isIdentifier(expr)) {\n return expr.text\n }\n if (ts.isPropertyAccessExpression(expr)) {\n // e.g., console.log → \"console.log\"\n const obj = getCalleeName(expr.expression, sourceFile)\n return obj ? `${obj}.${expr.name.text}` : expr.name.text\n }\n return expr.getText(sourceFile)\n}\n\n// ─── JSX parent tag extraction ───\n\nfunction getJsxParentTag(node: ts.Node): string {\n let current = node.parent\n\n while (current) {\n if (ts.isJsxElement(current)) {\n const tagName = current.openingElement.tagName.getText()\n return tagName\n }\n if (ts.isJsxFragment(current)) {\n return 'Fragment'\n }\n current = current.parent\n }\n\n return ''\n}\n\n// ─── Surrounding code builder ───\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n"],"mappings":"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"} |
| import { t as parseTsx } from "./tsx-parser-B_aI_C2r.mjs"; | ||
| export { parseTsx }; |
| import { i as metaFilePath, n as contentFilePath, r as documentFilePath, t as contentDirPath } from "./paths-CmVw5Cw2.mjs"; | ||
| import { c as writeText, s as writeJson } from "./fs-DLbVB-Ek.mjs"; | ||
| import { i as writeMeta } from "./meta-manager-CJUiTgP2.mjs"; | ||
| import { t as readConfig } from "./config-oxxgznz7.mjs"; | ||
| import { C as LocalReader, f as parseFrontmatter, g as resolveLocaleStrategy, o as listModels, s as readModel } from "./model-manager-DP2CZiMT.mjs"; | ||
| import { join } from "node:path"; | ||
| import { detectSecrets, validateFieldValue } from "@contentrain/types"; | ||
| import { rm } from "node:fs/promises"; | ||
| //#region src/core/validator/entry.ts | ||
| /** | ||
| * Validate a single content entry against its model's field schema. | ||
| * | ||
| * Merges the rule sets from MCP's legacy `validator.ts` (secret detection, | ||
| * schema validation, unique constraints) with Studio's `content-validation.ts` | ||
| * (email/url heuristics, polymorphic relation structure, nested object and | ||
| * array-of-object recursion). The union is the authoritative per-entry | ||
| * validator — both MCP's project validator and Studio's save path should | ||
| * converge on this function over time. | ||
| * | ||
| * Asynchronous relation-integrity checks (does the referenced entry exist?) | ||
| * live in `relation-integrity.ts` because they require I/O. | ||
| */ | ||
| function validateContent(data, fields, modelId, locale, entryId, ctx) { | ||
| const errors = []; | ||
| for (const [fieldId, def] of Object.entries(fields)) { | ||
| const value = data[fieldId]; | ||
| errors.push(...validateField(value, def, modelId, locale, entryId, fieldId, ctx)); | ||
| } | ||
| return { | ||
| valid: errors.filter((e) => e.severity === "error").length === 0, | ||
| errors | ||
| }; | ||
| } | ||
| /** Bounds `items`-inside-`items` nesting; far above any real schema. */ | ||
| const MAX_FIELD_DEPTH = 10; | ||
| function validateField(value, def, modelId, locale, entryId, fieldId, ctx, depth = 0) { | ||
| const errors = []; | ||
| const errCtx = { | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId | ||
| }; | ||
| if (depth > MAX_FIELD_DEPTH) return [{ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} exceeds the maximum nesting depth of ${MAX_FIELD_DEPTH}` | ||
| }]; | ||
| if (value !== null && value !== void 0 && value !== "") { | ||
| const secretErrors = detectSecrets(value); | ||
| for (const e of secretErrors) errors.push({ | ||
| ...e, | ||
| ...errCtx | ||
| }); | ||
| } | ||
| if (!(def.type === "relation" || def.type === "relations")) { | ||
| const fieldErrors = validateFieldValue(value, def); | ||
| if (fieldErrors.length > 0) { | ||
| for (const e of fieldErrors) errors.push({ | ||
| ...e, | ||
| ...errCtx | ||
| }); | ||
| if (fieldErrors.some((e) => e.severity === "error")) return errors; | ||
| } | ||
| } else if (def.required && (value === null || value === void 0 || value === "")) { | ||
| errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} is required` | ||
| }); | ||
| return errors; | ||
| } | ||
| if (value === null || value === void 0) return errors; | ||
| if (def.unique && ctx?.allEntries) { | ||
| const valueKey = String(value); | ||
| for (const [otherId, otherEntry] of Object.entries(ctx.allEntries)) { | ||
| if (otherId === ctx.currentEntryId) continue; | ||
| const otherValue = otherEntry[fieldId]; | ||
| if (otherValue !== null && otherValue !== void 0 && String(otherValue) === valueKey) { | ||
| errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be unique — "${String(value)}" already exists in entry ${otherId}` | ||
| }); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (def.type === "relation" && def.model) { | ||
| const targets = Array.isArray(def.model) ? def.model : [def.model]; | ||
| if (targets.length > 1) if (typeof value !== "object" || value === null || !("model" in value) || !("ref" in value)) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be { model, ref } for polymorphic relation` | ||
| }); | ||
| else { | ||
| const polyVal = value; | ||
| if (!targets.includes(polyVal.model)) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} target model "${polyVal.model}" must be one of: ${targets.join(", ")}` | ||
| }); | ||
| } | ||
| else if (typeof value !== "string") errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be a string (entry ID or slug)` | ||
| }); | ||
| } | ||
| if (def.type === "relations") if (!Array.isArray(value)) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must be an array` | ||
| }); | ||
| else { | ||
| if (def.min !== void 0 && value.length < def.min) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must have at least ${def.min} items` | ||
| }); | ||
| if (def.max !== void 0 && value.length > def.max) errors.push({ | ||
| severity: "error", | ||
| ...errCtx, | ||
| message: `${fieldId} must have at most ${def.max} items` | ||
| }); | ||
| const targets = Array.isArray(def.model) ? def.model : def.model ? [def.model] : []; | ||
| const polymorphic = targets.length > 1; | ||
| for (let i = 0; i < value.length; i++) { | ||
| const item = value[i]; | ||
| const itemCtx = { | ||
| ...errCtx, | ||
| field: `${fieldId}[${i}]` | ||
| }; | ||
| if (polymorphic) if (typeof item !== "object" || item === null || !("model" in item) || !("ref" in item)) errors.push({ | ||
| severity: "error", | ||
| ...itemCtx, | ||
| message: `${fieldId}[${i}] must be { model, ref } for polymorphic relations` | ||
| }); | ||
| else { | ||
| const polyItem = item; | ||
| if (!targets.includes(polyItem.model)) errors.push({ | ||
| severity: "error", | ||
| ...itemCtx, | ||
| message: `${fieldId}[${i}] target model "${polyItem.model}" must be one of: ${targets.join(", ")}` | ||
| }); | ||
| } | ||
| else if (typeof item !== "string") errors.push({ | ||
| severity: "error", | ||
| ...itemCtx, | ||
| message: `${fieldId}[${i}] must be a string (entry ID or slug)` | ||
| }); | ||
| } | ||
| } | ||
| if (def.type === "array" && Array.isArray(value) && def.items) { | ||
| const itemDef = typeof def.items === "string" ? { type: def.items } : def.items; | ||
| for (let i = 0; i < value.length; i++) errors.push(...validateField(value[i], itemDef, modelId, locale, entryId, `${fieldId}[${i}]`, void 0, depth + 1)); | ||
| } | ||
| if (def.type === "object" && def.fields && typeof value === "object" && value !== null && !Array.isArray(value)) { | ||
| const nested = validateContent(value, def.fields, modelId, locale, entryId, ctx); | ||
| for (const e of nested.errors) errors.push({ | ||
| ...e, | ||
| field: e.field ? `${fieldId}.${e.field}` : fieldId | ||
| }); | ||
| } | ||
| return errors; | ||
| } | ||
| //#endregion | ||
| //#region src/core/validator/relation-integrity.ts | ||
| /** | ||
| * Verify that relation and relations fields reference targets that actually | ||
| * exist. Two severities are supported — Studio's per-save flow emits | ||
| * `warning` (the referenced entry may still be drafted), while MCP's | ||
| * project-wide validator emits `error` and additionally flags missing | ||
| * target models via `resolveTarget`. | ||
| * | ||
| * The loader abstraction keeps this function I/O-agnostic: MCP wires it | ||
| * to filesystem reads through LocalReader, Studio wires it to a | ||
| * `GitProvider.readFile`-backed loader, mocks pass in-memory maps. | ||
| */ | ||
| async function checkRelationIntegrity(data, fields, modelId, locale, entryId, loadContent, opts = {}) { | ||
| const errors = []; | ||
| const severity = opts.severity ?? "warning"; | ||
| const resolve = opts.resolveTarget ?? (async (id, loc) => { | ||
| return { | ||
| exists: true, | ||
| content: await loadContent(id, loc) | ||
| }; | ||
| }); | ||
| for (const [fieldId, def] of Object.entries(fields)) { | ||
| const value = data[fieldId]; | ||
| if (value === null || value === void 0) continue; | ||
| if (def.type === "relation" && def.model) { | ||
| const targets = Array.isArray(def.model) ? def.model : [def.model]; | ||
| if (targets.length > 1 && typeof value === "object" && value !== null) { | ||
| const polyVal = value; | ||
| if (polyVal.model && polyVal.ref) { | ||
| const resolved = await resolve(polyVal.model, locale); | ||
| if (!resolved.exists) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: target model "${polyVal.model}" not found` | ||
| }); | ||
| else if (resolved.content && !(polyVal.ref in resolved.content)) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: "${polyVal.ref}" not found in ${polyVal.model}` | ||
| }); | ||
| } | ||
| } else if (typeof value === "string" && targets[0]) { | ||
| const resolved = await resolve(targets[0], locale); | ||
| if (!resolved.exists) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: target model "${targets[0]}" not found` | ||
| }); | ||
| else if (resolved.content && !(value in resolved.content)) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: "${value}" not found in ${targets[0]}` | ||
| }); | ||
| } | ||
| } | ||
| if (def.type === "relations" && def.model && Array.isArray(value)) { | ||
| const target = Array.isArray(def.model) ? def.model[0] : def.model; | ||
| if (target) { | ||
| const resolved = await resolve(target, locale); | ||
| if (!resolved.exists) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: target model "${target}" not found` | ||
| }); | ||
| else if (resolved.content) { | ||
| for (const ref of value) if (typeof ref === "string" && !(ref in resolved.content)) errors.push({ | ||
| severity, | ||
| model: modelId, | ||
| locale, | ||
| entry: entryId, | ||
| field: fieldId, | ||
| message: `Broken relation: "${ref}" not found in ${target}` | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return errors; | ||
| } | ||
| //#endregion | ||
| //#region src/core/validator/schedule.ts | ||
| /** | ||
| * Validate `publish_at` and `expire_at` meta fields. | ||
| * | ||
| * Rules (matches legacy `validator.ts:validateScheduleFields`): | ||
| * - `publish_at` must parse as a valid Date | ||
| * - `expire_at` must parse as a valid Date | ||
| * - When both are present, `expire_at` must be strictly after `publish_at` | ||
| */ | ||
| function validateScheduleFields(meta, ctx, issues) { | ||
| if (meta.publish_at !== void 0) { | ||
| const d = new Date(meta.publish_at); | ||
| if (Number.isNaN(d.getTime())) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| message: `Invalid publish_at date: "${meta.publish_at}". Must be a valid ISO 8601 date string.` | ||
| }); | ||
| } | ||
| if (meta.expire_at !== void 0) { | ||
| const d = new Date(meta.expire_at); | ||
| if (Number.isNaN(d.getTime())) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| message: `Invalid expire_at date: "${meta.expire_at}". Must be a valid ISO 8601 date string.` | ||
| }); | ||
| } | ||
| if (meta.publish_at !== void 0 && meta.expire_at !== void 0) { | ||
| const pubDate = new Date(meta.publish_at); | ||
| const expDate = new Date(meta.expire_at); | ||
| if (!Number.isNaN(pubDate.getTime()) && !Number.isNaN(expDate.getTime()) && expDate <= pubDate) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| message: `expire_at ("${meta.expire_at}") must be after publish_at ("${meta.publish_at}").` | ||
| }); | ||
| } | ||
| } | ||
| //#endregion | ||
| //#region src/core/validator/project.ts | ||
| async function readJsonViaReader(reader, path) { | ||
| try { | ||
| return JSON.parse(await reader.readFile(path)); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function readTextViaReader(reader, path) { | ||
| try { | ||
| return await reader.readFile(path); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Build a `resolveTarget` adapter for `checkRelationIntegrity` that walks | ||
| * the project's content store via the shared {@link RepoReader}. Mirrors | ||
| * the target-resolution shape the legacy `checkRelation` used — collection | ||
| * targets return their entry object-map, documents return a "slug exists" | ||
| * marker map, singletons and dictionaries return null content so the | ||
| * checker skips key enforcement for them. | ||
| */ | ||
| function buildProjectTargetResolver(reader, config) { | ||
| return async (targetModelId, targetLocale) => { | ||
| const targetModel = await readModel(reader, targetModelId); | ||
| if (!targetModel) return { exists: false }; | ||
| if (targetModel.kind === "document") { | ||
| const slugs = await discoverDocumentSlugs(reader, contentDirPath(targetModel), targetModel); | ||
| return { | ||
| exists: true, | ||
| content: Object.fromEntries(slugs.map((s) => [s, true])) | ||
| }; | ||
| } | ||
| if (targetModel.kind === "singleton" || targetModel.kind === "dictionary") return { | ||
| exists: true, | ||
| content: null | ||
| }; | ||
| const merged = {}; | ||
| const primary = await readJsonViaReader(reader, contentFilePath(targetModel, targetLocale)); | ||
| if (primary) Object.assign(merged, primary); | ||
| if (targetModel.i18n && targetLocale !== config.locales.default) { | ||
| const fallback = await readJsonViaReader(reader, contentFilePath(targetModel, config.locales.default)); | ||
| if (fallback) Object.assign(merged, fallback); | ||
| } | ||
| return { | ||
| exists: true, | ||
| content: merged | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Scan an entry's data fields for detected secrets in UNDECLARED keys — | ||
| * the legacy validator also flagged stray/rogue fields that were not in | ||
| * `model.fields`. `validateContent` only knows about declared fields, so | ||
| * this complementary pass preserves that coverage. | ||
| */ | ||
| function scanUndeclaredFieldsForSecrets(data, declared, ctx, issues) { | ||
| for (const [fieldName, value] of Object.entries(data)) { | ||
| if (declared && fieldName in declared) continue; | ||
| if (detectSecrets(value).length > 0) issues.push({ | ||
| severity: "error", | ||
| ...ctx, | ||
| field: fieldName, | ||
| message: `Potential secret detected in field "${fieldName}"` | ||
| }); | ||
| } | ||
| } | ||
| async function validateCollectionModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| const locales = model.i18n ? config.locales.supported : [config.locales.default]; | ||
| const localeEntryIds = {}; | ||
| const allEntryIds = /* @__PURE__ */ new Set(); | ||
| const resolveTarget = buildProjectTargetResolver(reader, config); | ||
| for (const locale of locales) { | ||
| const filePath = contentFilePath(model, locale); | ||
| const data = await readJsonViaReader(reader, filePath); | ||
| if (!data) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| message: `Locale file missing: ${locale}.json` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| await writeJson(join(projectRoot, filePath), {}); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| const ids = new Set(Object.keys(data)); | ||
| localeEntryIds[locale] = ids; | ||
| for (const id of ids) allEntryIds.add(id); | ||
| const keys = Object.keys(data); | ||
| const sorted = [...keys].toSorted(); | ||
| if (keys.join(",") !== sorted.join(",")) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: "Content file keys not in canonical order" | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const resorted = {}; | ||
| for (const key of sorted) resorted[key] = data[key]; | ||
| await writeJson(join(projectRoot, filePath), resorted); | ||
| fixed++; | ||
| } | ||
| } | ||
| for (const [entryId, fields] of Object.entries(data)) { | ||
| entriesChecked++; | ||
| scanUndeclaredFieldsForSecrets(fields, model.fields, { | ||
| model: model.id, | ||
| locale, | ||
| entry: entryId | ||
| }, issues); | ||
| if (!model.fields) continue; | ||
| const entryResult = validateContent(fields, model.fields, model.id, locale, entryId, { | ||
| allEntries: data, | ||
| currentEntryId: entryId | ||
| }); | ||
| issues.push(...entryResult.errors); | ||
| const relationErrors = await checkRelationIntegrity(fields, model.fields, model.id, locale, entryId, async () => null, { | ||
| severity: "error", | ||
| resolveTarget | ||
| }); | ||
| issues.push(...relationErrors); | ||
| } | ||
| } | ||
| if (model.i18n && Object.keys(localeEntryIds).length > 1) { | ||
| const localeKeys = Object.keys(localeEntryIds); | ||
| for (let i = 1; i < localeKeys.length; i++) { | ||
| const locA = localeKeys[0]; | ||
| const locB = localeKeys[i]; | ||
| const idsA = localeEntryIds[locA]; | ||
| const idsB = localeEntryIds[locB]; | ||
| for (const id of idsA) if (!idsB.has(id)) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale: locB, | ||
| entry: id, | ||
| message: `Entry parity: entry "${id}" exists in ${locA} but missing in ${locB}` | ||
| }); | ||
| for (const id of idsB) if (!idsA.has(id)) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale: locA, | ||
| entry: id, | ||
| message: `Entry parity: entry "${id}" exists in ${locB} but missing in ${locA}` | ||
| }); | ||
| } | ||
| } | ||
| const strayResult = await checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix); | ||
| fixed += strayResult.fixed; | ||
| for (const locale of locales) { | ||
| const metaRelPath = metaFilePath(model, locale, config.locales.default); | ||
| const metaData = await readJsonViaReader(reader, metaRelPath); | ||
| const contentData = await readJsonViaReader(reader, contentFilePath(model, locale)) ?? {}; | ||
| if (metaData) for (const metaEntryId of Object.keys(metaData)) { | ||
| validateScheduleFields(metaData[metaEntryId], { | ||
| model: model.id, | ||
| locale, | ||
| entry: metaEntryId | ||
| }, issues); | ||
| if (!(metaEntryId in contentData)) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| entry: metaEntryId, | ||
| message: `Orphan meta: meta entry "${metaEntryId}" exists but content entry missing` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| delete metaData[metaEntryId]; | ||
| const metaAbs = join(projectRoot, metaRelPath); | ||
| if (Object.keys(metaData).length > 0) await writeJson(metaAbs, metaData); | ||
| else await rm(metaAbs, { force: true }); | ||
| fixed++; | ||
| } | ||
| } | ||
| } | ||
| for (const entryId of Object.keys(contentData)) if (!metaData || !(entryId in metaData)) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| entry: entryId, | ||
| message: `Orphan content: entry "${entryId}" has no metadata` | ||
| }); | ||
| if (fix && projectRoot && !strayResult.unresolved) { | ||
| await writeMeta(projectRoot, model, { | ||
| locale, | ||
| entryId, | ||
| defaultLocale: config.locales.default | ||
| }, { | ||
| status: "draft", | ||
| source: "import", | ||
| updated_by: "contentrain-mcp" | ||
| }); | ||
| fixed++; | ||
| } | ||
| } | ||
| if (metaData) { | ||
| const entries = Object.entries(metaData); | ||
| const draftIds = entries.filter(([, m]) => m.status === "draft").map(([id]) => id); | ||
| const publishedCount = entries.filter(([, m]) => m.status === "published").length; | ||
| if (publishedCount > 0 && draftIds.length > 0) issues.push({ | ||
| severity: "notice", | ||
| model: model.id, | ||
| locale, | ||
| message: `Publish-state drift: ${draftIds.length} draft entr${draftIds.length === 1 ? "y" : "ies"} alongside ${publishedCount} published in the same collection — [${draftIds.join(", ")}]. If these were published before, restore them with contentrain_bulk update_status.` | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| /** | ||
| * Flag — and, with `fix`, remediate — meta files a non-i18n model should not have. | ||
| * | ||
| * Such a model keeps all content in one `data.json` and therefore exactly one | ||
| * meta record, at the default locale. Earlier writes derived the meta path from | ||
| * the caller's locale, so saving under a non-default locale left a second meta | ||
| * file, and readers disagreed about which was authoritative. | ||
| * | ||
| * The `fix` remediation is deterministic and never decides a status: | ||
| * - default-locale meta present → the strays are redundant, so delete them | ||
| * (the default-locale record stays authoritative — no status is merged). | ||
| * - default-locale meta absent, exactly one stray → that stray holds the only | ||
| * record, so migrate it to the default path (move) rather than orphan the | ||
| * content. | ||
| * - default-locale meta absent, several strays → which is authoritative is | ||
| * ambiguous, so leave the warning for the agent to resolve by hand. | ||
| * | ||
| * Returns the count of files remediated and whether strays remain unresolved. | ||
| * `unresolved` gates the caller's orphan-content fabrication: while a non-i18n | ||
| * model's meta still lives in a stray, the content is not truly orphaned, so | ||
| * minting a draft default-locale record would both be wrong and set up a trap | ||
| * (a later fix pass would then treat the real stray as redundant and delete it). | ||
| */ | ||
| async function checkStrayNonI18nMeta(reader, projectRoot, model, config, issues, fix) { | ||
| if (model.i18n) return { | ||
| fixed: 0, | ||
| unresolved: false | ||
| }; | ||
| const metaDir = `.contentrain/meta/${model.id}`; | ||
| const expected = `${config.locales.default}.json`; | ||
| let files; | ||
| try { | ||
| files = await reader.listDirectory(metaDir); | ||
| } catch { | ||
| return { | ||
| fixed: 0, | ||
| unresolved: false | ||
| }; | ||
| } | ||
| const strays = files.filter((f) => f.endsWith(".json") && f !== expected); | ||
| if (strays.length === 0) return { | ||
| fixed: 0, | ||
| unresolved: false | ||
| }; | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| message: `Meta layout mismatch: "${model.id}" has i18n disabled, so its content lives in a single data.json and its meta belongs at ${expected} alone — but [${strays.join(", ")}] also exist. Readers may disagree about which file is authoritative. Run contentrain_validate fix:true to prune the extras (the default-locale meta stays authoritative).` | ||
| }); | ||
| if (!fix || !projectRoot) return { | ||
| fixed: 0, | ||
| unresolved: true | ||
| }; | ||
| if (await readJsonViaReader(reader, `${metaDir}/${expected}`) === null) { | ||
| if (strays.length !== 1) return { | ||
| fixed: 0, | ||
| unresolved: true | ||
| }; | ||
| const stray = strays[0]; | ||
| const content = await readJsonViaReader(reader, `${metaDir}/${stray}`); | ||
| if (content === null) return { | ||
| fixed: 0, | ||
| unresolved: true | ||
| }; | ||
| await writeJson(join(projectRoot, metaDir, expected), content); | ||
| await rm(join(projectRoot, metaDir, stray), { force: true }); | ||
| return { | ||
| fixed: 1, | ||
| unresolved: false | ||
| }; | ||
| } | ||
| await Promise.all(strays.map((stray) => rm(join(projectRoot, metaDir, stray), { force: true }))); | ||
| return { | ||
| fixed: strays.length, | ||
| unresolved: false | ||
| }; | ||
| } | ||
| async function validateSingletonModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) { | ||
| const filePath = contentFilePath(model, locale); | ||
| const data = await readJsonViaReader(reader, filePath); | ||
| if (!data) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| message: `Locale file missing: ${locale}.json` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| await writeJson(join(projectRoot, filePath), {}); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| entriesChecked++; | ||
| scanUndeclaredFieldsForSecrets(data, model.fields, { | ||
| model: model.id, | ||
| locale | ||
| }, issues); | ||
| if (model.fields) { | ||
| const entryResult = validateContent(data, model.fields, model.id, locale); | ||
| issues.push(...entryResult.errors); | ||
| const resolveTarget = buildProjectTargetResolver(reader, config); | ||
| const relationErrors = await checkRelationIntegrity(data, model.fields, model.id, locale, void 0, async () => null, { | ||
| severity: "error", | ||
| resolveTarget | ||
| }); | ||
| issues.push(...relationErrors); | ||
| } | ||
| const keys = Object.keys(data); | ||
| const sorted = [...keys].toSorted(); | ||
| if (keys.join(",") !== sorted.join(",")) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: "Content file keys not in canonical order" | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const resorted = {}; | ||
| for (const key of sorted) resorted[key] = data[key]; | ||
| await writeJson(join(projectRoot, filePath), resorted); | ||
| fixed++; | ||
| } | ||
| } | ||
| const singletonMetaData = await readJsonViaReader(reader, metaFilePath(model, locale, config.locales.default)); | ||
| if (singletonMetaData) validateScheduleFields(singletonMetaData, { | ||
| model: model.id, | ||
| locale | ||
| }, issues); | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| async function validateDictionaryModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| const localeKeys = {}; | ||
| for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) { | ||
| const filePath = contentFilePath(model, locale); | ||
| const data = await readJsonViaReader(reader, filePath); | ||
| if (!data) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| message: `Locale file missing: ${locale}.json` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| await writeJson(join(projectRoot, filePath), {}); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| entriesChecked++; | ||
| localeKeys[locale] = new Set(Object.keys(data)); | ||
| for (const [key, value] of Object.entries(data)) if (detectSecrets(value).length > 0) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| field: key, | ||
| message: `Potential secret detected in key "${key}"` | ||
| }); | ||
| const valueToKeys = /* @__PURE__ */ new Map(); | ||
| for (const [key, value] of Object.entries(data)) { | ||
| const arr = valueToKeys.get(value); | ||
| if (arr) arr.push(key); | ||
| else valueToKeys.set(value, [key]); | ||
| } | ||
| for (const [value, dupeKeys] of valueToKeys) if (dupeKeys.length > 1) { | ||
| const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value; | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: `Duplicate value "${truncated}" mapped to ${dupeKeys.length} keys: [${dupeKeys.join(", ")}]` | ||
| }); | ||
| } | ||
| const keys = Object.keys(data); | ||
| const sorted = [...keys].toSorted(); | ||
| if (keys.join(",") !== sorted.join(",")) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| message: "Content file keys not in canonical order" | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const resorted = {}; | ||
| for (const key of sorted) resorted[key] = data[key]; | ||
| await writeJson(join(projectRoot, filePath), resorted); | ||
| fixed++; | ||
| } | ||
| } | ||
| } | ||
| if (model.i18n && Object.keys(localeKeys).length > 1) { | ||
| const localeNames = Object.keys(localeKeys); | ||
| for (let i = 1; i < localeNames.length; i++) { | ||
| const locA = localeNames[0]; | ||
| const locB = localeNames[i]; | ||
| const keysA = localeKeys[locA]; | ||
| const keysB = localeKeys[locB]; | ||
| for (const k of keysA) if (!keysB.has(k)) issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale: locB, | ||
| field: k, | ||
| message: `Key parity: key "${k}" exists in ${locA} but missing in ${locB}` | ||
| }); | ||
| for (const k of keysB) if (!keysA.has(k)) issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale: locA, | ||
| field: k, | ||
| message: `Key parity: key "${k}" exists in ${locB} but missing in ${locA}` | ||
| }); | ||
| } | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| async function discoverDocumentSlugs(reader, cDir, model) { | ||
| const strategy = resolveLocaleStrategy(model); | ||
| const entries = await reader.listDirectory(cDir); | ||
| if (!model.i18n) return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")); | ||
| if (strategy === "file") return entries.filter((e) => !e.startsWith(".")); | ||
| if (strategy === "suffix") { | ||
| const slugs = /* @__PURE__ */ new Set(); | ||
| for (const f of entries) { | ||
| if (!f.endsWith(".md")) continue; | ||
| const parts = f.replace(/\.md$/, "").split("."); | ||
| if (parts.length >= 2) { | ||
| parts.pop(); | ||
| slugs.add(parts.join(".")); | ||
| } | ||
| } | ||
| return [...slugs]; | ||
| } | ||
| if (strategy === "directory") { | ||
| const slugs = /* @__PURE__ */ new Set(); | ||
| const localeLists = await Promise.all(entries.filter((localeDir) => !localeDir.startsWith(".")).map((localeDir) => reader.listDirectory(`${cDir}/${localeDir}`))); | ||
| for (const files of localeLists) for (const f of files) if (f.endsWith(".md")) slugs.add(f.replace(/\.md$/, "")); | ||
| return [...slugs]; | ||
| } | ||
| return entries.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")); | ||
| } | ||
| async function validateDocumentModel(reader, projectRoot, model, config, issues, fix) { | ||
| let entriesChecked = 0; | ||
| let fixed = 0; | ||
| const cDir = contentDirPath(model); | ||
| if (!await reader.fileExists(cDir)) return { | ||
| entries: 0, | ||
| fixed: 0 | ||
| }; | ||
| const slugs = await discoverDocumentSlugs(reader, cDir, model); | ||
| const locales = model.i18n ? config.locales.supported : [config.locales.default]; | ||
| const rawByKey = /* @__PURE__ */ new Map(); | ||
| const frontmatterByLocale = {}; | ||
| for (const slug of slugs) { | ||
| if (slug.startsWith(".")) continue; | ||
| for (const locale of locales) { | ||
| const raw = await readTextViaReader(reader, documentFilePath(model, locale, slug)); | ||
| if (!raw) continue; | ||
| rawByKey.set(`${slug}\u0000${locale}`, raw); | ||
| const { frontmatter } = parseFrontmatter(raw); | ||
| frontmatterByLocale[locale] ??= {}; | ||
| frontmatterByLocale[locale][slug] = frontmatter; | ||
| } | ||
| } | ||
| for (const slug of slugs) { | ||
| if (slug.startsWith(".")) continue; | ||
| for (const locale of locales) { | ||
| const filePath = documentFilePath(model, locale, slug); | ||
| const raw = rawByKey.get(`${slug}\u0000${locale}`) ?? null; | ||
| if (!raw) { | ||
| if (model.i18n) { | ||
| issues.push({ | ||
| severity: "warning", | ||
| model: model.id, | ||
| locale, | ||
| slug, | ||
| message: `Missing translation: document "${slug}" missing ${locale} locale file` | ||
| }); | ||
| if (fix && projectRoot) { | ||
| const template = `---\nslug: ${slug}\n---\n`; | ||
| await writeText(join(projectRoot, filePath), template); | ||
| fixed++; | ||
| } | ||
| } | ||
| continue; | ||
| } | ||
| entriesChecked++; | ||
| const { frontmatter, body } = parseFrontmatter(raw); | ||
| scanUndeclaredFieldsForSecrets(frontmatter, { | ||
| ...model.fields, | ||
| body: true | ||
| }, { | ||
| model: model.id, | ||
| locale, | ||
| slug | ||
| }, issues); | ||
| if (detectSecrets(body).length > 0) issues.push({ | ||
| severity: "error", | ||
| model: model.id, | ||
| locale, | ||
| slug, | ||
| field: "body", | ||
| message: "Potential secret detected in document body" | ||
| }); | ||
| if (model.fields) { | ||
| const fieldsWithoutBody = Object.fromEntries(Object.entries(model.fields).filter(([name]) => name !== "body")); | ||
| const entryResult = validateContent(frontmatter, fieldsWithoutBody, model.id, locale, void 0, { | ||
| allEntries: frontmatterByLocale[locale] ?? {}, | ||
| currentEntryId: slug | ||
| }); | ||
| for (const err of entryResult.errors) issues.push({ | ||
| ...err, | ||
| slug | ||
| }); | ||
| const resolveTarget = buildProjectTargetResolver(reader, config); | ||
| const relationErrors = await checkRelationIntegrity(frontmatter, fieldsWithoutBody, model.id, locale, void 0, async () => null, { | ||
| severity: "error", | ||
| resolveTarget | ||
| }); | ||
| for (const err of relationErrors) issues.push({ | ||
| ...err, | ||
| slug | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| entries: entriesChecked, | ||
| fixed | ||
| }; | ||
| } | ||
| async function checkOrphanContent(reader, validModelIds, issues, _fix) { | ||
| const fixed = 0; | ||
| const contentBase = ".contentrain/content"; | ||
| const domains = await reader.listDirectory(contentBase); | ||
| const modelLists = await Promise.all(domains.filter((d) => !d.startsWith(".")).map(async (d) => ({ | ||
| domain: d, | ||
| dirs: await reader.listDirectory(`${contentBase}/${d}`) | ||
| }))); | ||
| for (const { dirs } of modelLists) for (const modelDir of dirs) { | ||
| if (modelDir.startsWith(".")) continue; | ||
| if (!validModelIds.has(modelDir)) issues.push({ | ||
| severity: "warning", | ||
| model: modelDir, | ||
| message: `Orphan content: content directory exists for deleted model "${modelDir}"` | ||
| }); | ||
| } | ||
| return fixed; | ||
| } | ||
| async function validateProject(input, options) { | ||
| const reader = typeof input === "string" ? new LocalReader(input) : input; | ||
| const projectRoot = typeof input === "string" ? input : void 0; | ||
| const fix = Boolean(options?.fix) && projectRoot !== void 0; | ||
| const issues = []; | ||
| let totalEntries = 0; | ||
| let totalFixed = 0; | ||
| let modelsChecked = 0; | ||
| const config = await readConfig(reader); | ||
| if (!config) return { | ||
| valid: false, | ||
| summary: { | ||
| errors: 1, | ||
| warnings: 0, | ||
| notices: 0, | ||
| models_checked: 0, | ||
| entries_checked: 0 | ||
| }, | ||
| issues: [{ | ||
| severity: "error", | ||
| message: "Project not initialized: config.json missing" | ||
| }], | ||
| fixed: 0 | ||
| }; | ||
| const modelSummaries = await listModels(reader); | ||
| const validModelIds = new Set(modelSummaries.map((m) => m.id)); | ||
| const modelsToCheck = options?.model ? modelSummaries.filter((m) => m.id === options.model) : modelSummaries; | ||
| for (const summary of modelsToCheck) { | ||
| const model = await readModel(reader, summary.id); | ||
| if (!model) continue; | ||
| modelsChecked++; | ||
| let result; | ||
| switch (model.kind) { | ||
| case "collection": | ||
| result = await validateCollectionModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| case "singleton": | ||
| result = await validateSingletonModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| case "dictionary": | ||
| result = await validateDictionaryModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| case "document": | ||
| result = await validateDocumentModel(reader, projectRoot, model, config, issues, fix); | ||
| break; | ||
| default: result = { | ||
| entries: 0, | ||
| fixed: 0 | ||
| }; | ||
| } | ||
| totalEntries += result.entries; | ||
| totalFixed += result.fixed; | ||
| } | ||
| if (!options?.model) totalFixed += await checkOrphanContent(reader, validModelIds, issues, fix); | ||
| if (!options?.model) { | ||
| const dictModels = modelsToCheck.filter((m) => m.kind === "dictionary"); | ||
| if (dictModels.length > 1) { | ||
| const globalValueMap = {}; | ||
| for (const summary of dictModels) { | ||
| const model = await readModel(reader, summary.id); | ||
| if (!model) continue; | ||
| for (const locale of model.i18n ? config.locales.supported : [config.locales.default]) { | ||
| if (!globalValueMap[locale]) globalValueMap[locale] = /* @__PURE__ */ new Map(); | ||
| const data = await readJsonViaReader(reader, contentFilePath(model, locale)); | ||
| if (!data) continue; | ||
| for (const [key, value] of Object.entries(data)) { | ||
| const refs = globalValueMap[locale].get(value); | ||
| if (refs) refs.push({ | ||
| model: model.id, | ||
| key | ||
| }); | ||
| else globalValueMap[locale].set(value, [{ | ||
| model: model.id, | ||
| key | ||
| }]); | ||
| } | ||
| } | ||
| } | ||
| for (const [locale, valueMap] of Object.entries(globalValueMap)) for (const [value, refs] of valueMap) if (new Set(refs.map((r) => r.model)).size > 1) { | ||
| const truncated = value.length > 40 ? `${value.slice(0, 40)}...` : value; | ||
| issues.push({ | ||
| severity: "notice", | ||
| locale, | ||
| message: `Cross-model duplicate value "${truncated}" in ${refs.map((r) => `${r.model}/${r.key}`).join(", ")}` | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| const errors = issues.filter((i) => i.severity === "error").length; | ||
| const warnings = issues.filter((i) => i.severity === "warning").length; | ||
| const notices = issues.filter((i) => i.severity === "notice").length; | ||
| return { | ||
| valid: errors === 0, | ||
| summary: { | ||
| errors, | ||
| warnings, | ||
| notices, | ||
| models_checked: modelsChecked, | ||
| entries_checked: totalEntries | ||
| }, | ||
| issues, | ||
| fixed: totalFixed | ||
| }; | ||
| } | ||
| //#endregion | ||
| export { validateContent as i, validateScheduleFields as n, checkRelationIntegrity as r, validateProject as t }; | ||
| //# sourceMappingURL=validator-D5VncJ8M.mjs.map |
Sorry, the diff of this file is too big to display
| import { i as __toESM } from "./chunk-BEJ448es.mjs"; | ||
| //#region src/core/ast-scanner/vue-parser.ts | ||
| const NodeTypes = { | ||
| ROOT: 0, | ||
| ELEMENT: 1, | ||
| TEXT: 2, | ||
| COMMENT: 3, | ||
| SIMPLE_EXPRESSION: 4, | ||
| INTERPOLATION: 5, | ||
| ATTRIBUTE: 6, | ||
| DIRECTIVE: 7, | ||
| COMPOUND_EXPRESSION: 8, | ||
| IF: 9, | ||
| IF_BRANCH: 10, | ||
| FOR: 11, | ||
| TEXT_CALL: 12, | ||
| VNODE_CALL: 13, | ||
| JS_CALL_EXPRESSION: 14 | ||
| }; | ||
| let _compiler = null; | ||
| async function loadCompiler() { | ||
| if (_compiler) return _compiler; | ||
| try { | ||
| _compiler = await import("./compiler-sfc.cjs-tFlmtjrJ.mjs").then((m) => /* @__PURE__ */ __toESM(m.default, 1)); | ||
| return _compiler; | ||
| } catch { | ||
| throw new Error("@vue/compiler-sfc is required to parse .vue files. Install it with: pnpm add -D @vue/compiler-sfc"); | ||
| } | ||
| } | ||
| const CODE_DIRECTIVES = new Set([ | ||
| "if", | ||
| "else-if", | ||
| "else", | ||
| "show", | ||
| "for", | ||
| "on", | ||
| "model", | ||
| "memo", | ||
| "once", | ||
| "pre", | ||
| "cloak", | ||
| "is", | ||
| "slot", | ||
| "key" | ||
| ]); | ||
| const CODE_DIRECTIVE_ARGS = new Set([ | ||
| "class", | ||
| "style", | ||
| "key", | ||
| "ref", | ||
| "is" | ||
| ]); | ||
| const CSS_ATTRIBUTES = new Set(["class", "style"]); | ||
| const SURROUNDING_MAX = 120; | ||
| function getSurroundingByLine(content, line) { | ||
| const lines = content.split("\n"); | ||
| const idx = line - 1; | ||
| if (idx >= 0 && idx < lines.length) return (lines[idx] ?? "").slice(0, SURROUNDING_MAX); | ||
| return ""; | ||
| } | ||
| function walkTemplate(node, templateLineOffset, sfcContent, results, parentTag = "") { | ||
| switch (node.type) { | ||
| case NodeTypes.TEXT: { | ||
| const trimmed = (typeof node.content === "string" ? node.content : node.loc?.source ?? "").trim(); | ||
| if (trimmed.length > 0 && /\S/.test(trimmed)) results.push({ | ||
| value: trimmed, | ||
| line: node.loc.start.line + templateLineOffset - 1, | ||
| column: node.loc.start.column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| break; | ||
| } | ||
| case NodeTypes.INTERPOLATION: | ||
| if (node.content && typeof node.content !== "string" && node.content.type === NodeTypes.SIMPLE_EXPRESSION) { | ||
| const stringMatch = (typeof node.content.content === "string" ? node.content.content : "").match(/^(['"`])(.+)\1$/); | ||
| if (stringMatch?.[2]) results.push({ | ||
| value: stringMatch[2], | ||
| line: node.loc.start.line + templateLineOffset - 1, | ||
| column: node.loc.start.column, | ||
| context: "template_text", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| } | ||
| break; | ||
| case NodeTypes.ELEMENT: { | ||
| const tag = node.tag ?? ""; | ||
| if (node.props) for (const prop of node.props) processProp(prop, tag, templateLineOffset, sfcContent, results); | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, tag); | ||
| break; | ||
| } | ||
| case NodeTypes.IF: | ||
| if (node.branches) { | ||
| for (const branch of node.branches) if (branch.children) for (const child of branch.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| } | ||
| break; | ||
| case NodeTypes.IF_BRANCH: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| case NodeTypes.FOR: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| case NodeTypes.ROOT: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| case NodeTypes.COMPOUND_EXPRESSION: | ||
| if (node.children) { | ||
| for (const child of node.children) if (typeof child !== "string") walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| } | ||
| break; | ||
| case NodeTypes.TEXT_CALL: | ||
| if (node.content && typeof node.content !== "string") walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag); | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| default: | ||
| if (node.children) for (const child of node.children) walkTemplate(child, templateLineOffset, sfcContent, results, parentTag); | ||
| break; | ||
| } | ||
| } | ||
| function processProp(prop, parentTag, templateLineOffset, sfcContent, results) { | ||
| if (prop.type === NodeTypes.ATTRIBUTE) { | ||
| if (!prop.value) return; | ||
| const attrName = prop.name; | ||
| const attrValue = prop.value.content; | ||
| if (!attrValue || attrValue.trim().length === 0) return; | ||
| if (CSS_ATTRIBUTES.has(attrName)) { | ||
| results.push({ | ||
| value: attrValue, | ||
| line: prop.value.loc.start.line + templateLineOffset - 1, | ||
| column: prop.value.loc.start.column, | ||
| context: "css_class", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| return; | ||
| } | ||
| results.push({ | ||
| value: attrValue, | ||
| line: prop.value.loc.start.line + templateLineOffset - 1, | ||
| column: prop.value.loc.start.column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: attrName, | ||
| surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| } else if (prop.type === NodeTypes.DIRECTIVE) { | ||
| const directiveName = prop.name; | ||
| if (CODE_DIRECTIVES.has(directiveName)) return; | ||
| if (directiveName === "bind" && prop.arg) { | ||
| const argName = typeof prop.arg.content === "string" ? prop.arg.content : ""; | ||
| if (CODE_DIRECTIVE_ARGS.has(argName)) return; | ||
| } | ||
| if (prop.exp) { | ||
| const expr = typeof prop.exp.content === "string" ? prop.exp.content : ""; | ||
| const argName = prop.arg && typeof prop.arg.content === "string" ? prop.arg.content : ""; | ||
| const stringMatch = expr.match(/^(['"`])(.+)\1$/); | ||
| if (stringMatch?.[2]) results.push({ | ||
| value: stringMatch[2], | ||
| line: prop.exp.loc.start.line + templateLineOffset - 1, | ||
| column: prop.exp.loc.start.column, | ||
| context: "template_attribute", | ||
| scope: "template", | ||
| parent: parentTag, | ||
| parentProperty: argName || directiveName, | ||
| surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1) | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| let _tsxParser = null; | ||
| async function loadTsxParser() { | ||
| if (_tsxParser) return _tsxParser; | ||
| try { | ||
| _tsxParser = (await import("./tsx-parser-ChduVwKJ.mjs")).parseTsx; | ||
| return _tsxParser; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| /** | ||
| * Map script block lang to a file extension that tsx-parser's getScriptKind understands. | ||
| * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations | ||
| * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals. | ||
| */ | ||
| function resolveScriptFileName(vueFileName, lang) { | ||
| if (lang === "tsx") return vueFileName.replace(/\.vue$/, ".tsx"); | ||
| if (lang === "ts") return vueFileName.replace(/\.vue$/, ".ts"); | ||
| if (lang === "jsx") return vueFileName.replace(/\.vue$/, ".jsx"); | ||
| return vueFileName.replace(/\.vue$/, ".js"); | ||
| } | ||
| function parseScriptBlock(scriptContent, scriptStartLine, fileName, parseTsx, lang) { | ||
| return parseTsx(scriptContent, resolveScriptFileName(fileName, lang)).map((r) => { | ||
| r.line = r.line + scriptStartLine - 1; | ||
| r.scope = "script"; | ||
| return r; | ||
| }); | ||
| } | ||
| async function parseVue(content, fileName) { | ||
| const compiler = await loadCompiler(); | ||
| const results = []; | ||
| const { descriptor } = compiler.parse(content, { filename: fileName }); | ||
| if (descriptor.template) { | ||
| const templateLineOffset = descriptor.template.loc.start.line; | ||
| const templateAst = descriptor.template.ast; | ||
| if (templateAst) walkTemplate(templateAst, templateLineOffset, content, results); | ||
| else try { | ||
| const compiled = compiler.compileTemplate({ | ||
| source: descriptor.template.content, | ||
| filename: fileName, | ||
| id: "ast-scanner" | ||
| }); | ||
| if (compiled.ast) walkTemplate(compiled.ast, templateLineOffset, content, results); | ||
| } catch {} | ||
| } | ||
| const scriptBlock = descriptor.scriptSetup ?? descriptor.script; | ||
| if (scriptBlock) { | ||
| const parseTsx = await loadTsxParser(); | ||
| if (parseTsx) { | ||
| const scriptStartLine = scriptBlock.loc.start.line; | ||
| const scriptResults = parseScriptBlock(scriptBlock.content, scriptStartLine, fileName, parseTsx, scriptBlock.lang); | ||
| results.push(...scriptResults); | ||
| } | ||
| } | ||
| return results; | ||
| } | ||
| //#endregion | ||
| export { parseVue }; | ||
| //# sourceMappingURL=vue-parser-COig4Y1a.mjs.map |
| {"version":3,"file":"vue-parser-COig4Y1a.mjs","names":[],"sources":["../src/core/ast-scanner/vue-parser.ts"],"sourcesContent":["// ─── Vue SFC Parser for Scanner v2 ───\n// Parses .vue Single File Components using @vue/compiler-sfc.\n// Extracts ALL strings with structural context metadata.\n// Scanner does NOT classify — agent does. When in doubt, INCLUDE.\n\n// ─── Types ───\n\nimport type { ExtractedString } from './types.js'\nexport type { ExtractedString }\n\n// ─── Lazy-loaded @vue/compiler-sfc ───\n\ninterface VueCompilerSFC {\n parse: (source: string, options?: { filename?: string }) => {\n descriptor: {\n template: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n ast?: VueTemplateNode\n } | null\n script: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n scriptSetup: {\n content: string\n loc: { start: { line: number; column: number; offset: number } }\n lang?: string\n } | null\n }\n }\n compileTemplate: (options: {\n source: string\n filename: string\n id: string\n }) => {\n ast?: VueTemplateNode\n }\n}\n\n// Vue template AST node types\nconst NodeTypes = {\n ROOT: 0,\n ELEMENT: 1,\n TEXT: 2,\n COMMENT: 3,\n SIMPLE_EXPRESSION: 4,\n INTERPOLATION: 5,\n ATTRIBUTE: 6,\n DIRECTIVE: 7,\n COMPOUND_EXPRESSION: 8,\n IF: 9,\n IF_BRANCH: 10,\n FOR: 11,\n TEXT_CALL: 12,\n VNODE_CALL: 13,\n JS_CALL_EXPRESSION: 14,\n} as const\n\ninterface VueTemplateLoc {\n start: { line: number; column: number; offset: number }\n end: { line: number; column: number; offset: number }\n source: string\n}\n\ninterface VueTemplateNode {\n type: number\n loc: VueTemplateLoc\n children?: VueTemplateNode[]\n tag?: string // for ELEMENT\n props?: VueTemplateProp[]\n content?: VueTemplateNode | string // for INTERPOLATION, TEXT, SIMPLE_EXPRESSION\n branches?: VueTemplateNode[] // for IF\n}\n\ninterface VueTemplateProp {\n type: number // ATTRIBUTE (6) or DIRECTIVE (7)\n name: string\n loc: VueTemplateLoc\n value?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n exp?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n arg?: {\n type: number\n content: string\n loc: VueTemplateLoc\n }\n}\n\nlet _compiler: VueCompilerSFC | null = null\n\nasync function loadCompiler(): Promise<VueCompilerSFC> {\n if (_compiler) return _compiler\n try {\n // Dynamic import — @vue/compiler-sfc is optional\n const mod = await import('@vue/compiler-sfc')\n _compiler = mod as unknown as VueCompilerSFC\n return _compiler\n } catch {\n throw new Error(\n '@vue/compiler-sfc is required to parse .vue files. '\n + 'Install it with: pnpm add -D @vue/compiler-sfc',\n )\n }\n}\n\n// ─── Directives that contain code expressions (not user content) ───\n\nconst CODE_DIRECTIVES = new Set([\n 'if', 'else-if', 'else', 'show',\n 'for',\n 'on', // @click, v-on:click\n 'model',\n 'memo',\n 'once',\n 'pre',\n 'cloak',\n 'is',\n 'slot',\n 'key',\n])\n\n// Directive args that are code/binding expressions, not content\nconst CODE_DIRECTIVE_ARGS = new Set([\n 'class', 'style', // :class, :style → CSS bindings\n 'key',\n 'ref',\n 'is',\n])\n\n// Static attributes whose values are CSS, not content\nconst CSS_ATTRIBUTES = new Set([\n 'class', 'style',\n])\n\n// ─── Surrounding text helper ───\n\nconst SURROUNDING_MAX = 120\n\nfunction _getSurrounding(content: string, offset: number): string {\n const lines = content.split('\\n')\n let charCount = 0\n for (let i = 0; i < lines.length; i++) {\n const lineLen = (lines[i]?.length ?? 0) + 1 // +1 for newline\n if (charCount + lineLen > offset) {\n return (lines[i] ?? '').slice(0, SURROUNDING_MAX)\n }\n charCount += lineLen\n }\n return ''\n}\n\nfunction getSurroundingByLine(content: string, line: number): string {\n const lines = content.split('\\n')\n const idx = line - 1\n if (idx >= 0 && idx < lines.length) {\n return (lines[idx] ?? '').slice(0, SURROUNDING_MAX)\n }\n return ''\n}\n\n// ─── Template AST Walker ───\n\nfunction walkTemplate(\n node: VueTemplateNode,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n parentTag: string = '',\n): void {\n switch (node.type) {\n case NodeTypes.TEXT: {\n // Static text between tags\n const text = typeof node.content === 'string'\n ? node.content\n : (node.loc?.source ?? '')\n const trimmed = text.trim()\n if (trimmed.length > 0 && /\\S/.test(trimmed)) {\n results.push({\n value: trimmed,\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n break\n }\n\n case NodeTypes.INTERPOLATION: {\n // {{ expression }} — extract string literals from within\n if (node.content && typeof node.content !== 'string' && node.content.type === NodeTypes.SIMPLE_EXPRESSION) {\n const expr = typeof node.content.content === 'string'\n ? node.content.content\n : ''\n // Only extract if the expression itself is a string literal\n // e.g., {{ 'Hello' }} or {{ \"World\" }}\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: node.loc.start.line + templateLineOffset - 1,\n column: node.loc.start.column,\n context: 'template_text',\n scope: 'template',\n parent: parentTag,\n surrounding: getSurroundingByLine(sfcContent, node.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable reference like {{ greeting }}, skip — that's code\n }\n break\n }\n\n case NodeTypes.ELEMENT: {\n const tag = node.tag ?? ''\n\n // Process props/attributes\n if (node.props) {\n for (const prop of node.props) {\n processProp(prop, tag, templateLineOffset, sfcContent, results)\n }\n }\n\n // Recurse into children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, tag)\n }\n }\n break\n }\n\n case NodeTypes.IF: {\n // v-if creates branches — walk each branch's children\n if (node.branches) {\n for (const branch of node.branches) {\n if (branch.children) {\n for (const child of branch.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n }\n break\n }\n\n case NodeTypes.IF_BRANCH: {\n // Walk children of if branch\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.FOR: {\n // v-for node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.ROOT: {\n // Root node — walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n case NodeTypes.COMPOUND_EXPRESSION: {\n // Compound expression — walk children\n if (node.children) {\n for (const child of node.children) {\n if (typeof child !== 'string') {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n }\n break\n }\n\n case NodeTypes.TEXT_CALL: {\n // Text call node (wrapper for text in v-if, etc.) — walk content\n if (node.content && typeof node.content !== 'string') {\n walkTemplate(node.content, templateLineOffset, sfcContent, results, parentTag)\n }\n // Also walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n\n default: {\n // For any unknown node type, try to walk children\n if (node.children) {\n for (const child of node.children) {\n walkTemplate(child, templateLineOffset, sfcContent, results, parentTag)\n }\n }\n break\n }\n }\n}\n\nfunction processProp(\n prop: VueTemplateProp,\n parentTag: string,\n templateLineOffset: number,\n sfcContent: string,\n results: ExtractedString[],\n): void {\n if (prop.type === NodeTypes.ATTRIBUTE) {\n // Static attribute: title=\"Hello\"\n if (!prop.value) return\n\n const attrName = prop.name\n const attrValue = prop.value.content\n\n if (!attrValue || attrValue.trim().length === 0) return\n\n // CSS attributes get css_class context\n if (CSS_ATTRIBUTES.has(attrName)) {\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'css_class',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n return\n }\n\n results.push({\n value: attrValue,\n line: prop.value.loc.start.line + templateLineOffset - 1,\n column: prop.value.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: attrName,\n surrounding: getSurroundingByLine(sfcContent, prop.value.loc.start.line + templateLineOffset - 1),\n })\n } else if (prop.type === NodeTypes.DIRECTIVE) {\n // Dynamic directive: :title=\"expr\", v-bind:title=\"expr\", @click=\"handler\"\n const directiveName = prop.name // 'bind', 'on', 'if', 'for', etc.\n\n // Skip code directives entirely (v-if, v-for, @click, etc.)\n if (CODE_DIRECTIVES.has(directiveName)) return\n\n // For v-bind (:attr=\"expr\"), check if the arg is a code binding\n if (directiveName === 'bind' && prop.arg) {\n const argName = typeof prop.arg.content === 'string' ? prop.arg.content : ''\n if (CODE_DIRECTIVE_ARGS.has(argName)) return\n }\n\n // Extract string literals from directive expressions\n if (prop.exp) {\n const expr = typeof prop.exp.content === 'string' ? prop.exp.content : ''\n const argName = prop.arg && typeof prop.arg.content === 'string' ? prop.arg.content : ''\n\n // Check if expression is a simple string literal: 'text' or \"text\"\n const stringMatch = expr.match(/^(['\"`])(.+)\\1$/)\n if (stringMatch?.[2]) {\n results.push({\n value: stringMatch[2],\n line: prop.exp.loc.start.line + templateLineOffset - 1,\n column: prop.exp.loc.start.column,\n context: 'template_attribute',\n scope: 'template',\n parent: parentTag,\n parentProperty: argName || directiveName,\n surrounding: getSurroundingByLine(sfcContent, prop.exp.loc.start.line + templateLineOffset - 1),\n })\n }\n // If it's a variable or complex expression, skip — scanner doesn't interpret code\n }\n }\n}\n\n// ─── Script Block Parsing ───\n\n// tsx-parser is being built in parallel; define the interface we expect\ntype TsxParserFn = (content: string, fileName: string) => ExtractedString[]\n\nlet _tsxParser: TsxParserFn | null = null\n\nasync function loadTsxParser(): Promise<TsxParserFn | null> {\n if (_tsxParser) return _tsxParser\n try {\n const mod = await import('./tsx-parser.js')\n _tsxParser = mod.parseTsx\n return _tsxParser\n } catch {\n // tsx-parser not yet available — fall back to no script parsing\n return null\n }\n}\n\n/**\n * Map script block lang to a file extension that tsx-parser's getScriptKind understands.\n * Without this, .vue files default to ScriptKind.JS — causing TypeScript type annotations\n * (e.g. defineEmits<{ 'update:modelValue': [...] }>) to be misclassified as object literals.\n */\nfunction resolveScriptFileName(vueFileName: string, lang?: string): string {\n if (lang === 'tsx') return vueFileName.replace(/\\.vue$/, '.tsx')\n if (lang === 'ts') return vueFileName.replace(/\\.vue$/, '.ts')\n if (lang === 'jsx') return vueFileName.replace(/\\.vue$/, '.jsx')\n return vueFileName.replace(/\\.vue$/, '.js')\n}\n\nfunction parseScriptBlock(\n scriptContent: string,\n scriptStartLine: number,\n fileName: string,\n parseTsx: TsxParserFn,\n lang?: string,\n): ExtractedString[] {\n // Resolve filename with correct extension for TypeScript parser\n const resolvedFileName = resolveScriptFileName(fileName, lang)\n const scriptResults = parseTsx(scriptContent, resolvedFileName)\n\n // Adjust line numbers by script block offset\n return scriptResults.map(r => {\n r.line = r.line + scriptStartLine - 1\n r.scope = 'script'\n return r\n })\n}\n\n// ─── Main Export ───\n\nexport async function parseVue(content: string, fileName: string): Promise<ExtractedString[]> {\n const compiler = await loadCompiler()\n const results: ExtractedString[] = []\n\n const { descriptor } = compiler.parse(content, { filename: fileName })\n\n // ─── Template Block ───\n if (descriptor.template) {\n const templateLineOffset = descriptor.template.loc.start.line\n const templateAst = descriptor.template.ast\n\n if (templateAst) {\n walkTemplate(templateAst, templateLineOffset, content, results)\n } else {\n // Fallback: compile template to get AST\n try {\n const compiled = compiler.compileTemplate({\n source: descriptor.template.content,\n filename: fileName,\n id: 'ast-scanner',\n })\n if (compiled.ast) {\n walkTemplate(compiled.ast, templateLineOffset, content, results)\n }\n } catch {\n // If template compilation fails, we skip template extraction\n // This is acceptable — malformed templates shouldn't block scanning\n }\n }\n }\n\n // ─── Script Block ───\n const scriptBlock = descriptor.scriptSetup ?? descriptor.script\n if (scriptBlock) {\n const parseTsx = await loadTsxParser()\n if (parseTsx) {\n const scriptStartLine = scriptBlock.loc.start.line\n const scriptResults = parseScriptBlock(\n scriptBlock.content,\n scriptStartLine,\n fileName,\n parseTsx,\n scriptBlock.lang,\n )\n results.push(...scriptResults)\n }\n }\n\n // Style blocks are intentionally skipped — no content strings in CSS\n\n return results\n}\n"],"mappings":"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"} |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
11075631
0.13%193
1.05%109606
0.1%13
8.33%