import { readFileSync, writeFileSync } from 'fs'; /** * Matches an empty `[autoload]` section (the header followed by only blank * lines, up to the next section header or end-of-file). Used by cleanup paths * to drop the section after the last entry is removed. */ export const EMPTY_AUTOLOAD_SECTION_REGEX = /\[autoload\]\s*(?=\n\[|\n*$)/g; /** * Mirrors the parser's `\w+` assumption (parseAutoloads / removeAutoloadEntry). * Enforced on write paths to prevent a name with newlines or INI section * delimiters from corrupting project.godot. */ export const VALID_AUTOLOAD_NAME_REGEX = /^\w+$/; function assertValidName(name) { if (!VALID_AUTOLOAD_NAME_REGEX.test(name)) { throw new Error(`Invalid autoload name '${name}': must contain only word characters (letters, digits, underscore)`); } } export function normalizeAutoloadPath(p) { return p.startsWith('res://') ? p : `res://${p}`; } export function parseAutoloads(projectFilePath, existingContent) { const content = existingContent ?? readFileSync(projectFilePath, 'utf8'); const autoloads = []; let inAutoloadSection = false; for (const line of content.split('\n')) { const trimmed = line.trim(); if (trimmed.startsWith('[')) { inAutoloadSection = trimmed === '[autoload]'; continue; } if (!inAutoloadSection || trimmed === '' || trimmed.startsWith(';') || trimmed.startsWith('#')) continue; // The surrounding `"?` are intentional: Godot always writes quotes, but // hand-edited project.godot files sometimes omit them. Tolerating both // shapes means a missing quote pair doesn't silently drop the entry. const match = trimmed.match(/^(\w+)="?(\*?)([^"]*?)"?$/); if (match) { autoloads.push({ name: match[1], singleton: match[2] === '*', path: match[3] }); } } return autoloads; } export function addAutoloadEntry(projectFilePath, name, path, singleton, existingContent) { assertValidName(name); const content = existingContent ?? readFileSync(projectFilePath, 'utf8'); const lines = content.split('\n'); const entry = `${name}="${singleton ? '*' : ''}${normalizeAutoloadPath(path)}"`; const sectionIdx = lines.findIndex((l) => l.trim() === '[autoload]'); if (sectionIdx === -1) { writeFileSync(projectFilePath, content.trimEnd() + '\n\n[autoload]\n' + entry + '\n', 'utf8'); return; } let insertIdx = sectionIdx + 1; while (insertIdx < lines.length && !lines[insertIdx].trim().startsWith('[')) { insertIdx++; } lines.splice(insertIdx, 0, entry); writeFileSync(projectFilePath, lines.join('\n'), 'utf8'); } /** * Remove the named autoload entry. Also drops the `[autoload]` section header * if the removed entry was the last one in it. Returns true when the file was * mutated. */ export function removeAutoloadEntry(projectFilePath, name) { const content = readFileSync(projectFilePath, 'utf8'); const lines = content.split('\n'); let inAutoloadSection = false; let removed = false; const filtered = lines.filter((line) => { const trimmed = line.trim(); if (trimmed.startsWith('[')) { inAutoloadSection = trimmed === '[autoload]'; return true; } if (inAutoloadSection) { const match = trimmed.match(/^(\w+)=/); if (match && match[1] === name) { removed = true; return false; } } return true; }); if (!removed) return false; let newContent = filtered.join('\n'); newContent = newContent.replace(EMPTY_AUTOLOAD_SECTION_REGEX, ''); newContent = newContent.trimEnd() + '\n'; writeFileSync(projectFilePath, newContent, 'utf8'); return true; } export function updateAutoloadEntry(projectFilePath, name, newPath, singleton) { assertValidName(name); const content = readFileSync(projectFilePath, 'utf8'); const lines = content.split('\n'); let inAutoloadSection = false; let updated = false; const newLines = lines.map((line) => { const trimmed = line.trim(); if (trimmed.startsWith('[')) { inAutoloadSection = trimmed === '[autoload]'; return line; } if (inAutoloadSection) { // The surrounding `"?` are intentional: Godot always writes quotes, but // hand-edited project.godot files sometimes omit them. Tolerating both // shapes means a missing quote pair doesn't silently drop the entry. const match = trimmed.match(/^(\w+)="?(\*?)([^"]*?)"?$/); if (match && match[1] === name) { const effectiveSingleton = singleton !== undefined ? singleton : match[2] === '*'; const effectivePath = newPath !== undefined ? normalizeAutoloadPath(newPath) : match[3]; updated = true; return `${name}="${effectiveSingleton ? '*' : ''}${effectivePath}"`; } } return line; }); if (updated) writeFileSync(projectFilePath, newLines.join('\n'), 'utf8'); return updated; } //# sourceMappingURL=autoload-ini.js.map