# ============================================================= # SPFx + Heft project bootstrap script (PowerShell) # Запустите из корня вашей ПУСТОЙ папки проекта: # .\setup-spfx-heft.ps1 # # Если запуск скриптов заблокирован, выполните сначала: # Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned # ============================================================= $ErrorActionPreference = "Stop" # Версия SPFx. Генератор ОБЯЗАН совпадать с версией пакетов ниже: # старый генератор создаёт проект на gulp + node-sass, который не собирается # на современных Node (node-gyp 3.8 требует Python 2). $SpfxVersion = "1.22.1" # ---- Запись файлов ---- # ВАЖНО: "Set-Content -Encoding UTF8" в Windows PowerShell 5.1 пишет UTF-8 С BOM. # Heft, tsc и node на BOM в .json/.js спотыкаются: # Unexpected token '', "{ ..." is not valid JSON # Поэтому все конфиги пишем строго UTF-8 без BOM. function Write-Utf8NoBom { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Content ) $full = [System.IO.Path]::Combine((Get-Location).ProviderPath, $Path) $dir = [System.IO.Path]::GetDirectoryName($full) if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } [System.IO.File]::WriteAllText($full, ($Content -replace "`r?`n", "`r`n"), (New-Object System.Text.UTF8Encoding($false))) } # ---- Цвета вывода ---- function Info { param($msg) Write-Host "[INFO] $msg" -ForegroundColor Cyan } function Success { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green } function Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow } function Header { param($msg) Write-Host "`n$msg" -ForegroundColor White } Header "=================================================" Header " SPFx + Heft — автоматическая инициализация" Header "=================================================`n" # --------------------------------------------------------- # 1. Проверка Node.js (SPFx 1.20+ требует Node 18+) # --------------------------------------------------------- Info "Проверка Node.js..." try { $nodeVersion = (node --version).Trim() } catch { Warn "Node.js не найден. Установите Node 18 LTS: https://nodejs.org" exit 1 } $nodeMajor = [int]($nodeVersion.TrimStart('v').Split('.')[0]) # SPFx 1.22 объявляет engines: node >=22.14.0 <23.0.0 — на других мажорах # генератор и rig просто не установятся. if ($nodeMajor -ne 22) { Warn "Обнаружен Node.js $nodeVersion. SPFx $SpfxVersion требует Node 22.x (>=22.14.0 <23)." Warn "Переключитесь через nvm-windows: nvm install 22 && nvm use 22" exit 1 } Success "Node.js $nodeVersion — подходит." # --------------------------------------------------------- # 1a. Папка должна быть пустой # --------------------------------------------------------- $leftovers = @("package.json", ".yo-rc.json", "node_modules", "gulpfile.js", "tslint.json") | Where-Object { Test-Path $_ } if ($leftovers.Count -gt 0) { Warn "В текущей папке уже есть следы предыдущей генерации: $($leftovers -join ', ')" Warn "Скрипт рассчитан на ПУСТУЮ папку — иначе Yeoman смешает старый и новый проект." Warn "Уберите служебные файлы и запустите заново:" Warn " Remove-Item -Recurse -Force node_modules, config, package.json, package-lock.json, .yo-rc.json, gulpfile.js, tslint.json, tsconfig.json -ErrorAction SilentlyContinue" Warn "ВНИМАНИЕ: src\ и teams\ здесь НЕ перечислены намеренно — в src может лежать ваш код." Warn "Проверьте их содержимое вручную перед удалением." exit 1 } # --------------------------------------------------------- # 2. Вспомогательная функция: установка глобального пакета # --------------------------------------------------------- # Проверяем именно ПАКЕТ через "npm ls -g", а не наличие команды: # у @microsoft/generator-sharepoint своего бинарника нет, и проверка по "yo" # считала его установленным, оставляя старый генератор (1.11.0), который # генерирует проект на @microsoft/sp-build-web + node-sass → node-gyp 3 → python2. function Get-GlobalPackageVersion { param([string]$Package) # Локально глушим Stop: "npm ls" отдаёт код 1 и пишет в stderr, когда пакета нет, # а в Windows PowerShell 5.1 stderr нативного exe при Stop роняет скрипт. $ErrorActionPreference = 'SilentlyContinue' $json = npm ls -g $Package --depth=0 --json 2>$null | Out-String if ([string]::IsNullOrWhiteSpace($json)) { return $null } try { $parsed = $json | ConvertFrom-Json } catch { return $null } $dep = $parsed.dependencies.$Package if ($null -eq $dep -or [string]::IsNullOrWhiteSpace($dep.version)) { return $null } return $dep.version } function Install-GlobalPackage { param( [string]$Package, [string]$Version = "" # пустая строка = любая установленная версия подходит ) $installed = Get-GlobalPackageVersion $Package if ($Version -ne "") { if ($installed -eq $Version) { Success "$Package@$Version уже установлен." return } if ($installed) { Warn "$Package установлен версии $installed, требуется $Version — переустанавливаем." } else { Info "Устанавливаем $Package@$Version глобально..." } npm install -g "$Package@$Version" } else { if ($installed) { Success "$Package уже установлен (версия $installed)." return } Info "Устанавливаем $Package глобально..." npm install -g $Package } if ($LASTEXITCODE -ne 0) { Warn "Не удалось установить $Package. Прерываем." exit 1 } Success "$Package установлен." } Install-GlobalPackage "yo" Install-GlobalPackage "@microsoft/generator-sharepoint" $SpfxVersion Install-GlobalPackage "@rushstack/heft" # --------------------------------------------------------- # 3. Создание package.json # --------------------------------------------------------- Info "Создаём package.json..." $packageJson = @' { "name": "spfx-heft-project", "version": "0.0.1", "private": true, "scripts": { "build": "heft build", "bundle": "heft build && gulp bundle --ship", "package-solution": "heft build && gulp package-solution --ship", "serve": "heft build && gulp serve", "clean": "heft clean" }, "overrides": { "ajv": "^8.0.0" } } '@ Write-Utf8NoBom "package.json" $packageJson Success "package.json создан." # --------------------------------------------------------- # 4. Создание структуры папок # --------------------------------------------------------- Info "Создаём структуру папок..." New-Item -ItemType Directory -Force -Path "config" | Out-Null New-Item -ItemType Directory -Force -Path "src\webparts" | Out-Null Success "Папки созданы." # --------------------------------------------------------- # 5. Запуск Yeoman-генератора SPFx # --------------------------------------------------------- Info "Запускаем Yeoman-генератор SPFx..." Write-Host "" Write-Host ">>> Сейчас откроется интерактивный генератор Yeoman." -ForegroundColor Yellow Write-Host ">>> Выберите тип: WebPart, Framework: No framework (или React)." -ForegroundColor Yellow Write-Host "" # --skip-install: не даём генератору поставить свой gulp-стек до того, # как мы его выпилим. Иначе npm полезет собирать node-sass. yo "@microsoft/sharepoint" --skip-install if ($LASTEXITCODE -ne 0) { Warn "Генератор Yeoman завершился с ошибкой. Прерываем." exit 1 } # Контроль: старый генератор мог протащить node-sass (нативная сборка, # node-gyp 3.8, требует Python 2 — на текущих Node не собирается вообще). if ((Test-Path "package.json") -and (Select-String -Path "package.json" -Pattern '"node-sass"' -Quiet)) { Warn "В package.json попал node-sass — значит, отработал устаревший генератор SPFx." Warn "Проверьте: npm ls -g @microsoft/generator-sharepoint (нужна $SpfxVersion)" exit 1 } Info "Убираем gulp-стек, оставшийся от генератора..." # Одним вызовом: каждый npm uninstall тянет за собой полный install остального. npm uninstall @microsoft/sp-build-web ajv gulp node-sass @microsoft/rush-stack-compiler-5.3 # --------------------------------------------------------- # 6. Установка Heft и Rush Stack зависимостей # --------------------------------------------------------- Info "Устанавливаем Heft и плагины..." $devDeps = @( "@microsoft/spfx-web-build-rig@$SpfxVersion", "@microsoft/spfx-heft-plugins@$SpfxVersion", "@microsoft/eslint-config-spfx@$SpfxVersion", "@microsoft/eslint-plugin-spfx@$SpfxVersion", "@microsoft/sp-module-interfaces@$SpfxVersion", "@rushstack/eslint-config@4.5.2", "@rushstack/heft@1.1.2", "@types/heft-jest@1.0.2", "@typescript-eslint/parser@8.46.2" ) npm install --save-dev @($devDeps) --save-exact --force --legacy-peer-deps if ($LASTEXITCODE -ne 0) { Warn "npm install завершился с ошибкой (код $LASTEXITCODE). Прерываем, чтобы не создавать конфиги поверх битого дерева." exit 1 } npm install typescript@~5.8.0 --save-dev if ($LASTEXITCODE -ne 0) { Warn "Не удалось установить typescript. Прерываем." exit 1 } Success "Heft и плагины установлены." # --------------------------------------------------------- # 7. config/rig.json # --------------------------------------------------------- Info "Создаём config\rig.json..." New-Item -ItemType Directory -Force -Path "config" | Out-Null $rigJson = @' { "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", "rigPackageName": "@microsoft/spfx-web-build-rig" } '@ Write-Utf8NoBom "config\rig.json" $rigJson Success "config\rig.json создан." # --------------------------------------------------------- # 8. config/sass.json # --------------------------------------------------------- Info "Создаём config\sass.json..." New-Item -ItemType Directory -Force -Path "config" | Out-Null $sassJson = @' { "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-sass-plugin.schema.json", "extends": "@microsoft/spfx-web-build-rig/profiles/default/config/sass.json" } '@ Write-Utf8NoBom "config\sass.json" $sassJson Success "config\sass.json создан." # --------------------------------------------------------- # 9. config/typescript.json # --------------------------------------------------------- Info "Создаём config\typescript.json..." New-Item -ItemType Directory -Force -Path "config" | Out-Null $typescriptJson = @' { "extends": "@microsoft/spfx-web-build-rig/profiles/default/config/typescript.json", "staticAssetsToCopy": { "fileExtensions": [".resx", ".jpg", ".png", ".woff", ".eot", ".ttf", ".svg", ".gif"], "includeGlobs": ["webparts/*/loc/*.js"] } } '@ Write-Utf8NoBom "config\typescript.json" $typescriptJson Success "config\typescript.json создан." # --------------------------------------------------------- # 8. tsconfig.json (если Yeoman ещё не создал) # --------------------------------------------------------- if (-not (Test-Path "tsconfig.json")) { Info "Создаём tsconfig.json..." $tsConfig = @' { "extends": "./node_modules/@microsoft/spfx-web-build-rig/profiles/default/tsconfig-base.json" } '@ Write-Utf8NoBom "tsconfig.json" $tsConfig Success "tsconfig.json создан." } else { Success "tsconfig.json уже существует, пропускаем." } # --------------------------------------------------------- # 9. .eslintrc.js (если не создан) # --------------------------------------------------------- if (-not (Test-Path ".eslintrc.js")) { Info "Создаём .eslintrc.js..." $eslintConfig = @' require('@rushstack/eslint-config/patch/modern-module-resolution'); module.exports = { extends: ['@microsoft/eslint-config-spfx/lib/profiles/default'], parserOptions: { tsconfigRootDir: __dirname } }; '@ Write-Utf8NoBom ".eslintrc.js" $eslintConfig Success ".eslintrc.js создан." } else { Success ".eslintrc.js уже существует, пропускаем." } # --------------------------------------------------------- # 10. .gitignore # --------------------------------------------------------- Info "Создаём .gitignore..." $gitignore = @' node_modules/ lib/ dist/ temp/ *.sppkg .heft/ sharepoint/solution/ '@ Write-Utf8NoBom ".gitignore" $gitignore Success ".gitignore создан." # --------------------------------------------------------- # 11. Финальный npm install # --------------------------------------------------------- $devDeps = @( "@microsoft/sp-component-base@$SpfxVersion", "@microsoft/sp-core-library@$SpfxVersion", "@microsoft/sp-lodash-subset@$SpfxVersion", "@microsoft/sp-office-ui-fabric-core@$SpfxVersion", "@microsoft/sp-property-pane@$SpfxVersion", "@microsoft/sp-webpart-base@$SpfxVersion" ) Info "Финальный npm install..." npm install @($devDeps) --save-exact --legacy-peer-deps if ($LASTEXITCODE -ne 0) { Warn "Финальный npm install завершился с ошибкой (код $LASTEXITCODE)." exit 1 } Success "Все зависимости установлены." # --------------------------------------------------------- # Готово # --------------------------------------------------------- Write-Host "" Write-Host "=================================================" -ForegroundColor Green Write-Host " Проект успешно инициализирован!" -ForegroundColor Green Write-Host "=================================================" -ForegroundColor Green Write-Host "" Write-Host " Доступные команды:" -ForegroundColor White Write-Host "" Write-Host " npm run serve " -NoNewline -ForegroundColor Cyan Write-Host "— локальная разработка (gulp serve)" Write-Host " npm run build " -NoNewline -ForegroundColor Cyan Write-Host "— сборка через Heft" Write-Host " npm run bundle " -NoNewline -ForegroundColor Cyan Write-Host "— бандлинг для продакшна" Write-Host " npm run package-solution " -NoNewline -ForegroundColor Cyan Write-Host "— создание .sppkg пакета" Write-Host " npm run clean " -NoNewline -ForegroundColor Cyan Write-Host "— очистка артефактов сборки" Write-Host "" Write-Host " Следующий шаг: " -NoNewline -ForegroundColor Yellow Write-Host "npm run serve" -ForegroundColor Cyan Write-Host ""