# cfe-init v1.9 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] [string]$Name, [string]$Synonym = $Name, [string]$NamePrefix, [string]$OutputDir = "src", [ValidateSet("Patch","Customization","AddOn")] [string]$Purpose = "Customization", [string]$Version, [string]$Vendor, [string]$CompatibilityMode = "Version8_3_24", [string]$ConfigPath, [switch]$NoRole ) $ErrorActionPreference = "Stop" function Esc-XmlText { param([string]$s) # Эскейп ТЕКСТА элемента: только & < > — кавычку и апостроф платформа держит сырыми. return $s.Replace('&','&').Replace('<','<').Replace('>','>') } [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Default NamePrefix --- if (-not $NamePrefix) { $NamePrefix = "${Name}_" } # --- Resolve output dir --- if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir = Join-Path (Get-Location).Path $OutputDir } # --- Check existing --- $cfgFile = Join-Path $OutputDir "Configuration.xml" if (Test-Path $cfgFile) { Write-Error "Configuration.xml already exists: $cfgFile" exit 1 } # MDClasses format version — inherited from the base config so the extension stays uniform # with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch). $formatVersion = "2.17" # --- Resolve ConfigPath --- $baseLangUuid = "00000000-0000-0000-0000-000000000000" if ($ConfigPath) { if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path (Get-Location).Path $ConfigPath } if (Test-Path $ConfigPath -PathType Container) { $candidate = Join-Path $ConfigPath "Configuration.xml" if (Test-Path $candidate) { $ConfigPath = $candidate } else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } } if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent # 3a. Read Language UUID from base config $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" if (Test-Path $baseLangFile) { $baseLangDoc = New-Object System.Xml.XmlDocument $baseLangDoc.PreserveWhitespace = $false $baseLangDoc.Load($baseLangFile) $langEl = $null foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } } if ($langEl) { $baseLangUuid = $langEl.GetAttribute("uuid") Write-Host "[INFO] Base config Language UUID: $baseLangUuid" } else { Write-Host "[WARN] No element in $baseLangFile" } } else { Write-Host "[WARN] Base config language not found: $baseLangFile" } # 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config $baseCfgDoc = New-Object System.Xml.XmlDocument $baseCfgDoc.PreserveWhitespace = $false $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path) $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version") if ($fmtVer) { $formatVersion = $fmtVer Write-Host "[INFO] Base config format version: $formatVersion" } $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) if ($compatNode -and $compatNode.InnerText) { $CompatibilityMode = $compatNode.InnerText.Trim() Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" } else { Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" } $ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs) if ($ifcNode -and $ifcNode.InnerText) { $InterfaceCompatibilityMode = $ifcNode.InnerText.Trim() Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode" } else { $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode" } } else { $InterfaceCompatibilityMode = "TaxiEnableVersion8_2" Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." } # --- Generate UUIDs --- $uuidCfg = [guid]::NewGuid().ToString() $uuidLang = [guid]::NewGuid().ToString() $uuidRole = [guid]::NewGuid().ToString() # 7 ContainedObject ObjectIds $co1 = [guid]::NewGuid().ToString() $co2 = [guid]::NewGuid().ToString() $co3 = [guid]::NewGuid().ToString() $co4 = [guid]::NewGuid().ToString() $co5 = [guid]::NewGuid().ToString() $co6 = [guid]::NewGuid().ToString() $co7 = [guid]::NewGuid().ToString() # --- Synonym XML --- $synonymXml = "" if ($Synonym) { $synonymXml = "`r`n`t`t`t`t`r`n`t`t`t`t`tru`r`n`t`t`t`t`t$(Esc-XmlText ($Synonym))`r`n`t`t`t`t`r`n`t`t`t" } # --- Optional properties --- # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор # пишет , а не . $vendorEl = if ($Vendor) { "$(Esc-XmlText ($Vendor))" } else { "" } $versionEl = if ($Version) { "$(Esc-XmlText ($Version))" } else { "" } # --- Role name --- $roleName = "${NamePrefix}ОсновнаяРоль" # --- DefaultRoles XML --- # Элемент целиком: без роли Конфигуратор пишет , а не пустую пару. $defaultRolesEl = "" if (-not $NoRole) { $defaultRolesEl = "`r`n`t`t`t`tRole.$roleName`r`n`t`t`t" } # --- ChildObjects --- $childObjectsXml = "`r`n`t`t`tРусский" if (-not $NoRole) { $childObjectsXml += "`r`n`t`t`t$roleName" } $childObjectsXml += "`r`n`t`t" # Объявления пространств имён — одной переменной: места эмиссии её только интерполируют. # Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте. $xmlnsDecl = 'xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' # Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209. # Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка. function Get-FormatRank([string]$ver) { if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] } return 0 } # 2.21 (8.5) добавила в шапку пространство палитры — ради у значений перечисления. # Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту, # дописать в конец нельзя. # Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и # DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5). $f221Captions = "" if ((Get-FormatRank $formatVersion) -ge 221) { $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=' $f221Captions = "`r`n`t`t`t`r`n`t`t`t" } # --- Configuration.xml --- $cfgXml = @" 9cd510cd-abfc-11d4-9434-004095e12fc7 $co1 9fcd25a0-4822-11d4-9414-008048da11f9 $co2 e3687481-0a87-462c-a166-9f34594f9bba $co3 9de14907-ec23-4a07-96f0-85521cb6b53b $co4 51f2d5d8-ea4d-4064-8892-82951750031e $co5 e68182ea-4237-4383-967f-90c1e3370bc7 $co6 fb282519-d103-4dd3-bc12-cb271d631dfc $co7 Adopted $(Esc-XmlText ($Name)) $synonymXml $Purpose true $(Esc-XmlText ($NamePrefix)) $CompatibilityMode ManagedApplication PlatformApplication Russian $defaultRolesEl $vendorEl $versionEl$f221Captions Language.Русский $InterfaceCompatibilityMode $childObjectsXml "@ # --- Languages/Русский.xml (adopted format) --- $langXml = @" Adopted Русский $baseLangUuid ru "@ # --- Role XML --- $roleXml = @" $(Esc-XmlText ($roleName)) "@ # --- Create directories --- if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } $langDir = Join-Path $OutputDir "Languages" if (-not (Test-Path $langDir)) { New-Item -ItemType Directory -Path $langDir -Force | Out-Null } # --- Write files with UTF-8 BOM --- $enc = New-Object System.Text.UTF8Encoding($true) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце. # # Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать # копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл. function Write-XmlFile([string]$path, [string]$text, $encoding) { $t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n" [System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding) } Write-XmlFile $cfgFile $cfgXml $enc $langFile = Join-Path $langDir "Русский.xml" Write-XmlFile $langFile $langXml $enc # --- Role --- if (-not $NoRole) { $roleDir = Join-Path $OutputDir "Roles" if (-not (Test-Path $roleDir)) { New-Item -ItemType Directory -Path $roleDir -Force | Out-Null } $roleFile = Join-Path $roleDir "$roleName.xml" Write-XmlFile $roleFile $roleXml $enc } # --- Output --- Write-Host "[OK] Создано расширение: $Name" Write-Host " Каталог: $OutputDir" Write-Host " Назначение: $Purpose" Write-Host " Префикс: $NamePrefix" Write-Host " Совместимость: $CompatibilityMode" Write-Host " Configuration.xml: $cfgFile" Write-Host " Languages: $langFile" if (-not $NoRole) { Write-Host " Role: $roleFile" }