diff --git a/.github/workflows/next-build-check.yml b/.github/workflows/next-build-check.yml new file mode 100644 index 00000000000..2c659cdf7a6 --- /dev/null +++ b/.github/workflows/next-build-check.yml @@ -0,0 +1,207 @@ +name: Next.js build check + +# Проверяет, что конечный пакет (packages/) реально собирается внутри Next.js-приложения +# для конкретного build-варианта из его package.json#exports. +# +# Запуск: +# - комментарий в PR: /next-build [build] +# (build не указан -> прогоняются все варианты из exports) +# - вручную через workflow_dispatch + +on: + issue_comment: + types: [created] + workflow_dispatch: + inputs: + package: + description: 'Имя папки пакета под packages/, например sdds-serv' + required: true + type: string + build: + description: 'Ключ из exports, например "." или "styled-components". Пусто = все варианты.' + required: false + type: string + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.inputs.package }} + cancel-in-progress: true + +jobs: + parse-command: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: read + outputs: + allowed: ${{ steps.parse.outputs.allowed }} + package: ${{ steps.parse.outputs.package }} + build: ${{ steps.parse.outputs.build }} + pr-number: ${{ steps.parse.outputs.pr-number }} + head-sha: ${{ steps.parse.outputs.head-sha }} + steps: + - name: Parse command and check authorization + id: parse + uses: actions/github-script@v9 + with: + script: | + if (context.eventName === 'workflow_dispatch') { + core.setOutput('allowed', 'true'); + core.setOutput('package', context.payload.inputs.package); + core.setOutput('build', context.payload.inputs.build || ''); + core.setOutput('pr-number', ''); + core.setOutput('head-sha', context.sha); + + return; + } + + const comment = context.payload.comment; + const issue = context.payload.issue; + + if (!issue?.pull_request) { + core.setOutput('allowed', 'false'); + + return; + } + + const match = comment.body.match(/^\/next-build\s+([A-Za-z0-9_-]+)(?:\s+(\S+))?\s*$/m); + + if (!match) { + core.setOutput('allowed', 'false'); + + return; + } + + const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ + ...context.repo, + username: comment.user.login, + }); + const permissionLevels = { + none: 0, + read: 1, + triage: 2, + write: 3, + maintain: 4, + admin: 5, + }; + + if ((permissionLevels[access.permission] || 0) < permissionLevels.write) { + core.setOutput('allowed', 'false'); + core.warning( + `/next-build triggered by ${comment.user.login} (${access.permission}) — not authorized, skipping.`, + ); + + return; + } + + const { data: pr } = await github.rest.pulls.get({ + ...context.repo, + pull_number: issue.number, + }); + + core.setOutput('allowed', 'true'); + core.setOutput('package', match[1]); + core.setOutput('build', match[2] || ''); + core.setOutput('pr-number', String(issue.number)); + core.setOutput('head-sha', pr.head.sha); + + await github.rest.reactions.createForIssueComment({ + ...context.repo, + comment_id: comment.id, + content: 'eyes', + }); + + build: + name: Build (${{ needs.parse-command.outputs.package }} / ${{ needs.parse-command.outputs.build || 'all' }}) + needs: parse-command + if: ${{ needs.parse-command.outputs.allowed == 'true' }} + runs-on: ubuntu-22.04 + permissions: + contents: read + outputs: + results: ${{ steps.run-checks.outputs.results }} + steps: + - uses: actions/checkout@v4 + with: + show-progress: false + ref: ${{ needs.parse-command.outputs.head-sha }} + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + package-manager-cache: false + + - name: Validate package exists + env: + PKG: ${{ needs.parse-command.outputs.package }} + run: | + if [[ ! "$PKG" =~ ^[A-Za-z0-9_-]+$ ]]; then + echo "::error::Недопустимое имя пакета: $PKG" + exit 1 + fi + + if [ ! -f "packages/$PKG/package.json" ]; then + echo "::error::packages/$PKG не найден" + exit 1 + fi + + - name: Install dependencies and build target package + env: + PKG: ${{ needs.parse-command.outputs.package }} + run: npm run setup:ci -- --scope="@salutejs/$PKG" --include-dependencies + + - name: Run Next.js build check(s) + id: run-checks + env: + PKG: ${{ needs.parse-command.outputs.package }} + BUILD: ${{ needs.parse-command.outputs.build }} + run: | + set -e + STATUS=0 + RESULTS_FILE=$(mktemp) + + if [ -n "$BUILD" ]; then + KEYS="$BUILD" + else + KEYS=$(node -e "console.log(Object.keys(require('./packages/${PKG}/package.json').exports).join(' '))") + fi + + for KEY in $KEYS; do + if npx ts-node --project utils/next-build-check/tsconfig.json utils/next-build-check/cli.ts --package="$PKG" --build="$KEY" --ci; then + echo "- ✅ \`$KEY\`" >> "$RESULTS_FILE" + else + echo "- ❌ \`$KEY\`" >> "$RESULTS_FILE" + STATUS=1 + fi + done + + { + echo "results<> "$GITHUB_OUTPUT" + + exit $STATUS + + comment-result: + name: Report result + needs: [parse-command, build] + if: ${{ always() && github.event_name == 'issue_comment' && needs.parse-command.outputs.allowed == 'true' }} + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ needs.parse-command.outputs.pr-number }} + header: Next.js build check + message: | + **Пакет:** `${{ needs.parse-command.outputs.package }}` + **Build:** `${{ needs.parse-command.outputs.build || 'все варианты из exports' }}` + + ${{ needs.build.outputs.results }} diff --git a/.gitignore b/.gitignore index cdc61c55167..64de28a39b6 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ s3_build temp **/.DS_Store **/.swc +*.tgz diff --git a/package.json b/package.json index e26dd91ec33..b995bbb9f29 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "cy:run": "node scripts/cy-run.js", "cy:cli": "node scripts/cy-cli.js", "cy:open": "cypress open --component --browser chromium", + "next:build": "ts-node --project utils/next-build-check/tsconfig.json utils/next-build-check/cli.ts", "cy:report:coverage": "nyc report --reporter=cobertura --reporter=html", "extract-docgen-info": "lerna run extract-docgen-info", "lint:scripts": "eslint ./scripts ./auto-plugins/src --ext .js,.ts --quiet", diff --git a/packages/plasma-b2c/.gitignore b/packages/plasma-b2c/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/plasma-b2c/.gitignore +++ b/packages/plasma-b2c/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/plasma-giga/.gitignore b/packages/plasma-giga/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/plasma-giga/.gitignore +++ b/packages/plasma-giga/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/plasma-homeds/.gitignore b/packages/plasma-homeds/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/plasma-homeds/.gitignore +++ b/packages/plasma-homeds/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/plasma-new-hope/.gitignore b/packages/plasma-new-hope/.gitignore index e46c5da9b57..16bfd96294a 100644 --- a/packages/plasma-new-hope/.gitignore +++ b/packages/plasma-new-hope/.gitignore @@ -31,5 +31,6 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz /ai-components-build-sb diff --git a/packages/plasma-new-hope/src/components/Select/Select.template-doc.mdx b/packages/plasma-new-hope/src/components/Select/Select.template-doc.mdx index fcb62c5f514..f0b3e17ee6d 100644 --- a/packages/plasma-new-hope/src/components/Select/Select.template-doc.mdx +++ b/packages/plasma-new-hope/src/components/Select/Select.template-doc.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/{{ package }}'; export function App() { diff --git a/packages/plasma-web/.gitignore b/packages/plasma-web/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/plasma-web/.gitignore +++ b/packages/plasma-web/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-bizcom/.gitignore b/packages/sdds-bizcom/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-bizcom/.gitignore +++ b/packages/sdds-bizcom/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-dfa/.gitignore b/packages/sdds-dfa/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-dfa/.gitignore +++ b/packages/sdds-dfa/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-finai/.gitignore b/packages/sdds-finai/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-finai/.gitignore +++ b/packages/sdds-finai/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-insol-next/.gitignore b/packages/sdds-insol-next/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-insol-next/.gitignore +++ b/packages/sdds-insol-next/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-insol/.gitignore b/packages/sdds-insol/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-insol/.gitignore +++ b/packages/sdds-insol/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-netology/.gitignore b/packages/sdds-netology/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-netology/.gitignore +++ b/packages/sdds-netology/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-os/.gitignore b/packages/sdds-os/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-os/.gitignore +++ b/packages/sdds-os/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-platform-ai/.gitignore b/packages/sdds-platform-ai/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-platform-ai/.gitignore +++ b/packages/sdds-platform-ai/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-sbcom/.gitignore b/packages/sdds-sbcom/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-sbcom/.gitignore +++ b/packages/sdds-sbcom/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-scan/.gitignore b/packages/sdds-scan/.gitignore index e42510a9862..ab6b957c169 100644 --- a/packages/sdds-scan/.gitignore +++ b/packages/sdds-scan/.gitignore @@ -31,3 +31,4 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz diff --git a/packages/sdds-serv/.gitignore b/packages/sdds-serv/.gitignore index e42510a9862..eb86501ed8c 100644 --- a/packages/sdds-serv/.gitignore +++ b/packages/sdds-serv/.gitignore @@ -31,3 +31,5 @@ emotion/es styled-components/* build-sb-draft emotion/* +*.tgz +*.tgz diff --git a/scripts/cy-cli.js b/scripts/cy-cli.js index 6a5a1cc38f4..19a83d4e391 100644 --- a/scripts/cy-cli.js +++ b/scripts/cy-cli.js @@ -8,43 +8,9 @@ const inquirer = require('inquirer'); const { spawnSync } = require('child_process'); -const prompt = inquirer.prompt || inquirer.default?.prompt || inquirer.createPromptModule?.(); +const { PACKAGES, PACKAGE_MAP } = require('./lib/final-packages'); -const PACKAGES = [ - 'b2c', - 'giga', - 'web', - 'cs', - 'insol', - 'insol-next', - 'netology', - 'bizcom', - 'serv', - 'scan', - 'os', - 'platform-ai', - 'finai', - 'dfa', - 'homeds', -]; - -const PACKAGE_MAP = { - b2c: 'plasma-b2c', - giga: 'plasma-giga', - web: 'plasma-web', - cs: 'sdds-cs', - insol: 'sdds-insol', - 'insol-next': 'sdds-insol-next', - netology: 'sdds-netology', - bizcom: 'sdds-bizcom', - serv: 'sdds-serv', - scan: 'sdds-scan', - os: 'sdds-os', - 'platform-ai': 'sdds-platform-ai', - finai: 'sdds-finai', - dfa: 'sdds-dfa', - homeds: 'plasma-homeds', -}; +const prompt = inquirer.prompt || inquirer.default?.prompt || inquirer.createPromptModule?.(); async function main() { const { action } = await prompt([ diff --git a/scripts/lib/final-packages.d.ts b/scripts/lib/final-packages.d.ts new file mode 100644 index 00000000000..0b981c547d6 --- /dev/null +++ b/scripts/lib/final-packages.d.ts @@ -0,0 +1,2 @@ +export const PACKAGES: string[]; +export const PACKAGE_MAP: Record; diff --git a/scripts/lib/final-packages.js b/scripts/lib/final-packages.js new file mode 100644 index 00000000000..41a1097a351 --- /dev/null +++ b/scripts/lib/final-packages.js @@ -0,0 +1,37 @@ +const PACKAGES = [ + 'b2c', + 'giga', + 'web', + 'cs', + 'insol', + 'insol-next', + 'netology', + 'bizcom', + 'serv', + 'scan', + 'os', + 'platform-ai', + 'finai', + 'dfa', + 'homeds', +]; + +const PACKAGE_MAP = { + b2c: 'plasma-b2c', + giga: 'plasma-giga', + web: 'plasma-web', + cs: 'sdds-cs', + insol: 'sdds-insol', + 'insol-next': 'sdds-insol-next', + netology: 'sdds-netology', + bizcom: 'sdds-bizcom', + serv: 'sdds-serv', + scan: 'sdds-scan', + os: 'sdds-os', + 'platform-ai': 'sdds-platform-ai', + finai: 'sdds-finai', + dfa: 'sdds-dfa', + homeds: 'plasma-homeds', +}; + +module.exports = { PACKAGES, PACKAGE_MAP }; diff --git a/utils/next-build-check/README.md b/utils/next-build-check/README.md new file mode 100644 index 00000000000..337ac972dfa --- /dev/null +++ b/utils/next-build-check/README.md @@ -0,0 +1,73 @@ +# Next.js build check + +Проверяет, что конкретный build-вариант из `package.json#exports` конечного пакета (`packages/`) не вызовет ошибок сборки, при использовании библиотеки внутри Next.js-приложения. + +## 🎯 Назначение + +Ошибки, с которыми может столкнуться внешний потребитель: сломанные `exports`/`files` в `package.json`, забытый `transpilePackages` для CSS-бандла, недостающий peer-зависимость. + +Инструмент подготавливает нужный пакет через `npm pack` (как перед реальной публикацией) и ставит получившийся тарболл в изолированное Next.js-приложение, как это сделал бы реальный пользователь. А затем выполняет `next build`. + +## Как это работает + +1. **Определение пакета и build-варианта** (`lib/resolve-target.ts`) — по флагам `--package`/ + `--build` или интерактивными промптами (`inquirer`), как в `scripts/cy-cli.js`. Список + алиасов пакетов — общий с ним (`scripts/lib/final-packages.js`). +2. **Проверка, что пакет уже собран** — `packages//dist` должен существовать (`npm run prepare` в конечном пакете, аалогично для запуска Cypress-тестов) +3. **`npm pack --ignore-scripts`** в `packages/` — получаем тот же `.tgz`, что уйдёт в + registry при публикации. +4. **Список компонентов build-варианта** (`lib/target.ts#getExportedComponentNames`) — читаем + настоящие имена экспортируемых компонентов из скомпилированного бандла (`export { A, B } from './components/…'`) +5. **Подбор примера на компонент** (`lib/docs-example.ts`) — имя `.mdx`-файла в доках 1-в-1 + совпадает с именем компонента (`Button.mdx`, `ButtonGroup.mdx`…). Если в + `website/-docs/docs/components/.mdx` (или `docs/ai/.mdx` — для + build-вариантов `./ai*`) есть блок ` ```tsx live ` , который либо импортирует тестируемый + пакет, либо просто использует `.tsx`, имя в + нижнем регистре), Next компилирует и пререндерит их все за один прогон. + +> Если CSS-бандл (target-путь из `exports` содержит `/css/`) — в `next.config.js` подставляется `transpilePackages` (сам пакет + все его `@salutejs/*` зависимости), иначе Next упадёт с `Global CSS cannot be imported from within node_modules`. + +`fixture/pages/` целиком генерируется на каждый прогон (включая `_app.jsx`) — в git ничего из +неё не коммитится, `writeFixtureFiles` пересоздаёт директорию, если её нет. + +## Локальный запуск + +```bash +# соберите пакет, если ещё не собран +npm run build --prefix packages/sdds-serv + +# интерактивно (выбор пакета и build через промпты) +npm run next:build + +# явно +npm run next:build -- --package=sdds-serv --build=styled-components --ci +``` + +`--build` — это ключ из `exports` без ведущего `./` (`.`, `styled-components`, `ai/emotion`…). +`--ci` отключает интерактивные промпты и требует `--package`/`--build` явно. + +## CI + +`.github/workflows/next-build-check.yml`: + +- комментарий в PR — `/next-build [build]` (без `build` — прогоняются все варианты из `exports`), доступно пользователям с правами `write`, `maintain` или `admin` на репозиторий; +- вручную — `workflow_dispatch` с теми же `package`/`build`. diff --git a/utils/next-build-check/cli.ts b/utils/next-build-check/cli.ts new file mode 100644 index 00000000000..470ae59e9f4 --- /dev/null +++ b/utils/next-build-check/cli.ts @@ -0,0 +1,124 @@ +#!/usr/bin/env -S npx ts-node + +/* eslint-disable no-console */ + +import fs from 'fs'; +import path from 'path'; + +import { parseArgs } from './lib/args'; +import { run } from './lib/exec'; +import { resolveTarget } from './lib/resolve-target'; +import { FIXTURE_DIR, writeFixtureFiles, buildAppSource, GeneratedPage } from './lib/fixture'; +import { findDocsExampleForComponent, extractExtraImports, buildDocsPageSource } from './lib/docs-example'; +import { + readExportTarget, + buildImportSpecifier, + getTranspilePackages, + getPeerDependenciesToInstall, + getExportedComponentNames, + findPopupProviderExportName, +} from './lib/target'; + +async function main(): Promise { + const args = parseArgs(process.argv.slice(2)); + const { pkgDir, pkgPath, pkgJson, exportKey } = await resolveTarget(args); + + console.log(`\nПроверяем Next.js-сборку: пакет=${pkgDir}, build=${exportKey}`); + + if (!fs.existsSync(path.join(pkgPath, 'dist'))) { + throw new Error( + `В packages/${pkgDir} нет папки dist — сначала соберите пакет (npm run build --prefix packages/${pkgDir}).`, + ); + } + + const exportsMap = pkgJson.exports; + + if (!exportsMap) { + throw new Error(`У пакета ${pkgDir} нет поля "exports" в package.json`); + } + + const exportsEntry = exportsMap[exportKey]; + const target = readExportTarget(exportsEntry); + const transpilePackages = target.includes('/css/') ? getTranspilePackages(pkgJson) : []; + const importSpecifier = buildImportSpecifier(pkgJson, exportKey); + const peerDeps = getPeerDependenciesToInstall(pkgJson); + const peerDepNames = new Set(peerDeps.map(([name]) => name)); + const ownDependencyNames = new Set(Object.keys(pkgJson.dependencies || {})); + + /** + * Проверяем каждый компонент, для которого есть реальный пример в документации — каждому + * своя страница (next build скомпилирует и пререндерит их все за один прогон) + */ + const componentNames = getExportedComponentNames(pkgPath, exportsEntry); + const popupProviderName = findPopupProviderExportName(pkgPath, exportsEntry); + + console.log(`Компоненты build-варианта (${componentNames.length}): ${componentNames.join(', ')}`); + + const pages: GeneratedPage[] = []; + const skippedComponentNames: string[] = []; + const extraPackageNames = new Set(); + + for (const componentName of componentNames) { + const docsExample = findDocsExampleForComponent(pkgDir, pkgJson, exportKey, componentName); + + if (!docsExample) { + skippedComponentNames.push(componentName); + } else { + const fileName = `${componentName.toLowerCase()}.tsx`; + const source = buildDocsPageSource(docsExample, pkgJson, importSpecifier, componentName, popupProviderName); + + pages.push({ fileName, source }); + extractExtraImports(docsExample.code, pkgJson) + .filter((name) => !peerDepNames.has(name) && !ownDependencyNames.has(name)) + .forEach((name) => extraPackageNames.add(name)); + } + } + + if (skippedComponentNames.length > 0) { + console.log(`Пропущены (нет "tsx live" примера в доках): ${skippedComponentNames.join(', ')}`); + } + + if (pages.length === 0) { + throw new Error('Ни для одного компонента не нашлось "tsx live" примера в документации — проверять нечего.'); + } + + const appSource = buildAppSource(importSpecifier, popupProviderName); + + let tarballPath: string | undefined; + + try { + run('npm', ['install'], { cwd: FIXTURE_DIR }); + + const packResult = run('npm', ['pack', '--json', '--ignore-scripts'], { + cwd: pkgPath, + stdio: ['inherit', 'pipe', 'inherit'], + }); + + if (!packResult.stdout) { + throw new Error('npm pack не вернул stdout'); + } + + const [packInfo] = JSON.parse(packResult.stdout.toString()); + + tarballPath = path.join(pkgPath, packInfo.filename); + + writeFixtureFiles({ pages, appSource, transpilePackages }); + + const peerArgs = peerDeps.map(([name, range]) => `${name}@${range}`); + + run('npm', ['install', '--no-save', '--ignore-scripts', tarballPath, ...peerArgs, ...extraPackageNames], { + cwd: FIXTURE_DIR, + }); + run('npx', ['next', 'build'], { cwd: FIXTURE_DIR }); + + console.log(`\nPASS: ${pkgDir} (${exportKey}) — Next.js сборка прошла успешно.`); + } catch (err) { + console.error(`\nFAIL: ${pkgDir} (${exportKey}) — ${(err as Error).message}`); + process.exitCode = 1; + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/utils/next-build-check/fixture/.gitignore b/utils/next-build-check/fixture/.gitignore new file mode 100644 index 00000000000..b9afa017f24 --- /dev/null +++ b/utils/next-build-check/fixture/.gitignore @@ -0,0 +1,7 @@ +node_modules +.next +*.tgz +package-lock.json +next-env.d.ts +next.config.js +pages/ diff --git a/utils/next-build-check/fixture/next.config.js b/utils/next-build-check/fixture/next.config.js new file mode 100644 index 00000000000..575fb177598 --- /dev/null +++ b/utils/next-build-check/fixture/next.config.js @@ -0,0 +1,22 @@ +// Генерируется ../cli.ts при каждом прогоне — вручную не рекомендуется изменять. +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: [ + '@salutejs/plasma-b2c', + '@salutejs/plasma-core', + '@salutejs/plasma-hope', + '@salutejs/plasma-new-hope', + '@salutejs/plasma-themes', + '@salutejs/plasma-tokens-b2c', + '@salutejs/plasma-tokens-web', + '@salutejs/plasma-typo', + ], + // Проверяем именно сборку пакета, а не линт/типизацию этого приложения: eslint иначе + // подхватывает корневой .eslintrc монорепы, а typescript — ловит устаревшие типы в + // скопированных примерах из доки. + eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, +}; + +module.exports = nextConfig; diff --git a/utils/next-build-check/fixture/package.json b/utils/next-build-check/fixture/package.json new file mode 100644 index 00000000000..cfde4c39a22 --- /dev/null +++ b/utils/next-build-check/fixture/package.json @@ -0,0 +1,16 @@ +{ + "name": "next-build-check-fixture", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "next build" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^5.0.0", + "@types/node": "^20.0.0", + "@types/react": "^18.2.0" + } +} diff --git a/utils/next-build-check/fixture/tsconfig.json b/utils/next-build-check/fixture/tsconfig.json new file mode 100644 index 00000000000..a566d5bcc1b --- /dev/null +++ b/utils/next-build-check/fixture/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/utils/next-build-check/lib/args.ts b/utils/next-build-check/lib/args.ts new file mode 100644 index 00000000000..8341401ffdd --- /dev/null +++ b/utils/next-build-check/lib/args.ts @@ -0,0 +1,17 @@ +import { Args } from './types'; + +export function parseArgs(argv: string[]): Args { + const args: Args = { ci: false }; + + for (const raw of argv) { + if (raw === '--ci') { + args.ci = true; + } else if (raw.startsWith('--package=')) { + args.package = raw.slice('--package='.length); + } else if (raw.startsWith('--build=')) { + args.build = raw.slice('--build='.length); + } + } + + return args; +} diff --git a/utils/next-build-check/lib/docs-example.ts b/utils/next-build-check/lib/docs-example.ts new file mode 100644 index 00000000000..8b5267a3b4c --- /dev/null +++ b/utils/next-build-check/lib/docs-example.ts @@ -0,0 +1,132 @@ +import fs from 'fs'; +import path from 'path'; + +import { ROOT } from './target'; +import { PackageJson } from './types'; + +export interface DocsExample { + code: string; + file: string; +} + +const LIVE_BLOCK_RE = /```tsx live\r?\n([\s\S]*?)```/; + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function getDocsSubfolder(exportKey: string): string { + return exportKey === './ai' || exportKey.startsWith('./ai/') ? 'ai' : 'components'; +} + +// Получаем пример из доки +export function findDocsExampleForComponent( + pkgDir: string, + pkgJson: PackageJson, + exportKey: string, + componentName: string, +): DocsExample | null { + const filePath = path.join( + ROOT, + 'website', + `${pkgDir}-docs`, + 'docs', + getDocsSubfolder(exportKey), + `${componentName}.mdx`, + ); + + if (!fs.existsSync(filePath)) { + return null; + } + + const content = fs.readFileSync(filePath, 'utf8'); + const match = content.match(LIVE_BLOCK_RE); + + if (!match) { + return null; + } + + const code = match[1]; + const importsPackage = code.includes(pkgJson.name); + const usesComponent = new RegExp(`<${escapeRegExp(componentName)}\\b`).test(code); + + if (!importsPackage && !usesComponent) { + return null; + } + + return { code, file: `${componentName}.mdx` }; +} + +/** + * Заменяет "from '@salutejs/sdds-serv'" / "from '@salutejs/sdds-serv/ai'" на импорт нужного + * build-варианта. + */ +export function retargetPackageImport(code: string, pkgJson: PackageJson, importSpecifier: string): string { + const pattern = new RegExp(`(from\\s+['"])${escapeRegExp(pkgJson.name)}(?:/[^'"]*)?(['"])`, 'g'); + + return code.replace(pattern, `$1${importSpecifier}$2`); +} +/* + * Находим прочие npm-пакеты, которые в пример из доки импортируются напрямую (например + * "@salutejs/plasma-icons" в примерах Button) и которые не входят в peerDependencies + * пакета + */ +export function extractExtraImports(code: string, pkgJson: PackageJson): string[] { + const specifiers = [...code.matchAll(/from\s+['"]([^'"]+)['"]/g)].map((m) => m[1]); + + const packageNames = specifiers + .filter( + (specifier) => specifier !== 'react' && !specifier.startsWith('.') && !specifier.startsWith(pkgJson.name), + ) + .map((specifier) => { + const parts = specifier.split('/'); + + return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]; + }); + + return [...new Set(packageNames)]; +} + +/** + * Проставляем UNSAFE_SSR_ENABLED для PopupProvider/NotificationProvider, если он есть в примере + */ +function injectPopupProviderSsrFlag(code: string, popupProviderName: string | null): string { + const names = [popupProviderName, 'NotificationsProvider'].filter((name): name is string => Boolean(name)); + + return names.reduce((acc, name) => { + if (!acc.includes(`<${name}`) || acc.includes(`<${name} UNSAFE_SSR_ENABLED`)) { + return acc; + } + + return acc.replace(new RegExp(`<${escapeRegExp(name)}(?=[\\s/>])`, 'g'), `<${name} UNSAFE_SSR_ENABLED`); + }, code); +} + +function hasNamedImport(code: string, name: string): boolean { + return new RegExp(`import\\s*\\{[^}]*\\b${escapeRegExp(name)}\\b[^}]*\\}\\s*from`, 's').test(code); +} + +/** + * Проверяем наличие импорта впримере доки + */ +function ensureComponentImport(code: string, componentName: string, importSpecifier: string): string { + if (hasNamedImport(code, componentName)) { + return code; + } + + return `import { ${componentName} } from '${importSpecifier}';\n${code}`; +} + +export function buildDocsPageSource( + example: DocsExample, + pkgJson: PackageJson, + importSpecifier: string, + componentName: string, + popupProviderName: string | null, +): string { + const retargeted = retargetPackageImport(example.code, pkgJson, importSpecifier); + const withImport = ensureComponentImport(retargeted, componentName, importSpecifier); + const withSsrFlag = injectPopupProviderSsrFlag(withImport, popupProviderName); + + return `// Сгенерировано ../cli.ts из docs/${example.file} (первый "tsx live" пример) — вручную не рекомендуется изменять.\n${withSsrFlag}\nexport default App;\n`; +} diff --git a/utils/next-build-check/lib/exec.ts b/utils/next-build-check/lib/exec.ts new file mode 100644 index 00000000000..2e90a48dc35 --- /dev/null +++ b/utils/next-build-check/lib/exec.ts @@ -0,0 +1,15 @@ +/* eslint-disable no-console */ + +import { spawnSync, SpawnSyncOptions, SpawnSyncReturns } from 'child_process'; + +export function run(command: string, cmdArgs: string[], options?: SpawnSyncOptions): SpawnSyncReturns { + console.log(`\n$ ${command} ${cmdArgs.join(' ')}`); + + const result = spawnSync(command, cmdArgs, { stdio: 'inherit', ...options }); + + if (result.status !== 0) { + throw new Error(`Команда завершилась с ошибкой: ${command} ${cmdArgs.join(' ')}`); + } + + return result; +} diff --git a/utils/next-build-check/lib/fixture.ts b/utils/next-build-check/lib/fixture.ts new file mode 100644 index 00000000000..500acffda9c --- /dev/null +++ b/utils/next-build-check/lib/fixture.ts @@ -0,0 +1,66 @@ +import fs from 'fs'; +import path from 'path'; + +export const FIXTURE_DIR = path.join(__dirname, '..', 'fixture'); +const PAGES_DIR = path.join(FIXTURE_DIR, 'pages'); +const APP_FILE_NAME = '_app.jsx'; + +export function buildAppSource(importSpecifier: string, popupProviderName: string | null): string { + if (!popupProviderName) { + return `// Сгенерировано ../cli.ts при каждом прогоне — вручную не рекомендуется изменять. +export default function App({ Component, pageProps }) { + return ; +} +`; + } + + return `// Сгенерировано ../cli.ts при каждом прогоне — вручную не рекомендуется изменять. +import { ${popupProviderName} } from '${importSpecifier}'; + +export default function App({ Component, pageProps }) { + return ( + <${popupProviderName} UNSAFE_SSR_ENABLED> + + + ); +} +`; +} + +export interface GeneratedPage { + fileName: string; + source: string; +} + +interface WriteFixtureFilesOptions { + pages: GeneratedPage[]; + appSource: string; + transpilePackages: string[]; +} + +export function writeFixtureFiles({ pages, appSource, transpilePackages }: WriteFixtureFilesOptions): void { + const nextConfig = `// Генерируется ../cli.ts при каждом прогоне — вручную не рекомендуется изменять. +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + transpilePackages: ${JSON.stringify(transpilePackages)}, + eslint: { ignoreDuringBuilds: true }, + typescript: { ignoreBuildErrors: true }, +}; + +module.exports = nextConfig; +`; + + fs.writeFileSync(path.join(FIXTURE_DIR, 'next.config.js'), nextConfig); + fs.mkdirSync(PAGES_DIR, { recursive: true }); + + for (const entry of fs.readdirSync(PAGES_DIR)) { + fs.unlinkSync(path.join(PAGES_DIR, entry)); + } + + fs.writeFileSync(path.join(PAGES_DIR, APP_FILE_NAME), appSource); + + for (const page of pages) { + fs.writeFileSync(path.join(PAGES_DIR, page.fileName), page.source); + } +} diff --git a/utils/next-build-check/lib/resolve-target.ts b/utils/next-build-check/lib/resolve-target.ts new file mode 100644 index 00000000000..63447c57959 --- /dev/null +++ b/utils/next-build-check/lib/resolve-target.ts @@ -0,0 +1,72 @@ +import fs from 'fs'; +import path from 'path'; +import inquirer from 'inquirer'; + +import { PACKAGES } from '../../../scripts/lib/final-packages'; + +import { ROOT, resolvePackageDir, normalizeExportKey } from './target'; +import { Args, PackageJson, ResolvedTarget } from './types'; + +const inquirerAny = inquirer as any; +const prompt: (questions: unknown[]) => Promise = + inquirerAny.prompt || inquirerAny.default?.prompt || inquirerAny.createPromptModule?.(); + +export async function resolveTarget(args: Args): Promise { + let pkgInput = args.package; + + if (!pkgInput) { + if (args.ci) { + throw new Error('--package is required in --ci mode'); + } + + ({ pkgInput } = await prompt([ + { + type: 'list', + name: 'pkgInput', + message: 'Выберите пакет:', + choices: PACKAGES, + }, + ])); + } + + const pkgDir = resolvePackageDir(pkgInput as string); + + if (!pkgDir) { + throw new Error(`Неизвестный пакет: ${pkgInput}`); + } + + const pkgPath = path.join(ROOT, 'packages', pkgDir); + const pkgJson: PackageJson = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8')); + const exportKeys = Object.keys(pkgJson.exports || {}); + + if (exportKeys.length === 0) { + throw new Error(`У пакета ${pkgDir} нет поля "exports" в package.json`); + } + + let buildInput = args.build; + + if (!buildInput) { + if (args.ci) { + throw new Error('--build is required in --ci mode'); + } + + ({ buildInput } = await prompt([ + { + type: 'list', + name: 'buildInput', + message: 'Выберите build (exports):', + choices: exportKeys, + }, + ])); + } + + const exportKey = normalizeExportKey(buildInput); + + if (!exportKeys.includes(exportKey)) { + throw new Error( + `Неизвестный build "${buildInput}" для пакета ${pkgDir}. Доступные варианты: ${exportKeys.join(', ')}`, + ); + } + + return { pkgDir, pkgPath, pkgJson, exportKey }; +} diff --git a/utils/next-build-check/lib/target.ts b/utils/next-build-check/lib/target.ts new file mode 100644 index 00000000000..0c7e4a92ea0 --- /dev/null +++ b/utils/next-build-check/lib/target.ts @@ -0,0 +1,196 @@ +import fs from 'fs'; +import path from 'path'; + +import { PACKAGE_MAP } from '../../../scripts/lib/final-packages'; + +import { ExportsEntry, PackageJson } from './types'; + +export const ROOT = path.resolve(__dirname, '..', '..', '..'); + +export function resolvePackageDir(input: string): string | null { + if (PACKAGE_MAP[input]) { + return PACKAGE_MAP[input]; + } + + if (fs.existsSync(path.join(ROOT, 'packages', input, 'package.json'))) { + return input; + } + + return null; +} + +export function normalizeExportKey(input: string | undefined): string { + if (!input || input === '.') { + return '.'; + } + + return input.startsWith('./') ? input : `./${input}`; +} + +export function readExportTarget(exportsEntry: ExportsEntry): string { + if (typeof exportsEntry === 'string') { + return exportsEntry; + } + + return exportsEntry.import || exportsEntry.default || exportsEntry.require || ''; +} + +export function buildImportSpecifier(pkgJson: PackageJson, exportKey: string): string { + return exportKey === '.' ? pkgJson.name : `${pkgJson.name}/${exportKey.replace(/^\.\//, '')}`; +} + +export function getTranspilePackages(pkgJson: PackageJson): string[] { + const deps = Object.keys(pkgJson.dependencies || {}).filter((dep) => dep.startsWith('@salutejs/')); + + return [pkgJson.name, ...deps]; +} + +/** + * Ставим локально в fixture/ нужные peerDependencies (styled-components/@emotion/*, + * @salutejs/plasma-icons и т.п.) — то, что реальный потребитель обязан поставить сам. + */ +export function getPeerDependenciesToInstall(pkgJson: PackageJson): [string, string][] { + const peerDeps = pkgJson.peerDependencies || {}; + + return Object.entries(peerDeps).filter(([name]) => name !== 'react' && name !== 'react-dom'); +} + +function readBundleSource(pkgPath: string, exportsEntry: ExportsEntry): string { + const target = typeof exportsEntry === 'string' ? exportsEntry : exportsEntry.import || exportsEntry.default; + + if (!target) { + throw new Error('У exports-записи нет поля "import"/"default"'); + } + + return fs.readFileSync(path.join(pkgPath, target), 'utf8'); +} + +/** + * Только реэкспорты с ОТНОСИТЕЛЬНЫМ путём ("from './components/Skeleton/Skeleton.js'") — то, + * что пакет экспортирует сам + */ +const RELATIVE_EXPORT_CLAUSE_RE = /export\s*\{([^}]+)\}\s*from\s*(['"])(\.[^'"]+)\2/g; +const EXPORT_STAR_RE = /export\s*\*\s*from\s*(['"])(\.\/components\/(?:_ai\/)?[A-Za-z0-9]+)\1/g; + +function extractNamedComponentExports(source: string): string[] { + const names = new Set(); + + for (const clauseMatch of source.matchAll(RELATIVE_EXPORT_CLAUSE_RE)) { + for (const rawName of clauseMatch[1].split(',')) { + const name = rawName + .trim() + .split(/\s+as\s+/) + .pop() + ?.trim(); + + if (name && /^[A-Z][A-Za-z0-9]*$/.test(name)) { + names.add(name); + } + } + } + + return [...names]; +} + +/** + * Проверяем каждый компонент из build-варианта. Находим имя экспортируемого компонента + * в скомпилированном бандле и ищем его пример в документации + */ +export function getExportedComponentNames(pkgPath: string, exportsEntry: ExportsEntry): string[] { + const target = typeof exportsEntry === 'string' ? exportsEntry : exportsEntry.import || exportsEntry.default; + + if (!target) { + throw new Error('У exports-записи нет поля "import"/"default"'); + } + + const source = readBundleSource(pkgPath, exportsEntry); + // CSS и "./ai" бандлы перечисляют экспорты прямо в entry-файле. + const names = new Set(extractNamedComponentExports(source)); + + /** + * styled-components/emotion реэкспортируют компоненты через "export * from './components/X'". + * Имена компонентов нужно смотреть в components/X/index.js. + */ + for (const starMatch of source.matchAll(EXPORT_STAR_RE)) { + const relPath = starMatch[2]; + const indexPath = path.join(pkgPath, path.dirname(target), relPath, 'index.js'); + const nestedNames = fs.existsSync(indexPath) + ? extractNamedComponentExports(fs.readFileSync(indexPath, 'utf8')) + : []; + + if (nestedNames.length > 0) { + nestedNames.forEach((name) => names.add(name)); + } else { + /** + * Если не удалось выделить имя компонента, берем название папка + */ + const folderName = relPath.split('/').pop(); + + if (folderName) { + names.add(folderName); + } + } + } + + if (names.size === 0) { + throw new Error('Не удалось определить компоненты для теста в бандле'); + } + + return [...names]; +} + +const EXPORT_CLAUSE_RE = /export\s*\{([^}]+)\}\s*from/g; + +/** + * Ищет "export { [ as Alias] } from ..." и возвращает Alias, иначе исходное имя + * NOTE: plasma-b2c реэкспортирует "PopupProvider as PopupBaseProvider", и regex-поиск даёт некорректный импорт + * import { PopupProvider } from '@salutejs/plasma-b2c/...'. + */ +function findExportedAliasName(source: string, sourceName: string): string | null { + for (const clauseMatch of source.matchAll(EXPORT_CLAUSE_RE)) { + for (const rawSpecifier of clauseMatch[1].split(',')) { + const parts = rawSpecifier.trim().split(/\s+as\s+/); + + if (parts[0]?.trim() === sourceName) { + return (parts[1] ?? parts[0]).trim(); + } + } + } + + return null; +} + +export function findPopupProviderExportName(pkgPath: string, exportsEntry: ExportsEntry): string | null { + const source = readBundleSource(pkgPath, exportsEntry); + const direct = findExportedAliasName(source, 'PopupProvider'); + + if (direct) { + return direct; + } + + const target = typeof exportsEntry === 'string' ? exportsEntry : exportsEntry.import || exportsEntry.default; + + if (!target) { + return null; + } + + const componentsDir = path.join(pkgPath, path.dirname(target), 'components'); + + if (!fs.existsSync(componentsDir)) { + return null; + } + + for (const folder of fs.readdirSync(componentsDir)) { + const indexPath = path.join(componentsDir, folder, 'index.js'); + + if (fs.existsSync(indexPath)) { + const found = findExportedAliasName(fs.readFileSync(indexPath, 'utf8'), 'PopupProvider'); + + if (found) { + return found; + } + } + } + + return null; +} diff --git a/utils/next-build-check/lib/types.ts b/utils/next-build-check/lib/types.ts new file mode 100644 index 00000000000..994a5b129d2 --- /dev/null +++ b/utils/next-build-check/lib/types.ts @@ -0,0 +1,28 @@ +export type ExportsEntry = + | string + | { + import?: string; + require?: string; + default?: string; + types?: string; + }; + +export type PackageJson = { + name: string; + exports?: Record; + dependencies?: Record; + peerDependencies?: Record; +}; + +export type Args = { + ci: boolean; + package?: string; + build?: string; +}; + +export type ResolvedTarget = { + pkgDir: string; + pkgPath: string; + pkgJson: PackageJson; + exportKey: string; +}; diff --git a/utils/next-build-check/tsconfig.json b/utils/next-build-check/tsconfig.json new file mode 100644 index 00000000000..21a304fb583 --- /dev/null +++ b/utils/next-build-check/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "nodenext", + "esModuleInterop": true, + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": ["**/*.ts"], + "exclude": ["fixture", "node_modules"] +} diff --git a/website/plasma-b2c-docs/docs/components/Editable.mdx b/website/plasma-b2c-docs/docs/components/Editable.mdx index 04f987dd151..e11bb4d2f8e 100644 --- a/website/plasma-b2c-docs/docs/components/Editable.mdx +++ b/website/plasma-b2c-docs/docs/components/Editable.mdx @@ -17,13 +17,13 @@ import { PropsTable, Description, StorybookLink } from '@site/src/components'; import React from 'react'; import { Editable } from '@salutejs/plasma-b2c'; import { IconEdit } from '@salutejs/plasma-icons'; -import { Headline1 } from '@salutejs/plasma-typo'; +import { H1 } from '@salutejs/plasma-typo'; export function App() { return ( } - textComponent={Headline1} + textComponent={H1} value="Default value" onChange={() => { console.log('handleChange') }} /> diff --git a/website/plasma-b2c-docs/docs/components/Select.mdx b/website/plasma-b2c-docs/docs/components/Select.mdx index 34b15cc10ed..f323846c55d 100644 --- a/website/plasma-b2c-docs/docs/components/Select.mdx +++ b/website/plasma-b2c-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/plasma-b2c'; export function App() { diff --git a/website/plasma-b2c-docs/docs/components/Toolbar.mdx b/website/plasma-b2c-docs/docs/components/Toolbar.mdx index efd6626538e..22592742d09 100644 --- a/website/plasma-b2c-docs/docs/components/Toolbar.mdx +++ b/website/plasma-b2c-docs/docs/components/Toolbar.mdx @@ -19,7 +19,7 @@ Toolbar может отображаться в горизонтальном и ```tsx live import React from 'react'; -import { Toolbar } from '@salutejs/plasma-b2c'; +import { Button, Toolbar, ToolbarDivider } from '@salutejs/plasma-b2c'; import { IconEdit } from '@salutejs/plasma-icons'; export function App() { diff --git a/website/plasma-giga-docs/docs/components/Select.mdx b/website/plasma-giga-docs/docs/components/Select.mdx index 4d4bb9b42e3..eeaa0c41144 100644 --- a/website/plasma-giga-docs/docs/components/Select.mdx +++ b/website/plasma-giga-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/plasma-giga'; export function App() { diff --git a/website/plasma-homeds-docs/docs/components/Select.mdx b/website/plasma-homeds-docs/docs/components/Select.mdx index 9fde989966f..d32a7613aab 100644 --- a/website/plasma-homeds-docs/docs/components/Select.mdx +++ b/website/plasma-homeds-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/plasma-homeds'; export function App() { diff --git a/website/plasma-web-docs/docs/components/Editable.mdx b/website/plasma-web-docs/docs/components/Editable.mdx index 7630acd2788..89fa19d006c 100644 --- a/website/plasma-web-docs/docs/components/Editable.mdx +++ b/website/plasma-web-docs/docs/components/Editable.mdx @@ -17,13 +17,13 @@ import { PropsTable, Description, StorybookLink } from '@site/src/components'; import React from 'react'; import { Editable } from '@salutejs/plasma-web'; import { IconEdit } from '@salutejs/plasma-icons'; -import { Headline1 } from '@salutejs/plasma-typo'; +import { H1 } from '@salutejs/plasma-typo'; export function App() { return ( } - textComponent={Headline1} + textComponent={H1} value="Default value" onChange={() => { console.log('handleChange') }} /> diff --git a/website/plasma-web-docs/docs/components/Select.mdx b/website/plasma-web-docs/docs/components/Select.mdx index f57a2180df1..bfd3f65bb2c 100644 --- a/website/plasma-web-docs/docs/components/Select.mdx +++ b/website/plasma-web-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/plasma-web'; export function App() { diff --git a/website/plasma-web-docs/docs/components/Toolbar.mdx b/website/plasma-web-docs/docs/components/Toolbar.mdx index 1dadbcaa5ed..9f85149aa52 100644 --- a/website/plasma-web-docs/docs/components/Toolbar.mdx +++ b/website/plasma-web-docs/docs/components/Toolbar.mdx @@ -19,7 +19,7 @@ Toolbar может отображаться в горизонтальном и ```tsx live import React from 'react'; -import { Toolbar } from '@salutejs/plasma-web'; +import { Button, Toolbar, ToolbarDivider } from '@salutejs/plasma-web'; import { IconEdit } from '@salutejs/plasma-icons'; export function App() { diff --git a/website/sdds-bizcom-docs/docs/components/Select.mdx b/website/sdds-bizcom-docs/docs/components/Select.mdx index 06b0bad91de..bfa2deee81c 100644 --- a/website/sdds-bizcom-docs/docs/components/Select.mdx +++ b/website/sdds-bizcom-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-bizcom'; export function App() { diff --git a/website/sdds-cs-docs/docs/components/Select.mdx b/website/sdds-cs-docs/docs/components/Select.mdx index 93d9d160c33..f530a7ff29d 100644 --- a/website/sdds-cs-docs/docs/components/Select.mdx +++ b/website/sdds-cs-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-cs'; export function App() { diff --git a/website/sdds-dfa-docs/docs/components/Select.mdx b/website/sdds-dfa-docs/docs/components/Select.mdx index 6ef54bec55a..578f56a4bc1 100644 --- a/website/sdds-dfa-docs/docs/components/Select.mdx +++ b/website/sdds-dfa-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-dfa'; export function App() { diff --git a/website/sdds-finai-docs/docs/components/Select.mdx b/website/sdds-finai-docs/docs/components/Select.mdx index 8c5941c383d..27ac5a057df 100644 --- a/website/sdds-finai-docs/docs/components/Select.mdx +++ b/website/sdds-finai-docs/docs/components/Select.mdx @@ -100,7 +100,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-finai'; export function App() { diff --git a/website/sdds-insol-docs/docs/components/Select.mdx b/website/sdds-insol-docs/docs/components/Select.mdx index 2c6222705fd..d0875222535 100644 --- a/website/sdds-insol-docs/docs/components/Select.mdx +++ b/website/sdds-insol-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-insol'; export function App() { diff --git a/website/sdds-netology-docs/docs/components/Select.mdx b/website/sdds-netology-docs/docs/components/Select.mdx index 81dfdd66872..0742713597c 100644 --- a/website/sdds-netology-docs/docs/components/Select.mdx +++ b/website/sdds-netology-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-netology'; export function App() { diff --git a/website/sdds-platform-ai-docs/docs/components/Select.mdx b/website/sdds-platform-ai-docs/docs/components/Select.mdx index e7578a7d420..9bed021b18a 100644 --- a/website/sdds-platform-ai-docs/docs/components/Select.mdx +++ b/website/sdds-platform-ai-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-platform-ai'; export function App() { diff --git a/website/sdds-sbcom-docs/docs/components/Select.mdx b/website/sdds-sbcom-docs/docs/components/Select.mdx index 7b74735cf78..4e4b491038d 100644 --- a/website/sdds-sbcom-docs/docs/components/Select.mdx +++ b/website/sdds-sbcom-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-sbcom'; export function App() { diff --git a/website/sdds-scan-docs/docs/components/Select.mdx b/website/sdds-scan-docs/docs/components/Select.mdx index 90c09c20c5c..3ac4e50c0c6 100644 --- a/website/sdds-scan-docs/docs/components/Select.mdx +++ b/website/sdds-scan-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-scan'; export function App() { diff --git a/website/sdds-serv-docs/docs/components/Select.mdx b/website/sdds-serv-docs/docs/components/Select.mdx index 6fb97136de6..f7b48402f0f 100644 --- a/website/sdds-serv-docs/docs/components/Select.mdx +++ b/website/sdds-serv-docs/docs/components/Select.mdx @@ -77,7 +77,7 @@ type Items = Array<{ В режиме `textfield-like` target внешне мимикрирует под компонент TextField и наследует все его свойства. ```tsx live - import React from 'react'; + import React, { useState } from 'react'; import { Select } from '@salutejs/sdds-serv'; export function App() {