diff --git a/AGENTS.md b/AGENTS.md index 2a9acf8..2c4f586 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ - подготовка нейтрального compiled artifact; - protocol `ModeAdapter`/`OutputPlan`; - проверка output paths; -- atomic writer, symlink protection и ownership state; +- staged directory writer и symlink protection; - logger и базовые result types. Core не генерирует JavaScript, declarations, manifest source, CSS или framework-specific exports. Изменение общего compiler может ожидаемо изменить SVG всех modes; изменение generated source должно быть локально одному adapter. @@ -61,13 +61,11 @@ Core не генерирует JavaScript, declarations, manifest source, CSS и Runtime генерируется как ESM JavaScript. Типизация добавляется отдельными `.d.ts`; TypeScript/TSX не используется как runtime output. -Core writer владеет корневым `.gitignore` и `.svg-sprite/state.json`. State не является manifest спрайта: он хранит owner mode, contract version и список управляемых файлов. +Core writer полностью владеет каталогом `.svg-sprite` и при каждой генерации заменяет его через временный каталог с rollback при ошибке. Корневым `.gitignore` writer владеет, когда exact-mode adapter запрашивает его через `OutputPlan`. Bare `standalone` не создаёт `.gitignore`; остальные modes создают. -Sprite-level asset, icon data, manifest и facade лежат непосредственно в `.svg-sprite/`. Framework runtime группируется отдельно: React adapters используют `.svg-sprite/react/`, будущие framework adapters получают собственный framework-каталог. +Sprite-level asset, icon data, manifest и facade лежат непосредственно в `.svg-sprite/`. `standalone@vite` и `standalone@webpack` генерируют нативный icon Web Component внутри своего facade; bare `standalone` остаётся без JavaScript runtime. Framework runtime группируется отдельно: React adapters используют `.svg-sprite/react/`, будущие framework adapters получают собственный framework-каталог. -Каждый adapter имеет собственный `contractVersion`. При изменении его generated-контракта повышается только версия этого adapter. - -Adapter возвращает файлы в памяти. Только core writer проверяет paths и markers, обновляет output и записывает `state.json` последним. +Adapter возвращает файлы в памяти. Только core writer проверяет paths, полностью заменяет `.svg-sprite` и обновляет управляемый `.gitignore`. ## Зависимости @@ -98,5 +96,4 @@ mode A -X-> shared output codegen 1. Изменяйте только его каталог и mode-neutral protocol, если это действительно необходимо. 2. Не переносите output-логику в core ради устранения дублирования. 3. Не меняйте generated-контракты других adapters автоматически. -4. Повышайте `contractVersion` только изменённого adapter. -5. Проверяйте отсутствие cross-mode imports. +4. Проверяйте отсутствие cross-mode imports. diff --git a/FEATURES.md b/FEATURES.md index 9207255..d15f11c 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,6 +1,6 @@ # Иконки без лишней цены для приложения -`@gromlab/svg-sprites` превращает SVG проекта в кешируемую, типизированную систему иконок для React и Next.js. В коде остаются простые компоненты, а приложение получает преимущества спрайтов без сложной инфраструктуры. +`@gromlab/svg-sprites` превращает SVG проекта в кешируемую, типизированную систему иконок для vanilla-приложений, React и Next.js. В коде остаются простые компоненты, а приложение получает преимущества спрайтов без сложной инфраструктуры. 1. **AI-friendly из коробки** @@ -42,9 +42,9 @@ `SpriteViewer` рендерит все спрайты проекта в одном месте и показывает, какие иконки вошли в каждый набор и как они выглядят. Для каждой иконки видны созданные CSS-переменные и их fallback-цвета. Значения можно менять прямо в Viewer и сразу наблюдать результат. Здесь же доступны готовые примеры подключения через React, ``, `` и CSS. -11. **React и Next.js** +11. **Standalone, React и Next.js** - Пакет генерирует типизированные React-компоненты и поддерживает Vite, Webpack 5, Next.js App Router и Pages Router с Turbopack или Webpack. + Для vanilla-приложений с Vite или Webpack пакет генерирует нативный типизированный Web Component без runtime-зависимостей. Для React и Next.js создаётся React-компонент с поддержкой Vite, Webpack 5, App Router, Pages Router и Turbopack. 12. **Чистый Git** @@ -52,4 +52,4 @@ 13. **В production только иконки** - `@gromlab/svg-sprites` выполняет всю работу на этапе генерации и остаётся в `devDependencies`. Compiler и CLI не попадают в клиентское приложение: после сборки остаются только локальный типизированный компонент и внешний SVG-файл. + Генератор можно запускать через `npx`, не добавляя package в проект. Локальная установка нужна только для необязательного Viewer, типизации config через package или программного API. Compiler и CLI не попадают в клиентское приложение: после сборки остаются только локальный типизированный компонент и внешний SVG-файл. diff --git a/README.md b/README.md index 3ea21c9..ab4a13a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ `@gromlab/svg-sprites` is an SVG sprite generator for modern web applications. It combines selected SVG icons into one or more external, cacheable sprites and prepares them for use in the UI. -For React and Next.js, the package generates typed components and external SVG assets with support for Vite, Webpack 5, and Turbopack. +For vanilla applications using Vite/Webpack, the package generates a native typed Web Component; for React and Next.js, it generates a React component. In every case, the SVG remains a separate cacheable asset. ## An SVG sprite as simple as a regular SVG icon @@ -24,6 +24,20 @@ The component accepts familiar SVG attributes: dimensions, `color`, `className`, You do not have to work with the sprite directly in your application. Use it like a regular SVG icon while benefiting from a single component, autocomplete, and TypeScript validation for every name. +In `standalone@vite` and `standalone@webpack`, the same approach works without React: + +```ts +import { defineAppIconElement } from './app-icons' + +defineAppIconElement() +``` + +```html + +``` + +Bare `standalone` remains minimal and generates only an SVG asset and JSON manifest, with no JavaScript runtime. + ## AI-friendly out of the box `@gromlab/svg-sprites` is designed to work with AI agents from the start. Add the ready-made skill and ask an agent to configure, migrate, or troubleshoot the package without lengthy instructions or manual documentation research. @@ -36,12 +50,15 @@ You do not have to work with the sprite directly in your application. Use it lik The main example uses the Next.js App Router and Turbopack. -### 1. Install the package +### 1. Generate without installing the package ```bash -npm install --save-dev @gromlab/svg-sprites +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites --help ``` +`npx` downloads the CLI temporarily. It does not add `@gromlab/svg-sprites` to +`package.json`, and the generated production runtime does not import the package. + ### 2. Specify the icons you need SVG files can remain in your project's existing structure: @@ -61,17 +78,15 @@ Create the sprite configuration: ```ts // src/ui/app-icons/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ +export default { mode: 'next@app/turbopack', name: 'app', - inputFiles: [ + input: [ '../../assets/icons/search.svg', '../../assets/icons/settings.svg', '../../features/profile/user.svg', ], -}) +} ``` ### 3. Add generation @@ -79,7 +94,7 @@ export default defineSpriteConfig({ ```json { "scripts": { - "sprites": "svg-sprites src/ui/app-icons/svg-sprite.config.ts", + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/app-icons/svg-sprite.config.ts", "predev": "npm run sprites", "prebuild": "npm run sprites" } @@ -233,13 +248,13 @@ The package generates low-level standalone sprites for static HTML, Vite, and We ## Clean Git history -The generator creates a local `.gitignore` that excludes generated files and keeps them from cluttering project history, pull requests, and the codebase. +Bundler and framework modes create a local `.gitignore` that excludes generated files and keeps them from cluttering project history, pull requests, and the codebase. Bare `standalone` leaves the repository policy to the application. -The repository contains the source SVG files, configuration, and `.gitignore` rule, while sprites, components, and types are regenerated locally and in CI through `prebuild`. +In bundler and framework modes, the repository contains the source SVG files, configuration, and `.gitignore` rule, while sprites, components, and types are regenerated locally and in CI through `prebuild`. ## Only icons in production -`@gromlab/svg-sprites` does its main work during generation and remains in `devDependencies`. +Generation can run entirely through `npx`, without adding the package to the project. Install it as a development dependency only when you need the Viewer, config types, or the programmatic API. Production components use only local generated code, styles, and the external SVG file. The compiler and CLI are not bundled into the client application, while `SpriteViewer` is imported separately only where a debug page is needed. @@ -249,15 +264,21 @@ This README introduces the project's capabilities and demonstrates the primary u ### Quick start -- [Next.js App Router](docs/en/next-app.md) -- [Next.js Pages Router](docs/en/next-pages.md) -- [React + Vite](docs/en/react-vite.md) -- [React + Webpack 5](docs/en/react-webpack.md) +- [Bare standalone](docs/en/guides/standalone.md) +- [Standalone + Vite](docs/en/guides/standalone-vite.md) +- [Standalone + Webpack 5](docs/en/guides/standalone-webpack.md) +- [React + Vite](docs/en/guides/react-vite.md) +- [React + Webpack 5](docs/en/guides/react-webpack.md) +- [Next.js App Router + Turbopack](docs/en/guides/next-app-turbopack.md) +- [Next.js App Router + Webpack](docs/en/guides/next-app-webpack.md) +- [Next.js Pages Router + Turbopack](docs/en/guides/next-pages-turbopack.md) +- [Next.js Pages Router + Webpack](docs/en/guides/next-pages-webpack.md) ### Technical resources -- [Technical reference](docs/en/reference.md) -- [Programmatic API](docs/en/programmatic-api.md) +- [Documentation index](docs/en/README.md) +- [Technical reference](docs/en/reference/technical.md) +- [Programmatic API](docs/en/reference/programmatic-api.md) ## License diff --git a/README_RU.md b/README_RU.md index 97ace8c..a41fcdc 100644 --- a/README_RU.md +++ b/README_RU.md @@ -6,7 +6,7 @@ `@gromlab/svg-sprites` — генератор SVG-спрайтов для современных веб-приложений. Он собирает выбранные SVG-иконки в один или несколько внешних кешируемых спрайтов и подготавливает их для использования в интерфейсе. -Для React и Next.js пакет создаёт типизированные компоненты и внешние SVG assets, поддерживая Vite, Webpack 5 и Turbopack. +Для vanilla-приложений с Vite/Webpack пакет создаёт нативный типизированный Web Component, а для React и Next.js — React-компонент. SVG во всех случаях остаётся отдельным кешируемым asset. ## SVG-спрайт так же прост, как обычная SVG-иконка @@ -24,6 +24,20 @@ В приложении не приходится работать со спрайтом напрямую. Вы используете его так же, как обычную SVG-иконку, но получаете один компонент, автокомплит и TypeScript-проверку всех имён. +В `standalone@vite` и `standalone@webpack` тот же подход доступен без React: + +```ts +import { defineAppIconElement } from './app-icons' + +defineAppIconElement() +``` + +```html + +``` + +Bare `standalone` остаётся минимальным и генерирует только SVG asset и JSON manifest без JavaScript runtime. + ## AI-friendly из коробки `@gromlab/svg-sprites` сразу рассчитан на работу с AI-агентами. Подключите готовый skill и поручите агенту настройку, миграцию или диагностику без длинных инструкций и ручного изучения документации. @@ -36,12 +50,15 @@ Основной пример использует Next.js App Router и Turbopack. -### 1. Установите пакет +### 1. Генерируйте без установки пакета ```bash -npm install --save-dev @gromlab/svg-sprites +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites --help ``` +`npx` временно скачивает CLI, не добавляет `@gromlab/svg-sprites` в +`package.json`, а generated production runtime не импортирует package. + ### 2. Укажите нужные иконки SVG могут оставаться в существующей структуре проекта: @@ -61,17 +78,15 @@ src/ ```ts // src/ui/app-icons/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ +export default { mode: 'next@app/turbopack', name: 'app', - inputFiles: [ + input: [ '../../assets/icons/search.svg', '../../assets/icons/settings.svg', '../../features/profile/user.svg', ], -}) +} ``` ### 3. Добавьте генерацию @@ -79,7 +94,7 @@ export default defineSpriteConfig({ ```json { "scripts": { - "sprites": "svg-sprites src/ui/app-icons/svg-sprite.config.ts", + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/app-icons/svg-sprite.config.ts", "predev": "npm run sprites", "prebuild": "npm run sprites" } @@ -233,13 +248,13 @@ Bare standalone подключает Viewer через browser script и HTML el ## Чистый Git -Генератор создаёт локальный `.gitignore`, который исключает generated-файлы и не позволяет им засорять историю, pull requests и код проекта. +Bundler и framework modes создают локальный `.gitignore`, который исключает generated-файлы и не позволяет им засорять историю, pull requests и код проекта. Bare `standalone` оставляет политику репозитория приложению. -В репозитории остаются исходные SVG, конфигурация и правило `.gitignore`, а локально и в CI спрайты, компоненты и типы заново создаются через `prebuild`. +В bundler и framework modes в репозитории остаются исходные SVG, конфигурация и правило `.gitignore`, а локально и в CI спрайты, компоненты и типы заново создаются через `prebuild`. ## В production только иконки -`@gromlab/svg-sprites` выполняет основную работу на этапе генерации и остаётся в `devDependencies`. +Генерация полностью работает через `npx`, без добавления package в проект. Устанавливайте его как development dependency, только если нужны Viewer, типы конфига или программный API. Production-компоненты используют только локальный generated-код, стили и внешний SVG-файл. Compiler и CLI не попадают в клиентское приложение, а `SpriteViewer` подключается отдельно только там, где нужна debug-страница. @@ -249,15 +264,21 @@ README знакомит с возможностями проекта и пока ### Быстрый старт -- [Next.js App Router](docs/ru/next-app.md) -- [Next.js Pages Router](docs/ru/next-pages.md) -- [React + Vite](docs/ru/react-vite.md) -- [React + Webpack 5](docs/ru/react-webpack.md) +- [Bare standalone](docs/ru/guides/standalone.md) +- [Standalone + Vite](docs/ru/guides/standalone-vite.md) +- [Standalone + Webpack 5](docs/ru/guides/standalone-webpack.md) +- [React + Vite](docs/ru/guides/react-vite.md) +- [React + Webpack 5](docs/ru/guides/react-webpack.md) +- [Next.js App Router + Turbopack](docs/ru/guides/next-app-turbopack.md) +- [Next.js App Router + Webpack](docs/ru/guides/next-app-webpack.md) +- [Next.js Pages Router + Turbopack](docs/ru/guides/next-pages-turbopack.md) +- [Next.js Pages Router + Webpack](docs/ru/guides/next-pages-webpack.md) ### Технические материалы -- [Технический справочник](docs/ru/reference.md) -- [Программный API](docs/ru/programmatic-api.md) +- [Индекс документации](docs/ru/README.md) +- [Технический справочник](docs/ru/reference/technical.md) +- [Программный API](docs/ru/reference/programmatic-api.md) ## Лицензия diff --git a/docs/en/README.md b/docs/en/README.md new file mode 100644 index 0000000..23519ca --- /dev/null +++ b/docs/en/README.md @@ -0,0 +1,29 @@ +# Documentation + +Choose one exact mode guide for setup. The guides are standalone documents and +can also be used unchanged by AI skills. + +## Quick Start Guides + +| Project | Exact mode | Guide | +|---|---|---| +| Static HTML or custom publishing | `standalone` | [Bare standalone](guides/standalone.md) | +| Vanilla + Vite | `standalone@vite` | [Standalone + Vite](guides/standalone-vite.md) | +| Vanilla + Webpack 5 | `standalone@webpack` | [Standalone + Webpack](guides/standalone-webpack.md) | +| React + Vite | `react@vite` | [React + Vite](guides/react-vite.md) | +| React + Webpack 5 | `react@webpack` | [React + Webpack](guides/react-webpack.md) | +| Next.js App Router + Turbopack | `next@app/turbopack` | [App Router + Turbopack](guides/next-app-turbopack.md) | +| Next.js App Router + Webpack | `next@app/webpack` | [App Router + Webpack](guides/next-app-webpack.md) | +| Next.js Pages Router + Turbopack | `next@pages/turbopack` | [Pages Router + Turbopack](guides/next-pages-turbopack.md) | +| Next.js Pages Router + Webpack | `next@pages/webpack` | [Pages Router + Webpack](guides/next-pages-webpack.md) | + +Every guide follows the same order: + +1. Generate the sprite through `npx` without adding the package to the project. +2. Optionally install and connect the debug Viewer. +3. Optionally type the config through the package or a local copy-paste type. + +## Reference + +- [Technical reference](reference/technical.md) +- [Programmatic API](reference/programmatic-api.md) diff --git a/docs/en/guides/README.md b/docs/en/guides/README.md new file mode 100644 index 0000000..d832d85 --- /dev/null +++ b/docs/en/guides/README.md @@ -0,0 +1,11 @@ +# Quick Start Guides + +- `standalone`: [bare standalone](standalone.md) +- `standalone@vite`: [standalone with Vite](standalone-vite.md) +- `standalone@webpack`: [standalone with Webpack](standalone-webpack.md) +- `react@vite`: [React with Vite](react-vite.md) +- `react@webpack`: [React with Webpack](react-webpack.md) +- `next@app/turbopack`: [App Router with Turbopack](next-app-turbopack.md) +- `next@app/webpack`: [App Router with Webpack](next-app-webpack.md) +- `next@pages/turbopack`: [Pages Router with Turbopack](next-pages-turbopack.md) +- `next@pages/webpack`: [Pages Router with Webpack](next-pages-webpack.md) diff --git a/docs/en/guides/next-app-turbopack.md b/docs/en/guides/next-app-turbopack.md new file mode 100644 index 0000000..aaef31a --- /dev/null +++ b/docs/en/guides/next-app-turbopack.md @@ -0,0 +1,153 @@ +# Next.js App Router Turbopack SVG Sprite Quick Start + +This guide targets the exact mode key `next@app/turbopack`: a generated typed React icon for the Next.js App Router and Turbopack. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config and source icons together: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'next@app/turbopack', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Generate once per invocation and keep the exact Turbopack flags on both commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --turbopack", + "build": "npm run sprites && next build --turbopack" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +The generated icon has no `'use client'` directive and is Server Component-compatible. Import it directly in an App Router page or layout: + +```tsx +// src/app/page.tsx +import { + IconsIcon, + iconsIconNames, +} from '../ui/icons/.svg-sprite/index.js' + +export default function Page() { + return ( +
+ +

{iconsIconNames.length} icons available

+
+ ) +} +``` + +Turbopack resolves the generated `new URL('../sprite.svg', import.meta.url)` and CSS Module, emitting a separate SVG asset. Keep the mode and the `--turbopack` dev/build flags aligned. + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Viewer is interactive, so place the React bridge in a separate Client Component: + +```tsx +// src/app/icon-debug/IconsViewer.tsx +'use client' + +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../../ui/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export function IconsViewer() { + return +} +``` + +Render it from the route's Server Component: + +```tsx +// src/app/icon-debug/page.tsx +import { IconsViewer } from './IconsViewer' + +export default function IconDebugPage() { + return +} +``` + +Keep the route internal or development-only. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@app/turbopack', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'next@app/turbopack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@app/turbopack', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/next-app-webpack.md b/docs/en/guides/next-app-webpack.md new file mode 100644 index 0000000..08ac234 --- /dev/null +++ b/docs/en/guides/next-app-webpack.md @@ -0,0 +1,153 @@ +# Next.js App Router Webpack SVG Sprite Quick Start + +This guide targets the exact mode key `next@app/webpack`: a generated typed React icon for the Next.js App Router and Webpack 5. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config adjacent to its source icons: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'next@app/webpack', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Generate once per invocation and keep the exact Webpack flags on both commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --webpack", + "build": "npm run sprites && next build --webpack" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +The generated icon has no `'use client'` directive and is Server Component-compatible. Import it directly in an App Router page or layout: + +```tsx +// src/app/page.tsx +import { + IconsIcon, + iconsIconNames, +} from '../ui/icons/.svg-sprite/index.js' + +export default function Page() { + return ( +
+ +

{iconsIconNames.length} icons available

+
+ ) +} +``` + +Webpack resolves the generated `new URL('../sprite.svg', import.meta.url)` and CSS Module, emitting a separate SVG asset. Keep the mode and the `--webpack` dev/build flags aligned. If custom Next.js webpack rules process SVG through SVGR, exclude `.svg-sprite/sprite.svg` from those rules. + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Viewer is interactive, so place the React bridge in a separate Client Component: + +```tsx +// src/app/icon-debug/IconsViewer.tsx +'use client' + +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../../ui/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export function IconsViewer() { + return +} +``` + +Render it from the route's Server Component: + +```tsx +// src/app/icon-debug/page.tsx +import { IconsViewer } from './IconsViewer' + +export default function IconDebugPage() { + return +} +``` + +Keep the route internal or development-only. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@app/webpack', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'next@app/webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@app/webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/next-pages-turbopack.md b/docs/en/guides/next-pages-turbopack.md new file mode 100644 index 0000000..a1e53d3 --- /dev/null +++ b/docs/en/guides/next-pages-turbopack.md @@ -0,0 +1,140 @@ +# Next.js Pages Router Turbopack SVG Sprite Quick Start + +This guide targets the exact mode key `next@pages/turbopack`: a generated typed React icon for the Next.js Pages Router and Turbopack. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config and source icons together: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'next@pages/turbopack', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Generate once per invocation and keep the exact Turbopack flags on both commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --turbopack", + "build": "npm run sprites && next build --turbopack" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +Import the generated component and icon-name list into a Pages Router page: + +```tsx +// src/pages/index.tsx +import { + IconsIcon, + iconsIconNames, +} from '../ui/icons/.svg-sprite/index.js' + +export default function HomePage() { + return ( +
+ +

{iconsIconNames.length} icons available

+
+ ) +} +``` + +The component works with SSR, SSG, and client-side navigation. Turbopack resolves the generated SVG URL and CSS Module and emits a separate asset. Keep the mode and the `--turbopack` dev/build flags aligned. + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Pages Router does not require an App Router Client Component boundary. Use the React bridge directly in a page with a static loader array: + +```tsx +// src/pages/icon-debug.tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../ui/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export default function IconDebugPage() { + return +} +``` + +Keep the page internal or development-only. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@pages/turbopack', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'next@pages/turbopack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@pages/turbopack', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/next-pages-webpack.md b/docs/en/guides/next-pages-webpack.md new file mode 100644 index 0000000..32a97b2 --- /dev/null +++ b/docs/en/guides/next-pages-webpack.md @@ -0,0 +1,140 @@ +# Next.js Pages Router Webpack SVG Sprite Quick Start + +This guide targets the exact mode key `next@pages/webpack`: a generated typed React icon for the Next.js Pages Router and Webpack 5. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config adjacent to its source icons: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'next@pages/webpack', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Generate once per invocation and keep the exact Webpack flags on both commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --webpack", + "build": "npm run sprites && next build --webpack" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +Import the generated component and icon-name list into a Pages Router page: + +```tsx +// src/pages/index.tsx +import { + IconsIcon, + iconsIconNames, +} from '../ui/icons/.svg-sprite/index.js' + +export default function HomePage() { + return ( +
+ +

{iconsIconNames.length} icons available

+
+ ) +} +``` + +The component works with SSR, SSG, and client-side navigation. Webpack resolves the generated SVG URL and CSS Module and emits a separate asset. Keep the mode and the `--webpack` dev/build flags aligned. If custom Next.js webpack rules process SVG through SVGR, exclude `.svg-sprite/sprite.svg` from those rules. + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Pages Router does not require an App Router Client Component boundary. Use the React bridge directly in a page with a static loader array: + +```tsx +// src/pages/icon-debug.tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../ui/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export default function IconDebugPage() { + return +} +``` + +Keep the page internal or development-only. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@pages/webpack', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'next@pages/webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@pages/webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/react-vite.md b/docs/en/guides/react-vite.md new file mode 100644 index 0000000..96ed9ea --- /dev/null +++ b/docs/en/guides/react-vite.md @@ -0,0 +1,148 @@ +# React Vite SVG Sprite Quick Start + +This guide targets the exact mode key `react@vite`: a generated typed React component with a Vite-managed SVG asset. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config and source icons together: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'react@vite', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Use the exact Vite dev/build commands and generate once per invocation: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && vite", + "build": "npm run sprites && tsc --noEmit && vite build" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +Import the generated component and icon-name list directly: + +```tsx +// src/App.tsx +import { + IconsIcon, + iconsIconNames, +} from './ui/icons/.svg-sprite/index.js' + +export function App() { + return ( +
+ + {iconsIconNames.length} icons available +
+ ) +} +``` + +`icon` is a generated union of SVG file names. Vite automatically handles the generated CSS Module and the `sprite.svg?no-inline` import, emitting the sprite as a separate asset. If your own TypeScript source imports Vite query assets, include Vite's ambient types: + +```json +{ + "compilerOptions": { + "types": ["vite/client"] + } +} +``` + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Use the React `SpriteViewer` bridge with a static loader array. Keep every `import()` path a string literal: + +```tsx +// src/IconsDebugPage.tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('./ui/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export function IconsDebugPage() { + return +} +``` + +Keep this component on a debug route or in an internal tool. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'react@vite', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'react@vite' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'react@vite', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/react-webpack.md b/docs/en/guides/react-webpack.md new file mode 100644 index 0000000..31a83fd --- /dev/null +++ b/docs/en/guides/react-webpack.md @@ -0,0 +1,156 @@ +# React Webpack SVG Sprite Quick Start + +This guide targets the exact mode key `react@webpack`: a generated typed React component using Webpack 5 Asset Modules and CSS Modules. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config adjacent to its source icons: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'react@webpack', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Use the exact Webpack 5 flags and generate once per invocation: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && webpack serve --mode development", + "build": "npm run sprites && webpack --mode production" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +Import the generated component and icon-name list directly: + +```tsx +// src/App.tsx +import { + IconsIcon, + iconsIconNames, +} from './ui/icons/.svg-sprite/index.js' + +export function App() { + return ( +
+ + {iconsIconNames.length} icons available +
+ ) +} +``` + +The generated component uses `new URL('../sprite.svg', import.meta.url)`, which Webpack 5 processes through Asset Modules and emits as a separate SVG asset. Exclude `.svg-sprite/sprite.svg` from SVG component or SVGR rules so they do not intercept that URL dependency. + +The generated component also imports `react-component.module.css`. Configure `.module.css` through `css-loader` with modules enabled, plus `style-loader` or `MiniCssExtractPlugin`: + +```js +// webpack.config.js (relevant rule) +export default { + module: { + rules: [ + { + test: /\.module\.css$/i, + use: ['style-loader', { loader: 'css-loader', options: { modules: true } }], + }, + ], + }, +} +``` + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Use the React `SpriteViewer` bridge with a static loader array. Keep every `import()` path a string literal so Webpack can create the chunk: + +```tsx +// src/IconsDebugPage.tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('./ui/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export function IconsDebugPage() { + return +} +``` + +Keep this component on a debug route or in an internal tool. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'react@webpack', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'react@webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'react@webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/standalone-vite.md b/docs/en/guides/standalone-vite.md new file mode 100644 index 0000000..27f151e --- /dev/null +++ b/docs/en/guides/standalone-vite.md @@ -0,0 +1,156 @@ +# Standalone Vite SVG Sprite Quick Start + +This guide targets the exact mode key `standalone@vite`: a native generated Web Component with Vite-managed SVG assets. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config and source icons together: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'standalone@vite', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate once directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Use the exact Vite dev/build commands and generate once per invocation: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && vite", + "build": "npm run sprites && vite build" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. The generated JavaScript and declarations live together, and the declarations are self-contained: they do not require `@gromlab/svg-sprites`. + +### Production usage + +Register the generated element once, then use ``: + +```ts +// src/main.ts +import { + defineIconsIconElement, + iconsIconNames, +} from './ui/icons/.svg-sprite/index.js' + +defineIconsIconElement() +console.log('Available icons:', iconsIconNames) +``` + +```html + +``` + +The host is `1em` by `1em`, so `font-size` controls its default size. Transformed colors use `currentColor` and custom properties such as `--icon-color-1`: + +```css +icons-icon { + font-size: 24px; + color: #2563eb; + --icon-color-2: #dbeafe; +} +``` + +Vite handles the generated `sprite.svg?no-inline` import automatically and emits a separate asset. If your own TypeScript source imports Vite query assets, include Vite's ambient types: + +```json +{ + "compilerOptions": { + "types": ["vite/client"] + } +} +``` + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Register the Viewer element, import its type, and assign the generated JavaScript manifest to `sources`: + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +import spriteManifest from './ui/icons/.svg-sprite/svg-sprite.manifest.js' + +document.querySelector('#debug')!.innerHTML = ` + +` + +const viewer = document.querySelector('gromlab-sprite-viewer')! +viewer.sources = [spriteManifest] +``` + +Keep this code on a debug route or in an internal tool. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'standalone@vite', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'standalone@vite' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'standalone@vite', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/standalone-webpack.md b/docs/en/guides/standalone-webpack.md new file mode 100644 index 0000000..29e477b --- /dev/null +++ b/docs/en/guides/standalone-webpack.md @@ -0,0 +1,143 @@ +# Standalone Webpack SVG Sprite Quick Start + +This guide targets the exact mode key `standalone@webpack`: a native generated Web Component using Webpack 5 Asset Modules. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`. + +Keep the config adjacent to its source icons: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.ts +``` + +Use a plain default object export with no package import: + +```ts +// src/ui/icons/svg-sprite.config.ts +export default { + mode: 'standalone@webpack', + name: 'icons', +} +``` + +When `input` is omitted, SVG files are read from `./icons` relative to the config. A `.js` config with a default export and a `.json` config are also supported. Generate directly with: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts +``` + +Use the exact Webpack 5 flags and generate once per invocation: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.ts", + "dev": "npm run sprites && webpack serve --mode development", + "build": "npm run sprites && webpack --mode production" + } +} +``` + +Do not add `predev` or `prebuild` hooks to these scripts; that would run generation twice. In CI, replace `latest` with an exact package version. + +Generation creates a local `.gitignore`; commit that file once, but do not commit `.svg-sprite/`. Generated declarations are self-contained and do not require the package. + +### Production usage + +Register the generated element once, then use ``: + +```ts +// src/main.ts +import { + defineIconsIconElement, + iconsIconNames, +} from './ui/icons/.svg-sprite/index.js' + +defineIconsIconElement() +console.log('Available icons:', iconsIconNames) +``` + +```html + +``` + +The generated facade uses `new URL('./sprite.svg', import.meta.url)`, which Webpack 5 processes through Asset Modules and emits as a separate asset. If the project has SVG-to-component or SVGR rules, exclude `.svg-sprite/sprite.svg` from them so they do not intercept this URL dependency. Check `output.publicPath` if the emitted URL is wrong. + +## 2. Debug and preview + +This section is optional. Only users who need the Viewer or icon previews should install: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Register the Viewer element, import its type, and assign the generated JavaScript manifest to `sources`: + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +import spriteManifest from './ui/icons/.svg-sprite/svg-sprite.manifest.js' + +document.querySelector('#debug')!.innerHTML = ` + +` + +const viewer = document.querySelector('gromlab-sprite-viewer')! +viewer.sources = [spriteManifest] +``` + +Keep this code on a debug route or in an internal tool. Viewer is not part of the production icon runtime. + +## 3. Type the config + +Choose one of these two paths. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'standalone@webpack', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Copy a mode-specific type directly into the config: + +```ts +type LocalSpriteConfig = { + mode: 'standalone@webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'standalone@webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/guides/standalone.md b/docs/en/guides/standalone.md new file mode 100644 index 0000000..4309921 --- /dev/null +++ b/docs/en/guides/standalone.md @@ -0,0 +1,171 @@ +# Standalone SVG Sprite Quick Start + +This guide targets the exact mode key `standalone`: static HTML or a custom asset pipeline with no generated JavaScript facade. + +## 1. Generate the sprite + +No package installation and no `package.json` dependency are needed. `npx` downloads the CLI temporarily, and generated runtime does not import `@gromlab/svg-sprites`; bare `standalone` does not generate a JavaScript runtime at all. + +Keep the config next to its source icons: + +```text +src/ui/icons/ +├── icons/ +│ ├── check.svg +│ └── folder.svg +└── svg-sprite.config.json +``` + +Create a JSON config: + +```json +{ + "mode": "standalone", + "name": "icons" +} +``` + +When `input` is omitted, the generator reads `./icons` next to the config. + +To include SVG files from nested directories, set one glob pattern: + +```json +{ + "mode": "standalone", + "name": "icons", + "input": "../assets/icons/**/*.svg" +} +``` + +An array can mix folders, individual SVG files, and glob patterns: + +```json +{ + "mode": "standalone", + "name": "icons", + "input": [ + "../assets/icons", + "../features/profile/user.svg", + "../features/admin/*.svg" + ] +} +``` + +All relative paths start at the config directory. Pass the config path explicitly: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.json +``` + +For repeatable local commands, the same temporary CLI can be used from a script: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/icons/svg-sprite.config.json" + } +} +``` + +Run that script once from your existing dev/build pipeline. If you use a `predev` or `prebuild` hook instead, do not also call `npm run sprites` inside the corresponding script. Bare `standalone` has no bundler-specific dev or build flag. In CI, replace `latest` with an exact package version. + +Bare `standalone` does not create or modify `.gitignore`; the application decides whether `.svg-sprite/` is committed or ignored. It emits no declarations, facade, or component. Generated declarations in typed modes are self-contained and do not require the package. + +### Production usage + +The application owns the public URL, versioning, and cache policy. Copy both generated assets into the deploy output: + +```bash +cp src/ui/icons/.svg-sprite/sprite.svg public/assets/icons.svg +cp src/ui/icons/.svg-sprite/svg-sprite.manifest.json public/assets/icons.manifest.json +``` + +Use the published URL manually: + +```html + + + +``` + +Safe icon names normally match their fragment IDs. Names containing spaces or other SVG-ID-unsafe characters receive a generated ID; read that icon's `id` from `icons.manifest.json` instead of constructing the fragment yourself. + +## 2. Debug and preview + +This section is optional. Only install the package locally if you need the debug Viewer or an icon preview: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Without a bundler, self-host the browser bundle by copying it from `node_modules` into the deploy output: + +```bash +cp node_modules/@gromlab/svg-sprites/dist/viewer-element.js public/debug/viewer-element.js +``` + +Then provide both public asset URLs through HTML attributes: + +```html + + + +``` + +For a quick preview, a pinned CDN file can replace the self-hosted script: + +```html + +``` + +Self-host the file for controlled environments and pin the CDN version if you use the alternative. Viewer is optional tooling, not part of the production icon runtime. + +## 3. Type the config + +This guide uses a JSON config, which works without TypeScript types. If config autocomplete is needed, replace `svg-sprite.config.json` with `svg-sprite.config.ts` and choose one of these approaches. + +### With a local package installation + +After installing the package locally, use the helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'standalone', + name: 'icons', +}) +``` + +You can alternatively import `type SpriteConfig` and apply `satisfies SpriteConfig`. + +### Without the package + +Keep a narrow local type directly in the config: + +```ts +type LocalSpriteConfig = { + mode: 'standalone' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'standalone', + name: 'icons', +} satisfies LocalSpriteConfig +``` diff --git a/docs/en/next-app.md b/docs/en/next-app.md index 1118741..97c95a7 100644 --- a/docs/en/next-app.md +++ b/docs/en/next-app.md @@ -1,113 +1,4 @@ -# Next.js App Router +# Next.js App Router Guides Moved -[← Back to home](../../README.md) - -Two explicit modes are supported: - -| Bundler | Mode key | -|---|---| -| Turbopack | `next@app/turbopack` | -| Webpack 5 | `next@app/webpack` | - -## 1. Install the package - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Create a sprite module - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - name: 'file-manager', - description: 'File manager icons', -}) -``` - -The root barrel is application-owned: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -## 3. Add generation - -For Turbopack: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager" - } -} -``` - -For Webpack, replace the mode key with `next@app/webpack`. - -Run the first generation before importing the generated module: - -```bash -npm run sprite:file-manager -``` - -## 4. Use it in a Server Component - -The generated component does not contain `'use client'`, so it can be imported directly into `page.tsx` or `layout.tsx`: - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function Page() { - return ( -
- -
- ) -} -``` - -Next.js emits a separate SVG asset with a content hash. The same generated code is used during SSR and in the browser, with no URL mismatch. - -## 5. Add SpriteViewer - -The viewer is interactive, so it requires a separate Client Component boundary: - -```tsx -'use client' - -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), -] - -export default function SpritesPage() { - return -} -``` - -## Verify the bundler - -Run the project's build script configured for the selected bundler: - -```bash -npm run build -``` - -The build script and the generator mode key must target the same bundler. +- [App Router + Turbopack](guides/next-app-turbopack.md) +- [App Router + Webpack](guides/next-app-webpack.md) diff --git a/docs/en/next-pages.md b/docs/en/next-pages.md index 20cfc11..c4763b9 100644 --- a/docs/en/next-pages.md +++ b/docs/en/next-pages.md @@ -1,105 +1,4 @@ -# Next.js Pages Router +# Next.js Pages Router Guides Moved -[← Back to home](../../README.md) - -Two explicit modes are supported: - -| Bundler | Mode key | -|---|---| -| Turbopack | `next@pages/turbopack` | -| Webpack 5 | `next@pages/webpack` | - -## 1. Install the package - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Create a sprite module - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@pages/webpack', - name: 'file-manager', - description: 'File manager icons', -}) -``` - -The root barrel is application-owned: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -## 3. Add generation - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager" - } -} -``` - -For Turbopack, replace the mode key with `next@pages/turbopack`. - -Run the first generation before importing the generated module: - -```bash -npm run sprite:file-manager -``` - -## 4. Use it on a page - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function FilesPage() { - return -} - -export function getServerSideProps() { - return { props: {} } -} -``` - -The component works the same way with SSR, SSG, and client-side navigation. Next.js emits a separate SVG asset with a content hash. - -## 5. Add SpriteViewer - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), -] - -export default function SpritesPage() { - return -} -``` - -## Verify the bundler - -Run the project's build script configured for the selected bundler: - -```bash -npm run build -``` - -The build script and the generator mode key must target the same bundler. +- [Pages Router + Turbopack](guides/next-pages-turbopack.md) +- [Pages Router + Webpack](guides/next-pages-webpack.md) diff --git a/docs/en/programmatic-api.md b/docs/en/programmatic-api.md index d5c42a0..ad49c97 100644 --- a/docs/en/programmatic-api.md +++ b/docs/en/programmatic-api.md @@ -1,147 +1,3 @@ -# Programmatic API +# Programmatic API Moved -[← Back to home](../../README.md) - -The package is ESM-only and provides one Node.js generation API. The framework-neutral Viewer is available from `@gromlab/svg-sprites/viewer`, its auto-register entry from `@gromlab/svg-sprites/viewer/element`, and the React bridge from `@gromlab/svg-sprites/react`. - -## `generateSprite` - -```ts -import { generateSprite } from '@gromlab/svg-sprites' - -const result = await generateSprite( - 'src/ui/file-manager/svg-sprite/svg-sprite.config.ts', -) -``` - -For static standalone mode, use `result.spritePath` in a build script to publish the -SVG under an application URL: - -```ts -import { copyFile } from 'node:fs/promises' - -const result = await generateSprite('src/sprite/svg-sprite.config.ts', { - mode: 'standalone', -}) -await copyFile(result.spritePath, 'dist/app-icons/sprite.svg') -``` - -`spritePath` is a filesystem path, not a browser URL. A deployment-neutral JSON -manifest is available through `result.manifestPath` and is copied independently. - -The first argument accepts the full path to an explicitly selected `.ts`, `.js`, or `.json` config file with any name. Passing a directory enables config-less mode and uses that directory as the sprite module root. - -The second argument contains optional overrides and always takes precedence over the config: - -```ts -await generateSprite('src/ui/file-manager/svg-sprite/custom-config.json', { - mode: 'react@webpack', - name: 'documents', - inputFolder: './assets', - inputFiles: ['../../shared/search.svg'], - transform: { - addTransition: false, - }, - generatedNotice: false, -}) -``` - -Configuration is resolved in this order: - -```text -defaults → config → API overrides -``` - -For fully programmatic generation, pass a directory and provide the required settings as overrides: - -```ts -await generateSprite('src/ui/file-manager/svg-sprite', { - mode: 'react@vite', - name: 'file-manager', - inputFiles: [ - '../../shared/search.svg', - '../../shared/settings.svg', - ], -}) -``` - -## Configuration - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@vite', - name: 'file-manager', - description: 'File manager icons', - inputFolder: './icons', - inputFiles: ['../../shared/check.svg'], - transform: { - removeSize: true, - replaceColors: true, - addTransition: true, - }, - generatedNotice: true, -}) -``` - -`defineSpriteConfig` is an identity helper for TypeScript autocomplete. JavaScript can export the same object with `export default`, while JSON contains the object directly. - -## Specialized wrappers - -The specialized functions are available as wrappers around `generateSprite`: - -```ts -import { generateNextSprite, generateReactSprite } from '@gromlab/svg-sprites' - -await generateReactSprite('path/to/config.ts', 'vite') -await generateNextSprite('path/to/config.ts', { - router: 'app', - bundler: 'turbopack', -}) -``` - -An explicitly supplied target overrides `mode` from the file. Prefer `generateSprite` in new code. - -## Config API - -```ts -import { - loadSpriteConfig, - resolveSpriteConfig, - validateSpriteConfig, -} from '@gromlab/svg-sprites' -``` - -- `loadSpriteConfig(file)` loads an explicitly selected `.ts`, `.js`, or `.json` file. -- `validateSpriteConfig(value)` performs runtime validation. -- `resolveSpriteConfig(root, config, overrides)` merges values, applies defaults, and resolves paths relative to `root`. - -## Low-level compiler - -```ts -import { - compileSprite, - compileSpriteContent, - createShapeTransform, -} from '@gromlab/svg-sprites' -``` - -These functions are intended for custom orchestration. Standard generation should use `generateSprite`. - -## Viewer runtime - -```ts -import '@gromlab/svg-sprites/viewer/element' -import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' -``` - -The browser entry registers ``. Bare standalone can also load the self-contained `dist/viewer-element.js` without a bundler. - -The React bridge keeps the component API: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -``` - -`SpriteViewer` accepts generated manifests, remote standalone sources, lazy loaders, or an `import.meta.glob` result. The React entry contains `'use client'` and is intended for debug tools; production components are imported from local sprite modules. +The canonical document is now the [programmatic API](reference/programmatic-api.md). diff --git a/docs/en/react-vite.md b/docs/en/react-vite.md index 4f882a5..ed23a3c 100644 --- a/docs/en/react-vite.md +++ b/docs/en/react-vite.md @@ -1,125 +1,3 @@ -# React + Vite +# React + Vite Guide Moved -[← Back to home](../../README.md) - -A quick guide to installing and using SVG sprites in a React and Vite project. - -The result is a typed React component and a separate cacheable SVG asset. - -## 1. Install the package - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Create the sprite directory - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -Place the source SVG files in `icons/`. - -## 3. Add the configuration - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@vite', - name: 'file-manager', - description: 'File manager icons', -}) -``` - -The root barrel is application-owned and explicitly re-exports the generated API: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -By default, SVG files are loaded from `./icons`. You can add shared icons from other directories through `inputFiles`: the directory and file list are combined into a single sprite. - -The complete list of options is available under [Unified configuration](reference.md#unified-configuration). - -## 4. Add generation to package.json - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Generated files are excluded from Git, so generation must run before `dev`, `build`, and `typecheck`. - -First run: - -```bash -npm run sprite:file-manager -``` - -## 5. Use the component - -The name `file-manager` is converted to `FileManagerIcon`: - -```tsx -import { FileManagerIcon } from './svg-sprite' - -export const OpenFolderButton = () => ( - -) -``` - -TypeScript checks the `icon` value against the file names: - -```tsx - // valid - // TypeScript error -``` - -Types, rendering methods, and color controls are described in the [technical reference](reference.md#react-component-and-typescript). - -Vite emits the sprite as a separate file named like `assets/sprite-.svg`. SVG path data is not included in JavaScript. - -## 6. Add a debug page - -After integrating the icons, you can display all React sprites with `SpriteViewer`: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' - -const sources = import.meta.glob( - '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', -) - -export const IconsDebugPage = () => ( - -) -``` - -Vite automatically finds the generated manifest for each React sprite. The `import.meta.glob` pattern must be a string literal, and generation must run before Vite starts. - -Only include the Viewer on a debug route or in an internal tool. - -## Troubleshooting - -- Missing `.svg-sprite/index.js`: run `npm run sprite:file-manager`. -- The Viewer cannot find the sprite: check the glob path to `.svg-sprite/svg-sprite.manifest.js`. -- `Refusing to overwrite a user file` error: there is a user file at a generated path. -- The icon does not change color: use `color` or `--icon-color-N`. +The canonical guide is now [React + Vite](guides/react-vite.md). diff --git a/docs/en/react-webpack.md b/docs/en/react-webpack.md index 1d1926c..dbdd36c 100644 --- a/docs/en/react-webpack.md +++ b/docs/en/react-webpack.md @@ -1,129 +1,3 @@ -# React + Webpack 5 +# React + Webpack Guide Moved -[← Back to home](../../README.md) - -A quick guide to installing and using SVG sprites in a React and Webpack 5 project. - -The result is a typed React component and a separate SVG asset emitted through Webpack Asset Modules. - -## 1. Install the package - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Create the sprite directory - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -Place the source SVG files in `icons/`. - -## 3. Add the configuration - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@webpack', - name: 'file-manager', - description: 'File manager icons', -}) -``` - -The root barrel is application-owned and explicitly re-exports the generated API: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -By default, SVG files are loaded from `./icons`. You can add shared icons from other directories through `inputFiles`: the directory and file list are combined into a single sprite. - -The complete list of options is available under [Unified configuration](reference.md#unified-configuration). - -## 4. Add generation to package.json - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Generated files are excluded from Git, so generation must run before `dev`, `build`, and `typecheck`. - -First run: - -```bash -npm run sprite:file-manager -``` - -## 5. Use the component - -```tsx -import { FileManagerIcon } from './svg-sprite' - -export const OpenFolderButton = () => ( - -) -``` - -TypeScript checks the `icon` value against the file names: - -```tsx - // valid - // TypeScript error -``` - -Types, rendering methods, and color controls are described in the [technical reference](reference.md#react-component-and-typescript). - -Webpack processes the generated `new URL('./sprite.svg', import.meta.url)` through Asset Modules and emits a separate SVG asset. - -If the project already uses a custom SVG loader, make sure it does not intercept the generated `sprite.svg` instead of Asset Modules. - -The generated component imports `react/react-component.module.css`, so Webpack must process CSS Modules through `css-loader` and `style-loader` or `MiniCssExtractPlugin`. If the TypeScript project does not include a declaration for CSS Modules, add one separately. - -## 6. Add a debug page - -Webpack does not support Vite's `import.meta.glob` API, so provide static loaders: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('./ui/file-manager/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), - () => import('./ui/navigation/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), -] - -export const IconsDebugPage = () => ( - -) -``` - -The paths in `import()` must be string literals. Webpack creates chunks for the manifests and associates them with the SVG assets. - -Only include the Viewer on a debug route or in an internal tool. - -## Troubleshooting - -- Missing `.svg-sprite/index.js`: run `npm run sprite:file-manager`. -- The Viewer does not load the sprite: check the `import()` path to `.svg-sprite/svg-sprite.manifest.js`. -- Incorrect asset URL: check `output.publicPath`. -- Another loader intercepts the SVG: exclude the generated sprite from the incompatible rule. - -For Next.js, use the separate mode keys described in the [App Router](next-app.md) and [Pages Router](next-pages.md) guides. +The canonical guide is now [React + Webpack](guides/react-webpack.md). diff --git a/docs/en/reference.md b/docs/en/reference.md index f132563..8138c45 100644 --- a/docs/en/reference.md +++ b/docs/en/reference.md @@ -1,555 +1,3 @@ -# Technical reference +# Technical Reference Moved -[← Back to home](../../README.md) - -Reference for the configuration, generated API, and behavior of `@gromlab/svg-sprites`. For step-by-step setup instructions, see the guide for your stack: - -- [Next.js App Router](next-app.md) -- [Next.js Pages Router](next-pages.md) -- [React + Vite](react-vite.md) -- [React + Webpack 5](react-webpack.md) - -## Requirements - -- Node.js 18 or newer; -- the package is distributed as ESM and is loaded with `import`; -- React 18 or 19 is required for generated components and `@gromlab/svg-sprites/react`; -- for typed package exports, use TypeScript 5+ with `moduleResolution: "bundler"`, `"node16"`, or `"nodenext"`. - -Install the package as a development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## CLI and generation modes - -The CLI accepts exactly one path: an explicitly selected config file or a directory for config-less generation: - -```text -svg-sprites [options] -``` - -| Environment | Mode | -|---|---| -| Static HTML / custom publishing | `standalone` | -| Standalone + Vite | `standalone@vite` | -| Standalone + Webpack 5 | `standalone@webpack` | -| React + Vite | `react@vite` | -| React + Webpack 5 | `react@webpack` | -| Next.js App Router + Turbopack | `next@app/turbopack` | -| Next.js App Router + Webpack 5 | `next@app/webpack` | -| Next.js Pages Router + Turbopack | `next@pages/turbopack` | -| Next.js Pages Router + Webpack 5 | `next@pages/webpack` | - -The config file may have any name and use the `.ts`, `.js`, or `.json` extension. The CLI does not discover it by convention: pass the file explicitly. The guides use `svg-sprite.config.ts` as the recommended name. - -When a directory is passed, all settings come from CLI options. When a config file is passed, CLI options override the file. The full order is `defaults → config → CLI`. - -Available options are `--mode`, `--name`, `--description`, `--input-folder`, repeatable `--input-file`, plus the `--remove-size`/`--no-remove-size`, `--replace-colors`/`--no-replace-colors`, `--add-transition`/`--no-add-transition`, and `--generated-notice`/`--no-generated-notice` pairs. Transform flags override individual fields, while supplying at least one `--input-file` replaces the complete config `inputFiles` array. - -The mode must match the application's publishing strategy. Bare `standalone` leaves the public URL to the application; Vite and Webpack modes generate bundler-specific SVG asset integration. - -## Unified configuration - -Each config file defines one independent sprite. - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - name: 'app', - description: 'Shared application icons', - inputFolder: './local-icons', - inputFiles: [ - '../../assets/icons/search.svg', - '../../assets/icons/settings.svg', - ], - transform: { - removeSize: true, - replaceColors: true, - addTransition: true, - }, - generatedNotice: true, -}) -``` - -| Option | Type | Default | Purpose | -|---|---|---|---| -| `mode` | `SpriteMode` | None | Generation mode; may be supplied by CLI/API | -| `name` | `string` | Derived from the directory | Name of the sprite, component, and public types | -| `description` | `string` | None | Description for types and the debug manifest | -| `inputFolder` | `string` | `./icons` | SVG directory relative to the module root | -| `inputFiles` | `string[]` | `[]` | Paths to individual SVG files relative to the module root | -| `transform` | `TransformOptions` | All enabled | SVG preparation settings | -| `generatedNotice` | `boolean` | `true` | Full or abbreviated warning in generated files | - -### Sprite name - -`name` is written in kebab-case and must start with an ASCII letter: - -```text -app → AppIcon -file-manager → FileManagerIcon -``` - -If `name` is omitted, the generator derives it from the directory. For a directory named `svg-sprite` or `svg-sprites`, the parent directory's name is used. - -### Icon sources - -`inputFolder` and `inputFiles` are combined into one set. This lets you keep local SVG files next to a module and add shared icons from other parts of the project without copying them. - -If `inputFiles` is populated and the implicit `./icons` directory does not exist, generation uses only the file list. An explicitly configured `inputFolder` that does not exist is an error. - -Only the top level of the directory is scanned. Nested directories are not traversed recursively. For a nested structure, list the exact paths through `inputFiles`. - -Identical absolute paths are deduplicated. Different SVG files with the same file name are treated as a conflict because the public icon name is derived from the basename. - -## Generated module - -After generation, a React or Next.js sprite directory looks like this: - -```text -app-icons/ -├── .gitignore -├── svg-sprite.config.ts -├── index.ts # optional user-owned barrel -└── .svg-sprite/ - ├── state.json - ├── index.js - ├── index.d.ts - ├── icon-data.js - ├── icon-data.d.ts - ├── sprite.svg - ├── svg-sprite.manifest.js - ├── svg-sprite.manifest.d.ts - └── react/ - ├── react-component.js - ├── react-component.d.ts - └── react-component.module.css -``` - -| File | Purpose | -|---|---| -| `.svg-sprite/index.js` | Production exports for the component and runtime icon-name list | -| `.svg-sprite/index.d.ts` | Public declarations for the component, props, styles, and icon-name union | -| `.svg-sprite/svg-sprite.manifest.js` | Debug metadata and the asset URL for `SpriteViewer` | -| `.svg-sprite/sprite.svg` | Compiled SVG sprite | -| `.svg-sprite/react/react-component.js` | React component runtime without TypeScript or JSX | -| `.svg-sprite/react/react-component.d.ts` | React component props, style, and declaration | -| `.svg-sprite/react/react-component.module.css` | Styles for the React implementation | -| `.svg-sprite/icon-data.js` | Runtime icon-name list and internal IDs | -| `.svg-sprite/*.d.ts` | TypeScript declarations for the corresponding JavaScript modules | -| `.svg-sprite/state.json` | Mode, contract version, and managed file list | - -Standalone contracts do not create `react/`. Bare `standalone` contains only the -runtime asset and deployment-neutral manifest data: - -```text -.svg-sprite/ -├── state.json -├── sprite.svg -└── svg-sprite.manifest.json -``` - -`standalone@vite` and `standalone@webpack` additionally create `index.*`, -`icon-data.*`, and a resolved `svg-sprite.manifest.*`. - -The generator overwrites and deletes only files that contain its marker. If a user file occupies a managed path, generation fails. The root `index.ts` is user-owned; create a barrel when needed: - -```ts -export * from './.svg-sprite' -``` - -## React component and TypeScript - -A sprite with `name: 'app'` exports: - -```ts -export { AppIcon, appIconNames } -export type { AppIconName, AppIconProps, AppIconStyle } -``` - -### Icon names - -SVG file names become valid `icon` values: - -```tsx - - // TypeScript error -``` - -The runtime list contains the same values: - -```ts -import { appIconNames } from '@/ui/app-icons' - -// readonly ['search', 'settings', 'user'] -``` - -Names containing spaces or other characters that are unsafe in SVG IDs remain part of the public API. For the internal fragment ID, the generator creates a stable, safe hash: - -```text -folder open.svg → icon="folder open" → id="icon-" -``` - -For these names, use the generated component or the `id` from the debug manifest instead of constructing the fragment ID manually. - -### SVG attributes - -By default, the component renders an `` and accepts standard SVG attributes: - -```tsx - -``` - -The component does not add accessibility semantics automatically. Pass appropriate `aria-*` attributes, a `role`, or a label based on the icon's purpose. - -### Wrapper - -`wrapped` renders a `` containing the SVG. In this mode, the remaining props apply to the ``: - -```tsx - -``` - -### Typed CSS custom properties - -`AppIconStyle` extends `CSSProperties` and supports properties in the form `--icon-color-N`: - -```tsx - -``` - -## Multiple sprites - -Each directory with a configuration creates an independent component, types, manifest, and SVG asset: - -```text -app-icons → AppIcon → shared icons -analytics-icons → AnalyticsIcon → analytics page icons -editor-icons → EditorIcon → editor icons -``` - -The same source SVG can be added to multiple configurations through `inputFiles`. You do not need to copy the file into each sprite directory. - -For multiple sprites, add a separate CLI command for each directory or combine the commands in a shared npm script. - -## Formats and rendering methods - -All current modes generate the `stack` format. - -| Format | `` | `` | CSS background | -|---|---:|---:|---:| -| `stack` | Yes | Yes | Yes | - -### Generated component - -For React and Next.js, use the generated component. It knows the internal IDs, constructs the URL, and provides a TypeScript API: - -```tsx - -``` - -### Manually with `` - -How you obtain `spriteUrl` depends on the bundler. - -Static HTML after the application publishes `.svg-sprite/sprite.svg`: - -```html - -``` - -Standalone Vite/Webpack provides generated `getIconsIconHref()` and an internal ID -map. Do not construct fragments from unsafe file names manually. - -Vite: - -```ts -import spriteUrl from './.svg-sprite/sprite.svg?no-inline' -``` - -Webpack 5, Turbopack, and Next.js: - -```ts -const spriteUrl = new URL('./.svg-sprite/sprite.svg', import.meta.url).href -``` - -After obtaining the URL, use it in JSX: - -```tsx - - - -``` - -For names that are unsafe as SVG IDs, use the internal `id` from the manifest. - -### With `` - -```tsx -Search -``` - -An SVG inside `` is isolated from the page's CSS. Setting `color` or `--icon-color-N` on the outer element does not change its internal colors. - -### With CSS - -```css -.icon { - background: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; -} -``` - -For a single-color silhouette, you can use a mask: - -```css -.icon { - background-color: currentColor; - mask: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; -} -``` - -A mask does not preserve original colors, gradients, or differences between `fill` and `stroke`. - -The path in CSS is resolved relative to the CSS file itself. In these examples, the CSS file is next to `svg-sprite.config.ts`. - -## Assets and caching - -The generated component or standalone facade passes the SVG to the bundler as a separate asset: - -- Vite uses a static import with `?no-inline`; -- Webpack 5, Turbopack, and Next.js use `new URL(..., import.meta.url)`; -- SVG path data is not serialized into generated JavaScript. - -Bare `standalone` does not participate in an asset pipeline: the application copies -or publishes `sprite.svg` and owns its URL, versioning, and cache policy. - -With standard asset naming, the bundler adds a content hash: - -```text -/assets/sprite-.svg -``` - -This allows the SVG to be cached separately from JavaScript. Changing React code does not change the sprite contents, while changing icons creates a new asset version. - -HTTP cache headers, CDN behavior, and `Cache-Control` are configured by the application or hosting platform. With Webpack, the final file name depends on the project's `assetModuleFilename`. - -## SVG transformations - -All transformations are enabled by default and can be configured independently: - -| Option | Behavior | -|---|---| -| `removeSize` | Removes `width` and `height` from the root `` while preserving an existing `viewBox` | -| `replaceColors` | Replaces detected `fill` and `stroke` values with `--icon-color-N` | -| `addTransition` | Adds transitions for `fill` and `stroke` to colored elements and generated styles | - -To disable an individual operation: - -```ts -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - transform: { - removeSize: false, - replaceColors: false, - addTransition: false, - }, -}) -``` - -Source SVG files are not modified. Transformations apply only to the generated sprite contents. - -## Color management - -### Monochrome icons - -If one color is detected, its fallback becomes `currentColor`: - -```svg -stroke="var(--icon-color-1, currentColor)" -``` - -Set the color through a prop or CSS: - -```tsx - -``` - -### Multicolor icons - -Each unique color gets its own custom property with the original color as its fallback: - -```svg -fill="var(--icon-color-1, #798198)" -fill="var(--icon-color-2, #ffffff)" -fill="var(--icon-color-3, #129d9d)" -``` - -You can override only the values you need: - -```css -.icon { - --icon-color-1: #4b5563; - --icon-color-3: #14b8a6; -} -``` - -### Limitations - -- `none`, `transparent`, `inherit`, `unset`, and `initial` are not replaced; -- colors in `fill`, `stroke`, and inline `style` attributes are handled most reliably; -- CSS classes and external stylesheets inside the SVG are not the primary transformation use case; -- `url(#...)` values may be replaced along with colors, so gradients and patterns require a separate sprite with `replaceColors: false`; -- masks, filters, and complex internal CSS rules require visual verification; -- page CSS custom properties are available through ``, but not inside `` or a CSS background. - -For a complex icon, you can disable `replaceColors` in a separate sprite configuration. - -## SpriteViewer - -The Viewer uses one Shadow DOM Web Component for every mode. React and future framework components are bridges to that same element, so the visuals and behavior are not duplicated. - -Bare `standalone` loads the self-contained browser bundle and supplies the JSON manifest URL and the published SVG URL: - -```html - - - -``` - -`viewer-element.js` has no additional runtime files and can be copied with the other static assets for self-hosting. - -`standalone@vite` and `standalone@webpack` register the same element through an npm entry and pass the generated JS manifest through the `sources` property: - -```ts -import '@gromlab/svg-sprites/viewer/element' -import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' -import spriteManifest from './svg-sprite/.svg-sprite/svg-sprite.manifest.js' - -const viewer = document.querySelector('gromlab-sprite-viewer')! -viewer.sources = [spriteManifest] -``` - -React and Next.js keep the component API: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -``` - -It accepts ready-made manifests, remote standalone sources, an array of lazy loaders, or a record in the format returned by `import.meta.glob`. - -Vite: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' - -const sources = import.meta.glob( - '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', -) - -export const IconsDebugPage = () => ( - -) -``` - -Webpack and Next.js: - -```tsx -const sources = [ - () => import('@/ui/app-icons/.svg-sprite/svg-sprite.manifest.js'), - () => import('@/features/analytics/icons/.svg-sprite/svg-sprite.manifest.js'), -] - -export const IconsDebugPage = () => ( - -) -``` - -The Viewer displays groups, search, `viewBox`, CSS custom properties, and fallback colors. React/Next manifests get React, SVG, IMG, and CSS tabs; standalone manifests get SVG, IMG, and CSS. You can change color values in the interface and immediately inspect the result. - -### Viewer theme - -By default, `colorTheme="auto"` follows `prefers-color-scheme`. You can explicitly pass `light` or `dark`: - -```tsx - -``` - -To synchronize it with the application theme: - -```tsx - -``` - -`@gromlab/svg-sprites/react` contains `'use client'` and renders the Web Component host; its internal Shadow DOM is created after the browser runtime loads. In the Next.js App Router, place the Viewer inside a separate Client Component boundary and use it only on a debug route or in an internal tool. - -## Generated files, Git, and CI - -A modern sprite module creates a local `.gitignore` for: - -```text -/.svg-sprite/ -``` - -Commit the local `.gitignore` to the repository once. It excludes the other generated files, so generation must run before commands that import the sprite module: - -```json -{ - "scripts": { - "sprites": "svg-sprites src/ui/app-icons/svg-sprite.config.ts", - "predev": "npm run sprites", - "prebuild": "npm run sprites", - "pretypecheck": "npm run sprites" - } -} -``` - -CI must install development dependencies and run the generation script before building or type-checking. - -If the sprite directory already contains a user-created `.gitignore` or a user-owned file inside `.svg-sprite`, the generator will not overwrite it. The root `index.ts` remains user-owned and may re-export the generated API. - -## Troubleshooting - -- Missing `.svg-sprite/index.js`: run the generation script before importing the generated module. -- Source not found: pass an existing config file or sprite module directory. -- Mode missing: add `mode` to the config or pass `--mode`. -- Icon missing from the type: check `inputFiles`, the `.svg` extension, and the nesting level under `inputFolder`. -- Name conflict: two different SVG files have the same basename; rename one of them. -- `Refusing to overwrite a user file`: a file without the generated marker occupies a managed path. -- The icon does not change color: use `` or the generated component and check `replaceColors`. -- Webpack emits an incorrect URL: check Asset Modules, `output.publicPath`, and SVG loaders. -- Static sprite returns 404: check the post-generation copy or server alias, and do not put a filesystem `spritePath` into HTML. -- The Viewer cannot find the sprite: check the path to `.svg-sprite/svg-sprite.manifest.js` and run generation before starting the application. -- Build and mode do not match: use the target that corresponds to the actual bundler. - -For custom orchestration and low-level compilation, see the [Programmatic API](programmatic-api.md). +The canonical document is now the [technical reference](reference/technical.md). diff --git a/docs/en/reference/README.md b/docs/en/reference/README.md new file mode 100644 index 0000000..a3bafd6 --- /dev/null +++ b/docs/en/reference/README.md @@ -0,0 +1,4 @@ +# Reference + +- [Technical reference](technical.md) +- [Programmatic API](programmatic-api.md) diff --git a/docs/en/reference/programmatic-api.md b/docs/en/reference/programmatic-api.md new file mode 100644 index 0000000..cef0c34 --- /dev/null +++ b/docs/en/reference/programmatic-api.md @@ -0,0 +1,147 @@ +# Programmatic API + +[Documentation index](../README.md) + +The package is ESM-only and provides one Node.js generation API. The framework-neutral Viewer is available from `@gromlab/svg-sprites/viewer`, its auto-register entry from `@gromlab/svg-sprites/viewer/element`, and the React bridge from `@gromlab/svg-sprites/react`. + +## `generateSprite` + +```ts +import { generateSprite } from '@gromlab/svg-sprites' + +const result = await generateSprite( + 'src/ui/file-manager/svg-sprite/svg-sprite.config.ts', +) +``` + +For static standalone mode, use `result.spritePath` in a build script to publish the +SVG under an application URL: + +```ts +import { copyFile } from 'node:fs/promises' + +const result = await generateSprite('src/sprite/svg-sprite.config.ts', { + mode: 'standalone', +}) +await copyFile(result.spritePath, 'dist/app-icons/sprite.svg') +``` + +`spritePath` is a filesystem path, not a browser URL. A deployment-neutral JSON +manifest is available through `result.manifestPath` and is copied independently. + +The first argument accepts the full path to an explicitly selected `.ts`, `.js`, or `.json` config file with any name. Passing a directory enables config-less mode and uses that directory as the sprite module root. + +The second argument contains optional overrides and always takes precedence over the config: + +```ts +await generateSprite('src/ui/file-manager/svg-sprite/custom-config.json', { + mode: 'react@webpack', + name: 'documents', + input: ['./assets', '../../shared/search.svg'], + transform: { + addTransition: false, + }, + generatedNotice: false, +}) +``` + +Configuration is resolved in this order: + +```text +defaults → config → API overrides +``` + +For fully programmatic generation, pass a directory and provide the required settings as overrides: + +```ts +await generateSprite('src/ui/file-manager/svg-sprite', { + mode: 'react@vite', + name: 'file-manager', + input: [ + '../../shared/search.svg', + '../../shared/settings.svg', + ], +}) +``` + +## Configuration + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'react@vite', + name: 'file-manager', + description: 'File manager icons', + input: ['./icons', '../../shared/check.svg'], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +`input` accepts one folder, SVG file, or glob pattern, or an array that combines them. When omitted, it defaults to `./icons`; relative paths start at the config directory. + +`defineSpriteConfig` is an identity helper for TypeScript autocomplete. JavaScript can export the same object with `export default`, while JSON contains the object directly. + +## Specialized wrappers + +The specialized functions are available as wrappers around `generateSprite`: + +```ts +import { generateNextSprite, generateReactSprite } from '@gromlab/svg-sprites' + +await generateReactSprite('path/to/config.ts', 'vite') +await generateNextSprite('path/to/config.ts', { + router: 'app', + bundler: 'turbopack', +}) +``` + +An explicitly supplied target overrides `mode` from the file. Prefer `generateSprite` in new code. + +## Config API + +```ts +import { + loadSpriteConfig, + resolveSpriteConfig, + validateSpriteConfig, +} from '@gromlab/svg-sprites' +``` + +- `loadSpriteConfig(file)` loads an explicitly selected `.ts`, `.js`, or `.json` file. +- `validateSpriteConfig(value)` performs runtime validation. +- `resolveSpriteConfig(root, config, overrides)` merges values, applies defaults, and resolves paths relative to `root`. + +## Low-level compiler + +```ts +import { + compileSprite, + compileSpriteContent, + createShapeTransform, +} from '@gromlab/svg-sprites' +``` + +These functions are intended for custom orchestration. Standard generation should use `generateSprite`. + +## Viewer runtime + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +``` + +The browser entry registers ``. Bare standalone can also load the self-contained `dist/viewer-element.js` without a bundler. + +The React bridge keeps the component API: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +``` + +`SpriteViewer` accepts generated manifests, remote standalone sources, lazy loaders, or an `import.meta.glob` result. The React entry contains `'use client'` and is intended for debug tools; production components are imported from local sprite modules. diff --git a/docs/en/reference/technical.md b/docs/en/reference/technical.md new file mode 100644 index 0000000..d89a8ce --- /dev/null +++ b/docs/en/reference/technical.md @@ -0,0 +1,632 @@ +# Technical reference + +[Documentation index](../README.md) + +Reference for the configuration, generated API, and behavior of `@gromlab/svg-sprites`. For step-by-step setup instructions, see the guide for your stack: + +- [Bare standalone](../guides/standalone.md) +- [Standalone + Vite](../guides/standalone-vite.md) +- [Standalone + Webpack 5](../guides/standalone-webpack.md) +- [React + Vite](../guides/react-vite.md) +- [React + Webpack 5](../guides/react-webpack.md) +- [Next.js App Router + Turbopack](../guides/next-app-turbopack.md) +- [Next.js App Router + Webpack](../guides/next-app-webpack.md) +- [Next.js Pages Router + Turbopack](../guides/next-pages-turbopack.md) +- [Next.js Pages Router + Webpack](../guides/next-pages-webpack.md) + +## Requirements + +- Node.js 18 or newer; +- the package is distributed as ESM and is loaded with `import`; +- React 18 or 19 is required only for React/Next generated components and `@gromlab/svg-sprites/react`; +- for typed package exports, use TypeScript 5+ with `moduleResolution: "bundler"`, `"node16"`, or `"nodenext"`. + +Generation does not require a project dependency. Run the CLI through `npx`: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites path/to/svg-sprite.config.ts +``` + +Install the package as a development dependency only when the project needs the +Viewer, config types, or the programmatic API: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +## CLI and generation modes + +The CLI accepts exactly one path: an explicitly selected config file or a directory for config-less generation: + +```text +svg-sprites [options] +``` + +| Environment | Mode | +|---|---| +| Static HTML / custom publishing | `standalone` | +| Standalone + Vite | `standalone@vite` | +| Standalone + Webpack 5 | `standalone@webpack` | +| React + Vite | `react@vite` | +| React + Webpack 5 | `react@webpack` | +| Next.js App Router + Turbopack | `next@app/turbopack` | +| Next.js App Router + Webpack 5 | `next@app/webpack` | +| Next.js Pages Router + Turbopack | `next@pages/turbopack` | +| Next.js Pages Router + Webpack 5 | `next@pages/webpack` | + +The config file may have any name and use the `.ts`, `.js`, or `.json` extension. The CLI does not discover it by convention: pass the file explicitly. The guides use `svg-sprite.config.ts` as the recommended name. + +When a directory is passed, all settings come from CLI options. When a config file is passed, CLI options override the file. The full order is `defaults → config → CLI`. + +Available options are `--mode`, `--name`, `--description`, repeatable `--input `, plus the `--remove-size`/`--no-remove-size`, `--replace-colors`/`--no-replace-colors`, `--add-transition`/`--no-add-transition`, and `--generated-notice`/`--no-generated-notice` pairs. Transform flags override individual fields, while supplying at least one `--input` replaces the complete config `input` value. + +Quote CLI glob patterns with single quotes so the shell does not expand them before the generator receives them: + +```bash +svg-sprites --input './icons/**/*.svg' --input '!./icons/legacy/**' svg-sprite.config.ts +``` + +The mode must match the application's publishing strategy. Bare `standalone` leaves the public URL to the application; Vite and Webpack modes generate bundler-specific SVG asset integration. + +## Unified configuration + +Each config file defines one independent sprite. + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@app/turbopack', + name: 'app', + description: 'Shared application icons', + input: [ + './local-icons', + '../../assets/icons/*.svg', + '!../../assets/icons/deprecated-*.svg', + ], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +| Option | Type | Default | Purpose | +|---|---|---|---| +| `mode` | `SpriteMode` | None | Generation mode; may be supplied by CLI/API | +| `name` | `string` | Derived from the directory | Name of the sprite, component, and public types | +| `description` | `string` | None | Description for types and the debug manifest | +| `input` | `string \| string[]` | `./icons` | SVG folders, files, and glob patterns relative to the config directory | +| `transform` | `TransformOptions` | All enabled | SVG preparation settings | +| `generatedNotice` | `boolean` | `true` | Full or abbreviated warning in generated files | + +### Sprite name + +`name` is written in kebab-case and must start with an ASCII letter: + +```text +app → AppIcon +file-manager → FileManagerIcon +``` + +If `name` is omitted, the generator derives it from the directory. For a directory named `svg-sprite` or `svg-sprites`, the parent directory's name is used. + +### Icon sources + +`SpriteConfig.input` is optional and has the type `string | string[]`. When it is omitted, the source is the literal `./icons` folder relative to the config directory. In config-less mode, relative paths start at the directory passed to the CLI or API. + +Each positive string may be a literal folder, a literal `.svg` file, or a glob pattern. A literal folder includes only its immediate `*.svg` children. Use an explicit pattern such as `icons/**/*.svg` to traverse nested directories. + +An array combines all positive sources. A pattern prefixed with `!` excludes its matches from the combined result globally, regardless of which positive source included them. + +Supported glob syntax includes: + +| Syntax | Meaning | +|---|---| +| `*` | Any characters within one path segment | +| `**` | Any number of nested directories | +| `?` | One character within a path segment | +| `{a,b}` | Either alternative | +| `[abc]` | One character from the set or range | +| `!pattern` | Exclude matches from the full combined input | + +Every positive source or pattern must find at least one SVG, otherwise generation fails. Duplicate paths are removed and the final file list is sorted deterministically. Different SVG files with the same basename remain a conflict because the basename defines the public icon name. + +## Generated module + +After generation, a React or Next.js sprite directory looks like this: + +```text +app-icons/ +├── .gitignore +├── svg-sprite.config.ts +├── index.ts # optional user-owned barrel +└── .svg-sprite/ + ├── index.js + ├── index.d.ts + ├── icon-data.js + ├── icon-data.d.ts + ├── sprite.svg + ├── svg-sprite.manifest.js + ├── svg-sprite.manifest.d.ts + └── react/ + ├── react-component.js + ├── react-component.d.ts + └── react-component.module.css +``` + +| File | Purpose | +|---|---| +| `.svg-sprite/index.js` | Mode-specific production facade and runtime icon-name list | +| `.svg-sprite/index.d.ts` | Public declarations for the facade, component, and icon-name union | +| `.svg-sprite/svg-sprite.manifest.js` | Debug metadata and the asset URL for `SpriteViewer` | +| `.svg-sprite/sprite.svg` | Compiled SVG sprite | +| `.svg-sprite/react/react-component.js` | React component runtime without TypeScript or JSX | +| `.svg-sprite/react/react-component.d.ts` | React component props, style, and declaration | +| `.svg-sprite/react/react-component.module.css` | Styles for the React implementation | +| `.svg-sprite/icon-data.js` | Runtime icon-name list and internal IDs | +| `.svg-sprite/*.d.ts` | TypeScript declarations for the corresponding JavaScript modules | + +Standalone contracts do not create `react/`. Bare `standalone` contains only the +runtime asset and deployment-neutral manifest data: + +```text +.svg-sprite/ +├── sprite.svg +└── svg-sprite.manifest.json +``` + +`standalone@vite` and `standalone@webpack` additionally create `index.*`, +`icon-data.*`, and a resolved `svg-sprite.manifest.*`. Their facade contains a +native generated Web Component with no external runtime dependencies. Bare +`standalone` intentionally does not generate a JavaScript component. + +The generator overwrites and deletes only files that contain its marker. If a user file occupies a managed path, generation fails. The root `index.ts` is user-owned; create a barrel when needed: + +```ts +export * from './.svg-sprite' +``` + +## Standalone Web Component and TypeScript + +In `standalone@vite` and `standalone@webpack`, a sprite with `name: 'app'` +exports the `defineAppIconElement()` registration function and the `` +tag: + +```ts +import { defineAppIconElement } from '@/ui/app-icons' + +defineAppIconElement() +``` + +After registration, use the element in HTML: + +```html + + + +``` + +The component renders `` in an open Shadow DOM, selects the internal +ID and `viewBox`, and obtains the asset URL through the corresponding Vite or +Webpack mechanism. The host defaults to `1em × 1em`; set `class`, `style`, +`color`, and `--icon-color-N` with ordinary CSS. + +The generated `HTMLElementTagNameMap` types the property API: + +```ts +const icon = document.createElement('app-icon') + +icon.icon = 'search' +icon.icon = 'unknown' // TypeScript error +``` + +TypeScript does not validate attribute values in plain HTML. Therefore an +unknown `icon="unknown"` is also validated at runtime: the component hides its +inner SVG and reports an error instead of creating a `#undefined` fragment. +Calling `defineAppIconElement()` repeatedly is safe for the same sprite; a +different element already registered as `` causes an error. + +## React component and TypeScript + +A sprite with `name: 'app'` exports: + +```ts +export { AppIcon, appIconNames } +export type { AppIconName, AppIconProps, AppIconStyle } +``` + +### Icon names + +SVG file names become valid `icon` values: + +```tsx + + // TypeScript error +``` + +The runtime list contains the same values: + +```ts +import { appIconNames } from '@/ui/app-icons' + +// readonly ['search', 'settings', 'user'] +``` + +Names containing spaces or other characters that are unsafe in SVG IDs remain part of the public API. For the internal fragment ID, the generator creates a stable, safe hash: + +```text +folder open.svg → icon="folder open" → id="icon-" +``` + +For these names, use the generated component or the `id` from the debug manifest instead of constructing the fragment ID manually. + +### SVG attributes + +By default, the component renders an `` and accepts standard SVG attributes: + +```tsx + +``` + +The component does not add accessibility semantics automatically. Pass appropriate `aria-*` attributes, a `role`, or a label based on the icon's purpose. + +### Wrapper + +`wrapped` renders a `` containing the SVG. In this mode, the remaining props apply to the ``: + +```tsx + +``` + +### Typed CSS custom properties + +`AppIconStyle` extends `CSSProperties` and supports properties in the form `--icon-color-N`: + +```tsx + +``` + +## Multiple sprites + +Each directory with a configuration creates an independent component, types, manifest, and SVG asset: + +```text +app-icons → AppIcon → shared icons +analytics-icons → AnalyticsIcon → analytics page icons +editor-icons → EditorIcon → editor icons +``` + +The same source SVG can be added to multiple configurations through `input`. You do not need to copy the file into each sprite directory. + +For multiple sprites, add a separate CLI command for each directory or combine the commands in a shared npm script. + +## Formats and rendering methods + +All current modes generate the `stack` format. + +| Format | `` | `` | CSS background | +|---|---:|---:|---:| +| `stack` | Yes | Yes | Yes | + +### Generated component + +For React and Next.js, use the generated React component. It knows the internal IDs, constructs the URL, and provides a TypeScript API: + +```tsx + +``` + +For `standalone@vite` and `standalone@webpack`, use the generated Web Component: + +```html + +``` + +### Manually with `` + +How you obtain `spriteUrl` depends on the bundler. + +Static HTML after the application publishes `.svg-sprite/sprite.svg`: + +```html + +``` + +Standalone Vite/Webpack provides generated `getIconsIconHref()` and an internal ID +map. Do not construct fragments from unsafe file names manually. + +Vite: + +```ts +import spriteUrl from './.svg-sprite/sprite.svg?no-inline' +``` + +Webpack 5, Turbopack, and Next.js: + +```ts +const spriteUrl = new URL('./.svg-sprite/sprite.svg', import.meta.url).href +``` + +After obtaining the URL, use it in JSX: + +```tsx + + + +``` + +For names that are unsafe as SVG IDs, use the internal `id` from the manifest. + +### With `` + +```tsx +Search +``` + +An SVG inside `` is isolated from the page's CSS. Setting `color` or `--icon-color-N` on the outer element does not change its internal colors. + +### With CSS + +```css +.icon { + background: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; +} +``` + +For a single-color silhouette, you can use a mask: + +```css +.icon { + background-color: currentColor; + mask: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; +} +``` + +A mask does not preserve original colors, gradients, or differences between `fill` and `stroke`. + +The path in CSS is resolved relative to the CSS file itself. In these examples, the CSS file is next to `svg-sprite.config.ts`. + +## Assets and caching + +The generated component or standalone facade passes the SVG to the bundler as a separate asset: + +- Vite uses a static import with `?no-inline`; +- Webpack 5, Turbopack, and Next.js use `new URL(..., import.meta.url)`; +- SVG path data is not serialized into generated JavaScript. + +Bare `standalone` does not participate in an asset pipeline: the application copies +or publishes `sprite.svg` and owns its URL, versioning, and cache policy. + +With standard asset naming, the bundler adds a content hash: + +```text +/assets/sprite-.svg +``` + +This allows the SVG to be cached separately from JavaScript. Changing React code does not change the sprite contents, while changing icons creates a new asset version. + +HTTP cache headers, CDN behavior, and `Cache-Control` are configured by the application or hosting platform. With Webpack, the final file name depends on the project's `assetModuleFilename`. + +## SVG transformations + +All transformations are enabled by default and can be configured independently: + +| Option | Behavior | +|---|---| +| `removeSize` | Removes `width` and `height` from the root `` while preserving an existing `viewBox` | +| `replaceColors` | Replaces detected `fill` and `stroke` values with `--icon-color-N` | +| `addTransition` | Adds transitions for `fill` and `stroke` to colored elements and generated styles | + +To disable an individual operation: + +```ts +export default defineSpriteConfig({ + mode: 'next@app/turbopack', + transform: { + removeSize: false, + replaceColors: false, + addTransition: false, + }, +}) +``` + +Source SVG files are not modified. Transformations apply only to the generated sprite contents. + +## Color management + +### Monochrome icons + +If one color is detected, its fallback becomes `currentColor`: + +```svg +stroke="var(--icon-color-1, currentColor)" +``` + +Set the color through a prop or CSS: + +```tsx + +``` + +### Multicolor icons + +Each unique color gets its own custom property with the original color as its fallback: + +```svg +fill="var(--icon-color-1, #798198)" +fill="var(--icon-color-2, #ffffff)" +fill="var(--icon-color-3, #129d9d)" +``` + +You can override only the values you need: + +```css +.icon { + --icon-color-1: #4b5563; + --icon-color-3: #14b8a6; +} +``` + +### Limitations + +- `none`, `transparent`, `inherit`, `unset`, and `initial` are not replaced; +- colors in `fill`, `stroke`, and inline `style` attributes are handled most reliably; +- CSS classes and external stylesheets inside the SVG are not the primary transformation use case; +- `url(#...)` values may be replaced along with colors, so gradients and patterns require a separate sprite with `replaceColors: false`; +- masks, filters, and complex internal CSS rules require visual verification; +- page CSS custom properties are available through ``, but not inside `` or a CSS background. + +For a complex icon, you can disable `replaceColors` in a separate sprite configuration. + +## SpriteViewer + +The Viewer uses one Shadow DOM Web Component for every mode. React and future framework components are bridges to that same element, so the visuals and behavior are not duplicated. + +Bare `standalone` loads the self-contained browser bundle and supplies the JSON manifest URL and the published SVG URL: + +```html + + + +``` + +`viewer-element.js` has no additional runtime files and can be copied with the other static assets for self-hosting. + +`standalone@vite` and `standalone@webpack` register the same element through an npm entry and pass the generated JS manifest through the `sources` property: + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +import spriteManifest from './svg-sprite/.svg-sprite/svg-sprite.manifest.js' + +const viewer = document.querySelector('gromlab-sprite-viewer')! +viewer.sources = [spriteManifest] +``` + +React and Next.js keep the component API: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +``` + +It accepts ready-made manifests, remote standalone sources, an array of lazy loaders, or a record in the format returned by `import.meta.glob`. + +Vite: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' + +const sources = import.meta.glob( + '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', +) + +export const IconsDebugPage = () => ( + +) +``` + +Webpack and Next.js: + +```tsx +const sources = [ + () => import('@/ui/app-icons/.svg-sprite/svg-sprite.manifest.js'), + () => import('@/features/analytics/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export const IconsDebugPage = () => ( + +) +``` + +The Viewer displays groups, search, `viewBox`, CSS custom properties, and fallback colors. React/Next manifests get React, SVG, IMG, and CSS tabs; standalone manifests get SVG, IMG, and CSS. You can change color values in the interface and immediately inspect the result. + +### Viewer theme + +By default, `colorTheme="auto"` follows `prefers-color-scheme`. You can explicitly pass `light` or `dark`: + +```tsx + +``` + +To synchronize it with the application theme: + +```tsx + +``` + +`@gromlab/svg-sprites/react` contains `'use client'` and renders the Web Component host; its internal Shadow DOM is created after the browser runtime loads. In the Next.js App Router, place the Viewer inside a separate Client Component boundary and use it only on a debug route or in an internal tool. + +## Generated files, Git, and CI + +Every mode except bare `standalone` creates a local `.gitignore` for: + +```text +/.svg-sprite/ +``` + +Commit the local `.gitignore` to the repository once. It excludes the other generated files, so generation must run before commands that import the sprite module: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/app-icons/svg-sprite.config.ts", + "predev": "npm run sprites", + "prebuild": "npm run sprites", + "pretypecheck": "npm run sprites" + } +} +``` + +CI must run generation before building or type-checking. Replace `latest` with an exact version for reproducibility. A local package installation is not required unless CI also uses the Viewer, package config types, or the programmatic API. + +Bare `standalone` does not create or modify `.gitignore`; the application decides whether its `.svg-sprite/` output is committed or ignored. In other modes, the generator will not overwrite a user-created `.gitignore`. It also refuses to overwrite a user-owned file inside `.svg-sprite`. The root `index.ts` remains user-owned and may re-export the generated API. + +## Troubleshooting + +- Missing `.svg-sprite/index.js`: run the generation script before importing the generated module. +- Source not found: pass an existing config file or sprite module directory. +- Mode missing: add `mode` to the config or pass `--mode`. +- Icon missing from the type: check `input`, the `.svg` extension, glob exclusions, and whether nested folders require `**/*.svg`. +- Name conflict: two different SVG files have the same basename; rename one of them. +- `Refusing to overwrite a user file`: a file without the generated marker occupies a managed path. +- The icon does not change color: use `` or the generated component and check `replaceColors`. +- Webpack emits an incorrect URL: check Asset Modules, `output.publicPath`, and SVG loaders. +- Static sprite returns 404: check the post-generation copy or server alias, and do not put a filesystem `spritePath` into HTML. +- The Viewer cannot find the sprite: check the path to `.svg-sprite/svg-sprite.manifest.js` and run generation before starting the application. +- Build and mode do not match: use the target that corresponds to the actual bundler. + +For custom orchestration and low-level compilation, see the [Programmatic API](programmatic-api.md). diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 0000000..e226abb --- /dev/null +++ b/docs/ru/README.md @@ -0,0 +1,29 @@ +# Документация + +Для настройки выберите guide одного exact mode. Каждый guide является +самостоятельным документом и без изменений используется в AI skills. + +## Гайды быстрого старта + +| Проект | Exact mode | Guide | +|---|---|---| +| Static HTML или собственная публикация | `standalone` | [Bare standalone](guides/standalone.md) | +| Vanilla + Vite | `standalone@vite` | [Standalone + Vite](guides/standalone-vite.md) | +| Vanilla + Webpack 5 | `standalone@webpack` | [Standalone + Webpack](guides/standalone-webpack.md) | +| React + Vite | `react@vite` | [React + Vite](guides/react-vite.md) | +| React + Webpack 5 | `react@webpack` | [React + Webpack](guides/react-webpack.md) | +| Next.js App Router + Turbopack | `next@app/turbopack` | [App Router + Turbopack](guides/next-app-turbopack.md) | +| Next.js App Router + Webpack | `next@app/webpack` | [App Router + Webpack](guides/next-app-webpack.md) | +| Next.js Pages Router + Turbopack | `next@pages/turbopack` | [Pages Router + Turbopack](guides/next-pages-turbopack.md) | +| Next.js Pages Router + Webpack | `next@pages/webpack` | [Pages Router + Webpack](guides/next-pages-webpack.md) | + +Все guides используют один порядок: + +1. Генерация спрайта через `npx` без добавления package в проект. +2. Необязательное подключение Viewer для дебага и превью. +3. Необязательная типизация конфига через package или локальный copy-paste type. + +## Справочники + +- [Технический справочник](reference/technical.md) +- [Программный API](reference/programmatic-api.md) diff --git a/docs/ru/guides/README.md b/docs/ru/guides/README.md new file mode 100644 index 0000000..e5788cc --- /dev/null +++ b/docs/ru/guides/README.md @@ -0,0 +1,11 @@ +# Гайды быстрого старта + +- `standalone`: [bare standalone](standalone.md) +- `standalone@vite`: [standalone с Vite](standalone-vite.md) +- `standalone@webpack`: [standalone с Webpack](standalone-webpack.md) +- `react@vite`: [React с Vite](react-vite.md) +- `react@webpack`: [React с Webpack](react-webpack.md) +- `next@app/turbopack`: [App Router с Turbopack](next-app-turbopack.md) +- `next@app/webpack`: [App Router с Webpack](next-app-webpack.md) +- `next@pages/turbopack`: [Pages Router с Turbopack](next-pages-turbopack.md) +- `next@pages/webpack`: [Pages Router с Webpack](next-pages-webpack.md) diff --git a/docs/ru/guides/next-app-turbopack.md b/docs/ru/guides/next-app-turbopack.md new file mode 100644 index 0000000..b56f876 --- /dev/null +++ b/docs/ru/guides/next-app-turbopack.md @@ -0,0 +1,153 @@ +# Next.js App Router с Turbopack + +Это автономный quick start для exact mode key `next@app/turbopack`: generated `IconsIcon` совместим с Server Components и asset pipeline Turbopack. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'next@app/turbopack', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Конфиг может быть `.ts`, `.js` с `default export` или `.json`. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +В CI зафиксируйте точную версию, например `@gromlab/svg-sprites@1.1.5`. Mode и команды Next должны указывать один bundler: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --turbopack", + "build": "npm run sprites && next build --turbopack", + "start": "next start", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не добавляйте одновременно `predev`/`prebuild` и явный `npm run sprites`. Generated `.svg-sprite` не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Его `.d.ts` self-contained и не импортируют generator package. + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Generated icon не содержит `'use client'`, поэтому его можно импортировать прямо в Server Component: + +```tsx +// app/page.tsx +import { IconsIcon, iconsIconNames } from '../src/sprite' + +export default function Page() { + return ( +
+ +
{iconsIconNames.length} иконок + + ) +} +``` + +Turbopack обрабатывает generated `new URL('../sprite.svg', import.meta.url).href` и выпускает внешний hashed asset. Не добавляйте Client Component boundary только ради `IconsIcon`. + +## 2. Дебаг и превью + +Viewer необязателен и нужен только для debug/preview: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Viewer интерактивен, поэтому создайте для него отдельный Client Component: + +```tsx +// app/icons-debug/sprite-viewer.tsx +'use client' + +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../../src/sprite/.svg-sprite/svg-sprite.manifest.js'), +] as const + +export function AppSpriteViewer() { + return +} +``` + +Server page импортирует только эту boundary: + +```tsx +// app/icons-debug/page.tsx +import { AppSpriteViewer } from './sprite-viewer' + +export default function IconsDebugPage() { + return +} +``` + +Viewer не входит в production icon runtime и не нужен обычным страницам с `IconsIcon`. + +## 3. Типизация конфига + +После локальной установки package используйте helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@app/turbopack', + name: 'icons', +}) +``` + +Возможен и type-only import `SpriteConfig` с `satisfies SpriteConfig`. + +Без package добавьте локальный type в config: + +```ts +type LocalSpriteConfig = { + mode: 'next@app/turbopack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@app/turbopack', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Exact literal защищает от смешивания App Router и других bundler contracts. diff --git a/docs/ru/guides/next-app-webpack.md b/docs/ru/guides/next-app-webpack.md new file mode 100644 index 0000000..40d10b0 --- /dev/null +++ b/docs/ru/guides/next-app-webpack.md @@ -0,0 +1,151 @@ +# Next.js App Router с Webpack + +Это автономный quick start для exact mode key `next@app/webpack`: generated `IconsIcon` совместим с Server Components и Webpack pipeline Next.js. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'next@app/webpack', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Также поддерживаются `.js` с `default export` и `.json`. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +В CI замените `latest` точной версией, например `@gromlab/svg-sprites@1.1.5`. Exact Next commands для этого mode: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --webpack", + "build": "npm run sprites && next build --webpack", + "start": "next start", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не сочетайте явный `npm run sprites` с `predev` или `prebuild`. `.svg-sprite` generated и не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Generated declarations self-contained и не зависят от `@gromlab/svg-sprites`. + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production usage в Server Component: + +```tsx +// app/page.tsx +import { IconsIcon, iconsIconNames } from '../src/sprite' + +export default function Page() { + return ( +
+ + {iconsIconNames.length} иконок +
+ ) +} +``` + +Generated component не содержит `'use client'`. Next Webpack обрабатывает `new URL('../sprite.svg', import.meta.url).href` и публикует отдельный SVG asset; не переписывайте этот URL и не переносите sprite в `public` вручную. + +## 2. Дебаг и превью + +Viewer необязателен. Для debug/preview установите package: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +App Router требует отдельную Client Component boundary для Viewer: + +```tsx +// app/icons-debug/sprite-viewer.tsx +'use client' + +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../../src/sprite/.svg-sprite/svg-sprite.manifest.js'), +] as const + +export function AppSpriteViewer() { + return +} +``` + +```tsx +// app/icons-debug/page.tsx +import { AppSpriteViewer } from './sprite-viewer' + +export default function IconsDebugPage() { + return +} +``` + +Статический loader позволяет Webpack связать manifest и emitted SVG. Viewer не входит в production runtime `IconsIcon`. + +## 3. Типизация конфига + +При локально установленном package используйте helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@app/webpack', + name: 'icons', +}) +``` + +Другой package-вариант: type-only import `SpriteConfig` и `satisfies SpriteConfig`. + +Без package вставьте локальный type прямо в config: + +```ts +type LocalSpriteConfig = { + mode: 'next@app/webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@app/webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Локальный exact literal исключает случайную генерацию Turbopack или Pages Router output. diff --git a/docs/ru/guides/next-pages-turbopack.md b/docs/ru/guides/next-pages-turbopack.md new file mode 100644 index 0000000..b3e104a --- /dev/null +++ b/docs/ru/guides/next-pages-turbopack.md @@ -0,0 +1,140 @@ +# Next.js Pages Router с Turbopack + +Это автономный quick start для exact mode key `next@pages/turbopack`: generated `IconsIcon` работает при SSR, SSG и клиентских переходах Pages Router. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'next@pages/turbopack', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Конфиг также может быть `.js` с `default export` или `.json`. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +Для CI зафиксируйте точную версию, например `@gromlab/svg-sprites@1.1.5`. Exact commands должны сохранять Turbopack и для dev, и для production build: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --turbopack", + "build": "npm run sprites && next build --turbopack", + "start": "next start", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не добавляйте `predev`/`prebuild`, если scripts уже явно вызывают `npm run sprites`. Generated `.svg-sprite` не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Generated `.d.ts` self-contained и не импортируют generator package. + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production usage на обычной page: + +```tsx +// pages/index.tsx +import { IconsIcon, iconsIconNames } from '../src/sprite' + +export default function Page() { + return ( +
+ + {iconsIconNames.length} иконок +
+ ) +} +``` + +Компонент одинаково работает с `getServerSideProps`, `getStaticProps` и client navigation. Turbopack разрешает generated `new URL('../sprite.svg', import.meta.url).href` в отдельный hashed asset. + +## 2. Дебаг и превью + +Viewer необязателен и нужен только для debug/preview: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +В Pages Router Viewer можно использовать прямо в page, без отдельной App Router Client Component boundary: + +```tsx +// pages/icons-debug.tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../src/sprite/.svg-sprite/svg-sprite.manifest.js'), +] as const + +export default function IconsDebugPage() { + return +} +``` + +Оставляйте эту page только во внутреннем debug-разделе. Viewer не входит в production icon runtime `IconsIcon`. + +## 3. Типизация конфига + +Если package установлен локально, используйте helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@pages/turbopack', + name: 'icons', +}) +``` + +Альтернатива: type-only import `SpriteConfig` и объект `satisfies SpriteConfig`. + +Без package добавьте copy-paste type в config: + +```ts +type LocalSpriteConfig = { + mode: 'next@pages/turbopack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@pages/turbopack', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Exact literal не позволяет незаметно смешать Pages Router с App Router или Webpack output. diff --git a/docs/ru/guides/next-pages-webpack.md b/docs/ru/guides/next-pages-webpack.md new file mode 100644 index 0000000..072b7a7 --- /dev/null +++ b/docs/ru/guides/next-pages-webpack.md @@ -0,0 +1,140 @@ +# Next.js Pages Router с Webpack + +Это автономный quick start для exact mode key `next@pages/webpack`: generated `IconsIcon` работает в Pages Router и публикует SVG через Webpack pipeline Next.js. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'next@pages/webpack', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Поддерживаются `.ts`, `.js` с `default export` и `.json` configs. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +В CI замените `latest` на точную проверенную версию, например `@gromlab/svg-sprites@1.1.5`. Exact Next commands для Webpack: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && next dev --webpack", + "build": "npm run sprites && next build --webpack", + "start": "next start", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не дублируйте эти вызовы через `predev`, `prebuild` или `pretypecheck`. `.svg-sprite` generated и не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Generated declarations self-contained и не требуют `@gromlab/svg-sprites`. + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production usage: + +```tsx +// pages/index.tsx +import { IconsIcon, iconsIconNames } from '../src/sprite' + +export default function Page() { + return ( +
+ + {iconsIconNames.length} иконок +
+ ) +} +``` + +Компонент поддерживает SSR, SSG и клиентские переходы. Next Webpack преобразует generated `new URL('../sprite.svg', import.meta.url).href` во внешний hashed asset; не конструируйте URL спрайта вручную. + +## 2. Дебаг и превью + +Viewer необязателен. Устанавливайте package только для debug/preview: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Pages Router позволяет разместить Viewer непосредственно в page без отдельной App Router boundary: + +```tsx +// pages/icons-debug.tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('../src/sprite/.svg-sprite/svg-sprite.manifest.js'), +] as const + +export default function IconsDebugPage() { + return +} +``` + +Статический loader даёт Webpack точный manifest module и связанный SVG asset. Не импортируйте Viewer из production pages, если preview там не нужен; runtime `IconsIcon` от него независим. + +## 3. Типизация конфига + +После локальной установки package доступен helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@pages/webpack', + name: 'icons', +}) +``` + +Также можно применить `satisfies SpriteConfig` с type-only импортом `SpriteConfig`. + +Без package вставьте локальный type прямо в config: + +```ts +type LocalSpriteConfig = { + mode: 'next@pages/webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'next@pages/webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Такой exact literal выявляет ошибочный выбор App Router или Turbopack ещё при проверке config. diff --git a/docs/ru/guides/react-vite.md b/docs/ru/guides/react-vite.md new file mode 100644 index 0000000..cedad15 --- /dev/null +++ b/docs/ru/guides/react-vite.md @@ -0,0 +1,141 @@ +# React-компонент для Vite + +Это автономный quick start для exact mode key `react@vite`: генератор создаёт типизированный `IconsIcon`, а Vite публикует отдельный SVG asset. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'react@vite', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Вместо `.ts` можно использовать `.js` с `default export` или `.json`. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +В CI замените `latest` на точную версию, например `@gromlab/svg-sprites@1.1.5`. Exact Vite commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && vite", + "build": "npm run sprites && tsc --noEmit && vite build", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не добавляйте `predev`, `prebuild` или `pretypecheck`, если соответствующие scripts уже явно запускают `npm run sprites`. Generated `.svg-sprite` не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Generated declarations self-contained: они описывают компонент и manifest без импорта `@gromlab/svg-sprites`. + +Пользовательский barrel возвращает generated API: + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production usage: + +```tsx +import { IconsIcon, iconsIconNames } from './sprite' + +export function SaveButton() { + return ( + + ) +} + +console.log(iconsIconNames) +``` + +Prop `icon` является union имён исходных файлов. Vite автоматически обрабатывает generated CSS Module и импорт `sprite.svg?no-inline`; query запрещает inline и заставляет Vite выпустить отдельный hashed SVG asset. Если TypeScript не знает Vite asset imports, добавьте `/// ` в `src/vite-env.d.ts`. + +## 2. Дебаг и превью + +Viewer необязателен и нужен только для debug/preview. Установите package отдельно: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Используйте React bridge со статическим массивом loaders: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('./sprite/.svg-sprite/svg-sprite.manifest.js'), +] as const + +export function IconsDebugPage() { + return +} +``` + +Строковый путь в `import()` должен указывать на generated JS manifest. Держите страницу за debug-маршрутом; `SpriteViewer` не входит в production runtime `IconsIcon`. + +## 3. Типизация конфига + +Если package установлен локально, используйте helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'react@vite', + name: 'icons', +}) +``` + +Также можно импортировать `SpriteConfig` только как type и написать объект `satisfies SpriteConfig`. + +Без package добавьте локальный type прямо в config: + +```ts +type LocalSpriteConfig = { + mode: 'react@vite' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'react@vite', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Локальный type проверяет только этот exact mode и не создаёт runtime-зависимость. diff --git a/docs/ru/guides/react-webpack.md b/docs/ru/guides/react-webpack.md new file mode 100644 index 0000000..a7591a2 --- /dev/null +++ b/docs/ru/guides/react-webpack.md @@ -0,0 +1,141 @@ +# React-компонент для Webpack 5 + +Это автономный quick start для exact mode key `react@webpack`: generated `IconsIcon` использует Webpack 5 Asset Modules и CSS Modules. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'react@webpack', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Поддерживаются `.ts`, `.js` с `default export` и `.json` configs. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +В CI замените `latest` на точную версию, например `@gromlab/svg-sprites@1.1.5`. Exact Webpack commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && webpack serve --mode development", + "build": "npm run sprites && webpack --mode production", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не сочетайте эти явные вызовы с `predev`/`prebuild`: иначе генерация задублируется. `.svg-sprite` не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Generated `.d.ts` self-contained и не импортируют generator package. + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production usage: + +```tsx +import { IconsIcon, iconsIconNames } from './sprite' + +export function SaveButton() { + return ( + + ) +} + +console.log(iconsIconNames) +``` + +Generated component получает URL через `new URL('../sprite.svg', import.meta.url).href`. Webpack 5 должен обработать SVG как Asset Module. Исключите generated `sprite.svg` из `@svgr/webpack`, inline/raw loaders и других общих SVG rules либо задайте для него `type: 'asset/resource'`. + +Компонент импортирует `react-component.module.css`. Webpack config должен обрабатывать `*.module.css` через `css-loader` с CSS Modules и `style-loader` или `MiniCssExtractPlugin`. Для TypeScript при необходимости добавьте декларацию `declare module '*.module.css'`. + +## 2. Дебаг и превью + +Viewer необязателен. Устанавливайте package только для debug/preview: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Webpack не использует `import.meta.glob`; передайте статический loader: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' + +const sources = [ + () => import('./sprite/.svg-sprite/svg-sprite.manifest.js'), +] as const + +export function IconsDebugPage() { + return +} +``` + +Webpack создаст chunk manifest и разрешит его SVG через тот же Asset Modules pipeline. Viewer держите только в debug route; production `IconsIcon` от него не зависит. + +## 3. Типизация конфига + +С локально установленным package доступен helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'react@webpack', + name: 'icons', +}) +``` + +Эквивалентная проверка: type-only import `SpriteConfig` и `satisfies SpriteConfig`. + +Без package используйте copy-paste type в config: + +```ts +type LocalSpriteConfig = { + mode: 'react@webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'react@webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Локальный literal не разрешит случайно выбрать Vite или Next mode. diff --git a/docs/ru/guides/standalone-vite.md b/docs/ru/guides/standalone-vite.md new file mode 100644 index 0000000..03e53fe --- /dev/null +++ b/docs/ru/guides/standalone-vite.md @@ -0,0 +1,151 @@ +# Нативный icon Web Component с Vite + +Это автономный quick start для exact mode key `standalone@vite`: generated facade регистрирует `` и отдаёт SVG в asset pipeline Vite. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +Рекомендуемая структура держит конфиг и иконки рядом: + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный plain config: + +```ts +export default { + mode: 'standalone@vite', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Конфиг также может быть `.js` с `default export` или `.json`. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +В CI замените `latest` на точную версию, например `@gromlab/svg-sprites@1.1.5`. Exact dev/build commands для Vite: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && vite", + "build": "npm run sprites && vite build", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не добавляйте одновременно `predev`/`prebuild` и явный `npm run sprites` в этих scripts. `.svg-sprite` является generated-каталогом и не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Generated `.d.ts` self-contained и не импортируют `@gromlab/svg-sprites`. + +Верните facade через пользовательский barrel: + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production entry регистрирует native Web Component: + +```ts +import { defineIconsIconElement, iconsIconNames } from './sprite' + +defineIconsIconElement() + +document.querySelector('#app')!.innerHTML = ` + +` + +console.log(iconsIconNames) +``` + +Generated facade импортирует `sprite.svg?no-inline`: Vite автоматически выпускает отдельный hashed SVG asset и не превращает его в data URL. TypeScript-проекту при необходимости добавьте стандартные Vite types: + +```ts +/// +``` + +Размер по умолчанию равен `1em`, поэтому компонент удобно масштабировать через `font-size`. Цвет задаётся через `color` и generated custom properties: + +```css +icons-icon { + font-size: 24px; + color: #334155; + --icon-color-2: #f59e0b; +} +``` + +## 2. Дебаг и превью + +Viewer необязателен и нужен только для debug/preview. Установите его отдельно: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Зарегистрируйте Viewer и передайте generated JS manifest через свойство `sources`: + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js' + +document.querySelector('#app')!.insertAdjacentHTML( + 'beforeend', + '', +) + +const viewer = document.querySelector('gromlab-sprite-viewer')! +viewer.viewerTitle = 'Иконки проекта' +viewer.sources = [spriteManifest] +``` + +Расположите этот код только в debug entry или внутреннем маршруте. Viewer не входит в production runtime ``. + +## 3. Типизация конфига + +При локально установленном package используйте helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'standalone@vite', + name: 'icons', +}) +``` + +Альтернатива с package: `import type { SpriteConfig }` и объект `satisfies SpriteConfig`. + +Без package скопируйте локальный type прямо в config: + +```ts +type LocalSpriteConfig = { + mode: 'standalone@vite' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'standalone@vite', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Локальный type ограничивает `mode` одним exact literal и ничего не загружает во время генерации. diff --git a/docs/ru/guides/standalone-webpack.md b/docs/ru/guides/standalone-webpack.md new file mode 100644 index 0000000..f60ca05 --- /dev/null +++ b/docs/ru/guides/standalone-webpack.md @@ -0,0 +1,145 @@ +# Нативный icon Web Component с Webpack 5 + +Это автономный quick start для exact mode key `standalone@webpack`: generated facade предоставляет ``, а Webpack 5 публикует SVG через Asset Modules. + +## 1. Генерация спрайта + +Главное преимущество: генератор не нужно устанавливать и добавлять в `package.json`. `npx` временно скачивает CLI, а generated production runtime не импортирует `@gromlab/svg-sprites`. + +```text +src/sprite/ +├── icons/ +│ ├── check.svg +│ └── warning.svg +├── index.ts +└── svg-sprite.config.ts +``` + +Минимальный config рядом с `icons/`: + +```ts +export default { + mode: 'standalone@webpack', + name: 'icons', +} +``` + +Если `input` не указан, SVG читаются из `./icons` относительно конфига. Поддерживаются также `.js` с `default export` и `.json`. + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts +``` + +Для CI закрепите точную версию, например `@gromlab/svg-sprites@1.1.5`. Exact Webpack commands: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/sprite/svg-sprite.config.ts", + "dev": "npm run sprites && webpack serve --mode development", + "build": "npm run sprites && webpack --mode production", + "typecheck": "npm run sprites && tsc --noEmit" + } +} +``` + +Не дублируйте запуск через `predev`/`prebuild`, если scripts уже явно вызывают `npm run sprites`. Generated `.svg-sprite` не коммитится. Generated локальный `.gitignore`, который исключает этот каталог, нужно добавить в Git один раз. Его declarations self-contained и не требуют `@gromlab/svg-sprites`. + +```ts +// src/sprite/index.ts +export * from './.svg-sprite/index.js' +``` + +Production usage: + +```ts +import { defineIconsIconElement, iconsIconNames } from './sprite' + +defineIconsIconElement() + +document.querySelector('#app')!.innerHTML = ` + +` + +console.log(iconsIconNames) +``` + +Generated facade использует `new URL('./sprite.svg', import.meta.url).href`. Webpack 5 Asset Modules выпускают отдельный asset; его итоговый URL учитывает `output.publicPath` и `assetModuleFilename`. + +Если проект использует `@svgr/webpack`, `svg-inline-loader`, `raw-loader` или общий SVG rule, исключите `src/sprite/.svg-sprite/sprite.svg` из этого правила. Generated SVG должен обрабатываться как `asset/resource`, а не как React-компонент или inline source. + +Размер Web Component по умолчанию `1em`; управляйте им и цветами обычным CSS: + +```css +icons-icon { + font-size: 24px; + color: #334155; + --icon-color-2: #f59e0b; +} +``` + +## 2. Дебаг и превью + +Viewer необязателен. Для debug/preview установите package как dev dependency: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +Подключите element entry и generated JS manifest: + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js' + +document.querySelector('#app')!.insertAdjacentHTML( + 'beforeend', + '', +) + +const viewer = document.querySelector('gromlab-sprite-viewer')! +viewer.viewerTitle = 'Иконки проекта' +viewer.sources = [spriteManifest] +``` + +Webpack свяжет manifest с тем же emitted SVG asset. Оставляйте Viewer только в debug entry: production `` от него не зависит. + +## 3. Типизация конфига + +После локальной установки package можно использовать helper: + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'standalone@webpack', + name: 'icons', +}) +``` + +Либо импортируйте только `SpriteConfig` как type и примените `satisfies SpriteConfig`. + +Без package добавьте copy-paste type в сам config: + +```ts +type LocalSpriteConfig = { + mode: 'standalone@webpack' + name?: string + description?: string + input?: string | string[] + transform?: { + removeSize?: boolean + replaceColors?: boolean + addTransition?: boolean + } + generatedNotice?: boolean +} + +export default { + mode: 'standalone@webpack', + name: 'icons', +} satisfies LocalSpriteConfig +``` + +Этот вариант сохраняет проверку exact mode без runtime import и без записи generator package в проект. diff --git a/docs/ru/guides/standalone.md b/docs/ru/guides/standalone.md new file mode 100644 index 0000000..bf36246 --- /dev/null +++ b/docs/ru/guides/standalone.md @@ -0,0 +1,108 @@ +# SVG-спрайт для сайта без сборщика + +Соберите SVG-иконки в один файл и используйте их на HTML-странице. + +## Генерация спрайта + +Устанавливать пакет в проект не нужно. + +В руководстве используется следующая структура проекта: + +```text +/ +├── index.html +└── assets/ + ├── app-icons/ + └── svg-icons/ + ├── check.svg + └── warning.svg +``` + +### 1. Создайте конфиг спрайта + +Выберите папку для спрайта. В этом примере используется `assets/app-icons`. Создайте в ней файл `svg-sprite.config.json`: + +```json +{ + "mode": "standalone", + "name": "icons" +} +``` + +### 2. Укажите источник иконок + +В `input` можно указать папку, отдельный SVG-файл или glob-шаблон. Для нескольких источников используйте массив с любой комбинацией этих значений: + +```json +{ + "mode": "standalone", + "name": "icons", + "input": "../svg-icons/**/*.svg" +} +``` + +### 3. Сгенерируйте спрайт + +Передайте команде путь к конфигу: + +```bash +npx --yes @gromlab/svg-sprites assets/app-icons/svg-sprite.config.json +``` + +Пакет соберёт иконки в каталог `.svg-sprite` рядом с конфигом: + +```text +assets/app-icons/.svg-sprite/ +├── sprite.svg +└── svg-sprite.manifest.json +``` + +- `sprite.svg` — готовый спрайт для использования на сайте. +- `svg-sprite.manifest.json` — данные об иконках для Viewer. + +Каталог `.svg-sprite` создаётся автоматически и полностью заменяется при каждой генерации. Не редактируйте его содержимое вручную. + +### 4. Используйте иконку + +В `index.html` укажите путь к созданному `sprite.svg`. После `#` добавьте имя нужной иконки без расширения `.svg`: + +```html + + + +``` + +Иконка из файла `check.svg` будет доступна как `#check`. + +## Дебаг и превью + +`sprite.svg` — технический файл, а не галерея иконок. При его открытии нельзя удобно просмотреть весь набор. Кроме того, градиенты, маски, фильтры и ссылки на внутренние `id` могут отображаться с артефактами. + +Для визуальной проверки используйте официальный Viewer. Он показывает все иконки спрайта и помогает проверить их цвета и отображение. + +Viewer необязателен и предназначен только для разработки. Устанавливать пакет через npm не нужно. + +Viewer работает напрямую с файлами из `.svg-sprite`. Ничего копировать не нужно. + +### Добавьте Viewer на страницу + +Добавьте в `index.html` module script и укажите пути к generated manifest и спрайту: + +```html + + + +``` + +Viewer можно вынести в отдельный HTML-файл в корне сайта, предназначенный только для разработки и проверки иконок. \ No newline at end of file diff --git a/docs/ru/next-app.md b/docs/ru/next-app.md index 653b1b6..9b8acea 100644 --- a/docs/ru/next-app.md +++ b/docs/ru/next-app.md @@ -1,113 +1,4 @@ -# Next.js App Router +# Guides Next.js App Router перемещены -[← Главная](../../README_RU.md) - -Поддерживаются два явных режима: - -| Сборщик | Mode key | -|---|---| -| Turbopack | `next@app/turbopack` | -| Webpack 5 | `next@app/webpack` | - -## 1. Установите пакет - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Создайте sprite-модуль - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - name: 'file-manager', - description: 'Иконки файлового менеджера', -}) -``` - -Корневой barrel принадлежит приложению: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -## 3. Добавьте генерацию - -Для Turbopack: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager" - } -} -``` - -Для Webpack замените mode key на `next@app/webpack`. - -До импорта generated-модуля выполните первую генерацию: - -```bash -npm run sprite:file-manager -``` - -## 4. Используйте в Server Component - -Generated-компонент не содержит `'use client'`, поэтому его можно импортировать непосредственно в `page.tsx` или `layout.tsx`: - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function Page() { - return ( -
- -
- ) -} -``` - -Next.js выпустит отдельный SVG asset с content hash. Один generated-код используется при SSR и в браузере без расхождения URL. - -## 5. Добавьте SpriteViewer - -Viewer интерактивен, поэтому для него нужна отдельная Client Component граница: - -```tsx -'use client' - -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), -] - -export default function SpritesPage() { - return -} -``` - -## Проверка сборщика - -Запустите build script проекта, настроенный на выбранный сборщик: - -```bash -npm run build -``` - -Build script и mode key генератора должны указывать один и тот же сборщик. +- [App Router + Turbopack](guides/next-app-turbopack.md) +- [App Router + Webpack](guides/next-app-webpack.md) diff --git a/docs/ru/next-pages.md b/docs/ru/next-pages.md index bd6526a..07429da 100644 --- a/docs/ru/next-pages.md +++ b/docs/ru/next-pages.md @@ -1,105 +1,4 @@ -# Next.js Pages Router +# Guides Next.js Pages Router перемещены -[← Главная](../../README_RU.md) - -Поддерживаются два явных режима: - -| Сборщик | Mode key | -|---|---| -| Turbopack | `next@pages/turbopack` | -| Webpack 5 | `next@pages/webpack` | - -## 1. Установите пакет - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Создайте sprite-модуль - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@pages/webpack', - name: 'file-manager', - description: 'Иконки файлового менеджера', -}) -``` - -Корневой barrel принадлежит приложению: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -## 3. Добавьте генерацию - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager" - } -} -``` - -Для Turbopack замените mode key на `next@pages/turbopack`. - -До импорта generated-модуля выполните первую генерацию: - -```bash -npm run sprite:file-manager -``` - -## 4. Используйте на странице - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function FilesPage() { - return -} - -export function getServerSideProps() { - return { props: {} } -} -``` - -Компонент одинаково работает при SSR, SSG и клиентских переходах. Next.js выпускает отдельный SVG asset с content hash. - -## 5. Добавьте SpriteViewer - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), -] - -export default function SpritesPage() { - return -} -``` - -## Проверка сборщика - -Запустите build script проекта, настроенный на выбранный сборщик: - -```bash -npm run build -``` - -Build script и mode key генератора должны указывать один и тот же сборщик. +- [Pages Router + Turbopack](guides/next-pages-turbopack.md) +- [Pages Router + Webpack](guides/next-pages-webpack.md) diff --git a/docs/ru/programmatic-api.md b/docs/ru/programmatic-api.md index 5a3c559..b5e68be 100644 --- a/docs/ru/programmatic-api.md +++ b/docs/ru/programmatic-api.md @@ -1,147 +1,3 @@ -# Программный API +# Программный API перемещён -[← Главная](../../README_RU.md) - -Пакет распространяется как ESM и предоставляет единый Node.js API генерации. Framework-neutral Viewer находится в `@gromlab/svg-sprites/viewer`, auto-register entry — в `@gromlab/svg-sprites/viewer/element`, React bridge — в `@gromlab/svg-sprites/react`. - -## `generateSprite` - -```ts -import { generateSprite } from '@gromlab/svg-sprites' - -const result = await generateSprite( - 'src/ui/file-manager/svg-sprite/svg-sprite.config.ts', -) -``` - -Для static standalone mode `result.spritePath` можно использовать в build-скрипте, -чтобы опубликовать SVG по URL приложения: - -```ts -import { copyFile } from 'node:fs/promises' - -const result = await generateSprite('src/sprite/svg-sprite.config.ts', { - mode: 'standalone', -}) -await copyFile(result.spritePath, 'dist/app-icons/sprite.svg') -``` - -`spritePath` является filesystem path, а не browser URL. Deployment-neutral JSON -manifest доступен через `result.manifestPath` и копируется независимо от SVG. - -Первый аргумент принимает полный путь к config-файлу с любым именем и расширением `.ts`, `.js` или `.json`. Каталог вместо файла включает config-less режим: корнем sprite-модуля становится этот каталог. - -Второй аргумент содержит необязательные overrides и всегда имеет приоритет над конфигом: - -```ts -await generateSprite('src/ui/file-manager/svg-sprite/custom-config.json', { - mode: 'react@webpack', - name: 'documents', - inputFolder: './assets', - inputFiles: ['../../shared/search.svg'], - transform: { - addTransition: false, - }, - generatedNotice: false, -}) -``` - -Порядок разрешения настроек: - -```text -defaults → config → API overrides -``` - -Для полностью программной генерации передайте каталог и все обязательные настройки через overrides: - -```ts -await generateSprite('src/ui/file-manager/svg-sprite', { - mode: 'react@vite', - name: 'file-manager', - inputFiles: [ - '../../shared/search.svg', - '../../shared/settings.svg', - ], -}) -``` - -## Конфигурация - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@vite', - name: 'file-manager', - description: 'Иконки файлового менеджера', - inputFolder: './icons', - inputFiles: ['../../shared/check.svg'], - transform: { - removeSize: true, - replaceColors: true, - addTransition: true, - }, - generatedNotice: true, -}) -``` - -`defineSpriteConfig` является identity helper для TypeScript autocomplete. JS может экспортировать тот же объект через `export default`, а JSON содержит объект непосредственно. - -## Специализированные обёртки - -Специализированные функции доступны как обёртки над `generateSprite`: - -```ts -import { generateNextSprite, generateReactSprite } from '@gromlab/svg-sprites' - -await generateReactSprite('path/to/config.ts', 'vite') -await generateNextSprite('path/to/config.ts', { - router: 'app', - bundler: 'turbopack', -}) -``` - -Явно переданный target перекрывает `mode` из файла. Для нового кода используйте `generateSprite`. - -## Config API - -```ts -import { - loadSpriteConfig, - resolveSpriteConfig, - validateSpriteConfig, -} from '@gromlab/svg-sprites' -``` - -- `loadSpriteConfig(file)` загружает явно указанный `.ts`, `.js` или `.json` файл. -- `validateSpriteConfig(value)` выполняет runtime-валидацию объекта. -- `resolveSpriteConfig(root, config, overrides)` объединяет значения, добавляет defaults и разрешает пути относительно `root`. - -## Низкоуровневый compiler - -```ts -import { - compileSprite, - compileSpriteContent, - createShapeTransform, -} from '@gromlab/svg-sprites' -``` - -Эти функции предназначены для собственного orchestration. Стандартная генерация должна выполняться через `generateSprite`. - -## Viewer runtime - -```ts -import '@gromlab/svg-sprites/viewer/element' -import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' -``` - -Browser entry регистрирует ``. Bare standalone также может загрузить самостоятельный `dist/viewer-element.js` без bundler. - -React bridge сохраняет компонентный API: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -``` - -`SpriteViewer` принимает generated manifests, remote standalone sources, lazy loaders или результат `import.meta.glob`. React entry содержит `'use client'` и предназначен для debug-инструментов; production-компоненты импортируются из локальных sprite-модулей приложения. +Canonical документ: [программный API](reference/programmatic-api.md). diff --git a/docs/ru/react-vite.md b/docs/ru/react-vite.md index 59b7bb0..0a92e9b 100644 --- a/docs/ru/react-vite.md +++ b/docs/ru/react-vite.md @@ -1,125 +1,3 @@ -# React + Vite +# Guide React + Vite перемещён -[← Главная](../../README_RU.md) - -Краткая инструкция по установке и использованию SVG-спрайтов в проекте на React и Vite. - -В результате вы получите типизированный React-компонент и отдельный кешируемый SVG asset. - -## 1. Установите пакет - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Создайте папку спрайта - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -Поместите исходные SVG-файлы в `icons/`. - -## 3. Добавьте конфиг - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@vite', - name: 'file-manager', - description: 'Иконки файлового менеджера', -}) -``` - -Корневой barrel принадлежит приложению и явно возвращает generated API: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -По умолчанию SVG берутся из `./icons`. Общие иконки из других папок можно добавить через `inputFiles`: папка и список объединяются в один спрайт. - -Полный список опций находится в разделе [«Единая конфигурация»](reference.md#единая-конфигурация). - -## 4. Добавьте генерацию в package.json - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Generated-файлы исключаются из Git, поэтому генерация должна выполняться перед `dev`, `build` и `typecheck`. - -Первый запуск: - -```bash -npm run sprite:file-manager -``` - -## 5. Используйте компонент - -Имя `file-manager` преобразуется в `FileManagerIcon`: - -```tsx -import { FileManagerIcon } from './svg-sprite' - -export const OpenFolderButton = () => ( - -) -``` - -Значение `icon` проверяется TypeScript по именам файлов: - -```tsx - // допустимо - // ошибка TypeScript -``` - -Типы, способы отображения и управление цветами описаны в [техническом справочнике](reference.md#react-компонент-и-typescript). - -Vite выпустит спрайт отдельным файлом вида `assets/sprite-.svg`. SVG path-данные не попадут в JavaScript. - -## 6. Добавьте debug-страницу - -После подключения иконок можно вывести все React-спрайты через `SpriteViewer`: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' - -const sources = import.meta.glob( - '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', -) - -export const IconsDebugPage = () => ( - -) -``` - -Vite автоматически найдёт generated manifest каждого React-спрайта. Шаблон `import.meta.glob` должен быть строковым литералом, а генерация должна выполниться до запуска Vite. - -Размещайте Viewer только на debug-маршруте или во внутреннем инструменте. - -## Если что-то не работает - -- Нет `.svg-sprite/index.js`: запустите `npm run sprite:file-manager`. -- Viewer не видит спрайт: проверьте glob-путь к `.svg-sprite/svg-sprite.manifest.js`. -- Ошибка `Refusing to overwrite a user file`: в generated-пути находится пользовательский файл. -- Иконка не меняет цвет: используйте `color` или `--icon-color-N`. +Canonical guide: [React + Vite](guides/react-vite.md). diff --git a/docs/ru/react-webpack.md b/docs/ru/react-webpack.md index 327afba..b47ded9 100644 --- a/docs/ru/react-webpack.md +++ b/docs/ru/react-webpack.md @@ -1,129 +1,3 @@ -# React + Webpack 5 +# Guide React + Webpack перемещён -[← Главная](../../README_RU.md) - -Краткая инструкция по установке и использованию SVG-спрайтов в проекте на React и Webpack 5. - -В результате вы получите типизированный React-компонент и отдельный SVG asset через Webpack Asset Modules. - -## 1. Установите пакет - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## 2. Создайте папку спрайта - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -├── index.ts -└── svg-sprite.config.ts -``` - -Поместите исходные SVG-файлы в `icons/`. - -## 3. Добавьте конфиг - -```ts -// src/ui/file-manager/svg-sprite/svg-sprite.config.ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@webpack', - name: 'file-manager', - description: 'Иконки файлового менеджера', -}) -``` - -Корневой barrel принадлежит приложению и явно возвращает generated API: - -```ts -// src/ui/file-manager/svg-sprite/index.ts -export * from './.svg-sprite' -``` - -По умолчанию SVG берутся из `./icons`. Общие иконки из других папок можно добавить через `inputFiles`: папка и список объединяются в один спрайт. - -Полный список опций находится в разделе [«Единая конфигурация»](reference.md#единая-конфигурация). - -## 4. Добавьте генерацию в package.json - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Generated-файлы исключаются из Git, поэтому генерация должна выполняться перед `dev`, `build` и `typecheck`. - -Первый запуск: - -```bash -npm run sprite:file-manager -``` - -## 5. Используйте компонент - -```tsx -import { FileManagerIcon } from './svg-sprite' - -export const OpenFolderButton = () => ( - -) -``` - -Значение `icon` проверяется TypeScript по именам файлов: - -```tsx - // допустимо - // ошибка TypeScript -``` - -Типы, способы отображения и управление цветами описаны в [техническом справочнике](reference.md#react-компонент-и-typescript). - -Webpack обработает generated `new URL('./sprite.svg', import.meta.url)` через Asset Modules и выпустит отдельный SVG asset. - -Если проект уже использует собственный SVG loader, убедитесь, что он не перехватывает generated `sprite.svg` вместо Asset Modules. - -Generated-компонент импортирует `react/react-component.module.css`, поэтому Webpack должен обрабатывать CSS Modules через `css-loader` и `style-loader` либо `MiniCssExtractPlugin`. Если TypeScript-проект не содержит декларации для CSS Modules, добавьте её отдельно. - -## 6. Добавьте debug-страницу - -Webpack не поддерживает Vite API `import.meta.glob`, поэтому передайте статические loaders: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('./ui/file-manager/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), - () => import('./ui/navigation/svg-sprite/.svg-sprite/svg-sprite.manifest.js'), -] - -export const IconsDebugPage = () => ( - -) -``` - -Пути в `import()` должны быть строковыми литералами. Webpack создаст chunks для манифестов и свяжет их с SVG assets. - -Размещайте Viewer только на debug-маршруте или во внутреннем инструменте. - -## Если что-то не работает - -- Нет `.svg-sprite/index.js`: запустите `npm run sprite:file-manager`. -- Viewer не загружает спрайт: проверьте путь в `import()` к `.svg-sprite/svg-sprite.manifest.js`. -- Неверный URL asset: проверьте `output.publicPath`. -- SVG перехватывает другой loader: исключите generated sprite из несовместимого правила. - -Для Next.js используйте отдельные mode key из руководств [App Router](next-app.md) и [Pages Router](next-pages.md). +Canonical guide: [React + Webpack](guides/react-webpack.md). diff --git a/docs/ru/reference.md b/docs/ru/reference.md index ce66bd1..caa74ab 100644 --- a/docs/ru/reference.md +++ b/docs/ru/reference.md @@ -1,555 +1,3 @@ -# Технический справочник +# Технический справочник перемещён -[← Главная](../../README_RU.md) - -Справочник по конфигурации, generated API и поведению `@gromlab/svg-sprites`. Пошаговую установку смотрите в руководстве для вашего стека: - -- [Next.js App Router](next-app.md) -- [Next.js Pages Router](next-pages.md) -- [React + Vite](react-vite.md) -- [React + Webpack 5](react-webpack.md) - -## Требования - -- Node.js 18 или новее; -- пакет распространяется как ESM и подключается через `import`; -- React 18 или 19 требуется для generated-компонентов и `@gromlab/svg-sprites/react`; -- для типизации package exports используйте TypeScript 5+ с `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. - -Пакет устанавливается как development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -## CLI и режимы генерации - -CLI принимает ровно один путь: явно выбранный config-файл либо каталог для config-less генерации: - -```text -svg-sprites [options] -``` - -| Среда | Mode | -|---|---| -| Static HTML / собственная публикация | `standalone` | -| Standalone + Vite | `standalone@vite` | -| Standalone + Webpack 5 | `standalone@webpack` | -| React + Vite | `react@vite` | -| React + Webpack 5 | `react@webpack` | -| Next.js App Router + Turbopack | `next@app/turbopack` | -| Next.js App Router + Webpack 5 | `next@app/webpack` | -| Next.js Pages Router + Turbopack | `next@pages/turbopack` | -| Next.js Pages Router + Webpack 5 | `next@pages/webpack` | - -Config-файл может иметь любое имя и расширение `.ts`, `.js` или `.json`. CLI не ищет конфиг по соглашению: файл нужно передать явно. В руководствах используется рекомендуемое имя `svg-sprite.config.ts`. - -Если передан каталог, все настройки берутся из CLI. Если передан config-файл, CLI-параметры перекрывают значения файла. Общий порядок: `defaults → config → CLI`. - -Доступны `--mode`, `--name`, `--description`, `--input-folder`, повторяемый `--input-file`, а также пары `--remove-size`/`--no-remove-size`, `--replace-colors`/`--no-replace-colors`, `--add-transition`/`--no-add-transition` и `--generated-notice`/`--no-generated-notice`. Переданные transform-флаги перекрывают отдельные поля, а хотя бы один `--input-file` заменяет весь массив `inputFiles` из config. - -Mode должен соответствовать способу публикации приложения. Bare `standalone` оставляет публичный URL приложению; Vite и Webpack modes генерируют bundler-specific подключение SVG asset. - -## Единая конфигурация - -Каждый config-файл описывает один независимый спрайт. - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - name: 'app', - description: 'Общие иконки приложения', - inputFolder: './local-icons', - inputFiles: [ - '../../assets/icons/search.svg', - '../../assets/icons/settings.svg', - ], - transform: { - removeSize: true, - replaceColors: true, - addTransition: true, - }, - generatedNotice: true, -}) -``` - -| Опция | Тип | По умолчанию | Назначение | -|---|---|---|---| -| `mode` | `SpriteMode` | Нет | Режим генерации; можно передать через CLI/API | -| `name` | `string` | Выводится из каталога | Имя спрайта, компонента и публичных типов | -| `description` | `string` | Нет | Описание для типов и debug manifest | -| `inputFolder` | `string` | `./icons` | Каталог с SVG относительно корня модуля | -| `inputFiles` | `string[]` | `[]` | Пути к отдельным SVG относительно корня модуля | -| `transform` | `TransformOptions` | Все включены | Настройки подготовки SVG | -| `generatedNotice` | `boolean` | `true` | Полное или короткое предупреждение в generated-файлах | - -### Имя спрайта - -`name` записывается в kebab-case и должно начинаться с латинской буквы: - -```text -app → AppIcon -file-manager → FileManagerIcon -``` - -Если `name` не задано, генератор выводит его из каталога. Для каталога с именем `svg-sprite` или `svg-sprites` используется имя родительского каталога. - -### Источники иконок - -`inputFolder` и `inputFiles` объединяются в один набор. Это позволяет хранить локальные SVG рядом с модулем и добавлять общие иконки из других частей проекта без копирования. - -Если `inputFiles` заполнен, а неявного каталога `./icons` нет, генерация работает только по списку файлов. Явно указанная отсутствующая `inputFolder` считается ошибкой. - -Каталог сканируется только на первом уровне. Вложенные каталоги рекурсивно не обходятся. Для вложенной структуры перечислите точные пути через `inputFiles`. - -Одинаковые абсолютные пути дедуплицируются. Разные SVG с одинаковым именем файла считаются конфликтом, потому что публичное имя иконки выводится из basename. - -## Generated-модуль - -После генерации React- или Next.js-каталог спрайта выглядит так: - -```text -app-icons/ -├── .gitignore -├── svg-sprite.config.ts -├── index.ts # необязательный пользовательский barrel -└── .svg-sprite/ - ├── state.json - ├── index.js - ├── index.d.ts - ├── icon-data.js - ├── icon-data.d.ts - ├── sprite.svg - ├── svg-sprite.manifest.js - ├── svg-sprite.manifest.d.ts - └── react/ - ├── react-component.js - ├── react-component.d.ts - └── react-component.module.css -``` - -| Файл | Назначение | -|---|---| -| `.svg-sprite/index.js` | Production exports компонента и runtime-списка имён | -| `.svg-sprite/index.d.ts` | Публичные декларации компонента, props, стилей и union-типа имён | -| `.svg-sprite/svg-sprite.manifest.js` | Debug metadata и URL asset для `SpriteViewer` | -| `.svg-sprite/sprite.svg` | Собранный SVG-спрайт | -| `.svg-sprite/react/react-component.js` | Runtime React-компонента без TypeScript и JSX | -| `.svg-sprite/react/react-component.d.ts` | Props, style и declaration React-компонента | -| `.svg-sprite/react/react-component.module.css` | Стили конкретной React-реализации | -| `.svg-sprite/icon-data.js` | Runtime-список имён и внутренние IDs | -| `.svg-sprite/*.d.ts` | TypeScript-декларации соответствующих JS-модулей | -| `.svg-sprite/state.json` | Mode, версия контракта и список управляемых файлов | - -Standalone-контракты не создают каталог `react/`. Bare `standalone` содержит только -runtime asset и deployment-neutral manifest data: - -```text -.svg-sprite/ -├── state.json -├── sprite.svg -└── svg-sprite.manifest.json -``` - -`standalone@vite` и `standalone@webpack` дополнительно создают `index.*`, -`icon-data.*` и resolved `svg-sprite.manifest.*`. - -Генератор перезаписывает и удаляет только файлы со своим marker. Если в managed-пути находится пользовательский файл, генерация завершается ошибкой. Корневой `index.ts` генератору не принадлежит; при необходимости создайте пользовательский barrel: - -```ts -export * from './.svg-sprite' -``` - -## React-компонент и TypeScript - -Спрайт с `name: 'app'` экспортирует: - -```ts -export { AppIcon, appIconNames } -export type { AppIconName, AppIconProps, AppIconStyle } -``` - -### Имена иконок - -Имена SVG-файлов становятся допустимыми значениями `icon`: - -```tsx - - // ошибка TypeScript -``` - -Runtime-список содержит те же значения: - -```ts -import { appIconNames } from '@/ui/app-icons' - -// readonly ['search', 'settings', 'user'] -``` - -Имена с пробелами и другими небезопасными для SVG ID символами остаются частью публичного API. Для внутреннего fragment ID генератор создаёт стабильный безопасный hash: - -```text -folder open.svg → icon="folder open" → id="icon-" -``` - -Для таких имён используйте generated-компонент или `id` из debug manifest, а не формируйте fragment ID вручную. - -### SVG-атрибуты - -По умолчанию компонент рендерит `` и принимает стандартные SVG-атрибуты: - -```tsx - -``` - -Компонент не добавляет accessibility-семантику автоматически. Передавайте подходящие `aria-*`, `role` или подпись в зависимости от назначения иконки. - -### Обёртка - -`wrapped` рендерит `` с внутренним SVG. Остальные props в этом режиме относятся к ``: - -```tsx - -``` - -### Типизированные CSS-переменные - -`AppIconStyle` расширяет `CSSProperties` и поддерживает свойства вида `--icon-color-N`: - -```tsx - -``` - -## Множественные спрайты - -Каждый каталог с конфигом создаёт независимый компонент, типы, manifest и SVG asset: - -```text -app-icons → AppIcon → общие иконки -analytics-icons → AnalyticsIcon → иконки страницы аналитики -editor-icons → EditorIcon → иконки редактора -``` - -Один исходный SVG можно добавить через `inputFiles` в несколько конфигураций. Копировать файл в каталоги каждого спрайта не требуется. - -Для нескольких спрайтов добавьте отдельную CLI-команду для каждого каталога или объедините команды в общем npm script. - -## Форматы и способы отображения - -Все текущие modes создают формат `stack`. - -| Формат | `` | `` | CSS background | -|---|---:|---:|---:| -| `stack` | Да | Да | Да | - -### Generated-компонент - -Для React и Next.js используйте generated-компонент. Он знает внутренние ID, формирует URL и предоставляет TypeScript API: - -```tsx - -``` - -### Вручную через `` - -Способ получения `spriteUrl` зависит от сборщика. - -Static HTML после публикации `.svg-sprite/sprite.svg` приложением: - -```html - -``` - -Standalone Vite/Webpack предоставляет generated `getIconsIconHref()` и mapping -внутренних IDs. Не конструируйте fragment из небезопасного имени файла вручную. - -Vite: - -```ts -import spriteUrl from './.svg-sprite/sprite.svg?no-inline' -``` - -Webpack 5, Turbopack и Next.js: - -```ts -const spriteUrl = new URL('./.svg-sprite/sprite.svg', import.meta.url).href -``` - -После получения URL используйте его в JSX: - -```tsx - - - -``` - -Для имён, небезопасных как SVG ID, используйте внутренний `id` из manifest. - -### Через `` - -```tsx -Поиск -``` - -SVG внутри `` изолирован от CSS страницы. `color` и `--icon-color-N` на внешнем элементе не изменяют его внутренние цвета. - -### Через CSS - -```css -.icon { - background: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; -} -``` - -Для одноцветного силуэта можно использовать mask: - -```css -.icon { - background-color: currentColor; - mask: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; -} -``` - -Mask не сохраняет исходные цвета, gradients и различия между `fill` и `stroke`. - -Путь в CSS разрешается относительно самого CSS-файла. В примерах CSS-файл находится рядом с `svg-sprite.config.ts`. - -## Assets и кеширование - -Generated component или standalone facade передаёт SVG сборщику как отдельный asset: - -- Vite использует статический импорт с `?no-inline`; -- Webpack 5, Turbopack и Next.js используют `new URL(..., import.meta.url)`; -- SVG path-данные не сериализуются в generated JavaScript. - -Bare `standalone` не участвует в asset pipeline: приложение само копирует или -публикует `sprite.svg` и отвечает за URL, версионирование и cache policy. - -При стандартном именовании assets сборщик добавляет content hash: - -```text -/assets/sprite-.svg -``` - -Это позволяет кешировать SVG отдельно от JavaScript. Изменение React-кода не меняет содержимое спрайта, а изменение иконок создаёт новую версию asset. - -HTTP cache headers, CDN и `Cache-Control` настраиваются приложением или платформой размещения. Для Webpack имя итогового файла зависит от `assetModuleFilename` проекта. - -## Трансформации SVG - -Все трансформации включены по умолчанию и настраиваются независимо: - -| Опция | Что делает | -|---|---| -| `removeSize` | Удаляет `width` и `height` с корневого ``, сохраняя существующий `viewBox` | -| `replaceColors` | Заменяет найденные `fill` и `stroke` на `--icon-color-N` | -| `addTransition` | Добавляет transitions для `fill` и `stroke` в цветные элементы и generated styles | - -Чтобы отключить отдельную операцию: - -```ts -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - transform: { - removeSize: false, - replaceColors: false, - addTransition: false, - }, -}) -``` - -Исходные SVG не изменяются. Трансформации применяются только к содержимому generated-спрайта. - -## Управление цветами - -### Монохромные иконки - -Если найден один цвет, fallback становится `currentColor`: - -```svg -stroke="var(--icon-color-1, currentColor)" -``` - -Цвет задаётся через prop или CSS: - -```tsx - -``` - -### Многоцветные иконки - -Каждый уникальный цвет получает отдельную переменную с исходным fallback: - -```svg -fill="var(--icon-color-1, #798198)" -fill="var(--icon-color-2, #ffffff)" -fill="var(--icon-color-3, #129d9d)" -``` - -Можно заменить только необходимые значения: - -```css -.icon { - --icon-color-1: #4b5563; - --icon-color-3: #14b8a6; -} -``` - -### Ограничения - -- `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются; -- надёжнее всего обрабатываются цвета в атрибутах `fill`, `stroke` и inline `style`; -- CSS-классы и внешние stylesheets внутри SVG не являются основным сценарием трансформации; -- значения `url(#...)` могут быть заменены вместе с цветами, поэтому gradients и patterns требуют отдельного спрайта с `replaceColors: false`; -- masks, filters и сложные внутренние CSS-правила требуют визуальной проверки; -- CSS-переменные страницы доступны через ``, но не внутри `` и CSS background. - -Для сложной иконки можно отключить `replaceColors` в конфигурации отдельного спрайта. - -## SpriteViewer - -Viewer использует один Web Component с Shadow DOM для всех modes. React и будущие framework-компоненты являются bridge к этому же элементу, поэтому визуал и поведение не дублируются. - -Bare `standalone` подключает самостоятельный browser bundle и передаёт URL JSON manifest и опубликованного SVG: - -```html - - - -``` - -`viewer-element.js` не имеет дополнительных runtime-файлов и может быть скопирован с остальными static assets для self-hosting. - -`standalone@vite` и `standalone@webpack` регистрируют тот же элемент через npm entry и передают generated JS manifest через свойство `sources`: - -```ts -import '@gromlab/svg-sprites/viewer/element' -import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' -import spriteManifest from './svg-sprite/.svg-sprite/svg-sprite.manifest.js' - -const viewer = document.querySelector('gromlab-sprite-viewer')! -viewer.sources = [spriteManifest] -``` - -React и Next.js сохраняют компонентный API: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -``` - -Он принимает готовые manifests, remote standalone sources, массив lazy loaders или record формата `import.meta.glob`. - -Vite: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' - -const sources = import.meta.glob( - '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', -) - -export const IconsDebugPage = () => ( - -) -``` - -Webpack и Next.js: - -```tsx -const sources = [ - () => import('@/ui/app-icons/.svg-sprite/svg-sprite.manifest.js'), - () => import('@/features/analytics/icons/.svg-sprite/svg-sprite.manifest.js'), -] - -export const IconsDebugPage = () => ( - -) -``` - -Viewer показывает группы, поиск, `viewBox`, CSS-переменные и fallback-цвета. React/Next manifests получают вкладки React, SVG, IMG и CSS; standalone manifests получают SVG, IMG и CSS. Цветовые значения можно менять в интерфейсе и сразу проверять результат. - -### Тема Viewer - -По умолчанию `colorTheme="auto"` следует `prefers-color-scheme`. Можно передать `light` или `dark` явно: - -```tsx - -``` - -Для синхронизации с темой приложения: - -```tsx - -``` - -`@gromlab/svg-sprites/react` содержит `'use client'` и рендерит Web Component host; внутренний Shadow DOM создаётся после загрузки browser runtime. В Next.js App Router размещайте Viewer внутри отдельной Client Component boundary и используйте только на debug-маршруте или во внутреннем инструменте. - -## Generated-файлы, Git и CI - -Современный sprite-модуль создаёт локальный `.gitignore` для: - -```text -/.svg-sprite/ -``` - -Локальный `.gitignore` следует один раз добавить в репозиторий. Он исключает остальные generated-файлы, поэтому генерацию нужно запускать перед командами, которые импортируют sprite-модуль: - -```json -{ - "scripts": { - "sprites": "svg-sprites src/ui/app-icons/svg-sprite.config.ts", - "predev": "npm run sprites", - "prebuild": "npm run sprites", - "pretypecheck": "npm run sprites" - } -} -``` - -CI должен устанавливать development dependencies и выполнять generation script до сборки или проверки типов. - -Если в каталоге спрайта уже находится пользовательский `.gitignore` либо пользовательский файл внутри `.svg-sprite`, генератор не перезапишет его. Корневой `index.ts` остаётся пользовательским и может переэкспортировать generated API. - -## Диагностика - -- Нет `.svg-sprite/index.js`: запустите generation script до импорта generated-модуля. -- Не найден источник: передайте существующий config-файл или каталог sprite-модуля. -- Не указан mode: добавьте `mode` в config либо передайте `--mode`. -- Иконка отсутствует в типе: проверьте `inputFiles`, расширение `.svg` и уровень вложенности `inputFolder`. -- Конфликт имени: два разных SVG имеют одинаковый basename; переименуйте один файл. -- `Refusing to overwrite a user file`: в managed-пути находится файл без generated marker. -- Иконка не меняет цвет: используйте `` или generated-компонент и проверьте `replaceColors`. -- Webpack выдаёт неверный URL: проверьте Asset Modules, `output.publicPath` и SVG loaders. -- Static sprite возвращает 404: проверьте post-generation copy или server alias и не передавайте filesystem `spritePath` в HTML. -- Viewer не видит спрайт: проверьте путь к `.svg-sprite/svg-sprite.manifest.js` и выполните генерацию до запуска приложения. -- Build и mode не совпадают: используйте target, соответствующий фактическому сборщику. - -Для собственного orchestration и низкоуровневой компиляции смотрите [Программный API](programmatic-api.md). +Canonical документ: [технический справочник](reference/technical.md). diff --git a/docs/ru/reference/README.md b/docs/ru/reference/README.md new file mode 100644 index 0000000..e9d9b94 --- /dev/null +++ b/docs/ru/reference/README.md @@ -0,0 +1,4 @@ +# Справочники + +- [Технический справочник](technical.md) +- [Программный API](programmatic-api.md) diff --git a/docs/ru/reference/programmatic-api.md b/docs/ru/reference/programmatic-api.md new file mode 100644 index 0000000..209c76f --- /dev/null +++ b/docs/ru/reference/programmatic-api.md @@ -0,0 +1,147 @@ +# Программный API + +[Индекс документации](../README.md) + +Пакет распространяется как ESM и предоставляет единый Node.js API генерации. Framework-neutral Viewer находится в `@gromlab/svg-sprites/viewer`, auto-register entry — в `@gromlab/svg-sprites/viewer/element`, React bridge — в `@gromlab/svg-sprites/react`. + +## `generateSprite` + +```ts +import { generateSprite } from '@gromlab/svg-sprites' + +const result = await generateSprite( + 'src/ui/file-manager/svg-sprite/svg-sprite.config.ts', +) +``` + +Для static standalone mode `result.spritePath` можно использовать в build-скрипте, +чтобы опубликовать SVG по URL приложения: + +```ts +import { copyFile } from 'node:fs/promises' + +const result = await generateSprite('src/sprite/svg-sprite.config.ts', { + mode: 'standalone', +}) +await copyFile(result.spritePath, 'dist/app-icons/sprite.svg') +``` + +`spritePath` является filesystem path, а не browser URL. Deployment-neutral JSON +manifest доступен через `result.manifestPath` и копируется независимо от SVG. + +Первый аргумент принимает полный путь к config-файлу с любым именем и расширением `.ts`, `.js` или `.json`. Каталог вместо файла включает config-less режим: корнем sprite-модуля становится этот каталог. + +Второй аргумент содержит необязательные overrides и всегда имеет приоритет над конфигом: + +```ts +await generateSprite('src/ui/file-manager/svg-sprite/custom-config.json', { + mode: 'react@webpack', + name: 'documents', + input: ['./assets', '../../shared/search.svg'], + transform: { + addTransition: false, + }, + generatedNotice: false, +}) +``` + +Порядок разрешения настроек: + +```text +defaults → config → API overrides +``` + +Для полностью программной генерации передайте каталог и все обязательные настройки через overrides: + +```ts +await generateSprite('src/ui/file-manager/svg-sprite', { + mode: 'react@vite', + name: 'file-manager', + input: [ + '../../shared/search.svg', + '../../shared/settings.svg', + ], +}) +``` + +## Конфигурация + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'react@vite', + name: 'file-manager', + description: 'Иконки файлового менеджера', + input: ['./icons', '../../shared/check.svg'], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +`input` принимает одну папку, SVG-файл или glob-паттерн либо массив, объединяющий такие источники. Если поле не задано, используется `./icons`; относительные пути считаются от папки с конфигом. + +`defineSpriteConfig` является identity helper для TypeScript autocomplete. JS может экспортировать тот же объект через `export default`, а JSON содержит объект непосредственно. + +## Специализированные обёртки + +Специализированные функции доступны как обёртки над `generateSprite`: + +```ts +import { generateNextSprite, generateReactSprite } from '@gromlab/svg-sprites' + +await generateReactSprite('path/to/config.ts', 'vite') +await generateNextSprite('path/to/config.ts', { + router: 'app', + bundler: 'turbopack', +}) +``` + +Явно переданный target перекрывает `mode` из файла. Для нового кода используйте `generateSprite`. + +## Config API + +```ts +import { + loadSpriteConfig, + resolveSpriteConfig, + validateSpriteConfig, +} from '@gromlab/svg-sprites' +``` + +- `loadSpriteConfig(file)` загружает явно указанный `.ts`, `.js` или `.json` файл. +- `validateSpriteConfig(value)` выполняет runtime-валидацию объекта. +- `resolveSpriteConfig(root, config, overrides)` объединяет значения, добавляет defaults и разрешает пути относительно `root`. + +## Низкоуровневый compiler + +```ts +import { + compileSprite, + compileSpriteContent, + createShapeTransform, +} from '@gromlab/svg-sprites' +``` + +Эти функции предназначены для собственного orchestration. Стандартная генерация должна выполняться через `generateSprite`. + +## Viewer runtime + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +``` + +Browser entry регистрирует ``. Bare standalone также может загрузить самостоятельный `dist/viewer-element.js` без bundler. + +React bridge сохраняет компонентный API: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +``` + +`SpriteViewer` принимает generated manifests, remote standalone sources, lazy loaders или результат `import.meta.glob`. React entry содержит `'use client'` и предназначен для debug-инструментов; production-компоненты импортируются из локальных sprite-модулей приложения. diff --git a/docs/ru/reference/technical.md b/docs/ru/reference/technical.md new file mode 100644 index 0000000..0122739 --- /dev/null +++ b/docs/ru/reference/technical.md @@ -0,0 +1,631 @@ +# Технический справочник + +[Индекс документации](../README.md) + +Справочник по конфигурации, generated API и поведению `@gromlab/svg-sprites`. Пошаговую установку смотрите в руководстве для вашего стека: + +- [Bare standalone](../guides/standalone.md) +- [Standalone + Vite](../guides/standalone-vite.md) +- [Standalone + Webpack 5](../guides/standalone-webpack.md) +- [React + Vite](../guides/react-vite.md) +- [React + Webpack 5](../guides/react-webpack.md) +- [Next.js App Router + Turbopack](../guides/next-app-turbopack.md) +- [Next.js App Router + Webpack](../guides/next-app-webpack.md) +- [Next.js Pages Router + Turbopack](../guides/next-pages-turbopack.md) +- [Next.js Pages Router + Webpack](../guides/next-pages-webpack.md) + +## Требования + +- Node.js 18 или новее; +- пакет распространяется как ESM и подключается через `import`; +- React 18 или 19 требуется только для React/Next generated-компонентов и `@gromlab/svg-sprites/react`; +- для типизации package exports используйте TypeScript 5+ с `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. + +Для генерации не нужна dependency проекта. Запускайте CLI через `npx`: + +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites path/to/svg-sprite.config.ts +``` + +Устанавливайте пакет как development dependency, только если проекту нужны +Viewer, типы конфига или программный API: + +```bash +npm install --save-dev @gromlab/svg-sprites +``` + +## CLI и режимы генерации + +CLI принимает ровно один путь: явно выбранный config-файл либо каталог для config-less генерации: + +```text +svg-sprites [options] +``` + +| Среда | Mode | +|---|---| +| Static HTML / собственная публикация | `standalone` | +| Standalone + Vite | `standalone@vite` | +| Standalone + Webpack 5 | `standalone@webpack` | +| React + Vite | `react@vite` | +| React + Webpack 5 | `react@webpack` | +| Next.js App Router + Turbopack | `next@app/turbopack` | +| Next.js App Router + Webpack 5 | `next@app/webpack` | +| Next.js Pages Router + Turbopack | `next@pages/turbopack` | +| Next.js Pages Router + Webpack 5 | `next@pages/webpack` | + +Config-файл может иметь любое имя и расширение `.ts`, `.js` или `.json`. CLI не ищет конфиг по соглашению: файл нужно передать явно. В руководствах используется рекомендуемое имя `svg-sprite.config.ts`. + +Если передан каталог, все настройки берутся из CLI. Если передан config-файл, CLI-параметры перекрывают значения файла. Общий порядок: `defaults → config → CLI`. + +Доступны `--mode`, `--name`, `--description`, повторяемый `--input `, а также пары `--remove-size`/`--no-remove-size`, `--replace-colors`/`--no-replace-colors`, `--add-transition`/`--no-add-transition` и `--generated-notice`/`--no-generated-notice`. Переданные transform-флаги перекрывают отдельные поля, а хотя бы один `--input` полностью заменяет значение `input` из config. + +В CLI заключайте glob-паттерны в одинарные кавычки, чтобы shell не раскрыл их до запуска генератора: + +```bash +svg-sprites --input './icons/**/*.svg' --input '!./icons/legacy/**' svg-sprite.config.ts +``` + +Mode должен соответствовать способу публикации приложения. Bare `standalone` оставляет публичный URL приложению; Vite и Webpack modes генерируют bundler-specific подключение SVG asset. + +## Единая конфигурация + +Каждый config-файл описывает один независимый спрайт. + +```ts +import { defineSpriteConfig } from '@gromlab/svg-sprites' + +export default defineSpriteConfig({ + mode: 'next@app/turbopack', + name: 'app', + description: 'Общие иконки приложения', + input: [ + './local-icons', + '../../assets/icons/*.svg', + '!../../assets/icons/deprecated-*.svg', + ], + transform: { + removeSize: true, + replaceColors: true, + addTransition: true, + }, + generatedNotice: true, +}) +``` + +| Опция | Тип | По умолчанию | Назначение | +|---|---|---|---| +| `mode` | `SpriteMode` | Нет | Режим генерации; можно передать через CLI/API | +| `name` | `string` | Выводится из каталога | Имя спрайта, компонента и публичных типов | +| `description` | `string` | Нет | Описание для типов и debug manifest | +| `input` | `string \| string[]` | `./icons` | Папки, SVG-файлы и glob-паттерны относительно папки конфига | +| `transform` | `TransformOptions` | Все включены | Настройки подготовки SVG | +| `generatedNotice` | `boolean` | `true` | Полное или короткое предупреждение в generated-файлах | + +### Имя спрайта + +`name` записывается в kebab-case и должно начинаться с латинской буквы: + +```text +app → AppIcon +file-manager → FileManagerIcon +``` + +Если `name` не задано, генератор выводит его из каталога. Для каталога с именем `svg-sprite` или `svg-sprites` используется имя родительского каталога. + +### Источники иконок + +`SpriteConfig.input` является необязательным и имеет тип `string | string[]`. Если поле отсутствует, источником служит папка `./icons` относительно папки конфига. В config-less режиме относительные пути считаются от каталога, переданного CLI или API. + +Каждая строка без префикса `!` может быть путём к конкретной папке, конкретному файлу `.svg` или glob-паттерном. Папка включает только непосредственные дочерние `*.svg`. Для рекурсивного обхода вложенных каталогов укажите явный паттерн, например `icons/**/*.svg`. + +Массив объединяет все включающие источники. Паттерн с префиксом `!` глобально исключает совпадения из общего результата независимо от того, какой источник их добавил. + +Поддерживается следующий glob-синтаксис: + +| Синтаксис | Значение | +|---|---| +| `*` | Любые символы внутри одного сегмента пути | +| `**` | Любое число вложенных каталогов | +| `?` | Один символ внутри сегмента пути | +| `{a,b}` | Одна из альтернатив | +| `[abc]` | Один символ из набора или диапазона | +| `!pattern` | Исключение совпадений из всего объединённого input | + +Каждый включающий источник или паттерн должен найти хотя бы один SVG, иначе генерация завершается ошибкой. Повторяющиеся пути удаляются, а итоговый список файлов детерминированно сортируется. Разные SVG с одинаковым basename по-прежнему считаются конфликтом, потому что basename задаёт публичное имя иконки. + +## Generated-модуль + +После генерации React- или Next.js-каталог спрайта выглядит так: + +```text +app-icons/ +├── .gitignore +├── svg-sprite.config.ts +├── index.ts # необязательный пользовательский barrel +└── .svg-sprite/ + ├── index.js + ├── index.d.ts + ├── icon-data.js + ├── icon-data.d.ts + ├── sprite.svg + ├── svg-sprite.manifest.js + ├── svg-sprite.manifest.d.ts + └── react/ + ├── react-component.js + ├── react-component.d.ts + └── react-component.module.css +``` + +| Файл | Назначение | +|---|---| +| `.svg-sprite/index.js` | Mode-specific production facade и runtime-список имён | +| `.svg-sprite/index.d.ts` | Публичные декларации facade, компонента и union-типа имён | +| `.svg-sprite/svg-sprite.manifest.js` | Debug metadata и URL asset для `SpriteViewer` | +| `.svg-sprite/sprite.svg` | Собранный SVG-спрайт | +| `.svg-sprite/react/react-component.js` | Runtime React-компонента без TypeScript и JSX | +| `.svg-sprite/react/react-component.d.ts` | Props, style и declaration React-компонента | +| `.svg-sprite/react/react-component.module.css` | Стили конкретной React-реализации | +| `.svg-sprite/icon-data.js` | Runtime-список имён и внутренние IDs | +| `.svg-sprite/*.d.ts` | TypeScript-декларации соответствующих JS-модулей | + +Standalone-контракты не создают каталог `react/`. Bare `standalone` содержит только +runtime asset и deployment-neutral manifest data: + +```text +.svg-sprite/ +├── sprite.svg +└── svg-sprite.manifest.json +``` + +`standalone@vite` и `standalone@webpack` дополнительно создают `index.*`, +`icon-data.*` и resolved `svg-sprite.manifest.*`. Их facade содержит нативный +generated Web Component без внешних runtime-зависимостей. Bare `standalone` +намеренно не создаёт JavaScript-компонент. + +Генератор перезаписывает и удаляет только файлы со своим marker. Если в managed-пути находится пользовательский файл, генерация завершается ошибкой. Корневой `index.ts` генератору не принадлежит; при необходимости создайте пользовательский barrel: + +```ts +export * from './.svg-sprite' +``` + +## Standalone Web Component и TypeScript + +В modes `standalone@vite` и `standalone@webpack` спрайт с `name: 'app'` +экспортирует функцию регистрации `defineAppIconElement()` и tag ``: + +```ts +import { defineAppIconElement } from '@/ui/app-icons' + +defineAppIconElement() +``` + +После регистрации элемент можно использовать в HTML: + +```html + + + +``` + +Компонент рендерит `` в открытом Shadow DOM, сам выбирает внутренний +ID и `viewBox`, а URL asset получает через соответствующий Vite или Webpack +механизм. Размер host по умолчанию равен `1em × 1em`; `class`, `style`, `color` +и `--icon-color-N` задаются обычным CSS. + +Generated `HTMLElementTagNameMap` типизирует property API: + +```ts +const icon = document.createElement('app-icon') + +icon.icon = 'search' +icon.icon = 'unknown' // ошибка TypeScript +``` + +Значения атрибутов в обычной HTML-разметке TypeScript не проверяет. Поэтому +неизвестный `icon="unknown"` дополнительно проверяется в runtime: компонент +скрывает внутренний SVG и сообщает об ошибке, не создавая fragment +`#undefined`. Повторный вызов `defineAppIconElement()` безопасен для того же +спрайта; конфликт с другим элементом под tag `` завершается ошибкой. + +## React-компонент и TypeScript + +Спрайт с `name: 'app'` экспортирует: + +```ts +export { AppIcon, appIconNames } +export type { AppIconName, AppIconProps, AppIconStyle } +``` + +### Имена иконок + +Имена SVG-файлов становятся допустимыми значениями `icon`: + +```tsx + + // ошибка TypeScript +``` + +Runtime-список содержит те же значения: + +```ts +import { appIconNames } from '@/ui/app-icons' + +// readonly ['search', 'settings', 'user'] +``` + +Имена с пробелами и другими небезопасными для SVG ID символами остаются частью публичного API. Для внутреннего fragment ID генератор создаёт стабильный безопасный hash: + +```text +folder open.svg → icon="folder open" → id="icon-" +``` + +Для таких имён используйте generated-компонент или `id` из debug manifest, а не формируйте fragment ID вручную. + +### SVG-атрибуты + +По умолчанию компонент рендерит `` и принимает стандартные SVG-атрибуты: + +```tsx + +``` + +Компонент не добавляет accessibility-семантику автоматически. Передавайте подходящие `aria-*`, `role` или подпись в зависимости от назначения иконки. + +### Обёртка + +`wrapped` рендерит `` с внутренним SVG. Остальные props в этом режиме относятся к ``: + +```tsx + +``` + +### Типизированные CSS-переменные + +`AppIconStyle` расширяет `CSSProperties` и поддерживает свойства вида `--icon-color-N`: + +```tsx + +``` + +## Множественные спрайты + +Каждый каталог с конфигом создаёт независимый компонент, типы, manifest и SVG asset: + +```text +app-icons → AppIcon → общие иконки +analytics-icons → AnalyticsIcon → иконки страницы аналитики +editor-icons → EditorIcon → иконки редактора +``` + +Один исходный SVG можно добавить через `input` в несколько конфигураций. Копировать файл в каталоги каждого спрайта не требуется. + +Для нескольких спрайтов добавьте отдельную CLI-команду для каждого каталога или объедините команды в общем npm script. + +## Форматы и способы отображения + +Все текущие modes создают формат `stack`. + +| Формат | `` | `` | CSS background | +|---|---:|---:|---:| +| `stack` | Да | Да | Да | + +### Generated-компонент + +Для React и Next.js используйте generated React-компонент. Он знает внутренние ID, формирует URL и предоставляет TypeScript API: + +```tsx + +``` + +Для `standalone@vite` и `standalone@webpack` используйте generated Web Component: + +```html + +``` + +### Вручную через `` + +Способ получения `spriteUrl` зависит от сборщика. + +Static HTML после публикации `.svg-sprite/sprite.svg` приложением: + +```html + +``` + +Standalone Vite/Webpack предоставляет generated `getIconsIconHref()` и mapping +внутренних IDs. Не конструируйте fragment из небезопасного имени файла вручную. + +Vite: + +```ts +import spriteUrl from './.svg-sprite/sprite.svg?no-inline' +``` + +Webpack 5, Turbopack и Next.js: + +```ts +const spriteUrl = new URL('./.svg-sprite/sprite.svg', import.meta.url).href +``` + +После получения URL используйте его в JSX: + +```tsx + + + +``` + +Для имён, небезопасных как SVG ID, используйте внутренний `id` из manifest. + +### Через `` + +```tsx +Поиск +``` + +SVG внутри `` изолирован от CSS страницы. `color` и `--icon-color-N` на внешнем элементе не изменяют его внутренние цвета. + +### Через CSS + +```css +.icon { + background: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; +} +``` + +Для одноцветного силуэта можно использовать mask: + +```css +.icon { + background-color: currentColor; + mask: url('./.svg-sprite/sprite.svg#search') center / contain no-repeat; +} +``` + +Mask не сохраняет исходные цвета, gradients и различия между `fill` и `stroke`. + +Путь в CSS разрешается относительно самого CSS-файла. В примерах CSS-файл находится рядом с `svg-sprite.config.ts`. + +## Assets и кеширование + +Generated component или standalone facade передаёт SVG сборщику как отдельный asset: + +- Vite использует статический импорт с `?no-inline`; +- Webpack 5, Turbopack и Next.js используют `new URL(..., import.meta.url)`; +- SVG path-данные не сериализуются в generated JavaScript. + +Bare `standalone` не участвует в asset pipeline: приложение само копирует или +публикует `sprite.svg` и отвечает за URL, версионирование и cache policy. + +При стандартном именовании assets сборщик добавляет content hash: + +```text +/assets/sprite-.svg +``` + +Это позволяет кешировать SVG отдельно от JavaScript. Изменение React-кода не меняет содержимое спрайта, а изменение иконок создаёт новую версию asset. + +HTTP cache headers, CDN и `Cache-Control` настраиваются приложением или платформой размещения. Для Webpack имя итогового файла зависит от `assetModuleFilename` проекта. + +## Трансформации SVG + +Все трансформации включены по умолчанию и настраиваются независимо: + +| Опция | Что делает | +|---|---| +| `removeSize` | Удаляет `width` и `height` с корневого ``, сохраняя существующий `viewBox` | +| `replaceColors` | Заменяет найденные `fill` и `stroke` на `--icon-color-N` | +| `addTransition` | Добавляет transitions для `fill` и `stroke` в цветные элементы и generated styles | + +Чтобы отключить отдельную операцию: + +```ts +export default defineSpriteConfig({ + mode: 'next@app/turbopack', + transform: { + removeSize: false, + replaceColors: false, + addTransition: false, + }, +}) +``` + +Исходные SVG не изменяются. Трансформации применяются только к содержимому generated-спрайта. + +## Управление цветами + +### Монохромные иконки + +Если найден один цвет, fallback становится `currentColor`: + +```svg +stroke="var(--icon-color-1, currentColor)" +``` + +Цвет задаётся через prop или CSS: + +```tsx + +``` + +### Многоцветные иконки + +Каждый уникальный цвет получает отдельную переменную с исходным fallback: + +```svg +fill="var(--icon-color-1, #798198)" +fill="var(--icon-color-2, #ffffff)" +fill="var(--icon-color-3, #129d9d)" +``` + +Можно заменить только необходимые значения: + +```css +.icon { + --icon-color-1: #4b5563; + --icon-color-3: #14b8a6; +} +``` + +### Ограничения + +- `none`, `transparent`, `inherit`, `unset` и `initial` не заменяются; +- надёжнее всего обрабатываются цвета в атрибутах `fill`, `stroke` и inline `style`; +- CSS-классы и внешние stylesheets внутри SVG не являются основным сценарием трансформации; +- значения `url(#...)` могут быть заменены вместе с цветами, поэтому gradients и patterns требуют отдельного спрайта с `replaceColors: false`; +- masks, filters и сложные внутренние CSS-правила требуют визуальной проверки; +- CSS-переменные страницы доступны через ``, но не внутри `` и CSS background. + +Для сложной иконки можно отключить `replaceColors` в конфигурации отдельного спрайта. + +## SpriteViewer + +Viewer использует один Web Component с Shadow DOM для всех modes. React и будущие framework-компоненты являются bridge к этому же элементу, поэтому визуал и поведение не дублируются. + +Bare `standalone` подключает самостоятельный browser bundle и передаёт URL JSON manifest и опубликованного SVG: + +```html + + + +``` + +`viewer-element.js` не имеет дополнительных runtime-файлов и может быть скопирован с остальными static assets для self-hosting. + +`standalone@vite` и `standalone@webpack` регистрируют тот же элемент через npm entry и передают generated JS manifest через свойство `sources`: + +```ts +import '@gromlab/svg-sprites/viewer/element' +import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' +import spriteManifest from './svg-sprite/.svg-sprite/svg-sprite.manifest.js' + +const viewer = document.querySelector('gromlab-sprite-viewer')! +viewer.sources = [spriteManifest] +``` + +React и Next.js сохраняют компонентный API: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +``` + +Он принимает готовые manifests, remote standalone sources, массив lazy loaders или record формата `import.meta.glob`. + +Vite: + +```tsx +import { SpriteViewer } from '@gromlab/svg-sprites/react' +import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' + +const sources = import.meta.glob( + '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', +) + +export const IconsDebugPage = () => ( + +) +``` + +Webpack и Next.js: + +```tsx +const sources = [ + () => import('@/ui/app-icons/.svg-sprite/svg-sprite.manifest.js'), + () => import('@/features/analytics/icons/.svg-sprite/svg-sprite.manifest.js'), +] + +export const IconsDebugPage = () => ( + +) +``` + +Viewer показывает группы, поиск, `viewBox`, CSS-переменные и fallback-цвета. React/Next manifests получают вкладки React, SVG, IMG и CSS; standalone manifests получают SVG, IMG и CSS. Цветовые значения можно менять в интерфейсе и сразу проверять результат. + +### Тема Viewer + +По умолчанию `colorTheme="auto"` следует `prefers-color-scheme`. Можно передать `light` или `dark` явно: + +```tsx + +``` + +Для синхронизации с темой приложения: + +```tsx + +``` + +`@gromlab/svg-sprites/react` содержит `'use client'` и рендерит Web Component host; внутренний Shadow DOM создаётся после загрузки browser runtime. В Next.js App Router размещайте Viewer внутри отдельной Client Component boundary и используйте только на debug-маршруте или во внутреннем инструменте. + +## Generated-файлы, Git и CI + +Все modes, кроме bare `standalone`, создают локальный `.gitignore` для: + +```text +/.svg-sprite/ +``` + +Локальный `.gitignore` следует один раз добавить в репозиторий. Он исключает остальные generated-файлы, поэтому генерацию нужно запускать перед командами, которые импортируют sprite-модуль: + +```json +{ + "scripts": { + "sprites": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/app-icons/svg-sprite.config.ts", + "predev": "npm run sprites", + "prebuild": "npm run sprites", + "pretypecheck": "npm run sprites" + } +} +``` + +CI должен выполнять generation script до сборки или проверки типов. Для воспроизводимости замените `latest` на точную версию. Локальная установка package не нужна, если CI не использует Viewer, package-типы config или программный API. + +Bare `standalone` не создаёт и не изменяет `.gitignore`: приложение само решает, коммитить или игнорировать его `.svg-sprite/`. В остальных modes генератор не перезапишет пользовательский `.gitignore`. Он также откажется перезаписывать пользовательский файл внутри `.svg-sprite`. Корневой `index.ts` остаётся пользовательским и может переэкспортировать generated API. + +## Диагностика + +- Нет `.svg-sprite/index.js`: запустите generation script до импорта generated-модуля. +- Не найден источник: передайте существующий config-файл или каталог sprite-модуля. +- Не указан mode: добавьте `mode` в config либо передайте `--mode`. +- Иконка отсутствует в типе: проверьте `input`, расширение `.svg`, glob-исключения и необходимость `**/*.svg` для вложенных папок. +- Конфликт имени: два разных SVG имеют одинаковый basename; переименуйте один файл. +- `Refusing to overwrite a user file`: в managed-пути находится файл без generated marker. +- Иконка не меняет цвет: используйте `` или generated-компонент и проверьте `replaceColors`. +- Webpack выдаёт неверный URL: проверьте Asset Modules, `output.publicPath` и SVG loaders. +- Static sprite возвращает 404: проверьте post-generation copy или server alias и не передавайте filesystem `spritePath` в HTML. +- Viewer не видит спрайт: проверьте путь к `.svg-sprite/svg-sprite.manifest.js` и выполните генерацию до запуска приложения. +- Build и mode не совпадают: используйте target, соответствующий фактическому сборщику. + +Для собственного orchestration и низкоуровневой компиляции смотрите [Программный API](programmatic-api.md). diff --git a/integration/apps/next-app-turbopack/src/sprite/svg-sprite.config.ts b/integration/apps/next-app-turbopack/src/sprite/svg-sprite.config.ts index 985b455..bbcc2c5 100644 --- a/integration/apps/next-app-turbopack/src/sprite/svg-sprite.config.ts +++ b/integration/apps/next-app-turbopack/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'next@app/turbopack', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/next-app-webpack/src/sprite/svg-sprite.config.ts b/integration/apps/next-app-webpack/src/sprite/svg-sprite.config.ts index 35850ae..8baf3fd 100644 --- a/integration/apps/next-app-webpack/src/sprite/svg-sprite.config.ts +++ b/integration/apps/next-app-webpack/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'next@app/webpack', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/next-pages-turbopack/src/sprite/svg-sprite.config.ts b/integration/apps/next-pages-turbopack/src/sprite/svg-sprite.config.ts index c26c99d..30b8867 100644 --- a/integration/apps/next-pages-turbopack/src/sprite/svg-sprite.config.ts +++ b/integration/apps/next-pages-turbopack/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'next@pages/turbopack', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/next-pages-webpack/src/sprite/svg-sprite.config.ts b/integration/apps/next-pages-webpack/src/sprite/svg-sprite.config.ts index f059336..c9eb5b8 100644 --- a/integration/apps/next-pages-webpack/src/sprite/svg-sprite.config.ts +++ b/integration/apps/next-pages-webpack/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'next@pages/webpack', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/react-vite/src/sprite/svg-sprite.config.ts b/integration/apps/react-vite/src/sprite/svg-sprite.config.ts index fb603c4..8b5d43e 100644 --- a/integration/apps/react-vite/src/sprite/svg-sprite.config.ts +++ b/integration/apps/react-vite/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'react@vite', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/react-webpack/src/sprite/svg-sprite.config.ts b/integration/apps/react-webpack/src/sprite/svg-sprite.config.ts index 9043d84..18e52eb 100644 --- a/integration/apps/react-webpack/src/sprite/svg-sprite.config.ts +++ b/integration/apps/react-webpack/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'react@webpack', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/standalone-vite/src/generated-contract.typecheck.ts b/integration/apps/standalone-vite/src/generated-contract.typecheck.ts new file mode 100644 index 0000000..0a988fa --- /dev/null +++ b/integration/apps/standalone-vite/src/generated-contract.typecheck.ts @@ -0,0 +1,9 @@ +import type { IconsIconElement } from './sprite' + +const icon = document.createElement('icons-icon') +const typedIcon: IconsIconElement = icon + +typedIcon.icon = 'check' + +// @ts-expect-error Generated icon names form a literal union. +typedIcon.icon = 'missing' diff --git a/integration/apps/standalone-vite/src/main.ts b/integration/apps/standalone-vite/src/main.ts index ead284d..366ca2e 100644 --- a/integration/apps/standalone-vite/src/main.ts +++ b/integration/apps/standalone-vite/src/main.ts @@ -1,7 +1,7 @@ import '@gromlab/svg-sprites/viewer/element' import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js' -import { getIconsIconHref, iconsSpriteUrl } from './sprite' +import { defineIconsIconElement, iconsSpriteUrl } from './sprite' import './style.css' const check = spriteManifest.icons.find((icon) => icon.name === 'check') @@ -9,16 +9,17 @@ if (!check || spriteManifest.spriteUrl !== iconsSpriteUrl) { throw new Error('Generated Vite facade and manifest disagree.') } +defineIconsIconElement() + document.querySelector('#app')!.innerHTML = `

Standalone + Vite

- - - + >
` diff --git a/integration/apps/standalone-vite/src/sprite/index.ts b/integration/apps/standalone-vite/src/sprite/index.ts index 659338f..b9c7a54 100644 --- a/integration/apps/standalone-vite/src/sprite/index.ts +++ b/integration/apps/standalone-vite/src/sprite/index.ts @@ -1,7 +1,9 @@ export { + defineIconsIconElement, getIconsIconHref, iconsIconIds, iconsIconNames, + iconsIconTagName, iconsSpriteUrl, } from './.svg-sprite/index.js' -export type { IconsIconName } from './.svg-sprite/index.js' +export type { IconsIconElement, IconsIconName } from './.svg-sprite/index.js' diff --git a/integration/apps/standalone-vite/src/sprite/svg-sprite.config.ts b/integration/apps/standalone-vite/src/sprite/svg-sprite.config.ts index 1ffd01d..4e2b016 100644 --- a/integration/apps/standalone-vite/src/sprite/svg-sprite.config.ts +++ b/integration/apps/standalone-vite/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'standalone@vite', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/standalone-webpack/src/generated-contract.typecheck.ts b/integration/apps/standalone-webpack/src/generated-contract.typecheck.ts new file mode 100644 index 0000000..0a988fa --- /dev/null +++ b/integration/apps/standalone-webpack/src/generated-contract.typecheck.ts @@ -0,0 +1,9 @@ +import type { IconsIconElement } from './sprite' + +const icon = document.createElement('icons-icon') +const typedIcon: IconsIconElement = icon + +typedIcon.icon = 'check' + +// @ts-expect-error Generated icon names form a literal union. +typedIcon.icon = 'missing' diff --git a/integration/apps/standalone-webpack/src/main.ts b/integration/apps/standalone-webpack/src/main.ts index a8063c8..8298bf2 100644 --- a/integration/apps/standalone-webpack/src/main.ts +++ b/integration/apps/standalone-webpack/src/main.ts @@ -1,24 +1,25 @@ import '@gromlab/svg-sprites/viewer/element' import type { SpriteViewerElement } from '@gromlab/svg-sprites/viewer' import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js' -import { getIconsIconHref, iconsSpriteUrl } from './sprite' +import { defineIconsIconElement, iconsSpriteUrl } from './sprite' const check = spriteManifest.icons.find((icon) => icon.name === 'check') if (!check || spriteManifest.spriteUrl !== iconsSpriteUrl) { throw new Error('Generated Webpack facade and manifest disagree.') } +defineIconsIconElement() + document.querySelector('#app')!.innerHTML = `

Standalone + Webpack

- - - + >
` diff --git a/integration/apps/standalone-webpack/src/sprite/index.ts b/integration/apps/standalone-webpack/src/sprite/index.ts index 659338f..b9c7a54 100644 --- a/integration/apps/standalone-webpack/src/sprite/index.ts +++ b/integration/apps/standalone-webpack/src/sprite/index.ts @@ -1,7 +1,9 @@ export { + defineIconsIconElement, getIconsIconHref, iconsIconIds, iconsIconNames, + iconsIconTagName, iconsSpriteUrl, } from './.svg-sprite/index.js' -export type { IconsIconName } from './.svg-sprite/index.js' +export type { IconsIconElement, IconsIconName } from './.svg-sprite/index.js' diff --git a/integration/apps/standalone-webpack/src/sprite/svg-sprite.config.ts b/integration/apps/standalone-webpack/src/sprite/svg-sprite.config.ts index 6d4be55..0d1c666 100644 --- a/integration/apps/standalone-webpack/src/sprite/svg-sprite.config.ts +++ b/integration/apps/standalone-webpack/src/sprite/svg-sprite.config.ts @@ -3,6 +3,6 @@ import { defineSpriteConfig } from '@gromlab/svg-sprites' export default defineSpriteConfig({ mode: 'standalone@webpack', name: 'icons', - inputFiles: ['../../../../fixtures/icons/check.svg'], + input: '../../../../fixtures/icons/check.svg', generatedNotice: false, }) diff --git a/integration/apps/standalone/src/sprite/.gitignore b/integration/apps/standalone/src/sprite/.gitignore deleted file mode 100644 index 7f3344e..0000000 --- a/integration/apps/standalone/src/sprite/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# @generated by @gromlab/svg-sprites. Do not edit. -/.svg-sprite/ diff --git a/integration/apps/standalone/src/sprite/svg-sprite.config.json b/integration/apps/standalone/src/sprite/svg-sprite.config.json index 8c51c85..2fedb7e 100644 --- a/integration/apps/standalone/src/sprite/svg-sprite.config.json +++ b/integration/apps/standalone/src/sprite/svg-sprite.config.json @@ -1,6 +1,6 @@ { "mode": "standalone", "name": "app-icons", - "inputFiles": ["../../../../fixtures/icons/check.svg"], + "input": "../../../../fixtures/icons/check.svg", "generatedNotice": false } diff --git a/integration/package-lock.json b/integration/package-lock.json index 8514732..52290d2 100644 --- a/integration/package-lock.json +++ b/integration/package-lock.json @@ -23,7 +23,8 @@ "dependencies": { "colorette": "^2.0.20", "jiti": "^2.6.1", - "svg-sprite": "^2.0.4" + "svg-sprite": "^2.0.4", + "tinyglobby": "^0.2.17" }, "bin": { "svg-sprites": "dist/cli.js" diff --git a/integration/tests/apps.spec.ts b/integration/tests/apps.spec.ts index 3a4de4e..ee4ea9b 100644 --- a/integration/tests/apps.spec.ts +++ b/integration/tests/apps.spec.ts @@ -111,6 +111,12 @@ for (const app of apps) { await expect(icon).toBeVisible() await expect(icon).toHaveAttribute('data-app', app.id) + if (app.id === 'standalone-vite' || app.id === 'standalone-webpack') { + expect(await icon.evaluate((element) => element.tagName.toLowerCase())).toBe('icons-icon') + expect(await icon.evaluate((element) => element.shadowRoot !== null)).toBe(true) + await expect(icon.locator('svg')).toHaveAttribute('viewBox', '0 0 24 24') + } + const href = await icon.locator('use').getAttribute('href') expect(href).toBeTruthy() expect(href).not.toMatch(/^(?:blob|data|file):/) diff --git a/package-lock.json b/package-lock.json index d6411fa..1f1f65f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "colorette": "^2.0.20", "jiti": "^2.6.1", - "svg-sprite": "^2.0.4" + "svg-sprite": "^2.0.4", + "tinyglobby": "^0.2.17" }, "bin": { "svg-sprites": "dist/cli.js" @@ -1895,7 +1896,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -2443,7 +2443,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2916,10 +2915,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", diff --git a/package.json b/package.json index 1056b34..08a61c5 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,8 @@ "dist/chunk-*.js", "dist/chunk-*.js.map", "README_RU.md", - "docs/en/*.md", - "docs/ru/*.md", + "docs/en", + "docs/ru", "LICENSE", "THIRD_PARTY_NOTICES.md" ], @@ -97,7 +97,8 @@ "dependencies": { "colorette": "^2.0.20", "jiti": "^2.6.1", - "svg-sprite": "^2.0.4" + "svg-sprite": "^2.0.4", + "tinyglobby": "^0.2.17" }, "peerDependencies": { "react": ">=18 <20" diff --git a/skills/README.md b/skills/README.md index 5c7c978..eaefff2 100644 --- a/skills/README.md +++ b/skills/README.md @@ -1,6 +1,6 @@ # AI skills -Исходники английского и русского скиллов находятся в `skills/svg-sprites/src/{en,ru}/`. Готовые переносимые артефакты генерируются в `skills/artifacts/`, игнорируются Git и упаковываются в ZIP во время release workflow. +Исходники обязательного контекста английского и русского skills находятся в `skills/svg-sprites/src/{en,ru}/`. Канонические exact-mode guides находятся в `docs/{en,ru}/guides/` и копируются в соответствующий skill без изменения. Готовые переносимые артефакты генерируются в `skills/artifacts/`, игнорируются Git и упаковываются в ZIP во время release workflow. Обе языковые версии имеют одинаковую структуру: @@ -17,15 +17,11 @@ src// │ ├── 60-verification.md │ └── 70-diagnostics.md └── references/ - ├── react-vite.md - ├── react-webpack.md - ├── next-app.md - ├── next-pages.md ├── programmatic-api.md └── complex-svg.md ``` -`core/` содержит обязательные знания, раскрываемые прямо в итоговый `SKILL.md`. `references/` содержит самостоятельные инструкции для агента по конкретным стекам и редким сценариям. Пользовательские `README*.md` и `docs/{en,ru}/*.md` дополнительно копируются в `references/upstream/` как вторичный источник полного публичного API. +`core/` содержит обязательные знания, раскрываемые прямо в итоговый `SKILL.md`. Локальный `references/` содержит только agent-specific материалы. Девять файлов из `docs//guides/` копируются в `references/guides/`; английский artifact получает только английские guides, русский только русские. Второй набор mode guides и каталог `references/upstream/` не создаются. ## Композиция Markdown diff --git a/skills/svg-sprites/skill.config.mjs b/skills/svg-sprites/skill.config.mjs index 5c0002e..642c338 100644 --- a/skills/svg-sprites/skill.config.mjs +++ b/skills/svg-sprites/skill.config.mjs @@ -1,12 +1,20 @@ const agentReferences = [ - 'react-vite.md', - 'react-webpack.md', - 'next-app.md', - 'next-pages.md', 'programmatic-api.md', 'complex-svg.md', ] +const guideFiles = [ + 'standalone.md', + 'standalone-vite.md', + 'standalone-webpack.md', + 'react-vite.md', + 'react-webpack.md', + 'next-app-turbopack.md', + 'next-app-webpack.md', + 'next-pages-turbopack.md', + 'next-pages-webpack.md', +] + function documents(language) { return [ { entry: `src/${language}/SKILL.md`, to: 'SKILL.md', skill: true }, @@ -17,36 +25,28 @@ function documents(language) { ] } -const upstream = [ - { from: '../../README.md', to: 'references/upstream/README.md' }, - { from: '../../README_RU.md', to: 'references/upstream/README_RU.md' }, - { - fromDirectory: '../../docs/en', - toDirectory: 'references/upstream/docs/en', - extensions: ['.md'], - }, - { - fromDirectory: '../../docs/ru', - toDirectory: 'references/upstream/docs/ru', - extensions: ['.md'], - }, -] +function guides(language) { + return guideFiles.map((file) => ({ + from: `../../docs/${language}/guides/${file}`, + to: `references/guides/${file}`, + })) +} export default [ { name: 'svg-sprites', - description: 'Use only when configuring, generating, or troubleshooting @gromlab/svg-sprites. Triggers: @gromlab/svg-sprites, svg-sprite.config.ts, defineSpriteConfig, generateSprite, standalone, standalone@vite, standalone@webpack, react@vite, react@webpack, next@app, next@pages, inputFiles, SpriteViewer, or --icon-color-N. Do NOT use for custom SVG sprites, favicons, raster images, icon fonts, choosing an icon set, or inline SVG without this package.', + description: 'Use only when configuring, generating, or troubleshooting @gromlab/svg-sprites. Triggers: @gromlab/svg-sprites, svg-sprite.config.ts, defineSpriteConfig, generateSprite, standalone, standalone@vite, standalone@webpack, react@vite, react@webpack, next@app, next@pages, SpriteConfig.input, --input, SpriteViewer, or --icon-color-N. Do NOT use for custom SVG sprites, favicons, raster images, icon fonts, choosing an icon set, or inline SVG without this package.', output: '../artifacts/svg-sprites', maxSkillBytes: 48_000, documents: documents('en'), - copy: upstream, + copy: guides('en'), }, { name: 'svg-sprites-ru', - description: 'Используй только при настройке, изменении или диагностике @gromlab/svg-sprites. Триггеры: @gromlab/svg-sprites, svg-sprite.config.ts, defineSpriteConfig, generateSprite, standalone, standalone@vite, standalone@webpack, react@vite, react@webpack, next@app, next@pages, inputFiles, SpriteViewer и --icon-color-N. НЕ используй для самописных SVG-спрайтов, inline SVG, favicon, растровых изображений, icon fonts или выбора библиотеки иконок.', + description: 'Используй только при настройке, изменении или диагностике @gromlab/svg-sprites. Триггеры: @gromlab/svg-sprites, svg-sprite.config.ts, defineSpriteConfig, generateSprite, standalone, standalone@vite, standalone@webpack, react@vite, react@webpack, next@app, next@pages, SpriteConfig.input, --input, SpriteViewer и --icon-color-N. НЕ используй для самописных SVG-спрайтов, inline SVG, favicon, растровых изображений, icon fonts или выбора библиотеки иконок.', output: '../artifacts/svg-sprites-ru', maxSkillBytes: 48_000, documents: documents('ru'), - copy: upstream, + copy: guides('ru'), }, ] diff --git a/skills/svg-sprites/src/en/SKILL.md b/skills/svg-sprites/src/en/SKILL.md index 6211008..ed0909b 100644 --- a/skills/svg-sprites/src/en/SKILL.md +++ b/skills/svg-sprites/src/en/SKILL.md @@ -11,8 +11,9 @@ ## References to open as needed -- For React + Vite, open [React + Vite](./references/react-vite.md); for React with a custom Webpack 5 setup, open [React + Webpack](./references/react-webpack.md). -- For Next.js, open the guide for the [App Router](./references/next-app.md) or [Pages Router](./references/next-pages.md), then select the section for the bundler actually in use. +- For static publishing, open [bare standalone](./references/guides/standalone.md). For vanilla bundler apps, open [standalone + Vite](./references/guides/standalone-vite.md) or [standalone + Webpack](./references/guides/standalone-webpack.md). +- For React, open the exact [Vite](./references/guides/react-vite.md) or [Webpack](./references/guides/react-webpack.md) guide. +- For the Next.js App Router, open the exact [Turbopack](./references/guides/next-app-turbopack.md) or [Webpack](./references/guides/next-app-webpack.md) guide. +- For the Next.js Pages Router, open the exact [Turbopack](./references/guides/next-pages-turbopack.md) or [Webpack](./references/guides/next-pages-webpack.md) guide. - To invoke the generator from Node.js, open the [programmatic API](./references/programmatic-api.md). - For gradients, filters, `url(#...)`, unusual colors, and `viewBox` issues, open [complex SVGs](./references/complex-svg.md). -- Use the complete user documentation as a secondary source: [package README](./references/upstream/README.md). diff --git a/skills/svg-sprites/src/en/core/00-package-overview.md b/skills/svg-sprites/src/en/core/00-package-overview.md index d10fc99..c4ebb9d 100644 --- a/skills/svg-sprites/src/en/core/00-package-overview.md +++ b/skills/svg-sprites/src/en/core/00-package-overview.md @@ -1,15 +1,16 @@ ## What the package does -`@gromlab/svg-sprites` is a CLI generator that builds SVG sprites from user-provided SVG files. The package does not include its own icon set: it compiles the project's SVGs into an external sprite asset, supports standalone projects, and creates typed components for React/Next.js. +`@gromlab/svg-sprites` is a CLI generator that builds SVG sprites from user-provided SVG files. The package does not include its own icon set: it compiles the project's SVGs into an external sprite asset, creates a native typed Web Component for standalone bundler modes, and creates a React component for React/Next.js. The package supports multiple independent sprites in one project. Each explicitly selected config file or config-less directory describes one sprite and gets its own: - SVG asset; - mode-specific manifest data; - icon name types and `.svg-sprite/index.js` for bundler modes; +- a native Web Component with an explicit registration function for `standalone@vite`/`standalone@webpack`; - a React component only for React/Next.js; - a deployment-neutral JSON manifest without a public URL for bare `standalone`. The project determines how many sprite directories exist and where they live. For example, `name: 'file-manager'` produces `FileManagerIcon`, while another directory with `name: 'navigation'` produces a separate `NavigationIcon`. The names `FileManagerIcon` and `fileManagerIconNames` used below are examples of the API for one possible sprite, not fixed package exports. -Generated production runtime does not import `@gromlab/svg-sprites` at runtime. Install the package as a development dependency so configuration helpers and the local CLI use the version recorded in the project's lockfile. +Generated production runtime and declarations do not import `@gromlab/svg-sprites`. Generation can run through a pinned `npx --package` command without adding the package to the project. Install it as a development dependency only for the Viewer, package-provided config types, or the programmatic API. diff --git a/skills/svg-sprites/src/en/core/10-mode-selection.md b/skills/svg-sprites/src/en/core/10-mode-selection.md index 52c8e72..206fd8c 100644 --- a/skills/svg-sprites/src/en/core/10-mode-selection.md +++ b/skills/svg-sprites/src/en/core/10-mode-selection.md @@ -21,10 +21,10 @@ The CLI accepts exactly one path. A `.ts`, `.js`, or `.json` file loads that exa ```json { "scripts": { - "sprite:": "svg-sprites ", - "sprite::cli": "svg-sprites --mode " + "sprite:": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites ", + "sprite::cli": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites --mode " } } ``` -Do not use incomplete `react`, `next@app`, `next@pages`, or `standalone@` keys, or the removed `legacy` mode. Use bare `standalone` only when the application publishes the SVG itself; use the complete Vite/Webpack key otherwise. Create one command per config file or directory when the project has multiple sprites. +Generation through `npx` does not add the package to the project. Pin an exact package version instead of `latest` in CI. Do not use incomplete `react`, `next@app`, `next@pages`, or `standalone@` keys, or the removed `legacy` mode. Use bare `standalone` only when the application publishes the SVG itself; use the complete Vite/Webpack key otherwise. Create one command per config file or directory when the project has multiple sprites. diff --git a/skills/svg-sprites/src/en/core/20-project-inspection.md b/skills/svg-sprites/src/en/core/20-project-inspection.md index 8ae0c1a..800f700 100644 --- a/skills/svg-sprites/src/en/core/20-project-inspection.md +++ b/skills/svg-sprites/src/en/core/20-project-inspection.md @@ -9,14 +9,14 @@ Establish the project's actual contract before making changes: 5. For a new sprite, choose a target directory without imposing a particular application layer or architecture. 6. Check TypeScript and alias settings. Package subpath exports require TypeScript 5+ with `moduleResolution: 'bundler'`, `'node16'`, or `'nodenext'`. -All config paths are relative to the directory containing the explicitly selected config file; in config-less mode they are relative to the supplied directory: +All input paths are relative to the directory containing the explicitly selected config file; in config-less mode they are relative to the supplied directory. Inspect `input` using this single contract: -- `inputFolder` defaults to `./icons`; -- `inputFiles` contains additional relative paths to individual SVGs and is merged with `inputFolder`; -- the same absolute file is deduplicated, but different files with the same icon basename cause an error; -- scanning `inputFolder` is shallow: only immediate files ending in `.svg` are read, and nested directories are not traversed; -- an explicitly configured `inputFolder` that does not exist is an error; -- if `inputFolder` is omitted, `./icons` does not exist, and `inputFiles` is non-empty, generation uses only `inputFiles`; -- an empty final input set, a missing file, or a path that does not point to an `.svg` file is an error. +- `input?: string | string[]` defaults to `./icons`; +- each string is a folder, an exact SVG file, or a glob; +- a folder is scanned shallowly; nested files are included only by an explicit recursive glob such as `./icons/**/*.svg`; +- an array combines positive sources, while an item prefixed with `!` excludes its matches from the combined set; +- every positive source must resolve to at least one SVG, so a missing or empty folder, an unmatched glob, a missing file, or a non-SVG exact file is an error; +- resolved files are deduplicated and sorted deterministically; +- different files with the same basename are a conflict, even when they came from different sources. -Do not copy a shared SVG into several folders: add its relative path to `inputFiles` in every sprite that needs it. If a recursive structure is required, list the files explicitly or reorganize the sources; the generator does not perform recursive scanning. +Do not copy a shared SVG into several folders: add its exact path or a suitable glob to `input` in every sprite that needs it. Use `**/*.svg` only when recursive inclusion is intentional. diff --git a/skills/svg-sprites/src/en/core/30-react-next-setup.md b/skills/svg-sprites/src/en/core/30-react-next-setup.md index 57d9ecd..4a2c363 100644 --- a/skills/svg-sprites/src/en/core/30-react-next-setup.md +++ b/skills/svg-sprites/src/en/core/30-react-next-setup.md @@ -12,27 +12,21 @@ src/ui/file-manager/svg-sprite/ One `svg-sprite.config.ts` creates one independent sprite. For multiple sets, choose multiple directories and assign each a unique `name`. -Install the package as a development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -Use the configuration helper for autocomplete and type checking: +The generator does not need to be installed in the project. Start with a plain +config that has no package import: ```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ +export default { mode: 'react@vite', name: 'file-manager', description: 'File manager icons', - inputFolder: './icons', - inputFiles: ['../../shared/icons/close.svg'], -}) + input: ['./icons', '../../shared/icons/close.svg'], +} ``` -The object contract is the same for React and Next.js; only the full `mode` differs. +`input` accepts one folder, exact SVG file, or glob, or an array that combines them. Prefix an array item with `!` to exclude matches. Folders are shallow; use an explicit `**/*.svg` glob for recursion. Omit `input` to use `./icons`. Every path is resolved from the config directory, and every positive source must match at least one SVG. + +The object contract is the same for React and Next.js; only the full `mode` differs. Install the package only for the optional Viewer, programmatic API, or package-provided config typing. Exact guides also provide a local copy-paste config type for projects that remain package-free. `name` must begin with an ASCII letter and use kebab-case. The example `file-manager` produces `FileManagerIcon`, `FileManagerIconName`, and `fileManagerIconNames`. Another sprite gets its own names. If `name` is omitted, the generator derives it from the directory. @@ -41,7 +35,7 @@ Add a separate command with the selected mode key and one path: ```json { "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", + "sprite:file-manager": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", "sprites": "npm run sprite:file-manager" } } @@ -49,9 +43,18 @@ Add a separate command with the selected mode key and one path: For Next.js, set the full key in the config, for example `next@app/turbopack`. For multiple sprites, add one `sprite:` command per config file and invoke them sequentially from `sprites`. -Generated files in `.svg-sprite` are excluded from Git by default, so run `sprites` before any process that needs the component, types, or asset. If the project imports the sprite-module root, create a user-owned `index.ts` with `export * from './.svg-sprite'`. Add generation to `predev`, `prebuild`, and, when a `typecheck` script exists, `pretypecheck`. +To select sources from the CLI instead, repeat `--input `; the values form the same array contract, including `!` exclusions: -If a lifecycle script is missing, create it. If it already exists, preserve its command and append generation with `&&`; for example, change `"prebuild": "npm run lint"` to `"prebuild": "npm run lint && npm run sprites"`. Never replace an existing `pre*` script with generation alone, and never create a duplicate JSON key. +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts \ + --input ./icons \ + --input '../../shared/icons/**/*.svg' \ + --input '!../../shared/icons/legacy-*.svg' +``` + +Generated files in `.svg-sprite` are excluded from Git by default, so run `sprites` before any process that needs the component, types, or asset. If the project imports the sprite-module root, create a user-owned `index.ts` with `export * from './.svg-sprite'`. Generated declarations are self-contained and do not import the generator package. + +Run generation either from `predev`/`prebuild`/`pretypecheck` hooks or explicitly inside the corresponding commands. Do not use both forms for the same command, or generation runs twice. Preserve existing script commands and never create a duplicate JSON key. Run the first generation manually: diff --git a/skills/svg-sprites/src/en/core/40-generated-contract.md b/skills/svg-sprites/src/en/core/40-generated-contract.md index 178d31f..136c737 100644 --- a/skills/svg-sprites/src/en/core/40-generated-contract.md +++ b/skills/svg-sprites/src/en/core/40-generated-contract.md @@ -9,7 +9,6 @@ svg-sprite/ ├── index.ts # optional user-owned barrel ├── .gitignore # managed by the generator └── .svg-sprite/ - ├── state.json # ownership registry and contract version ├── index.js ├── index.d.ts ├── icon-data.js @@ -25,23 +24,25 @@ svg-sprite/ Standalone does not create `react/`. Bare `standalone` generates `sprite.svg` and `svg-sprite.manifest.json`; `standalone@vite`/`standalone@webpack` additionally -generate `index.*`, `icon-data.*`, and a resolved manifest. +generate `index.*`, `icon-data.*`, and a resolved manifest. Their `index.*` also +contains a native generated Web Component; bare `standalone` gets no JavaScript runtime +and does not create or modify `.gitignore`. -Edit the source SVGs, selected config, and user-owned `index.ts`. Do not manually change `.gitignore` or anything in `.svg-sprite`: the next generation will overwrite them. To import from the sprite-module root, create a barrel: +Edit the source SVGs, selected config, and user-owned `index.ts`. Do not manually change anything in `.svg-sprite`: the next generation will overwrite it. In modes other than bare `standalone`, the generated `.gitignore` is also managed and must not be edited manually. To import from the sprite-module root, create a barrel: ```ts export * from './.svg-sprite' ``` -The generator owns `.gitignore` and files inside `.svg-sprite`. The `state.json` registry allows stale generated files to be removed, but the writer refuses to overwrite or delete any file without a generated marker. Do not remove the marker, bypass the refusal, or replace generated paths with symlinks: move the user-owned file or choose a different directory. +The generator owns the complete `.svg-sprite` directory and replaces it on every run. Never put user files inside it. The generator also owns `.gitignore` when the selected mode creates it; bare `standalone` leaves an existing `.gitignore` untouched. Generated paths must not contain symlinks. -The internal `index.js` exports the component from `react/react-component.js` and the readonly name array; the adjacent `index.d.ts` adds props/style types and the icon-name union. The manifest contains the mode, URL, target, icon list, and icon metadata for debug tools and is not imported by the production component. +The internal `index.js` exports the component from `react/react-component.js` and the readonly name array; the adjacent `index.d.ts` adds props/style types and the icon-name union. Bundler-mode manifest declarations define their types locally and do not import the generator package. The manifest contains the mode, URL, target, icon list, and icon metadata for debug tools and is not imported by the production component. The sprite remains a separate content-hashed asset; SVG path data is not embedded in JavaScript: - `react@vite` generates a static `sprite.svg?no-inline` import, preventing Vite from inlining it; -- `standalone@vite` uses the same Vite asset mechanism but exports an href helper without React; -- `standalone@webpack` uses Webpack Asset Modules without React; +- `standalone@vite` uses the same Vite asset mechanism and exports an href helper plus a native Web Component without React; +- `standalone@webpack` uses Webpack Asset Modules and exports the same mode-local Web Component without React; - React Webpack 5 and all Next modes generate `new URL('./sprite.svg', import.meta.url).href`, which must be processed by the selected bundler's Asset Modules; - a custom Webpack SVG loader must not intercept the generated `sprite.svg`; - in Next mode, the generated component does not contain `'use client'` and works in Server Components, SSR, and SSG; do not add a client boundary solely for an icon; diff --git a/skills/svg-sprites/src/en/core/50-usage-and-colors.md b/skills/svg-sprites/src/en/core/50-usage-and-colors.md index 0a75c00..72acb83 100644 --- a/skills/svg-sprites/src/en/core/50-usage-and-colors.md +++ b/skills/svg-sprites/src/en/core/50-usage-and-colors.md @@ -1,6 +1,20 @@ ## Usage, accessibility, and colors -The component name depends on the specific sprite's `name`. These examples use `name: 'file-manager'`, so the generated component is called `FileManagerIcon`. For `name: 'navigation'`, use the generated `NavigationIcon`. +The component name depends on the specific sprite's `name`. In `standalone@vite` and `standalone@webpack`, `name: 'file-manager'` creates the `` tag and the `defineFileManagerIconElement()` function: + +```ts +import { defineFileManagerIconElement } from './svg-sprite' + +defineFileManagerIconElement() +``` + +```html + +``` + +The native element has no runtime dependencies, selects the generated ID and `viewBox`, obtains the URL through the bundler, and renders `` in Shadow DOM. Its `icon` property is typed with the exact name union, while plain HTML attribute values are validated only at runtime. It defaults to `1em × 1em`; resize the host with CSS. Bare `standalone` does not generate a Web Component. + +In React/Next.js, the same `name: 'file-manager'` creates the `FileManagerIcon` React component. For `name: 'navigation'`, use the generated `NavigationIcon`. Import the component from the root of its sprite directory. `width` and `height` are optional: ordinary CSS classes can control the size. @@ -64,10 +78,10 @@ The `removeSize`, `replaceColors`, and `addTransition` transforms are enabled by Automatic replacement targets `fill`/`stroke` attributes and inline `style`. The values `none`, `transparent`, `inherit`, `unset`, and `initial` are not replaced. Check CSS classes and external stylesheets, gradients, patterns, filters, and `url(#...)` against the actual output. Page variables work through ``, but do not cross into an external document loaded through `` or `background-image`; a CSS mask preserves only a monochrome silhouette. -`SpriteViewer` is optional. Import it from `@gromlab/svg-sprites/react` only on a debug route: +`SpriteViewer` is optional. Install `@gromlab/svg-sprites` as a development dependency only when the project needs the Viewer, then import it from `@gromlab/svg-sprites/react` on a debug route: - in Vite, pass the result of a string-literal `import.meta.glob('/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js')`; -- in Webpack, pass an array of static `() => import('.../manifest')` loaders; +- in Webpack, pass an array of static `() => import('.../.svg-sprite/svg-sprite.manifest.js')` loaders; - in Next.js, use the same static loaders, and for the App Router put the Viewer in a separate file with `'use client'`. -The Viewer accepts manifests/loaders and provides search, themes, colors, and examples, but production components do not depend on it. Import it from the package already installed as a development dependency. +The Viewer accepts manifests/loaders and provides search, themes, colors, and examples, but production components do not depend on it. diff --git a/skills/svg-sprites/src/en/core/60-verification.md b/skills/svg-sprites/src/en/core/60-verification.md index a2251a2..70a8e5f 100644 --- a/skills/svg-sprites/src/en/core/60-verification.md +++ b/skills/svg-sprites/src/en/core/60-verification.md @@ -3,7 +3,7 @@ After changing a config or SVG, perform these required quick checks: 1. Run the exact sprite command, for example `npm run sprite:file-manager`; it must exit with code `0` and report the name, icon count, mode, and `.svg-sprite` directory. -2. Confirm that `.svg-sprite/index.js`, `.svg-sprite/index.d.ts`, `sprite.svg`, the `icon-data.js`/`.d.ts` and manifest `.js`/`.d.ts` pairs, `react/react-component.js`, its `.d.ts` and CSS Module, and `state.json` exist. +2. Confirm that `.svg-sprite/index.js`, `.svg-sprite/index.d.ts`, `sprite.svg`, the `icon-data.js`/`.d.ts` and manifest `.js`/`.d.ts` pairs, `react/react-component.js`, its `.d.ts` and CSS Module exist. 3. Confirm that the new icon appears in the readonly name array and is accepted by the `icon` prop. 4. Run the project's existing type check, for example `npm run typecheck`. 5. Check that `target` in `.svg-sprite/svg-sprite.manifest.js` matches the selected mode key; the generated asset expression must use `?no-inline` for Vite and `new URL(...)` for Webpack/Next. diff --git a/skills/svg-sprites/src/en/core/70-diagnostics.md b/skills/svg-sprites/src/en/core/70-diagnostics.md index e1a1dff..e056031 100644 --- a/skills/svg-sprites/src/en/core/70-diagnostics.md +++ b/skills/svg-sprites/src/en/core/70-diagnostics.md @@ -8,9 +8,10 @@ Match the symptom to the relevant check and fix the root cause: | `Expected one config file or module directory` | Multiple paths were passed | Create one command per sprite and combine the scripts. | | `Sprite mode is required` | Mode is absent from both config and CLI | Add `mode` to the object or pass the full `--mode`. | | `Unsupported sprite config extension` | The supplied file is not `.ts`, `.js`, or `.json` | Use a supported config format. | -| `Input directory does not exist` | The relative path is wrong or an explicitly configured folder is missing | Resolve it from the config directory; create the folder or correct `inputFolder`. | -| Icons from a subdirectory are missing | Recursive scanning was assumed | Move the SVG to the top level of `inputFolder` or list it in `inputFiles`. | -| `SVG file does not exist`, `File is not an SVG`, or an empty input set | Invalid `inputFiles`, extension, or sources | Correct the path/extension and provide at least one input SVG. | +| A positive input source has no SVG matches | A folder is missing or empty, a glob matches nothing, or an exact path is missing or not an SVG | Resolve the source from the config directory and correct `input`; every positive item must produce at least one SVG. | +| Icons from a subdirectory are missing | A folder source was expected to scan recursively | Use an explicit glob such as `./icons/**/*.svg`; folders are shallow. | +| An excluded icon is still present | The exclusion lacks a leading `!`, is not in the `input` array, or is relative to the wrong directory | Add a matching `!` item and resolve it from the config directory. | +| CLI source selection is incomplete | Multiple sources were packed into one `--input` value or an option was omitted | Repeat `--input ` once per source or exclusion. | | Icon name or SVG ID collision | Two different files have the same basename, or a hash ID collides with a name | Rename one source SVG; do not select a file implicitly. | | `Refusing to overwrite/delete a user file` | A user file occupies a managed path or lost its marker | Do not bypass the protection: move the file or choose another sprite directory and regenerate. | | Missing `.svg-sprite/index.js` or name absent from autocomplete | Generation did not run, the user barrel does not export `.svg-sprite`, or the type server cached an old module | Run the sprite command, check `export * from './.svg-sprite'`, then typecheck; restart the TypeScript server if necessary. | diff --git a/skills/svg-sprites/src/en/references/complex-svg.md b/skills/svg-sprites/src/en/references/complex-svg.md index 0d6cfe7..c3d5db3 100644 --- a/skills/svg-sprites/src/en/references/complex-svg.md +++ b/skills/svg-sprites/src/en/references/complex-svg.md @@ -131,7 +131,7 @@ It is preserved as the fragment ID. Other names, such as `folder open.svg` or `2 Do not manually construct `#folder open`. Use the generated component or `.svg-sprite/svg-sprite.manifest.js`, which records both `name` and the actual `id`. -Different files with the same basename are forbidden, even from different directories. Rename one source meaningfully; `inputFiles` order does not select a winner. +Different files with the same basename are forbidden, even from different directories. Rename one source meaningfully; source order or overlap never selects a winner. ## Rendering method @@ -173,4 +173,4 @@ External stack-fragment support and paint-server behavior can vary across browse - A manual fragment fails for a name containing spaces: use the ID from the manifest. - One complex icon requires different transforms: move it to a separate sprite; per-icon transform config is not supported. -Target-specific execution and verification are documented in [react-vite.md](react-vite.md), [react-webpack.md](react-webpack.md), [next-app.md](next-app.md), and [next-pages.md](next-pages.md). +Target-specific execution and verification are documented in the exact-mode files under [guides](guides/standalone.md). diff --git a/skills/svg-sprites/src/en/references/next-app.md b/skills/svg-sprites/src/en/references/next-app.md deleted file mode 100644 index 44e8413..0000000 --- a/skills/svg-sprites/src/en/references/next-app.md +++ /dev/null @@ -1,149 +0,0 @@ -# Next.js App Router: operational reference - -## When to use this reference - -Use this document when the application uses `app/` or `src/app/` and needs a generated component for Server Components, SSR, or SSG. First determine the actual bundler: Turbopack and Webpack use different targets. For `pages/`, use [next-pages.md](next-pages.md). - -## Target matrix - -| Bundler | CLI mode | -|---|---| -| Turbopack | `next@app/turbopack` | -| Webpack 5 | `next@app/webpack` | - -Do not infer the target only from the presence of `next.config.*`. Check the actual flags in `dev`/`build`. The generator mode and verification-build bundler must agree. - -## Prepare the sprite directory - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -Install the package as a development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - name: 'file-manager', - description: 'File manager icons', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -- The project chooses the directory for each specific sprite; it does not have to be a module/feature directory. Each config describes one of potentially many application sprites. -- `svg-sprite.config.ts` is the recommended name; the CLI accepts an explicitly selected `.ts`, `.js`, or `.json` file with any name. -- Paths are relative to its directory; `inputFolder` scanning is shallow. -- The local folder and `inputFiles` are merged. Equal basenames from different files are forbidden. -- If no name is configured, it is derived from the directory; for a directory named `svg-sprite`, the parent directory name is used. An explicit `name` is more stable when files move. -- `name` must use kebab-case and begin with a letter. -- `FileManagerIcon` below is only an example generated name for `name: 'file-manager'`. -- The Next preset always generates `stack` and includes a root sprite `viewBox`; this is not a user setting. - -## Generation - -Example scripts for Turbopack: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -For Webpack, replace only the complete mode with `next@app/webpack`. Do not run both targets sequentially for one directory: the second generation overwrites the first target's files. Run `npm run sprite:file-manager` for the first generation. - -## Server Component - -The generated production component does not contain `'use client'`. Import it directly into a server `page.tsx` or `layout.tsx`: - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function Page() { - return ( -
- -
- ) -} -``` - -`width` and `height` are optional in JSX; set the size with a CSS class or `wrapped`. Do not add a Client Component boundary solely for the icon. The generated module builds the asset URL with static `new URL('./sprite.svg', import.meta.url).href`; the same mechanism is used during SSR and in the browser. - -Do not move the SVG into `public`. The generator rewrites `.svg-sprite` and `.gitignore`; the root `index.ts` is application-owned and may contain `export * from './.svg-sprite'`. - -## SpriteViewer - -The Viewer is interactive and imported from a client-only entry. Create a separate Client Component page or child component: - -```tsx -'use client' - -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/manifest'), - () => import('@/ui/navigation/svg-sprite/manifest'), -] - -export default function SpritesPage() { - return -} -``` - -The `import()` paths must be string literals. Keep production icon imports outside `@gromlab/svg-sprites/react`: the package React entry is only for the Viewer and contains `'use client'`. - -## Verification - -Start with generation and typechecking: - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -If the target or Next build/deployment pipeline changed, or a runtime issue is being diagnosed, run the project's production build configured for the selected bundler: - -```bash -npm run build -``` - -After the required generation and typecheck, verify that: - -- `.svg-sprite/svg-sprite.manifest.js` contains the exact target `next@app/turbopack` or `next@app/webpack`; -- the server page compiles without adding `'use client'`. - -During a conditional production/runtime check, also verify that: - -- HTML contains an `href` to `.svg#id`, and the asset URL is available after `next start`; -- the URL does not use `data:`, `file:`, or `blob:`; -- SSR markup and the hydrated page refer to the same asset; -- complex SVGs were examined according to [complex-svg.md](complex-svg.md); claim visual and accessibility results only when the corresponding tools are available. - -## Common failures - -- `Next.js mode requires a router and bundler`: `--mode next` is invalid; specify the full mode. -- Build passes with one bundler but the runtime asset breaks with another: regenerate for the target actually used by the build. -- Webpack was selected, but the build used Turbopack: use the project's Webpack build command and `next@app/webpack`. -- Viewer causes a Server Component error: the Viewer file needs `'use client'`; the generated icon component does not. -- Config not found: pass the full path to an existing `.ts`, `.js`, or `.json` config file. -- Two CI jobs generate different targets in one checkout: separate their directories or ensure each job uses one consistent target. -- Asset is missing under `basePath`/CDN: inspect Next asset handling and deployment configuration; do not replace the generated URL manually. -- Package subpath type error: use TypeScript 5+ with `moduleResolution: "bundler"`, `"node16"`, or `"nodenext"`. - -To invoke the generator from a build script, open [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/en/references/next-pages.md b/skills/svg-sprites/src/en/references/next-pages.md deleted file mode 100644 index a24d531..0000000 --- a/skills/svg-sprites/src/en/references/next-pages.md +++ /dev/null @@ -1,138 +0,0 @@ -# Next.js Pages Router: operational reference - -## When to use this reference - -Use this document for pages under `pages/` or `src/pages/`, including SSR with `getServerSideProps`, SSG, and client-side navigation. If the route is under `app/`, use [next-app.md](next-app.md). - -## Selecting a target - -| Bundler | CLI mode | -|---|---| -| Turbopack | `next@pages/turbopack` | -| Webpack 5 | `next@pages/webpack` | - -Determine the actual script flags before selecting a mode. The presence of the Pages Router does not automatically imply Webpack; select the target from the bundler used by the project. - -## Structure and config - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -Install the package as a development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@pages/webpack', - name: 'file-manager', - description: 'File manager icons', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -The project chooses the directory containing each specific sprite config, and it does not have to be a module/feature directory; each config describes one of potentially many application sprites. - -All source paths are resolved from the config directory. `inputFolder` defaults to `./icons`, and scanning is shallow. `inputFiles` is merged with the folder. The same path is deduplicated, but two different `check.svg` files conflict. An explicitly configured missing folder is an error even when `inputFiles` is non-empty. - -`name` uses kebab-case and determines the public names. `FileManagerIcon` below is only an example generated name for `name: 'file-manager'`. Next modes always create `stack`, not `symbol`. - -## Commands - -Example lifecycle scripts for Webpack: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -If pre-scripts already exist, add generation to their current command chain. Generated imports are absent from Git, so generation must precede TypeScript and Next compilation. For Turbopack, replace the complete mode with `next@pages/turbopack`. Run `npm run sprite:file-manager` for the first generation. - -## Usage in the Pages Router - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function FilesPage() { - return -} - -export function getServerSideProps() { - return { props: {} } -} -``` - -`width` and `height` are optional in JSX; set the size with a CSS class or `wrapped`. The component works during SSR, SSG, and client navigation. It uses `new URL('./sprite.svg', import.meta.url).href` so Next emits an external hashed asset. Do not copy the generated SVG into `public` or construct the URL manually. - -Import the component and types from the local user-owned `svg-sprite/index.ts`, which re-exports `.svg-sprite`. Do not edit `.svg-sprite` or the generated `.gitignore`. - -## SpriteViewer - -Pages Router components run on the client as well, so a separate `'use client'` directive is unnecessary: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/manifest'), - () => import('@/ui/navigation/svg-sprite/manifest'), -] - -export default function SpritesPage() { - return -} -``` - -Use literal imports and expose the page only as a debug/internal tool. If the page participates in production routing, restrict access through the application's own controls. - -## Verification - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -If the target or Next build/deployment pipeline changed, or a runtime issue is being diagnosed, run the project's production build configured for the selected bundler: - -```bash -npm run build -``` - -After the required generation and typecheck, verify that: - -- `.svg-sprite/svg-sprite.manifest.js` contains `next@pages/turbopack` or `next@pages/webpack`; -- `getServerSideProps`/`getStaticProps` does not import the package React Viewer entry. - -After the conditional production build, and only when browser tools are available, inspect the SSR route and navigation to it: - -- initial HTML contains `.svg#id`; -- the SVG URL responds successfully and is not `data:`, `file:`, or `blob:`; -- after client navigation, the icon keeps the same valid URL; -- gradients, masks, and colors were examined according to [complex-svg.md](complex-svg.md); do not claim a visual or accessibility result without the necessary tools. - -## Common failures - -- Mode uses `next@app/...`: the module may generate, but its manifest and target contract are wrong; use `next@pages/...`. -- The command and mode select different bundlers: use the project's matching build command and regenerate. -- Viewer manifest is missing from the chunk: the `import()` path must be a string literal and must exist before the Next build. -- Icon appears after a full reload but disappears during navigation: check external asset availability with `basePath`, `assetPrefix`, and the production origin. -- `Refusing to overwrite a user file`: the sprite directory contains an unmanaged file with a reserved name; move it. -- Icon name is absent from the type: folder scanning is not recursive; move the SVG to the top level or add its exact path to `inputFiles`. - -For programmatic generation, use [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/en/references/programmatic-api.md b/skills/svg-sprites/src/en/references/programmatic-api.md index 42727c0..fc6344b 100644 --- a/skills/svg-sprites/src/en/references/programmatic-api.md +++ b/skills/svg-sprites/src/en/references/programmatic-api.md @@ -18,23 +18,28 @@ await generateSprite('src/ui/icons/svg-sprite.config.ts') await generateSprite('src/ui/icons', { mode: 'react@vite', name: 'app', - inputFolder: './icons', + input: './icons', }) ``` A directory enables config-less mode. `mode` must exist after settings are merged. +`input?: string | string[]` defaults to `./icons`. Each value is a folder, an exact SVG file, or a glob, resolved from the config directory or the config-less source directory. Folder scans are shallow; recurse only with an explicit glob such as `./icons/**/*.svg`. Arrays combine sources, and items prefixed with `!` exclude matches. Every positive item must resolve to at least one SVG. The final files are deduplicated and sorted, while different files with the same basename cause an error. + ## Overrides ```ts await generateSprite('src/ui/icons/custom.json', { mode: 'react@webpack', - inputFiles: ['../../shared/search.svg'], + input: [ + '../../shared/icons/**/*.svg', + '!../../shared/icons/legacy-*.svg', + ], transform: { addTransition: false }, }) ``` -Values are applied as `defaults → config → API overrides`. `transform` is merged by field; a supplied `inputFiles` replaces the config array. +Values are applied as `defaults → config → API overrides`. `transform` is merged by field; a supplied `input` replaces the config value. The specialized `generateReactSprite` and `generateNextSprite` functions remain as compatibility wrappers, but prefer `generateSprite` in new code. diff --git a/skills/svg-sprites/src/en/references/react-vite.md b/skills/svg-sprites/src/en/references/react-vite.md deleted file mode 100644 index f4daf52..0000000 --- a/skills/svg-sprites/src/en/references/react-vite.md +++ /dev/null @@ -1,166 +0,0 @@ -# React with Vite: operational reference - -## When to use this reference - -Use this document when the project uses React without Next.js and is built with Vite, or when the generated component contains a `sprite.svg?no-inline` import. Do not apply this target to Webpack; use [react-webpack.md](react-webpack.md) instead. - -## Establish context first - -1. Check `package.json`: confirm React, Vite, and the actual `dev`, `build`, and `typecheck` commands. -2. Find existing `svg-sprite.config.ts` files and scripts containing `svg-sprites`. Do not create a second directory for an existing sprite. -3. Choose a target directory for the specific sprite. It does not have to be a module or feature directory; pass the full path to its config file. -4. Do not manually edit `.svg-sprite` or the local `.gitignore`; the generator owns them. The root `index.ts` is application-owned. - -Minimal structure: - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -## Setup - -Install the package as a development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -`src/ui/file-manager/svg-sprite/svg-sprite.config.ts`: - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@vite', - name: 'file-manager', - description: 'File manager icons', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -- Each config describes one specific sprite; an application may contain many independent configs and sprites. -- Every config path is resolved relative to the directory containing `svg-sprite.config.ts`. -- `inputFolder` defaults to `./icons`; folder scanning is shallow and includes files ending in `.svg`. -- `inputFolder` and `inputFiles` are merged, and duplicate absolute paths are deduplicated. -- If `inputFiles` is non-empty and the implicit `./icons` directory is absent, generation uses only the list. An explicitly configured missing `inputFolder` is always an error. -- Different files with the same basename, such as two `check.svg` files, conflict because they define the same public icon name. -- `name` must use kebab-case and begin with an ASCII letter. `FileManagerIcon`, `FileManagerIconName`, and `fileManagerIconNames` below are only examples of generated names for `name: 'file-manager'`. -- The React preset always creates the `stack` format; `symbol` cannot be selected here. - -## Command and scripts - -Add the local CLI to `package.json` and run it before processes that need generated imports: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -If the project already has `predev` or `prebuild`, integrate generation into the existing orchestration instead of overwriting the script. - -Run `npm run sprite:file-manager` for the first generation. - -## Usage - -Import only from the public local entry point: - -```tsx -import { FileManagerIcon, fileManagerIconNames } from './svg-sprite' -import type { FileManagerIconName, FileManagerIconStyle } from './svg-sprite' - -const colorStyle: FileManagerIconStyle = { - '--icon-color-1': '#2563eb', -} - -export function FolderIcon({ icon }: { icon: FileManagerIconName }) { - return -} - -export const availableIcons = fileManagerIconNames -``` - -`width` and `height` are optional in JSX; a CSS class can control the size. Without `wrapped`, the component renders `` and accepts SVG attributes. With `wrapped={true}`, the root becomes a `` and the inner SVG fills its width and height: - -```tsx - -``` - -For normal imports, use a user-owned barrel with `export * from './.svg-sprite'`; the remaining files in the hidden directory are internal. - -## Target-specific behavior - -The Vite target generates a static import: - -```ts -import spriteUrl from './sprite.svg?no-inline' -``` - -The `?no-inline` query is required: it prevents Vite from converting a small SVG into a data URL. Do not remove the query or copy the generated SVG into `public`; Vite must emit a separate content-hashed asset. - -Use the same mechanism for low-level `` integration: - -```tsx -import spriteUrl from './svg-sprite/.svg-sprite/sprite.svg?no-inline' - - - - -``` - -A manual `#check` fragment is safe only for names matching `^[a-zA-Z][a-zA-Z0-9_-]*$`. For spaces and other characters, the generated component uses a stable hash ID; the exact ID is available in `.svg-sprite/svg-sprite.manifest.js`. - -## SpriteViewer - -After generation, add the Viewer only to a debug route: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' - -const sources = import.meta.glob( - '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', -) - -export function IconsDebugPage() { - return -} -``` - -The `import.meta.glob` argument must remain a string literal. Generation must finish before Vite starts, or new manifests will not be included in the glob. - -## Verification - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -These are the required quick checks. Inspect the generated files statically: - -- the user-owned `index.ts` re-exports the component, props, style, name union, and runtime array from `.svg-sprite`; -- `.svg-sprite/svg-sprite.manifest.js` contains `target: "vite"`, `format: "stack"`, and the expected icon count. - -If the target or asset pipeline changed, or a runtime issue is being diagnosed, also run the production build. When browser tools are available, inspect Network for a separate `.svg` asset rather than `data:image/svg+xml`, a successful URL, and ``. For complex colors, `defs`, and dimensions, follow [complex-svg.md](complex-svg.md); do not claim a visual or accessibility result without the necessary tools. - -## Common failures - -- Config not found: pass the full path to an existing `.ts`, `.js`, or `.json` config file. -- `Sprite mode is required`: set `mode: 'react@vite'` in the config or pass `--mode react@vite`. -- Icon missing from autocomplete: check the case-sensitive `.svg` suffix and shallow location, then regenerate before typechecking. -- `Refusing to overwrite a user file`: do not remove the marker or bypass the writer; move the user-owned file or choose another sprite directory. -- Viewer is empty: check the literal glob, the existence of `.svg-sprite/svg-sprite.manifest.js`, and the `predev` execution order. -- SVG was inlined: confirm that the module targets `vite` and its import still contains `?no-inline`. -- TypeScript cannot resolve the package subpath: use TypeScript 5+ with `moduleResolution: "bundler"`, `"node16"`, or `"nodenext"`. - -For execution without the CLI, use [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/en/references/react-webpack.md b/skills/svg-sprites/src/en/references/react-webpack.md deleted file mode 100644 index 46c71ac..0000000 --- a/skills/svg-sprites/src/en/references/react-webpack.md +++ /dev/null @@ -1,152 +0,0 @@ -# React with Webpack 5: operational reference - -## When to use this reference - -Use this document for a React application built with Webpack 5 when SVGs must be processed through Asset Modules. If the project uses Vite, see [react-vite.md](react-vite.md). For Next.js, do not select `react@webpack`; router-specific targets are documented in [next-app.md](next-app.md) and [next-pages.md](next-pages.md). - -## Diagnose the environment before making changes - -1. Confirm Webpack major version 5 in `package.json` or the lockfile. -2. Inspect `.svg` entries in `module.rules`, `output.publicPath`, the dev server, and existing asset conventions. -3. Find existing `svg-sprite.config.ts` files and generation scripts. -4. Choose the project directory for the specific sprite. It does not have to match a module or feature directory. Pass the full path to its config file. - -Minimal structure: - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -## Configuration and generation - -Install the package as a development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@webpack', - name: 'file-manager', - description: 'File manager icons', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -Each config describes one of potentially many independent application sprites. - -Paths are relative to `svg-sprite.config.ts`. The folder is scanned only at its top level. `inputFolder` and `inputFiles` are merged; duplicate paths are deduplicated, but equal basenames from different files cause an ID conflict. The implicit `./icons` folder may be absent when `inputFiles` is non-empty; an explicitly configured missing folder is an error. `FileManagerIcon` below is only an example generated name for `name: 'file-manager'`. - -Recommended lifecycle hooks: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Do not overwrite existing pre-scripts; add generation to their current command chain. Run `npm run sprite:file-manager` for the first generation. The React preset always emits `stack`. - -## Public component - -```tsx -import { FileManagerIcon } from './svg-sprite' - -export function OpenFolderButton() { - return ( - - ) -} -``` - -`width` and `height` are optional in JSX; set the size with a CSS class or `wrapped`. The user-owned barrel also exports `FileManagerIconProps`, `FileManagerIconStyle`, `FileManagerIconName`, and `fileManagerIconNames`. Do not edit `.svg-sprite` or the generated `.gitignore`. - -## Webpack target behavior - -The generated component obtains its URL only through this static expression: - -```ts -const spriteUrl = new URL('./sprite.svg', import.meta.url).href -``` - -Webpack 5 recognizes it as an Asset Module and replaces it with the public URL of a separate SVG. For correct handling: - -- do not move the path into a variable or modify the generated expression; -- ensure Babel/TypeScript does not transform `import.meta.url` before Webpack processes it; -- do not let `@svgr/webpack`, `svg-inline-loader`, `raw-loader`, or a general SVG rule intercept `svg-sprite/.svg-sprite/sprite.svg`; -- with custom rules, either exclude the generated sprite from component/raw loaders or add a dedicated `type: 'asset/resource'` rule; -- check `output.publicPath`, especially for CDN, subpath, and dev-server deployments. - -Example dedicated rule when an existing loader intercepts every SVG: - -```js -{ - test: /svg-sprite[\\/]\.svg-sprite[\\/]sprite\.svg$/, - type: 'asset/resource', -} -``` - -Integrate this rule with the project's current configuration; do not add a duplicate matcher when standard Asset Modules already handle `new URL` correctly. - -## SpriteViewer - -Webpack does not provide `import.meta.glob`. Pass static lazy imports with string-literal paths: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('./ui/file-manager/svg-sprite/manifest'), - () => import('./ui/navigation/svg-sprite/manifest'), -] - -export function IconsDebugPage() { - return -} -``` - -A dynamically assembled `import(path)` is not suitable: Webpack cannot reliably associate the manifests with their assets. Expose the Viewer only on a debug/internal route. - -## Verification - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -These are the required quick checks. After generation, verify that: - -- `.svg-sprite/svg-sprite.manifest.js` contains `target: "webpack"` and `format: "stack"`; -- the generated component contains `new URL('./sprite.svg', import.meta.url).href`. - -A production build and browser/Network checks are additionally required when the target, Webpack asset rules, `publicPath`/deployment pipeline changed, or a runtime issue is being diagnosed. In that case, verify a separate hashed SVG asset, an HTTP(S) `.svg#id` URL, content type, fragment ID, and the correct CDN/subpath URL. Do not claim visual or accessibility correctness without the necessary tools. - -For icons with gradients, filters, masks, or internal CSS, perform the checks in [complex-svg.md](complex-svg.md). - -## Common failures - -- `Unsupported React target`: the programmatic API received a value other than `'webpack'`; the CLI mode must be exactly `react@webpack`. -- Webpack tries to render the SVG as a React component: the generated sprite matched the SVGR rule; exclude it or prioritize `asset/resource`. -- URL points to the wrong host/subpath: fix `output.publicPath` and runtime deployment settings, not the generated file. -- `import.meta` is unsupported: confirm the build really uses Webpack 5 and that an intermediate transpiler preserves the expression. -- Viewer does not load the manifest: check the literal path, chunk loading, and generation before compilation. -- `Refusing to overwrite a user file`: the directory already contains a user-owned `.gitignore` or file in `.svg-sprite`; move it rather than bypassing protection. -- Icon color does not change: use `color` for monochrome icons or `--icon-color-N` through ``; page CSS does not cross into ``. - -For custom build orchestration, see [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/ru/SKILL.md b/skills/svg-sprites/src/ru/SKILL.md index 6b0c635..ffe0ca6 100644 --- a/skills/svg-sprites/src/ru/SKILL.md +++ b/skills/svg-sprites/src/ru/SKILL.md @@ -11,8 +11,9 @@ ## Справочники по необходимости -- Для React + Vite открой [React + Vite](./references/react-vite.md), для React с собственным Webpack 5 — [React + Webpack](./references/react-webpack.md). -- Для Next.js открой руководство для [App Router](./references/next-app.md) или [Pages Router](./references/next-pages.md) и выбери раздел фактического сборщика. +- Для статической публикации открой [bare standalone](./references/guides/standalone.md). Для vanilla-приложений со сборщиком открой [standalone + Vite](./references/guides/standalone-vite.md) или [standalone + Webpack](./references/guides/standalone-webpack.md). +- Для React открой exact guide для [Vite](./references/guides/react-vite.md) или [Webpack](./references/guides/react-webpack.md). +- Для Next.js App Router открой exact guide для [Turbopack](./references/guides/next-app-turbopack.md) или [Webpack](./references/guides/next-app-webpack.md). +- Для Next.js Pages Router открой exact guide для [Turbopack](./references/guides/next-pages-turbopack.md) или [Webpack](./references/guides/next-pages-webpack.md). - Для вызова генератора из Node.js открой [программный API](./references/programmatic-api.md). - Для gradients, filters, `url(#...)`, нестандартных цветов и проблем с `viewBox` открой [сложные SVG](./references/complex-svg.md). -- Полную пользовательскую документацию используй как вторичный источник: [README пакета](./references/upstream/README_RU.md). diff --git a/skills/svg-sprites/src/ru/core/00-package-overview.md b/skills/svg-sprites/src/ru/core/00-package-overview.md index cf2e412..ffa5f06 100644 --- a/skills/svg-sprites/src/ru/core/00-package-overview.md +++ b/skills/svg-sprites/src/ru/core/00-package-overview.md @@ -1,15 +1,16 @@ ## Что делает пакет -`@gromlab/svg-sprites` — CLI-генератор SVG-спрайтов для пользовательских SVG-файлов. Пакет не содержит собственного набора иконок: он собирает SVG проекта во внешний sprite asset, поддерживает standalone-проекты и для React/Next.js создаёт типизированный компонент. +`@gromlab/svg-sprites` — CLI-генератор SVG-спрайтов для пользовательских SVG-файлов. Пакет не содержит собственного набора иконок: он собирает SVG проекта во внешний sprite asset, создаёт нативный типизированный Web Component для standalone bundler modes и React-компонент для React/Next.js. Пакет рассчитан на несколько независимых спрайтов в одном проекте. Каждый явно выбранный config-файл или config-less каталог описывает один спрайт и получает собственные: - SVG asset; - mode-specific manifest data; - для bundler modes — типы имён и production entry `.svg-sprite/index.js`; +- для `standalone@vite`/`standalone@webpack` — нативный Web Component с явной функцией регистрации; - только для React/Next.js — React-компонент; - для bare `standalone` — deployment-neutral JSON manifest без публичного URL. Количество и расположение каталогов определяет проект. Например, `name: 'file-manager'` создаёт `FileManagerIcon`, а другой каталог с `name: 'navigation'` создаст отдельный `NavigationIcon`. Имена `FileManagerIcon` и `fileManagerIconNames` ниже являются примерами API одного из возможных спрайтов, а не фиксированными экспортами пакета. -Generated production runtime не импортирует `@gromlab/svg-sprites` во время выполнения. Устанавливай пакет как development dependency, чтобы config helpers и локальный CLI использовали версию из lockfile проекта. +Generated production runtime и declarations не импортируют `@gromlab/svg-sprites`. Генерация работает через `npx --package` с зафиксированной версией без добавления package в проект. Устанавливай его как development dependency только для Viewer, package-типов config или программного API. diff --git a/skills/svg-sprites/src/ru/core/10-mode-selection.md b/skills/svg-sprites/src/ru/core/10-mode-selection.md index bd35b6e..1791654 100644 --- a/skills/svg-sprites/src/ru/core/10-mode-selection.md +++ b/skills/svg-sprites/src/ru/core/10-mode-selection.md @@ -21,10 +21,10 @@ CLI принимает ровно один путь. Путь к файлу `.ts ```json { "scripts": { - "sprite:": "svg-sprites ", - "sprite::cli": "svg-sprites --mode " + "sprite:": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites ", + "sprite::cli": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites --mode " } } ``` -Не используй неполные `react`, `next@app`, `next@pages`, `standalone@` или удалённый `legacy`. Bare `standalone` выбирай только когда приложение само публикует SVG; для Vite/Webpack используй соответствующий полный key. Для нескольких спрайтов создай отдельную команду для каждого config-файла или каталога. +Генерация через `npx` не добавляет package в проект. В CI укажи точную версию вместо `latest`. Не используй неполные `react`, `next@app`, `next@pages`, `standalone@` или удалённый `legacy`. Bare `standalone` выбирай только когда приложение само публикует SVG; для Vite/Webpack используй соответствующий полный key. Для нескольких спрайтов создай отдельную команду для каждого config-файла или каталога. diff --git a/skills/svg-sprites/src/ru/core/20-project-inspection.md b/skills/svg-sprites/src/ru/core/20-project-inspection.md index 309b129..9cfaecd 100644 --- a/skills/svg-sprites/src/ru/core/20-project-inspection.md +++ b/skills/svg-sprites/src/ru/core/20-project-inspection.md @@ -9,14 +9,14 @@ 5. Для нового спрайта выбери целевой каталог, не навязывая конкретный слой или архитектуру приложения. 6. Проверь TypeScript и alias-настройки. Для package subpath exports нужен TypeScript 5+ с `moduleResolution: 'bundler'`, `'node16'` или `'nodenext'`. -Все пути конфига считаются относительно каталога, содержащего явно переданный config-файл; в config-less режиме — относительно переданного каталога: +Все input-пути считаются относительно каталога, содержащего явно переданный config-файл; в config-less режиме — относительно переданного каталога. Проверяй `input` как единый контракт: -- `inputFolder` по умолчанию равен `./icons`; -- `inputFiles` содержит дополнительные относительные пути к отдельным SVG и объединяется с `inputFolder`; -- одинаковый абсолютный файл дедуплицируется, но разные файлы с одинаковым basename иконки вызывают ошибку; -- сканирование `inputFolder` плоское: читаются только непосредственные файлы с окончанием `.svg`, вложенные каталоги не обходятся; -- отсутствующая явно заданная `inputFolder` является ошибкой; -- если `inputFolder` не задана, `./icons` отсутствует, а `inputFiles` непустой, генерация работает только по `inputFiles`; -- пустой итоговый набор, отсутствующий файл или путь не к `.svg` является ошибкой. +- `input?: string | string[]` по умолчанию равен `./icons`; +- каждая строка задаёт папку, точный SVG-файл или glob; +- папка сканируется плоско; вложенные файлы включаются только явным recursive glob, например `./icons/**/*.svg`; +- массив объединяет positive-источники, а элемент с префиксом `!` исключает свои совпадения из общего набора; +- каждый positive-источник должен разрешаться хотя бы в один SVG, поэтому отсутствующая или пустая папка, glob без совпадений, отсутствующий файл или точный путь не к SVG являются ошибкой; +- разрешённые файлы дедуплицируются и детерминированно сортируются; +- разные файлы с одинаковым basename конфликтуют, даже если получены из разных источников. -Не копируй общий SVG в несколько папок: добавь его относительный путь в `inputFiles` каждого нужного спрайта. Если требуется рекурсивная структура, перечисли файлы явно или измени структуру источников; генератор не выполняет recursive scan. +Не копируй общий SVG в несколько папок: добавь его точный путь или подходящий glob в `input` каждого нужного спрайта. Используй `**/*.svg` только для намеренного рекурсивного включения. diff --git a/skills/svg-sprites/src/ru/core/30-react-next-setup.md b/skills/svg-sprites/src/ru/core/30-react-next-setup.md index 8214bad..30b1714 100644 --- a/skills/svg-sprites/src/ru/core/30-react-next-setup.md +++ b/skills/svg-sprites/src/ru/core/30-react-next-setup.md @@ -12,27 +12,21 @@ src/ui/file-manager/svg-sprite/ Один `svg-sprite.config.ts` создаёт один независимый спрайт. Для нескольких наборов выбери несколько каталогов и дай каждому уникальное `name`. -Установи пакет как development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -Используй config helper для autocomplete и проверки типов: +Генератор не нужно устанавливать в проект. Начни с plain config без package +import: ```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ +export default { mode: 'react@vite', name: 'file-manager', description: 'Иконки файлового менеджера', - inputFolder: './icons', - inputFiles: ['../../shared/icons/close.svg'], -}) + input: ['./icons', '../../shared/icons/close.svg'], +} ``` -Контракт объекта одинаков для React и Next.js; отличается полный `mode`. +`input` принимает одну папку, точный SVG-файл или glob либо массив, объединяющий эти источники. Элемент массива с префиксом `!` исключает совпадения. Папки сканируются плоско; для рекурсии нужен явный glob `**/*.svg`. Если `input` не задан, используется `./icons`. Все пути считаются от каталога конфига, и каждый positive-источник должен найти хотя бы один SVG. + +Контракт объекта одинаков для React и Next.js; отличается полный `mode`. Устанавливай package только для необязательного Viewer, программного API или package-типизации config. В exact guides также есть локальный copy-paste type для проектов без package. `name` должен начинаться с латинской буквы и записываться в kebab-case; из примера `file-manager` будут созданы `FileManagerIcon`, `FileManagerIconName` и `fileManagerIconNames`. Другой спрайт получает собственные имена. Если `name` не задан, генератор выводит его из каталога. @@ -41,7 +35,7 @@ export default defineSpriteConfig({ ```json { "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", + "sprite:file-manager": "npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", "sprites": "npm run sprite:file-manager" } } @@ -49,9 +43,18 @@ export default defineSpriteConfig({ Для Next.js укажи в config полный ключ, например `next@app/turbopack`. Для нескольких спрайтов добавь по команде `sprite:` на каждый config-файл и последовательно вызови их из `sprites`. -Generated-файлы в `.svg-sprite` по умолчанию исключаются из Git, поэтому запускай `sprites` до процессов, которым нужны компонент, типы или asset. Если проект импортирует корень sprite-модуля, создай пользовательский `index.ts` с `export * from './.svg-sprite'`. Добавь генерацию к `predev`, `prebuild` и, если есть `typecheck`, к `pretypecheck`. +Чтобы задать источники через CLI, повторяй `--input `; значения образуют тот же массивный контракт, включая исключения с `!`: -Если lifecycle script отсутствует, создай его. Если он уже существует, сохрани его команду и допиши генерацию через `&&`, например преобразуй `"prebuild": "npm run lint"` в `"prebuild": "npm run lint && npm run sprites"`. Никогда не заменяй существующий `pre*` одной генерацией и не создавай второй одноимённый JSON key. +```bash +npx --yes --package=@gromlab/svg-sprites@latest svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts \ + --input ./icons \ + --input '../../shared/icons/**/*.svg' \ + --input '!../../shared/icons/legacy-*.svg' +``` + +Generated-файлы в `.svg-sprite` по умолчанию исключаются из Git, поэтому запускай `sprites` до процессов, которым нужны компонент, типы или asset. Если проект импортирует корень sprite-модуля, создай пользовательский `index.ts` с `export * from './.svg-sprite'`. Generated declarations self-contained и не импортируют generator package. + +Запускай генерацию либо через `predev`/`prebuild`/`pretypecheck`, либо явно внутри соответствующих команд. Не используй обе формы для одной команды, иначе генерация выполнится дважды. Сохраняй существующие команды и не создавай второй одноимённый JSON key. Запусти первую генерацию вручную: diff --git a/skills/svg-sprites/src/ru/core/40-generated-contract.md b/skills/svg-sprites/src/ru/core/40-generated-contract.md index 13fa6a1..5561bfe 100644 --- a/skills/svg-sprites/src/ru/core/40-generated-contract.md +++ b/skills/svg-sprites/src/ru/core/40-generated-contract.md @@ -9,7 +9,6 @@ svg-sprite/ ├── index.ts # необязательный пользовательский barrel ├── .gitignore # управляет генератор └── .svg-sprite/ - ├── state.json # реестр владения и версия контракта ├── index.js ├── index.d.ts ├── icon-data.js @@ -25,23 +24,25 @@ svg-sprite/ Standalone не создаёт `react/`. Bare `standalone` генерирует `sprite.svg` и `svg-sprite.manifest.json`; `standalone@vite`/`standalone@webpack` дополнительно -генерируют `index.*`, `icon-data.*` и resolved manifest. +генерируют `index.*`, `icon-data.*` и resolved manifest. Их `index.*` также +содержит нативный generated Web Component; bare `standalone` не получает JS runtime, +не создаёт и не изменяет `.gitignore`. -Редактируй исходные SVG, config-файл и пользовательский `index.ts`. Не изменяй вручную `.gitignore` и содержимое `.svg-sprite`: повторная генерация их перезапишет. Для импорта из корня sprite-модуля создай barrel: +Редактируй исходные SVG, config-файл и пользовательский `index.ts`. Не изменяй вручную содержимое `.svg-sprite`: повторная генерация его перезапишет. Во всех modes, кроме bare `standalone`, generated `.gitignore` также находится под управлением генератора. Для импорта из корня sprite-модуля создай barrel: ```ts export * from './.svg-sprite' ``` -Генератор владеет `.gitignore` и файлами внутри `.svg-sprite`. Реестр `state.json` позволяет удалить устаревший generated-файл, но writer откажется перезаписывать или удалять файл без generated-маркера. Не удаляй маркер, не обходи отказ и не подменяй generated-пути symlink: перенеси пользовательский файл или выбери другой каталог. +Генератор полностью владеет каталогом `.svg-sprite` и заменяет его при каждом запуске. Никогда не помещай туда пользовательские файлы. Генератор также владеет `.gitignore`, когда выбранный mode его создаёт; bare `standalone` оставляет существующий `.gitignore` без изменений. Generated-пути не должны содержать symlink. -Внутренний `index.js` экспортирует компонент из `react/react-component.js` и readonly-массив имён; соседний `index.d.ts` добавляет props/style-типы и union имени. Manifest содержит mode, URL, target, список и метаданные иконок для debug-инструментов и не импортируется production-компонентом. +Внутренний `index.js` экспортирует компонент из `react/react-component.js` и readonly-массив имён; соседний `index.d.ts` добавляет props/style-типы и union имени. Manifest declarations bundler modes объявляют типы локально и не импортируют generator package. Manifest содержит mode, URL, target, список и метаданные иконок для debug-инструментов и не импортируется production-компонентом. Спрайт остаётся отдельным asset с content hash; SVG path-данные не встраиваются в JavaScript: - `react@vite` генерирует статический импорт `sprite.svg?no-inline`, запрещающий Vite inline; -- `standalone@vite` использует тот же Vite asset-механизм, но экспортирует href helper без React; -- `standalone@webpack` использует Webpack Asset Modules без React; +- `standalone@vite` использует тот же Vite asset-механизм и экспортирует href helper и нативный Web Component без React; +- `standalone@webpack` использует Webpack Asset Modules и экспортирует такой же mode-local Web Component без React; - React Webpack 5 и все Next modes генерируют `new URL('./sprite.svg', import.meta.url).href`, который должен обработать Asset Modules соответствующего сборщика; - кастомный Webpack SVG loader не должен перехватывать generated `sprite.svg`; - в Next mode generated-компонент не содержит `'use client'` и работает в Server Components, SSR и SSG; не добавляй клиентскую границу только ради иконки; diff --git a/skills/svg-sprites/src/ru/core/50-usage-and-colors.md b/skills/svg-sprites/src/ru/core/50-usage-and-colors.md index 99f2974..39d712e 100644 --- a/skills/svg-sprites/src/ru/core/50-usage-and-colors.md +++ b/skills/svg-sprites/src/ru/core/50-usage-and-colors.md @@ -1,6 +1,20 @@ ## Использование, доступность и цвета -Имя компонента зависит от `name` конкретного спрайта. В примерах используется `name: 'file-manager'`, поэтому generated-компонент называется `FileManagerIcon`. Для `name: 'navigation'` используй сгенерированный `NavigationIcon`. +Имя компонента зависит от `name` конкретного спрайта. В `standalone@vite` и `standalone@webpack` значение `name: 'file-manager'` создаёт tag `` и функцию `defineFileManagerIconElement()`: + +```ts +import { defineFileManagerIconElement } from './svg-sprite' + +defineFileManagerIconElement() +``` + +```html + +``` + +Нативный элемент не имеет runtime-зависимостей, сам выбирает generated ID и `viewBox`, получает URL через bundler и рендерит `` в Shadow DOM. Его property `icon` типизирован точным union имён, но строковые HTML attributes проверяются только в runtime. Размер по умолчанию равен `1em × 1em`; меняй его через CSS на host. Bare `standalone` Web Component не генерирует. + +В React/Next.js тот же `name: 'file-manager'` создаёт React-компонент `FileManagerIcon`. Для `name: 'navigation'` используй сгенерированный `NavigationIcon`. Импортируй компонент из корня соответствующего каталога спрайта. `width` и `height` не обязательны: размером можно управлять обычным CSS-классом. @@ -64,10 +78,10 @@ Generated-компонент не выбирает семантику за пр Автозамена рассчитана на `fill`/`stroke` attributes и inline `style`. Значения `none`, `transparent`, `inherit`, `unset`, `initial` не заменяются. CSS-классы и внешние stylesheets, gradients, patterns, filters и `url(#...)` проверяй на реальном результате. Переменные страницы работают через ``, но не проникают во внешний документ при `` или `background-image`; CSS mask оставляет только одноцветный силуэт. -`SpriteViewer` необязателен. Подключай его из `@gromlab/svg-sprites/react` только на debug-маршруте: +`SpriteViewer` необязателен. Установи `@gromlab/svg-sprites` как development dependency, только если проекту нужен Viewer, и подключай его из `@gromlab/svg-sprites/react` на debug-маршруте: - в Vite передай результат строкового literal `import.meta.glob('/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js')`; -- в Webpack передай массив статических `() => import('.../manifest')`; +- в Webpack передай массив статических `() => import('.../.svg-sprite/svg-sprite.manifest.js')`; - в Next.js используй такие же статические loaders, а для App Router помести Viewer в отдельный файл с `'use client'`. -Viewer принимает manifests/loaders, показывает поиск, темы, цвета и примеры, но production-компоненты от него не зависят. Импортируй его из пакета, уже установленного как development dependency. +Viewer принимает manifests/loaders, показывает поиск, темы, цвета и примеры, но production-компоненты от него не зависят. diff --git a/skills/svg-sprites/src/ru/core/60-verification.md b/skills/svg-sprites/src/ru/core/60-verification.md index 82d7d65..a79e050 100644 --- a/skills/svg-sprites/src/ru/core/60-verification.md +++ b/skills/svg-sprites/src/ru/core/60-verification.md @@ -3,7 +3,7 @@ После изменения конфига или SVG выполни обязательные быстрые проверки: 1. Запусти точную sprite-команду, например `npm run sprite:file-manager`; процесс должен завершиться с кодом `0` и сообщить имя, число иконок, mode и каталог `.svg-sprite`. -2. Проверь наличие `.svg-sprite/index.js`, `.svg-sprite/index.d.ts`, `sprite.svg`, пары `icon-data.js`/`.d.ts`, manifest `.js`/`.d.ts`, `react/react-component.js`, его `.d.ts` и CSS Module, а также `state.json`. +2. Проверь наличие `.svg-sprite/index.js`, `.svg-sprite/index.d.ts`, `sprite.svg`, пары `icon-data.js`/`.d.ts`, manifest `.js`/`.d.ts`, `react/react-component.js`, его `.d.ts` и CSS Module. 3. Убедись, что новая иконка присутствует в readonly-массиве имён и принимается prop `icon`. 4. Запусти существующую проверку типов проекта, например `npm run typecheck`. 5. Проверь в `.svg-sprite/svg-sprite.manifest.js`, что `target` совпадает с выбранным mode key; generated asset expression должен быть `?no-inline` для Vite и `new URL(...)` для Webpack/Next. diff --git a/skills/svg-sprites/src/ru/core/70-diagnostics.md b/skills/svg-sprites/src/ru/core/70-diagnostics.md index 88350a4..e29d895 100644 --- a/skills/svg-sprites/src/ru/core/70-diagnostics.md +++ b/skills/svg-sprites/src/ru/core/70-diagnostics.md @@ -8,9 +8,10 @@ | `Expected one config file or module directory` | Передано несколько путей | Создай отдельную команду на каждый спрайт и объедини scripts. | | `Sprite mode is required` | Mode отсутствует и в config, и в CLI | Добавь `mode` в объект или передай полный `--mode`. | | `Unsupported sprite config extension` | Передан файл не `.ts`, `.js` или `.json` | Используй поддерживаемый формат config-файла. | -| `Input directory does not exist` | Ошибка относительного пути или отсутствует явно заданная папка | Считай путь от каталога конфига; создай папку или исправь `inputFolder`. | -| Иконки из подпапки не появились | Ожидался recursive scan | Перемести SVG на верхний уровень `inputFolder` или перечисли его в `inputFiles`. | -| `SVG file does not exist`, `File is not an SVG` или пустой набор | Неверный `inputFiles`, расширение или источники | Исправь путь/расширение и обеспечь хотя бы один входной SVG. | +| Positive input-источник не нашёл SVG | Папка отсутствует или пуста, glob не совпал либо точный путь отсутствует или ведёт не к SVG | Разреши источник от каталога конфига и исправь `input`; каждый positive-элемент должен дать хотя бы один SVG. | +| Иконки из подпапки не появились | От папки ожидалось рекурсивное сканирование | Используй явный glob, например `./icons/**/*.svg`; папки сканируются плоско. | +| Исключённая иконка всё ещё присутствует | У исключения нет префикса `!`, оно находится не в массиве `input` или считается не от того каталога | Добавь совпадающий `!`-элемент и считай его от каталога конфига. | +| CLI выбрал не все источники | Несколько источников поместили в одно значение `--input` или пропустили option | Повтори `--input ` отдельно для каждого источника или исключения. | | Конфликт имени иконки или SVG ID | Два разных файла имеют одинаковый basename либо hash-ID столкнулся с именем | Переименуй один исходный SVG; не выбирай файл неявно. | | `Refusing to overwrite/delete a user file` | Пользовательский файл занял managed-путь или потерял marker | Не обходи защиту: перенеси файл либо выбери другой sprite-каталог и перегенерируй. | | Нет `.svg-sprite/index.js` или имя отсутствует в autocomplete | Генерация не запускалась после изменения, пользовательский barrel не экспортирует `.svg-sprite` либо type server держит старый модуль | Запусти sprite-команду, проверь `export * from './.svg-sprite'`, затем typecheck; при необходимости перезапусти TypeScript server. | diff --git a/skills/svg-sprites/src/ru/references/complex-svg.md b/skills/svg-sprites/src/ru/references/complex-svg.md index acc304b..d1070c0 100644 --- a/skills/svg-sprites/src/ru/references/complex-svg.md +++ b/skills/svg-sprites/src/ru/references/complex-svg.md @@ -131,7 +131,7 @@ transform: { Не создавай вручную `#folder open`. Используй generated component либо `.svg-sprite/svg-sprite.manifest.js`, где записаны `name` и фактический `id`. -Разные файлы с одинаковым basename запрещены даже из разных directories. Переименуй один source осмысленно; порядок `inputFiles` не является способом выбрать победителя. +Разные файлы с одинаковым basename запрещены даже из разных directories. Переименуй один source осмысленно; порядок или пересечение источников не выбирают победителя. ## Способ отображения @@ -173,4 +173,4 @@ External stack fragment support и поведение paint servers могут - Ручной fragment не работает для имени с пробелом: используй ID из manifest. - Один сложный icon требует иных transforms: вынеси его в отдельный sprite; per-icon transform config отсутствует. -Target-specific запуск и проверка описаны в [react-vite.md](react-vite.md), [react-webpack.md](react-webpack.md), [next-app.md](next-app.md) и [next-pages.md](next-pages.md). +Target-specific запуск и проверка описаны в exact-mode файлах каталога [guides](guides/standalone.md). diff --git a/skills/svg-sprites/src/ru/references/next-app.md b/skills/svg-sprites/src/ru/references/next-app.md deleted file mode 100644 index 5f97928..0000000 --- a/skills/svg-sprites/src/ru/references/next-app.md +++ /dev/null @@ -1,149 +0,0 @@ -# Next.js App Router: операционный reference - -## Когда открывать - -Открывай этот документ, если приложение использует каталог `app/` или `src/app/` и нужен generated-компонент для Server Components, SSR или SSG. Сначала установи реальный сборщик: target для Turbopack и Webpack различается. Для `pages/` используй [next-pages.md](next-pages.md). - -## Матрица target - -| Сборщик | CLI mode | -|---|---| -| Turbopack | `next@app/turbopack` | -| Webpack 5 | `next@app/webpack` | - -Не выводи target только из наличия `next.config.*`. Проверь фактические flags в `dev`/`build`. Mode генератора и сборщик контрольной сборки должны совпадать. - -## Подготовка каталога спрайта - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -Установи пакет как development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@app/turbopack', - name: 'file-manager', - description: 'Иконки файлового менеджера', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -- Каталог выбирает проект конкретного спрайта и не обязан быть module/feature-каталогом. Каждый config описывает один из потенциально многих спрайтов приложения. -- `svg-sprite.config.ts` — рекомендуемое имя; CLI принимает явно переданный `.ts`, `.js` или `.json` файл с любым именем. -- Пути считаются от его каталога; `inputFolder` сканируется нерекурсивно. -- Локальная папка и `inputFiles` объединяются. Одинаковые basename разных файлов запрещены. -- Если имя не задано, оно выводится из каталога: для папки `svg-sprite` берётся имя родительской папки. Явное `name` надёжнее при перемещениях. -- `name` должен быть kebab-case и начинаться с буквы. -- `FileManagerIcon` ниже — только пример generated-имени из `name: 'file-manager'`. -- Next preset всегда генерирует `stack` и включает root `viewBox` спрайта; это не пользовательская настройка. - -## Генерация - -Пример scripts для Turbopack: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Для Webpack замени только mode целиком на `next@app/webpack`. Не запускай два target последовательно для одного каталога: второй перезапишет generated target первого. Для первой генерации запусти `npm run sprite:file-manager`. - -## Server Component - -Generated production component не содержит `'use client'`. Импортируй его непосредственно в server `page.tsx` или `layout.tsx`: - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function Page() { - return ( -
- -
- ) -} -``` - -`width` и `height` в JSX необязательны: размер можно задать CSS-классом или через `wrapped`. Не добавляй Client Component boundary только ради иконки. Generated module формирует asset URL через статический `new URL('./sprite.svg', import.meta.url).href`; один механизм используется при SSR и в браузере. - -Не перемещай SVG в `public`. Генератор перезаписывает `.svg-sprite` и `.gitignore`; корневой `index.ts` принадлежит приложению и может содержать `export * from './.svg-sprite'`. - -## SpriteViewer - -Viewer интерактивен и импортируется из client-only entry. Создай отдельную Client Component страницу или дочерний компонент: - -```tsx -'use client' - -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/manifest'), - () => import('@/ui/navigation/svg-sprite/manifest'), -] - -export default function SpritesPage() { - return -} -``` - -Пути `import()` должны быть строковыми литералами. Оставь production icon imports за пределами `@gromlab/svg-sprites/react`: package React entry предназначен только для Viewer и содержит `'use client'`. - -## Проверка - -Сначала генерация и типы: - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -Если менялись target или Next build/deployment pipeline либо диагностируется runtime, запусти production-сборку проекта, настроенную на выбранный сборщик: - -```bash -npm run build -``` - -После обязательных генерации и typecheck проверь: - -- `.svg-sprite/svg-sprite.manifest.js` содержит точный target `next@app/turbopack` либо `next@app/webpack`; -- server page компилируется без добавления `'use client'`; - -При условной production/runtime-проверке проверь также: - -- HTML содержит `href` на `.svg#id`, а URL asset доступен после `next start`; -- URL не использует `data:`, `file:` или `blob:`; -- SSR markup и hydrated page ссылаются на один asset; -- сложные SVG исследованы по [complex-svg.md](complex-svg.md); визуальный и a11y результат утверждай только при наличии соответствующих инструментов. - -## Типовые ошибки - -- `Next.js mode requires a router and bundler`: нельзя использовать `--mode next`; укажи полный mode. -- Сборка проходит на одном bundler, а runtime asset ломается на другом: перегенерируй тем target, которым реально выполняется build. -- Выбран Webpack, но build ушёл в Turbopack: используй Webpack-команду проекта и `next@app/webpack`. -- Viewer вызывает ошибку Server Component: файл с Viewer должен иметь `'use client'`; generated icon component этого не требует. -- Config не найден: передай полный путь к существующему `.ts`, `.js` или `.json` config-файлу. -- Две CI jobs генерируют разные target в одном checkout: раздели каталоги или обеспечь один согласованный target на job. -- Asset не найден под `basePath`/CDN: проверяй Next asset handling и deployment config, не подменяй generated URL вручную. -- Ошибка package subpath types: используй TypeScript 5+ и `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. - -Для вызова генератора из build script открой [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/ru/references/next-pages.md b/skills/svg-sprites/src/ru/references/next-pages.md deleted file mode 100644 index d305cda..0000000 --- a/skills/svg-sprites/src/ru/references/next-pages.md +++ /dev/null @@ -1,138 +0,0 @@ -# Next.js Pages Router: операционный reference - -## Когда открывать - -Открывай этот документ для страниц в `pages/` или `src/pages/`, включая SSR через `getServerSideProps`, SSG и клиентские переходы. Если маршрут находится в `app/`, используй [next-app.md](next-app.md). - -## Выбор target - -| Сборщик | CLI mode | -|---|---| -| Turbopack | `next@pages/turbopack` | -| Webpack 5 | `next@pages/webpack` | - -Определи реальные flags scripts до выбора mode. Наличие Pages Router не означает автоматически Webpack: выбирай target по сборщику проекта. - -## Структура и конфиг - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -Установи пакет как development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'next@pages/webpack', - name: 'file-manager', - description: 'Иконки файлового менеджера', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -Каталог с config выбирает проект конкретного спрайта и не обязан быть module/feature-каталогом; каждый config описывает один из потенциально многих спрайтов приложения. - -Все source paths разрешаются от каталога конфига. `inputFolder` по умолчанию `./icons`, сканирование не рекурсивно. `inputFiles` объединяется с папкой. Один и тот же путь дедуплицируется, но два разных `check.svg` конфликтуют. Явная отсутствующая папка считается ошибкой даже при заполненном `inputFiles`. - -`name` задаётся в kebab-case и определяет публичные имена. `FileManagerIcon` ниже — только пример generated-имени из `name: 'file-manager'`. Next modes всегда создают `stack`, не `symbol`. - -## Команды - -Пример lifecycle для Webpack: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Если pre-scripts уже существуют, добавь генерацию в текущую цепочку. Generated imports отсутствуют в Git, поэтому генерация должна предшествовать TypeScript и Next compilation. Для Turbopack замени mode целиком на `next@pages/turbopack`. Для первой генерации запусти `npm run sprite:file-manager`. - -## Использование в Pages Router - -```tsx -import { FileManagerIcon } from '@/ui/file-manager/svg-sprite' - -export default function FilesPage() { - return -} - -export function getServerSideProps() { - return { props: {} } -} -``` - -`width` и `height` в JSX необязательны: размер можно задать CSS-классом или через `wrapped`. Компонент работает при SSR, SSG и client navigation. Он использует `new URL('./sprite.svg', import.meta.url).href`, чтобы Next выпустил внешний hashed asset. Не копируй generated SVG в `public` и не конструируй URL вручную. - -Импортируй компонент и типы из локального пользовательского `svg-sprite/index.ts`, который переэкспортирует `.svg-sprite`. Не редактируй `.svg-sprite` и созданный `.gitignore`. - -## SpriteViewer - -Pages Router компоненты выполняются и на клиенте, поэтому отдельная директива `'use client'` не нужна: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('@/ui/file-manager/svg-sprite/manifest'), - () => import('@/ui/navigation/svg-sprite/manifest'), -] - -export default function SpritesPage() { - return -} -``` - -Используй literal imports и размещай страницу только как debug/internal tool. Если page участвует в production routing, ограничь доступ средствами самого приложения. - -## Проверка - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -Если менялись target или Next build/deployment pipeline либо диагностируется runtime, запусти production build проекта, настроенный на выбранный сборщик: - -```bash -npm run build -``` - -После обязательных генерации и typecheck проверь: - -- `.svg-sprite/svg-sprite.manifest.js` содержит `next@pages/turbopack` либо `next@pages/webpack`; -- `getServerSideProps`/`getStaticProps` не импортируют package React Viewer entry. - -После условного production build и только при наличии браузерных инструментов проверь SSR route и переход на него: - -- initial HTML содержит `.svg#id`; -- SVG URL отвечает успешно и не является `data:`, `file:` или `blob:`; -- после client navigation иконка сохраняет тот же корректный URL; -- gradients, masks и цвета исследованы по [complex-svg.md](complex-svg.md); не утверждай визуальный или a11y результат без доступных инструментов. - -## Типовые ошибки - -- В mode указан `next@app/...`: модуль может сгенерироваться, но manifest и контракт target неверны; используй `next@pages/...`. -- Command и mode выбирают разные bundler: используй соответствующую build-команду проекта и перегенерируй. -- Viewer manifest не попадает в chunk: путь `import()` должен быть строковым литералом и существовать до Next build. -- Иконка есть после full reload, но пропадает при переходе: проверь доступность внешнего asset с `basePath`, `assetPrefix` и production origin. -- `Refusing to overwrite a user file`: sprite-каталог содержит неуправляемый файл с зарезервированным именем; перенеси его. -- Имя иконки отсутствует в типе: папка не сканируется рекурсивно; перемести SVG на первый уровень либо добавь точный путь в `inputFiles`. - -Для программной генерации используй [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/ru/references/programmatic-api.md b/skills/svg-sprites/src/ru/references/programmatic-api.md index 93a9686..e6b1b7f 100644 --- a/skills/svg-sprites/src/ru/references/programmatic-api.md +++ b/skills/svg-sprites/src/ru/references/programmatic-api.md @@ -18,23 +18,28 @@ await generateSprite('src/ui/icons/svg-sprite.config.ts') await generateSprite('src/ui/icons', { mode: 'react@vite', name: 'app', - inputFolder: './icons', + input: './icons', }) ``` Каталог включает config-less режим. После объединения настроек `mode` обязателен. +`input?: string | string[]` по умолчанию равен `./icons`. Каждое значение задаёт папку, точный SVG-файл или glob и считается от каталога конфига либо config-less source-каталога. Папка сканируется плоско; рекурсия включается только явным glob, например `./icons/**/*.svg`. Массив объединяет источники, а элементы с префиксом `!` исключают совпадения. Каждый positive-элемент должен разрешаться хотя бы в один SVG. Итоговые файлы дедуплицируются и сортируются, а разные файлы с одинаковым basename вызывают ошибку. + ## Overrides ```ts await generateSprite('src/ui/icons/custom.json', { mode: 'react@webpack', - inputFiles: ['../../shared/search.svg'], + input: [ + '../../shared/icons/**/*.svg', + '!../../shared/icons/legacy-*.svg', + ], transform: { addTransition: false }, }) ``` -Порядок: `defaults → config → API overrides`. `transform` объединяется по отдельным полям; переданный `inputFiles` заменяет массив из config. +Порядок: `defaults → config → API overrides`. `transform` объединяется по отдельным полям; переданный `input` заменяет значение из config. Специализированные `generateReactSprite` и `generateNextSprite` оставлены как совместимые обёртки, но для нового кода предпочитай `generateSprite`. diff --git a/skills/svg-sprites/src/ru/references/react-vite.md b/skills/svg-sprites/src/ru/references/react-vite.md deleted file mode 100644 index 60812d9..0000000 --- a/skills/svg-sprites/src/ru/references/react-vite.md +++ /dev/null @@ -1,166 +0,0 @@ -# React с Vite: операционный reference - -## Когда открывать - -Открывай этот документ, если проект использует React без Next.js и собирается Vite, либо если generated-компонент содержит импорт `sprite.svg?no-inline`. Не применяй этот target к Webpack: для него открой [react-webpack.md](react-webpack.md). - -## Сначала установи контекст - -1. Проверь `package.json`: должны быть React, Vite и фактические команды `dev`, `build`, `typecheck`. -2. Найди существующие `svg-sprite.config.ts` и scripts с `svg-sprites`. Не создавай второй каталог для уже существующего спрайта. -3. Выбери целевой каталог для конкретного спрайта. Это не обязан быть каталог module или feature: генератор принимает каталог с config, а не путь к самому config или `icons/`. -4. Не редактируй вручную `.svg-sprite` и локальный `.gitignore`: ими владеет генератор. Корневой `index.ts` принадлежит приложению. - -Минимальная структура: - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -## Настройка - -Установи пакет как development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -`src/ui/file-manager/svg-sprite/svg-sprite.config.ts`: - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@vite', - name: 'file-manager', - description: 'Иконки файлового менеджера', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -- Каждый такой config описывает один конкретный спрайт; в приложении может быть много независимых config и спрайтов. -- Все пути в конфиге разрешаются относительно каталога `svg-sprite.config.ts`. -- `inputFolder` по умолчанию равен `./icons`; сканирование папки не рекурсивно и включает файлы с окончанием `.svg`. -- `inputFolder` и `inputFiles` объединяются, одинаковый абсолютный путь дедуплицируется. -- Если `inputFiles` заполнен, а неявного `./icons` нет, генерация работает только по списку. Явно заданная отсутствующая `inputFolder` всегда является ошибкой. -- Разные файлы с одинаковым basename, например два `check.svg`, конфликтуют как одно публичное имя иконки. -- `name` должен быть kebab-case и начинаться с латинской буквы. `FileManagerIcon`, `FileManagerIconName` и `fileManagerIconNames` ниже — только пример generated-имён для `name: 'file-manager'`. -- React preset всегда создаёт формат `stack`; выбрать `symbol` здесь нельзя. - -## Команда и scripts - -Добавь локальный CLI в `package.json` и запускай до процессов, которым нужны generated imports: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Если в проекте уже есть `predev` или `prebuild`, объедини команды в существующем orchestration вместо перезаписи script. - -Для первой генерации запусти `npm run sprite:file-manager`. - -## Использование - -Импортируй только публичный локальный entry: - -```tsx -import { FileManagerIcon, fileManagerIconNames } from './svg-sprite' -import type { FileManagerIconName, FileManagerIconStyle } from './svg-sprite' - -const colorStyle: FileManagerIconStyle = { - '--icon-color-1': '#2563eb', -} - -export function FolderIcon({ icon }: { icon: FileManagerIconName }) { - return -} - -export const availableIcons = fileManagerIconNames -``` - -`width` и `height` в JSX необязательны: размер можно задать CSS-классом. Без `wrapped` компонент рендерит `` и принимает SVG attributes. С `wrapped={true}` корнем становится ``, а внутренний SVG занимает его ширину и высоту: - -```tsx - -``` - -Для обычного импорта используй пользовательский barrel с `export * from './.svg-sprite'`; остальные файлы скрытого каталога считаются внутренними. - -## Нюанс target - -Vite target генерирует статический импорт: - -```ts -import spriteUrl from './sprite.svg?no-inline' -``` - -Query `?no-inline` обязателен: он не даёт Vite превратить небольшой SVG в data URL. Не удаляй query и не копируй generated SVG в `public`; Vite должен выпустить отдельный asset с content hash. - -Для низкоуровневого `` применяй тот же механизм: - -```tsx -import spriteUrl from './svg-sprite/.svg-sprite/sprite.svg?no-inline' - - - - -``` - -Ручной fragment `#check` безопасен только для имён вида `^[a-zA-Z][a-zA-Z0-9_-]*$`. Для пробелов и других символов generated-компонент использует стабильный hash ID; точный ID находится в `.svg-sprite/svg-sprite.manifest.js`. - -## SpriteViewer - -После генерации добавь Viewer только на debug-маршрут: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' -import type { SpriteManifestModule } from '@gromlab/svg-sprites/react' - -const sources = import.meta.glob( - '/src/**/svg-sprite/.svg-sprite/svg-sprite.manifest.js', -) - -export function IconsDebugPage() { - return -} -``` - -Аргумент `import.meta.glob` должен оставаться строковым литералом. Генерация обязана завершиться до старта Vite, иначе новые manifests не попадут в glob. - -## Проверка результата - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -Это быстрые обязательные проверки. Проверь generated-файлы статически: - -- пользовательский `index.ts` переэкспортирует компонент, props, style, union имени и runtime-массив из `.svg-sprite`; -- `.svg-sprite/svg-sprite.manifest.js` содержит `target: "vite"`, `format: "stack"` и ожидаемое число иконок; - -Если менялись target, asset pipeline или диагностируется runtime, дополнительно запусти production build. При наличии браузерных инструментов проверь Network: отдельный `.svg` asset вместо `data:image/svg+xml`, успешный URL и ``. Для сложных цветов, `defs` и размеров следуй [complex-svg.md](complex-svg.md), не утверждая визуальный или a11y результат без доступных инструментов. - -## Типовые ошибки - -- Config не найден: передай полный путь к существующему `.ts`, `.js` или `.json` config-файлу. -- `Sprite mode is required`: добавь `mode: 'react@vite'` в config либо передай `--mode react@vite`. -- Иконки нет в autocomplete: проверь case-sensitive окончание `.svg`, нерекурсивное расположение и повторно запусти генерацию до typecheck. -- `Refusing to overwrite a user file`: не удаляй marker и не обходи writer; перенеси пользовательский файл или выбери другой sprite-каталог. -- Viewer пуст: проверь строковый glob, существование `.svg-sprite/svg-sprite.manifest.js` и порядок запуска `predev`. -- SVG оказался inline: проверь, что модуль сгенерирован target `vite` и импорт сохранил `?no-inline`. -- TypeScript не разрешает package subpath: используй TypeScript 5+ и `moduleResolution: "bundler"`, `"node16"` или `"nodenext"`. - -Для запуска без CLI используй [programmatic-api.md](programmatic-api.md). diff --git a/skills/svg-sprites/src/ru/references/react-webpack.md b/skills/svg-sprites/src/ru/references/react-webpack.md deleted file mode 100644 index 601b1c2..0000000 --- a/skills/svg-sprites/src/ru/references/react-webpack.md +++ /dev/null @@ -1,152 +0,0 @@ -# React с Webpack 5: операционный reference - -## Когда открывать - -Открывай этот документ для React-приложения на Webpack 5, когда SVG должен проходить через Asset Modules. Если проект собирается Vite, используй [react-vite.md](react-vite.md). Для Next.js не выбирай `react@webpack`: router-specific targets описаны в [next-app.md](next-app.md) и [next-pages.md](next-pages.md). - -## Диагностика окружения до изменений - -1. Подтверди Webpack major version 5 в `package.json` или lockfile. -2. Изучи `module.rules` для `.svg`, `output.publicPath`, dev-server и существующие asset conventions. -3. Найди существующие `svg-sprite.config.ts` и scripts генерации. -4. Выбери каталог проекта конкретного спрайта. Он не обязан совпадать с module или feature-каталогом. Команда принимает каталог с config, не файл config и не папку иконок. - -Минимальная структура: - -```text -src/ui/file-manager/svg-sprite/ -├── icons/ -│ ├── check.svg -│ └── folder.svg -└── svg-sprite.config.ts -``` - -## Конфигурация и генерация - -Установи пакет как development dependency: - -```bash -npm install --save-dev @gromlab/svg-sprites -``` - -```ts -import { defineSpriteConfig } from '@gromlab/svg-sprites' - -export default defineSpriteConfig({ - mode: 'react@webpack', - name: 'file-manager', - description: 'Иконки файлового менеджера', - inputFolder: './icons', - inputFiles: ['../../../../shared/icons/check.svg'], -}) -``` - -Каждый config описывает один из потенциально многих независимых спрайтов приложения. - -Пути считаются от `svg-sprite.config.ts`. Папка сканируется только на первом уровне. `inputFolder` и `inputFiles` объединяются; одинаковый путь дедуплицируется, но одинаковые basename у разных файлов вызывают конфликт ID. Неявный `./icons` можно не создавать, если заполнен `inputFiles`; явно заданная отсутствующая папка является ошибкой. `FileManagerIcon` ниже — только пример generated-имени из `name: 'file-manager'`. - -Рекомендуемые lifecycle hooks: - -```json -{ - "scripts": { - "sprite:file-manager": "svg-sprites src/ui/file-manager/svg-sprite/svg-sprite.config.ts", - "predev": "npm run sprite:file-manager", - "prebuild": "npm run sprite:file-manager", - "pretypecheck": "npm run sprite:file-manager" - } -} -``` - -Не затирай уже существующие pre-scripts: включи генерацию в их текущую цепочку. Для первой генерации запусти `npm run sprite:file-manager`. React preset всегда выпускает `stack`. - -## Публичный компонент - -```tsx -import { FileManagerIcon } from './svg-sprite' - -export function OpenFolderButton() { - return ( - - ) -} -``` - -`width` и `height` в JSX необязательны: размер можно задать CSS-классом или через `wrapped`. Пользовательский barrel также экспортирует `FileManagerIconProps`, `FileManagerIconStyle`, `FileManagerIconName` и `fileManagerIconNames`. Не редактируй `.svg-sprite` или созданный `.gitignore`. - -## Нюанс Webpack target - -Generated-компонент получает URL только через статическое выражение: - -```ts -const spriteUrl = new URL('./sprite.svg', import.meta.url).href -``` - -Webpack 5 распознаёт его как Asset Module и заменяет на публичный URL отдельного SVG. Для корректной обработки: - -- не оборачивай путь в переменную и не меняй generated expression; -- убедись, что Babel/TypeScript не преобразует `import.meta.url` до Webpack; -- не позволяй `@svgr/webpack`, `svg-inline-loader`, `raw-loader` или общему SVG rule перехватить `svg-sprite/.svg-sprite/sprite.svg`; -- при custom rule либо исключи generated sprite из component/raw loader, либо добавь отдельное правило `type: 'asset/resource'`; -- проверь `output.publicPath`, особенно при CDN, subpath deployment и dev-server. - -Пример отдельного правила, если существующий loader перехватывает все SVG: - -```js -{ - test: /svg-sprite[\\/]\.svg-sprite[\\/]sprite\.svg$/, - type: 'asset/resource', -} -``` - -Согласуй это правило с текущим конфигом проекта: не добавляй дублирующий matcher, если стандартные Asset Modules уже обрабатывают `new URL` правильно. - -## SpriteViewer - -Webpack не предоставляет `import.meta.glob`. Передай статические lazy imports со строковыми литералами: - -```tsx -import { SpriteViewer } from '@gromlab/svg-sprites/react' - -const sources = [ - () => import('./ui/file-manager/svg-sprite/manifest'), - () => import('./ui/navigation/svg-sprite/manifest'), -] - -export function IconsDebugPage() { - return -} -``` - -Динамически собранный путь `import(path)` не подходит: Webpack не сможет точно связать manifests и assets. Viewer должен быть доступен только на debug/internal route. - -## Проверка - -```bash -npm run sprite:file-manager -npm run typecheck -``` - -Это быстрые обязательные проверки. После генерации проверь: - -- `.svg-sprite/svg-sprite.manifest.js` содержит `target: "webpack"` и `format: "stack"`; -- generated component содержит `new URL('./sprite.svg', import.meta.url).href`; - -Production build и браузер/Network нужны дополнительно, если менялись target, Webpack asset rules, `publicPath`/deployment pipeline или диагностируется runtime. Тогда проверь отдельный hashed SVG asset, HTTP(S)-URL `.svg#id`, content type, fragment ID и корректный CDN/subpath URL. Не утверждай визуальную или a11y корректность без доступных инструментов. - -Для иконок с gradients, filters, masks или внутренним CSS выполни проверки из [complex-svg.md](complex-svg.md). - -## Типовые ошибки - -- `Unsupported React target`: программному API передано не `'webpack'`; CLI mode должен быть ровно `react@webpack`. -- Webpack пытается отрендерить SVG как React-компонент: generated sprite попал под SVGR rule; исключи его или приоритизируй `asset/resource`. -- URL ведёт на неверный host/subpath: исправь `output.publicPath` и настройки runtime deployment, не generated-файл. -- `import.meta` не поддержан: проверь, что сборка действительно Webpack 5 и промежуточный transpiler сохраняет выражение. -- Viewer не загружает manifest: проверь literal path, chunk loading и наличие генерации до компиляции. -- `Refusing to overwrite a user file`: каталог уже содержит пользовательский `.gitignore` или файл в `.svg-sprite`; перенеси его, не обходи защиту. -- Иконка не меняет цвет: используй `color` для монохромной либо `--icon-color-N` через ``; CSS страницы не проникает внутрь ``. - -Для custom build orchestration см. [programmatic-api.md](programmatic-api.md). diff --git a/src/cli/parse-args.ts b/src/cli/parse-args.ts index b74a276..c6b57be 100644 --- a/src/cli/parse-args.ts +++ b/src/cli/parse-args.ts @@ -25,8 +25,7 @@ export const CLI_USAGE = [ ' --mode ', ' --name ', ' --description ', - ' --input-folder ', - ' --input-file Repeat for multiple files', + ' --input Repeat for multiple inputs', ' --[no-]remove-size', ' --[no-]replace-colors', ' --[no-]add-transition', @@ -86,15 +85,12 @@ export function parseCliArgs(argv: string[]): CliArgs | { help: true } { index = nextIndex continue } - if (argument === '--input-folder' || argument.startsWith('--input-folder=')) { - const [value, nextIndex] = optionValue(argv, index, '--input-folder') - overrides.inputFolder = value - index = nextIndex - continue - } - if (argument === '--input-file' || argument.startsWith('--input-file=')) { - const [value, nextIndex] = optionValue(argv, index, '--input-file') - overrides.inputFiles = [...(overrides.inputFiles ?? []), value] + if (argument === '--input' || argument.startsWith('--input=')) { + const [value, nextIndex] = optionValue(argv, index, '--input') + const input = overrides.input === undefined + ? [] + : Array.isArray(overrides.input) ? overrides.input : [overrides.input] + overrides.input = [...input, value] index = nextIndex continue } diff --git a/src/config.ts b/src/config.ts index 30940b1..bbf3fc2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -10,8 +10,7 @@ const CONFIG_FIELDS = new Set([ 'mode', 'name', 'description', - 'inputFolder', - 'inputFiles', + 'input', 'transform', 'generatedNotice', ]) @@ -64,6 +63,9 @@ export function validateSpriteConfig(value: unknown): asserts value is SpriteCon if ('output' in config || 'sprites' in config || 'preview' in config) { throw configError('legacy config fields are no longer supported.') } + if ('inputFolder' in config || 'inputFiles' in config) { + throw configError('"inputFolder" and "inputFiles" are no longer supported. Use "input".') + } for (const field of Object.keys(config)) { if (!CONFIG_FIELDS.has(field)) throw configError(`unknown field "${field}".`) } @@ -76,19 +78,14 @@ export function validateSpriteConfig(value: unknown): asserts value is SpriteCon if (config.description !== undefined && typeof config.description !== 'string') { throw configError('"description" must be a string.') } - if ('icons' in config) { - throw configError('"icons" was renamed to "inputFolder".') - } - if (config.inputFolder !== undefined && ( - typeof config.inputFolder !== 'string' || config.inputFolder.trim() === '' + if (config.input !== undefined && ( + typeof config.input !== 'string' + ? !Array.isArray(config.input) + || config.input.length === 0 + || config.input.some((entry) => typeof entry !== 'string' || entry.trim() === '') + : config.input.trim() === '' )) { - throw configError('"inputFolder" must be a non-empty string.') - } - if (config.inputFiles !== undefined && ( - !Array.isArray(config.inputFiles) - || config.inputFiles.some((filePath) => typeof filePath !== 'string' || filePath.trim() === '') - )) { - throw configError('"inputFiles" must be an array of non-empty strings.') + throw configError('"input" must be a non-empty string or an array of non-empty strings.') } if (config.transform !== undefined) { if ( @@ -211,20 +208,20 @@ export function resolveSpriteConfig( const name = merged.name ?? getDefaultName(rootDir) validateSpriteName(name) - const inputFiles = (merged.inputFiles ?? []).map((filePath) => path.resolve(rootDir, filePath)) - const defaultInputFolder = path.resolve(rootDir, 'icons') - const inputFolder = merged.inputFolder === undefined - && inputFiles.length > 0 - && !fs.existsSync(defaultInputFolder) - ? null - : path.resolve(rootDir, merged.inputFolder ?? 'icons') + const configuredInput = merged.input ?? 'icons' + const input = (Array.isArray(configuredInput) ? configuredInput : [configuredInput]) + .map((entry) => { + const negated = entry.startsWith('!') + const resolved = path.resolve(rootDir, negated ? entry.slice(1) : entry) + const normalized = path.sep === '/' ? resolved : resolved.replaceAll(path.sep, '/') + return negated ? `!${normalized}` : normalized + }) return { mode: merged.mode, name, description: merged.description, - inputFolder, - inputFiles, + input, transform: { removeSize: transform.removeSize ?? true, replaceColors: transform.replaceColors ?? true, diff --git a/src/core/mode-adapter.ts b/src/core/mode-adapter.ts index 38af85b..2da8014 100644 --- a/src/core/mode-adapter.ts +++ b/src/core/mode-adapter.ts @@ -42,6 +42,8 @@ export type ModeResultMetadata = ReactModeResultMetadata | NextModeResultMetadat export type OutputPlan = { readonly files: readonly GeneratedFile[] + /** Создавать управляемый `.gitignore` для generated-каталога. По умолчанию: true. */ + readonly createGitignore?: boolean readonly paths: { readonly generatedDir: '.svg-sprite' readonly sprite: string @@ -53,6 +55,5 @@ export type OutputPlan = { export interface ModeAdapter { readonly mode: M - readonly contractVersion: number generate(context: ModeAdapterContext): Promise } diff --git a/src/core/output-writer.ts b/src/core/output-writer.ts index d5bbbe4..3bdfe37 100644 --- a/src/core/output-writer.ts +++ b/src/core/output-writer.ts @@ -2,44 +2,17 @@ import { randomUUID } from 'node:crypto' import fs from 'node:fs' import path from 'node:path' import type { SpriteMode } from '../targets/types.js' -import type { GeneratedFile } from './mode-adapter.js' import { GENERATED_MARKER, GENERATED_NOTICE_MARKER, - GENERATOR, } from './generated-markers.js' +import type { GeneratedFile } from './mode-adapter.js' -const STATE_PATH = '.svg-sprite/state.json' -const PREVIOUS_STATE_PATHS = [ - '.svg-sprite-data/state.json', - 'generated/state.json', -] as const -const LEGACY_STATE_PATH = 'generated/.svg-sprites.manifest.json' const DATA_ROOT = '.svg-sprite' -const PREVIOUS_DATA_ROOT = '.svg-sprite-data' -const SYSTEM_FILES: readonly GeneratedFile[] = [ - { - path: '.gitignore', - content: `# ${GENERATED_MARKER}. Do not edit.\n/${DATA_ROOT}/\n`, - }, -] - -type OutputState = { - schemaVersion: 1 - generator: typeof GENERATOR - owner: { - mode: SpriteMode - contractVersion: number - } - files: string[] - warning?: string -} - -type LegacyState = { - version: 1 - generator: typeof GENERATOR - files: string[] -} +const DATA_PREFIX = `${DATA_ROOT}/` +const BACKUP_SUFFIX = '.old' +const STAGING_SUFFIX = '.tmp' +const GITIGNORE_CONTENT = `# ${GENERATED_MARKER}. Do not edit.\n/${DATA_ROOT}/\n` function normalizeManagedPath(relativePath: string): string { const normalized = relativePath.replaceAll('\\', '/') @@ -49,9 +22,6 @@ function normalizeManagedPath(relativePath: string): string { normalized === '' || path.posix.isAbsolute(normalized) || parts.some((part) => part === '' || part === '.' || part === '..') - || normalized === STATE_PATH - || PREVIOUS_STATE_PATHS.includes(normalized as typeof PREVIOUS_STATE_PATHS[number]) - || normalized === LEGACY_STATE_PATH ) { throw new Error(`Invalid generated file path: ${relativePath}`) } @@ -59,23 +29,11 @@ function normalizeManagedPath(relativePath: string): string { return normalized } -function resolveManagedPath(rootDir: string, relativePath: string): string { - const normalized = normalizeManagedPath(relativePath) - const resolved = path.resolve(rootDir, normalized) - const relative = path.relative(rootDir, resolved) - - if (relative.startsWith('..') || path.isAbsolute(relative)) { - throw new Error(`Invalid generated file path: ${relativePath}`) - } - - return resolved -} - -function resolveInternalPath(rootDir: string, relativePath: string): string { +function resolveInsideRoot(rootDir: string, relativePath: string): string { const resolved = path.resolve(rootDir, relativePath) const relative = path.relative(rootDir, resolved) if (relative.startsWith('..') || path.isAbsolute(relative)) { - throw new Error(`Invalid internal output path: ${relativePath}`) + throw new Error(`Invalid generated path: ${relativePath}`) } return resolved } @@ -97,115 +55,8 @@ function assertNoSymlinks(rootDir: string, filePath: string): void { } } -function parseJsonFile(filePath: string): unknown { - try { - return JSON.parse(fs.readFileSync(filePath, 'utf-8')) - } catch { - throw new Error(`Cannot parse generated state: ${filePath}`) - } -} - -function readOutputState(statePath: string): OutputState { - const state = parseJsonFile(statePath) - if ( - !state - || typeof state !== 'object' - || !('schemaVersion' in state) - || state.schemaVersion !== 1 - || !('generator' in state) - || state.generator !== GENERATOR - || !('owner' in state) - || !state.owner - || typeof state.owner !== 'object' - || !('mode' in state.owner) - || typeof state.owner.mode !== 'string' - || !('contractVersion' in state.owner) - || typeof state.owner.contractVersion !== 'number' - || !('files' in state) - || !Array.isArray(state.files) - || !state.files.every((file) => typeof file === 'string') - ) { - throw new Error(`Invalid generated state: ${statePath}`) - } - return state as OutputState -} - -function readPreviousFiles(rootDir: string): { - files: string[] - obsoleteStatePath: string | null -} { - for (const relativeStatePath of [STATE_PATH, ...PREVIOUS_STATE_PATHS]) { - const statePath = resolveInternalPath(rootDir, relativeStatePath) - assertNoSymlinks(rootDir, statePath) - if (!fs.existsSync(statePath)) continue - return { - files: readOutputState(statePath).files.map(normalizeManagedPath), - obsoleteStatePath: relativeStatePath === STATE_PATH ? null : statePath, - } - } - - const legacyStatePath = resolveInternalPath(rootDir, LEGACY_STATE_PATH) - assertNoSymlinks(rootDir, legacyStatePath) - if (!fs.existsSync(legacyStatePath)) return { files: [], obsoleteStatePath: null } - - const legacyState = parseJsonFile(legacyStatePath) - if ( - !legacyState - || typeof legacyState !== 'object' - || !('version' in legacyState) - || legacyState.version !== 1 - || !('generator' in legacyState) - || legacyState.generator !== GENERATOR - || !('files' in legacyState) - || !Array.isArray(legacyState.files) - || !legacyState.files.every((file) => typeof file === 'string') - ) { - throw new Error(`Invalid generated state: ${legacyStatePath}`) - } - - return { - files: (legacyState as LegacyState).files.map(normalizeManagedPath), - obsoleteStatePath: legacyStatePath, - } -} - -function removeEmptyStateDirectory(rootDir: string, statePath: string): void { - const directory = path.dirname(statePath) - if (directory === rootDir) return - try { - fs.rmdirSync(directory) - } catch (error) { - if (error instanceof Error && 'code' in error && ( - error.code === 'ENOENT' || error.code === 'ENOTEMPTY' - )) return - throw error - } -} - -function removeEmptyManagedParents(rootDir: string, filePath: string): void { - for (const relativeDataRoot of [DATA_ROOT, PREVIOUS_DATA_ROOT]) { - const dataRoot = resolveInternalPath(rootDir, relativeDataRoot) - let directory = path.dirname(filePath) - const relative = path.relative(dataRoot, directory) - if (relative.startsWith('..') || path.isAbsolute(relative)) continue - - while (directory !== dataRoot) { - try { - fs.rmdirSync(directory) - } catch (error) { - if (error instanceof Error && 'code' in error && ( - error.code === 'ENOENT' || error.code === 'ENOTEMPTY' - )) return - throw error - } - directory = path.dirname(directory) - } - return - } -} - function hasGeneratedMarker(filePath: string): boolean { - if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false + if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isFile()) return false const content = fs.readFileSync(filePath, 'utf-8') return content.includes(GENERATED_MARKER) || content.includes(GENERATED_NOTICE_MARKER) } @@ -228,70 +79,142 @@ function writeFileAtomic(rootDir: string, filePath: string, content: string | Ui } } -/** Применяет mode output plan и последним записывает ownership state. */ +function validateGitignore(rootDir: string, createGitignore: boolean): void { + if (!createGitignore) return + const gitignorePath = resolveInsideRoot(rootDir, '.gitignore') + assertNoSymlinks(rootDir, gitignorePath) + + if (fs.existsSync(gitignorePath) && !hasGeneratedMarker(gitignorePath)) { + throw new Error( + `Refusing to overwrite a user file: ${gitignorePath}\n` + + 'Move the file or choose another sprite directory.', + ) + } +} + +function updateGitignore(rootDir: string, createGitignore: boolean): void { + const gitignorePath = resolveInsideRoot(rootDir, '.gitignore') + + if (createGitignore) { + writeFileAtomic(rootDir, gitignorePath, GITIGNORE_CONTENT) + } else if (hasGeneratedMarker(gitignorePath)) { + fs.unlinkSync(gitignorePath) + } +} + +function transientPaths(rootDir: string, suffix: string): string[] { + return fs.readdirSync(rootDir) + .filter((entry) => entry.startsWith(`${DATA_ROOT}.`) && entry.endsWith(suffix)) + .map((entry) => resolveInsideRoot(rootDir, entry)) +} + +function recoverInterruptedReplacement(rootDir: string): void { + const outputPath = resolveInsideRoot(rootDir, DATA_ROOT) + assertNoSymlinks(rootDir, outputPath) + + const backups = transientPaths(rootDir, BACKUP_SUFFIX) + .sort((left, right) => fs.lstatSync(right).mtimeMs - fs.lstatSync(left).mtimeMs) + + if (!fs.existsSync(outputPath) && backups.length > 0) { + const [latestBackup] = backups.splice(0, 1) + assertNoSymlinks(rootDir, latestBackup) + fs.renameSync(latestBackup, outputPath) + } + + for (const transientPath of [ + ...backups, + ...transientPaths(rootDir, STAGING_SUFFIX), + ]) { + assertNoSymlinks(rootDir, transientPath) + fs.rmSync(transientPath, { recursive: true, force: true }) + } +} + +function stageOutput(rootDir: string, files: readonly GeneratedFile[]): string { + const stagingPath = resolveInsideRoot( + rootDir, + `${DATA_ROOT}.${process.pid}.${randomUUID()}${STAGING_SUFFIX}`, + ) + assertNoSymlinks(rootDir, stagingPath) + fs.mkdirSync(stagingPath) + + try { + for (const file of files) { + const relativePath = file.path.slice(DATA_PREFIX.length) + const filePath = path.resolve(stagingPath, relativePath) + const relative = path.relative(stagingPath, filePath) + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error(`Invalid generated file path: ${file.path}`) + } + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFileSync(filePath, file.content, { flag: 'wx' }) + } + return stagingPath + } catch (error) { + fs.rmSync(stagingPath, { recursive: true, force: true }) + throw error + } +} + +function replaceOutput(rootDir: string, stagingPath: string): void { + const outputPath = resolveInsideRoot(rootDir, DATA_ROOT) + const backupPath = resolveInsideRoot( + rootDir, + `${DATA_ROOT}.${process.pid}.${randomUUID()}${BACKUP_SUFFIX}`, + ) + assertNoSymlinks(rootDir, outputPath) + + if (fs.existsSync(outputPath) && !fs.lstatSync(outputPath).isDirectory()) { + throw new Error(`Generated output path must be a directory: ${outputPath}`) + } + + let movedPrevious = false + try { + if (fs.existsSync(outputPath)) { + fs.renameSync(outputPath, backupPath) + movedPrevious = true + } + fs.renameSync(stagingPath, outputPath) + } catch (error) { + if (movedPrevious && !fs.existsSync(outputPath) && fs.existsSync(backupPath)) { + fs.renameSync(backupPath, outputPath) + } + throw error + } + + if (movedPrevious) fs.rmSync(backupPath, { recursive: true, force: true }) +} + +/** Полностью заменяет generated-каталог output plan. */ export function writeOutputPlan( rootDir: string, mode: SpriteMode, - contractVersion: number, files: readonly GeneratedFile[], - generatedNotice: boolean, + createGitignore = true, ): void { - const normalizedFiles = [...SYSTEM_FILES, ...files].map((file) => ({ + const normalizedFiles = files.map((file) => ({ ...file, path: normalizeManagedPath(file.path), })) - const nextFiles = normalizedFiles.map((file) => file.path) + const paths = normalizedFiles.map((file) => file.path) - if (new Set(nextFiles).size !== nextFiles.length) { + if (paths.some((filePath) => !filePath.startsWith(DATA_PREFIX))) { + throw new Error(`Mode "${mode}" produced a file outside ${DATA_ROOT}.`) + } + if (new Set(paths).size !== paths.length) { throw new Error(`Mode "${mode}" produced duplicate generated file paths.`) } - const previous = readPreviousFiles(rootDir) - const obsoleteFiles: string[] = [] + recoverInterruptedReplacement(rootDir) + validateGitignore(rootDir, createGitignore) + const stagingPath = stageOutput(rootDir, normalizedFiles) - for (const file of normalizedFiles) { - const filePath = resolveManagedPath(rootDir, file.path) - assertNoSymlinks(rootDir, filePath) - if (fs.existsSync(filePath) && !hasGeneratedMarker(filePath)) { - throw new Error( - `Refusing to overwrite a user file: ${filePath}\n` - + 'Move the file or choose another sprite directory.', - ) + try { + replaceOutput(rootDir, stagingPath) + updateGitignore(rootDir, createGitignore) + } finally { + if (fs.existsSync(stagingPath)) { + fs.rmSync(stagingPath, { recursive: true, force: true }) } } - - for (const relativePath of previous.files) { - if (nextFiles.includes(relativePath)) continue - const filePath = resolveManagedPath(rootDir, relativePath) - assertNoSymlinks(rootDir, filePath) - - if (fs.existsSync(filePath)) { - if (!hasGeneratedMarker(filePath)) { - throw new Error(`Refusing to delete a user file: ${filePath}`) - } - obsoleteFiles.push(filePath) - } - } - - for (const file of normalizedFiles) { - writeFileAtomic(rootDir, resolveManagedPath(rootDir, file.path), file.content) - } - for (const filePath of obsoleteFiles) fs.unlinkSync(filePath) - for (const filePath of obsoleteFiles) removeEmptyManagedParents(rootDir, filePath) - if (previous.obsoleteStatePath && fs.existsSync(previous.obsoleteStatePath)) { - fs.unlinkSync(previous.obsoleteStatePath) - removeEmptyStateDirectory(rootDir, previous.obsoleteStatePath) - } - - const state: OutputState = { - schemaVersion: 1, - generator: GENERATOR, - owner: { mode, contractVersion }, - files: nextFiles, - ...(generatedNotice && { - warning: 'АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ. Не редактируйте вручную: изменения будут перезаписаны.', - }), - } - const statePath = resolveInternalPath(rootDir, STATE_PATH) - writeFileAtomic(rootDir, statePath, `${JSON.stringify(state, null, 2)}\n`) } diff --git a/src/core/prepare-sprite.ts b/src/core/prepare-sprite.ts index a63046a..d3be1bf 100644 --- a/src/core/prepare-sprite.ts +++ b/src/core/prepare-sprite.ts @@ -26,8 +26,7 @@ export function prepareSprite(config: ResolvedSpriteConfig): PreparedSprite { const folder = resolveSpriteSources({ name: config.name, format: 'stack', - inputFolder: config.inputFolder, - inputFiles: config.inputFiles, + input: config.input, }) const iconNames = folder.files .map((filePath) => path.basename(filePath, '.svg')) diff --git a/src/generate.ts b/src/generate.ts index ede6935..bcfaf0b 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -45,9 +45,8 @@ export async function generateSprite( writeOutputPlan( resolvedSource.rootDir, adapter.mode, - adapter.contractVersion, plan.files, - config.generatedNotice, + plan.createGitignore, ) const generatedDir = path.resolve(resolvedSource.rootDir, plan.paths.generatedDir) diff --git a/src/modes/next-app-turbopack/adapter.ts b/src/modes/next-app-turbopack/adapter.ts index c059912..b4a7e5e 100644 --- a/src/modes/next-app-turbopack/adapter.ts +++ b/src/modes/next-app-turbopack/adapter.ts @@ -5,8 +5,6 @@ import { generateOutputFiles } from './output.js' export const nextAppTurbopackAdapter: ModeAdapter<'next@app/turbopack'> = { mode: 'next@app/turbopack', - contractVersion: 5, - async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true, diff --git a/src/modes/next-app-turbopack/output.ts b/src/modes/next-app-turbopack/output.ts index 7bea198..0da51d8 100644 --- a/src/modes/next-app-turbopack/output.ts +++ b/src/modes/next-app-turbopack/output.ts @@ -151,7 +151,37 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact } function manifestTypes(config: ResolvedSpriteConfig): string { - return [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') + return [ + header(config.generatedNotice), + '', + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifest = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + ' componentName: string', + " mode: 'next@app/turbopack'", + " target: 'next@app/turbopack'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' spriteUrl: string', + ' icons: readonly SpriteManifestIcon[]', + '}', + '', + 'export declare const spriteManifest: SpriteManifest', + 'export default spriteManifest', + '', + ].join('\n') } export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { diff --git a/src/modes/next-app-webpack/adapter.ts b/src/modes/next-app-webpack/adapter.ts index 9d20de5..8171c3d 100644 --- a/src/modes/next-app-webpack/adapter.ts +++ b/src/modes/next-app-webpack/adapter.ts @@ -5,7 +5,6 @@ import { generateOutputFiles } from './output.js' export const nextAppWebpackAdapter: ModeAdapter<'next@app/webpack'> = { mode: 'next@app/webpack', - contractVersion: 5, async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true }) const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') diff --git a/src/modes/next-app-webpack/output.ts b/src/modes/next-app-webpack/output.ts index cebb996..4050849 100644 --- a/src/modes/next-app-webpack/output.ts +++ b/src/modes/next-app-webpack/output.ts @@ -70,7 +70,37 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') } -const manifestTypes = (config: ResolvedSpriteConfig) => [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') +const manifestTypes = (config: ResolvedSpriteConfig) => [ + header(config.generatedNotice), + '', + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifest = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + ' componentName: string', + " mode: 'next@app/webpack'", + " target: 'next@app/webpack'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' spriteUrl: string', + ' icons: readonly SpriteManifestIcon[]', + '}', + '', + 'export declare const spriteManifest: SpriteManifest', + 'export default spriteManifest', + '', +].join('\n') export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { return [ diff --git a/src/modes/next-pages-turbopack/adapter.ts b/src/modes/next-pages-turbopack/adapter.ts index 8f02f7e..57dd627 100644 --- a/src/modes/next-pages-turbopack/adapter.ts +++ b/src/modes/next-pages-turbopack/adapter.ts @@ -5,7 +5,6 @@ import { generateOutputFiles } from './output.js' export const nextPagesTurbopackAdapter: ModeAdapter<'next@pages/turbopack'> = { mode: 'next@pages/turbopack', - contractVersion: 5, async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true }) const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') diff --git a/src/modes/next-pages-turbopack/output.ts b/src/modes/next-pages-turbopack/output.ts index 816f987..6ad7da7 100644 --- a/src/modes/next-pages-turbopack/output.ts +++ b/src/modes/next-pages-turbopack/output.ts @@ -70,7 +70,37 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') } -const manifestTypes = (config: ResolvedSpriteConfig) => [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') +const manifestTypes = (config: ResolvedSpriteConfig) => [ + header(config.generatedNotice), + '', + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifest = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + ' componentName: string', + " mode: 'next@pages/turbopack'", + " target: 'next@pages/turbopack'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' spriteUrl: string', + ' icons: readonly SpriteManifestIcon[]', + '}', + '', + 'export declare const spriteManifest: SpriteManifest', + 'export default spriteManifest', + '', +].join('\n') export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { return [ diff --git a/src/modes/next-pages-webpack/adapter.ts b/src/modes/next-pages-webpack/adapter.ts index a09142c..d6df8ef 100644 --- a/src/modes/next-pages-webpack/adapter.ts +++ b/src/modes/next-pages-webpack/adapter.ts @@ -5,7 +5,6 @@ import { generateOutputFiles } from './output.js' export const nextPagesWebpackAdapter: ModeAdapter<'next@pages/webpack'> = { mode: 'next@pages/webpack', - contractVersion: 5, async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true }) const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack') diff --git a/src/modes/next-pages-webpack/output.ts b/src/modes/next-pages-webpack/output.ts index 431cfeb..0c8bdcf 100644 --- a/src/modes/next-pages-webpack/output.ts +++ b/src/modes/next-pages-webpack/output.ts @@ -70,7 +70,37 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact return [header(config.generatedNotice), "const spriteUrl = new URL('./sprite.svg', import.meta.url).href", '', `export const spriteManifest = ${source}`, '', 'export default spriteManifest', ''].join('\n') } -const manifestTypes = (config: ResolvedSpriteConfig) => [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') +const manifestTypes = (config: ResolvedSpriteConfig) => [ + header(config.generatedNotice), + '', + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifest = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + ' componentName: string', + " mode: 'next@pages/webpack'", + " target: 'next@pages/webpack'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' spriteUrl: string', + ' icons: readonly SpriteManifestIcon[]', + '}', + '', + 'export declare const spriteManifest: SpriteManifest', + 'export default spriteManifest', + '', +].join('\n') export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { return [ diff --git a/src/modes/react-vite/adapter.ts b/src/modes/react-vite/adapter.ts index 976f41a..9279d1e 100644 --- a/src/modes/react-vite/adapter.ts +++ b/src/modes/react-vite/adapter.ts @@ -5,8 +5,6 @@ import { generateOutputFiles } from './output.js' export const reactViteAdapter: ModeAdapter<'react@vite'> = { mode: 'react@vite', - contractVersion: 5, - async generate(context) { const bytes = await compileSpriteContent( context.prepared.folder, diff --git a/src/modes/react-vite/output.ts b/src/modes/react-vite/output.ts index c63a1af..7de94c5 100644 --- a/src/modes/react-vite/output.ts +++ b/src/modes/react-vite/output.ts @@ -241,7 +241,30 @@ function generateManifest(config: ResolvedSpriteConfig, artifact: CompiledSprite function generateManifestDeclarations(config: ResolvedSpriteConfig): string { return [ blockHeader(config.generatedNotice), - "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", + '', + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifest = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + ' componentName: string', + " mode: 'react@vite'", + " target: 'vite'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' spriteUrl: string', + ' icons: readonly SpriteManifestIcon[]', + '}', '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', diff --git a/src/modes/react-webpack/adapter.ts b/src/modes/react-webpack/adapter.ts index cebb82e..63a7954 100644 --- a/src/modes/react-webpack/adapter.ts +++ b/src/modes/react-webpack/adapter.ts @@ -5,8 +5,6 @@ import { generateOutputFiles } from './output.js' export const reactWebpackAdapter: ModeAdapter<'react@webpack'> = { mode: 'react@webpack', - contractVersion: 5, - async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: false, diff --git a/src/modes/react-webpack/output.ts b/src/modes/react-webpack/output.ts index 425323e..edf86d3 100644 --- a/src/modes/react-webpack/output.ts +++ b/src/modes/react-webpack/output.ts @@ -168,7 +168,37 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact } function manifestTypes(config: ResolvedSpriteConfig): string { - return [header(config.generatedNotice), "import type { SpriteManifest } from '@gromlab/svg-sprites/react'", '', 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', ''].join('\n') + return [ + header(config.generatedNotice), + '', + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifest = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + ' componentName: string', + " mode: 'react@webpack'", + " target: 'webpack'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' spriteUrl: string', + ' icons: readonly SpriteManifestIcon[]', + '}', + '', + 'export declare const spriteManifest: SpriteManifest', + 'export default spriteManifest', + '', + ].join('\n') } export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): GeneratedFile[] { diff --git a/src/modes/standalone-vite/adapter.ts b/src/modes/standalone-vite/adapter.ts index 31814c5..d3d8625 100644 --- a/src/modes/standalone-vite/adapter.ts +++ b/src/modes/standalone-vite/adapter.ts @@ -5,8 +5,6 @@ import { generateOutputFiles } from './output.js' export const standaloneViteAdapter: ModeAdapter<'standalone@vite'> = { mode: 'standalone@vite', - contractVersion: 1, - async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: false, diff --git a/src/modes/standalone-vite/output.ts b/src/modes/standalone-vite/output.ts index 0fd8e27..04c25c1 100644 --- a/src/modes/standalone-vite/output.ts +++ b/src/modes/standalone-vite/output.ts @@ -68,9 +68,10 @@ function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSprite ].join('\n') } -function index(config: ResolvedSpriteConfig): string { +function index(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { const prefix = camel(config.name) const pascalName = pascal(config.name) + const iconTagName = `${config.name}-icon` return [ header(config.generatedNotice), "import spriteUrl from './sprite.svg?no-inline'", @@ -78,9 +79,92 @@ function index(config: ResolvedSpriteConfig): string { '', `export { ${prefix}IconIds, ${prefix}IconNames }`, `export const ${prefix}SpriteUrl = spriteUrl`, + `export const ${prefix}IconTagName = ${JSON.stringify(iconTagName)}`, + `const ${prefix}IconViewBoxes = ${JSON.stringify(Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.viewBox])), null, 2)}`, + `const ${prefix}IconElementMarker = Symbol.for('@gromlab/svg-sprites/icon-element')`, + `const ${prefix}IconElementSignature = ${prefix}IconTagName + '|' + ${prefix}SpriteUrl`, + `function has${pascalName}Icon(icon) {`, + ` return Object.prototype.hasOwnProperty.call(${prefix}IconIds, icon)`, + '}', `export function get${pascalName}IconHref(icon) {`, ` return ${prefix}SpriteUrl + '#' + ${prefix}IconIds[icon]`, '}', + `export function define${pascalName}IconElement() {`, + ` if (typeof globalThis.customElements === 'undefined' || typeof globalThis.HTMLElement === 'undefined') return`, + ` const existing = globalThis.customElements.get(${prefix}IconTagName)`, + ' if (existing) {', + ` if (existing[${prefix}IconElementMarker] === ${prefix}IconElementSignature) return`, + ` throw new Error('Cannot define <' + ${prefix}IconTagName + '>: this custom element name is already registered.')`, + ' }', + ` class ${pascalName}IconElement extends globalThis.HTMLElement {`, + " static observedAttributes = ['icon']", + ' constructor() {', + ' super()', + ' const ownerDocument = this.ownerDocument', + " const shadowRoot = this.attachShadow({ mode: 'open' })", + " const style = ownerDocument.createElement('style')", + " style.textContent = ':host{display:inline-block;width:1em;height:1em;vertical-align:-0.125em}:host([hidden]){display:none}svg{display:block;width:100%;height:100%}svg[hidden]{display:none}'", + " this._svg = ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg')", + " this._svg.setAttribute('part', 'svg')", + " this._svg.setAttribute('aria-hidden', 'true')", + " this._svg.setAttribute('focusable', 'false')", + " this._svg.setAttribute('hidden', '')", + " this._use = ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'use')", + " this._use.setAttribute('part', 'use')", + ' this._svg.append(this._use)', + ' shadowRoot.append(style, this._svg)', + ' }', + ' connectedCallback() {', + " if (Object.prototype.hasOwnProperty.call(this, 'icon')) {", + ' const icon = this.icon', + ' delete this.icon', + ' this.icon = icon', + ' }', + ' this._renderIcon()', + ' }', + ' attributeChangedCallback() {', + ' this._renderIcon()', + ' }', + ' get icon() {', + " const icon = this.getAttribute('icon')", + ` return icon !== null && has${pascalName}Icon(icon) ? icon : null`, + ' }', + ' set icon(icon) {', + ' if (icon === null || icon === undefined) {', + " this.removeAttribute('icon')", + ' return', + ' }', + ` if (!has${pascalName}Icon(icon)) throw new TypeError('Unknown ${config.name} icon: ' + icon)`, + " this.setAttribute('icon', icon)", + ' }', + ' _renderIcon() {', + " const icon = this.getAttribute('icon')", + ' if (icon === null) {', + ' this._clearIcon()', + ' return', + ' }', + ` if (!has${pascalName}Icon(icon)) {`, + ' this._clearIcon()', + ` if (this._invalidIcon !== icon) console.error('<' + ${prefix}IconTagName + '>: unknown icon "' + icon + '".')`, + ' this._invalidIcon = icon', + ' return', + ' }', + ' this._invalidIcon = undefined', + ` const viewBox = ${prefix}IconViewBoxes[icon]`, + " if (viewBox === null) this._svg.removeAttribute('viewBox')", + " else this._svg.setAttribute('viewBox', viewBox)", + ` this._use.setAttribute('href', get${pascalName}IconHref(icon))`, + " this._svg.removeAttribute('hidden')", + ' }', + ' _clearIcon() {', + " this._use.removeAttribute('href')", + " this._svg.removeAttribute('viewBox')", + " this._svg.setAttribute('hidden', '')", + ' }', + ' }', + ` Object.defineProperty(${pascalName}IconElement, ${prefix}IconElementMarker, { value: ${prefix}IconElementSignature })`, + ` globalThis.customElements.define(${prefix}IconTagName, ${pascalName}IconElement)`, + '}', '', ].join('\n') } @@ -88,6 +172,7 @@ function index(config: ResolvedSpriteConfig): string { function indexDeclarations(config: ResolvedSpriteConfig): string { const prefix = camel(config.name) const pascalName = pascal(config.name) + const iconTagName = `${config.name}-icon` return [ header(config.generatedNotice), `import type { ${pascalName}IconName } from './icon-data.js'`, @@ -95,7 +180,17 @@ function indexDeclarations(config: ResolvedSpriteConfig): string { `export { ${prefix}IconIds, ${prefix}IconNames } from './icon-data.js'`, `export type { ${pascalName}IconName } from './icon-data.js'`, `export declare const ${prefix}SpriteUrl: string`, + `export declare const ${prefix}IconTagName: ${JSON.stringify(iconTagName)}`, `export declare function get${pascalName}IconHref(icon: ${pascalName}IconName): string`, + `export interface ${pascalName}IconElement extends HTMLElement {`, + ` icon: ${pascalName}IconName | null`, + '}', + `export declare function define${pascalName}IconElement(): void`, + 'declare global {', + ' interface HTMLElementTagNameMap {', + ` ${JSON.stringify(iconTagName)}: ${pascalName}IconElement`, + ' }', + '}', '', ].join('\n') } @@ -133,11 +228,33 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact function manifestTypes(config: ResolvedSpriteConfig): string { return [ header(config.generatedNotice), - "import type { StandaloneSpriteManifest, StandaloneSpriteManifestData } from '@gromlab/svg-sprites'", '', - "export declare const spriteManifestData: StandaloneSpriteManifestData<'standalone@vite'>", - "export declare function createSpriteManifest(spriteUrl: string): StandaloneSpriteManifest<'standalone@vite'>", - "export declare const spriteManifest: StandaloneSpriteManifest<'standalone@vite'>", + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifestData = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + " mode: 'standalone@vite'", + " target: 'vite'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' icons: readonly SpriteManifestIcon[]', + '}', + 'export type SpriteManifest = SpriteManifestData & { spriteUrl: string }', + '', + 'export declare const spriteManifestData: SpriteManifestData', + 'export declare function createSpriteManifest(spriteUrl: string): SpriteManifest', + 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', '', ].join('\n') @@ -148,7 +265,7 @@ export function generateOutputFiles( artifact: CompiledSpriteArtifact, ): GeneratedFile[] { return [ - { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config, artifact) }, { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: svg(artifact.bytes, config.generatedNotice) }, { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, diff --git a/src/modes/standalone-webpack/adapter.ts b/src/modes/standalone-webpack/adapter.ts index a0f245b..6742c8d 100644 --- a/src/modes/standalone-webpack/adapter.ts +++ b/src/modes/standalone-webpack/adapter.ts @@ -5,8 +5,6 @@ import { generateOutputFiles } from './output.js' export const standaloneWebpackAdapter: ModeAdapter<'standalone@webpack'> = { mode: 'standalone@webpack', - contractVersion: 1, - async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: false, diff --git a/src/modes/standalone-webpack/output.ts b/src/modes/standalone-webpack/output.ts index a00afd0..ef15f6a 100644 --- a/src/modes/standalone-webpack/output.ts +++ b/src/modes/standalone-webpack/output.ts @@ -68,18 +68,102 @@ function iconDeclarations(config: ResolvedSpriteConfig, artifact: CompiledSprite ].join('\n') } -function index(config: ResolvedSpriteConfig): string { +function index(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string { const prefix = camel(config.name) const pascalName = pascal(config.name) + const iconTagName = `${config.name}-icon` return [ header(config.generatedNotice), `import { ${prefix}IconIds, ${prefix}IconNames } from './icon-data.js'`, '', `export { ${prefix}IconIds, ${prefix}IconNames }`, `export const ${prefix}SpriteUrl = new URL('./sprite.svg', import.meta.url).href`, + `export const ${prefix}IconTagName = ${JSON.stringify(iconTagName)}`, + `const ${prefix}IconViewBoxes = ${JSON.stringify(Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.viewBox])), null, 2)}`, + `const ${prefix}IconElementMarker = Symbol.for('@gromlab/svg-sprites/icon-element')`, + `const ${prefix}IconElementSignature = ${prefix}IconTagName + '|' + ${prefix}SpriteUrl`, + `function has${pascalName}Icon(icon) {`, + ` return Object.prototype.hasOwnProperty.call(${prefix}IconIds, icon)`, + '}', `export function get${pascalName}IconHref(icon) {`, ` return ${prefix}SpriteUrl + '#' + ${prefix}IconIds[icon]`, '}', + `export function define${pascalName}IconElement() {`, + ` if (typeof globalThis.customElements === 'undefined' || typeof globalThis.HTMLElement === 'undefined') return`, + ` const existing = globalThis.customElements.get(${prefix}IconTagName)`, + ' if (existing) {', + ` if (existing[${prefix}IconElementMarker] === ${prefix}IconElementSignature) return`, + ` throw new Error('Cannot define <' + ${prefix}IconTagName + '>: this custom element name is already registered.')`, + ' }', + ` class ${pascalName}IconElement extends globalThis.HTMLElement {`, + " static observedAttributes = ['icon']", + ' constructor() {', + ' super()', + ' const ownerDocument = this.ownerDocument', + " const shadowRoot = this.attachShadow({ mode: 'open' })", + " const style = ownerDocument.createElement('style')", + " style.textContent = ':host{display:inline-block;width:1em;height:1em;vertical-align:-0.125em}:host([hidden]){display:none}svg{display:block;width:100%;height:100%}svg[hidden]{display:none}'", + " this._svg = ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'svg')", + " this._svg.setAttribute('part', 'svg')", + " this._svg.setAttribute('aria-hidden', 'true')", + " this._svg.setAttribute('focusable', 'false')", + " this._svg.setAttribute('hidden', '')", + " this._use = ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'use')", + " this._use.setAttribute('part', 'use')", + ' this._svg.append(this._use)', + ' shadowRoot.append(style, this._svg)', + ' }', + ' connectedCallback() {', + " if (Object.prototype.hasOwnProperty.call(this, 'icon')) {", + ' const icon = this.icon', + ' delete this.icon', + ' this.icon = icon', + ' }', + ' this._renderIcon()', + ' }', + ' attributeChangedCallback() {', + ' this._renderIcon()', + ' }', + ' get icon() {', + " const icon = this.getAttribute('icon')", + ` return icon !== null && has${pascalName}Icon(icon) ? icon : null`, + ' }', + ' set icon(icon) {', + ' if (icon === null || icon === undefined) {', + " this.removeAttribute('icon')", + ' return', + ' }', + ` if (!has${pascalName}Icon(icon)) throw new TypeError('Unknown ${config.name} icon: ' + icon)`, + " this.setAttribute('icon', icon)", + ' }', + ' _renderIcon() {', + " const icon = this.getAttribute('icon')", + ' if (icon === null) {', + ' this._clearIcon()', + ' return', + ' }', + ` if (!has${pascalName}Icon(icon)) {`, + ' this._clearIcon()', + ` if (this._invalidIcon !== icon) console.error('<' + ${prefix}IconTagName + '>: unknown icon "' + icon + '".')`, + ' this._invalidIcon = icon', + ' return', + ' }', + ' this._invalidIcon = undefined', + ` const viewBox = ${prefix}IconViewBoxes[icon]`, + " if (viewBox === null) this._svg.removeAttribute('viewBox')", + " else this._svg.setAttribute('viewBox', viewBox)", + ` this._use.setAttribute('href', get${pascalName}IconHref(icon))`, + " this._svg.removeAttribute('hidden')", + ' }', + ' _clearIcon() {', + " this._use.removeAttribute('href')", + " this._svg.removeAttribute('viewBox')", + " this._svg.setAttribute('hidden', '')", + ' }', + ' }', + ` Object.defineProperty(${pascalName}IconElement, ${prefix}IconElementMarker, { value: ${prefix}IconElementSignature })`, + ` globalThis.customElements.define(${prefix}IconTagName, ${pascalName}IconElement)`, + '}', '', ].join('\n') } @@ -87,6 +171,7 @@ function index(config: ResolvedSpriteConfig): string { function indexDeclarations(config: ResolvedSpriteConfig): string { const prefix = camel(config.name) const pascalName = pascal(config.name) + const iconTagName = `${config.name}-icon` return [ header(config.generatedNotice), `import type { ${pascalName}IconName } from './icon-data.js'`, @@ -94,7 +179,17 @@ function indexDeclarations(config: ResolvedSpriteConfig): string { `export { ${prefix}IconIds, ${prefix}IconNames } from './icon-data.js'`, `export type { ${pascalName}IconName } from './icon-data.js'`, `export declare const ${prefix}SpriteUrl: string`, + `export declare const ${prefix}IconTagName: ${JSON.stringify(iconTagName)}`, `export declare function get${pascalName}IconHref(icon: ${pascalName}IconName): string`, + `export interface ${pascalName}IconElement extends HTMLElement {`, + ` icon: ${pascalName}IconName | null`, + '}', + `export declare function define${pascalName}IconElement(): void`, + 'declare global {', + ' interface HTMLElementTagNameMap {', + ` ${JSON.stringify(iconTagName)}: ${pascalName}IconElement`, + ' }', + '}', '', ].join('\n') } @@ -132,11 +227,33 @@ function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact function manifestTypes(config: ResolvedSpriteConfig): string { return [ header(config.generatedNotice), - "import type { StandaloneSpriteManifest, StandaloneSpriteManifestData } from '@gromlab/svg-sprites'", '', - "export declare const spriteManifestData: StandaloneSpriteManifestData<'standalone@webpack'>", - "export declare function createSpriteManifest(spriteUrl: string): StandaloneSpriteManifest<'standalone@webpack'>", - "export declare const spriteManifest: StandaloneSpriteManifest<'standalone@webpack'>", + 'export type SpriteManifestColor = {', + ' variable: `--icon-color-${number}`', + ' fallback: string', + '}', + 'export type SpriteManifestIcon = {', + ' name: string', + ' id: string', + ' viewBox: string | null', + ' colors: readonly SpriteManifestColor[]', + '}', + 'export type SpriteManifestData = {', + ' schemaVersion: 1', + " generator: '@gromlab/svg-sprites'", + ' name: string', + ' description?: string', + " mode: 'standalone@webpack'", + " target: 'webpack'", + " format: 'stack' | 'symbol'", + ' iconCount: number', + ' icons: readonly SpriteManifestIcon[]', + '}', + 'export type SpriteManifest = SpriteManifestData & { spriteUrl: string }', + '', + 'export declare const spriteManifestData: SpriteManifestData', + 'export declare function createSpriteManifest(spriteUrl: string): SpriteManifest', + 'export declare const spriteManifest: SpriteManifest', 'export default spriteManifest', '', ].join('\n') @@ -147,7 +264,7 @@ export function generateOutputFiles( artifact: CompiledSpriteArtifact, ): GeneratedFile[] { return [ - { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) }, + { path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config, artifact) }, { path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) }, { path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: svg(artifact.bytes, config.generatedNotice) }, { path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) }, diff --git a/src/modes/standalone/adapter.ts b/src/modes/standalone/adapter.ts index e0fa5f4..981afff 100644 --- a/src/modes/standalone/adapter.ts +++ b/src/modes/standalone/adapter.ts @@ -5,8 +5,6 @@ import { generateOutputFiles } from './output.js' export const standaloneAdapter: ModeAdapter<'standalone'> = { mode: 'standalone', - contractVersion: 2, - async generate(context) { const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: false, @@ -15,6 +13,7 @@ export const standaloneAdapter: ModeAdapter<'standalone'> = { return { files: generateOutputFiles(context.config, artifact), + createGitignore: false, paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', diff --git a/src/scanner.ts b/src/scanner.ts index 055cdcf..a2b8336 100644 --- a/src/scanner.ts +++ b/src/scanner.ts @@ -1,54 +1,91 @@ import fs from 'node:fs' import path from 'node:path' +import { + convertPathToPattern, + globSync, + isDynamicPattern, +} from 'tinyglobby' import type { SpriteFolder } from './types.js' -/** - * Сканирует папку и возвращает отсортированные абсолютные пути к SVG-файлам. - */ -function scanDirectory(dirPath: string): string[] { - if (!fs.existsSync(dirPath)) { - throw new Error(`Input directory does not exist: ${dirPath}`) - } - - return fs - .readdirSync(dirPath) - .filter((file) => file.endsWith('.svg')) - .sort() - .map((file) => path.join(dirPath, file)) +type PositiveInput = { + source: string + pattern: string + directory: string | null } -/** - * Резолвит массив путей к SVG-файлам в абсолютные пути. - * Проверяет существование каждого файла. - */ -function resolveFiles(files: string[]): string[] { - return files.map((filePath) => { - const resolved = path.resolve(filePath) +function resolvePositiveInput(source: string): PositiveInput { + if (!fs.existsSync(source)) { + if (isDynamicPattern(source)) return { source, pattern: source, directory: null } + throw new Error(`Input path does not exist: ${source}`) + } - if (!fs.existsSync(resolved)) { - throw new Error(`SVG file does not exist: ${resolved}`) + const stats = fs.statSync(source) + if (stats.isDirectory()) { + return { + source, + pattern: `${convertPathToPattern(source)}/*.svg`, + directory: source, } + } + if (!stats.isFile()) { + throw new Error(`Input path must be a file, directory, or glob pattern: ${source}`) + } + if (!source.endsWith('.svg')) { + throw new Error(`File is not an SVG: ${source}`) + } - if (!resolved.endsWith('.svg')) { - throw new Error(`File is not an SVG: ${resolved}`) - } + return { source, pattern: convertPathToPattern(source), directory: null } +} - return resolved - }) +function resolveExclusion(source: string): string { + const target = source.slice(1) + if (!fs.existsSync(target)) return source + + const stats = fs.statSync(target) + if (stats.isDirectory()) return `!${convertPathToPattern(target)}/**` + if (stats.isFile()) return `!${convertPathToPattern(target)}` + return source } type ResolveSpriteSourcesOptions = { name: string format: SpriteFolder['format'] - inputFolder: string | null - inputFiles: string[] + input: string[] } -/** Объединяет SVG из папки и явного списка в один источник спрайта. */ +const GLOB_OPTIONS = { + absolute: true, + expandDirectories: false, + onlyFiles: true, +} as const + +/** Разрешает пути, папки и glob-шаблоны в один набор исходных SVG. */ export function resolveSpriteSources(options: ResolveSpriteSourcesOptions): SpriteFolder { - const { name, format, inputFolder, inputFiles } = options - const folderFiles = inputFolder === null ? [] : scanDirectory(inputFolder) - const files = [...new Set([...folderFiles, ...resolveFiles(inputFiles)])] + const { name, format, input } = options + const positives = input + .filter((entry) => !entry.startsWith('!')) + .map(resolvePositiveInput) + const exclusions = input + .filter((entry) => entry.startsWith('!')) + .map(resolveExclusion) + + if (positives.length === 0) { + throw new Error('Sprite input must include at least one path or positive glob pattern.') + } + + for (const { source, pattern } of positives) { + const matchedSvg = globSync(pattern, GLOB_OPTIONS).some((filePath) => filePath.endsWith('.svg')) + if (!matchedSvg) throw new Error(`Input matched no SVG files: ${source}`) + } + + const files = [...new Set( + globSync([ + ...positives.map(({ pattern }) => pattern), + ...exclusions, + ], GLOB_OPTIONS) + .filter((filePath) => filePath.endsWith('.svg')) + .map((filePath) => path.resolve(filePath)), + )].sort() if (files.length === 0) { throw new Error(`Sprite "${name}" has no SVG files in configured inputs.`) @@ -57,7 +94,7 @@ export function resolveSpriteSources(options: ResolveSpriteSourcesOptions): Spri return { name, format, - path: inputFolder, + path: input.length === 1 ? positives[0].directory : null, files, } } diff --git a/src/types.ts b/src/types.ts index f98ab9f..8aa06a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,10 +32,8 @@ export type SpriteConfig = { name?: string /** Описание спрайта для generated types и manifest. */ description?: string - /** Папка с исходными SVG относительно корня модуля. По умолчанию: ./icons. */ - inputFolder?: string - /** Дополнительные SVG-файлы относительно корня модуля. По умолчанию: []. */ - inputFiles?: string[] + /** Путь или glob-шаблоны исходных SVG относительно корня модуля. По умолчанию: ./icons. */ + input?: string | string[] /** Настройки трансформации SVG. По умолчанию все включены. */ transform?: TransformOptions /** Добавлять развёрнутое предупреждение в generated-файлы. По умолчанию: true. */ @@ -47,8 +45,7 @@ export type ResolvedSpriteConfig = { mode: SpriteMode name: string description?: string - inputFolder: string | null - inputFiles: string[] + input: string[] transform: Required generatedNotice: boolean } @@ -59,7 +56,7 @@ export type SpriteFolder = { name: string /** Формат спрайта. */ format: SpriteFormat - /** Абсолютный путь к папке (для input-папки) или null (для input-массива). */ + /** Абсолютный путь для единственного input-каталога, иначе null. */ path: string | null /** Абсолютные пути к SVG-файлам. */ files: string[] diff --git a/test/cli-input.test.mjs b/test/cli-input.test.mjs new file mode 100644 index 0000000..d138908 --- /dev/null +++ b/test/cli-input.test.mjs @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict' +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import test from 'node:test' + +test('CLI accepts repeated --input paths and glob patterns', () => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-cli-input-')) + const iconsDir = path.join(rootDir, 'icons') + fs.mkdirSync(iconsDir) + fs.writeFileSync(path.join(iconsDir, 'check.svg'), '') + fs.writeFileSync(path.join(iconsDir, 'close.svg'), '') + + const result = spawnSync(process.execPath, [ + 'dist/cli.js', + '--mode=standalone', + '--name=cli-icons', + '--input', + 'icons/*.svg', + '--input', + '!icons/close.svg', + rootDir, + ], { + cwd: path.resolve('.'), + encoding: 'utf8', + }) + + assert.equal(result.status, 0, result.stderr) + const sprite = fs.readFileSync(path.join(rootDir, '.svg-sprite', 'sprite.svg'), 'utf8') + assert.match(sprite, /id="check"/) + assert.doesNotMatch(sprite, /id="close"/) +}) + +test('CLI rejects removed input flags', () => { + const result = spawnSync(process.execPath, [ + 'dist/cli.js', + '--input-folder=icons', + '.', + ], { + cwd: path.resolve('.'), + encoding: 'utf8', + }) + + assert.notEqual(result.status, 0) + assert.match(result.stderr, /Unknown argument: --input-folder=icons/) +}) diff --git a/test/documentation-guides.test.mjs b/test/documentation-guides.test.mjs new file mode 100644 index 0000000..e23c37f --- /dev/null +++ b/test/documentation-guides.test.mjs @@ -0,0 +1,76 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import test from 'node:test' + +const rootDir = path.resolve(import.meta.dirname, '..') +const docsDir = path.join(rootDir, 'docs') +const guideModes = new Map([ + ['standalone.md', 'standalone'], + ['standalone-vite.md', 'standalone@vite'], + ['standalone-webpack.md', 'standalone@webpack'], + ['react-vite.md', 'react@vite'], + ['react-webpack.md', 'react@webpack'], + ['next-app-turbopack.md', 'next@app/turbopack'], + ['next-app-webpack.md', 'next@app/webpack'], + ['next-pages-turbopack.md', 'next@pages/turbopack'], + ['next-pages-webpack.md', 'next@pages/webpack'], +]) + +const sectionHeadings = { + en: ['Generate the sprite', 'Debug and preview', 'Type the config'], + ru: ['Генерация спрайта', 'Дебаг и превью', 'Типизация конфига'], +} + +function markdownFiles(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const entryPath = path.join(directory, entry.name) + return entry.isDirectory() + ? markdownFiles(entryPath) + : entry.name.endsWith('.md') ? [entryPath] : [] + }) +} + +test('exact-mode guides are reusable by docs and skills', () => { + for (const language of ['en', 'ru']) { + const guidesDir = path.join(docsDir, language, 'guides') + const guideFiles = fs.readdirSync(guidesDir) + .filter((file) => file !== 'README.md') + .sort() + assert.deepEqual(guideFiles, [...guideModes.keys()].sort()) + + for (const [file, mode] of guideModes) { + const source = fs.readFileSync(path.join(guidesDir, file), 'utf8') + assert.equal(source.match(/^# /gm)?.length, 1, `${language}/${file} must have one H1`) + const headings = source.match(/^## .+$/gm)?.map((heading) => ( + heading.replace(/^## (?:\d+\. )?/, '') + )) + assert.deepEqual(headings, sectionHeadings[language]) + assert.match(source, /npx --yes (?:--package=@gromlab\/svg-sprites@latest svg-sprites|@gromlab\/svg-sprites@latest)/) + assert.match(source, /npm install --save-dev @gromlab\/svg-sprites/) + assert.match(source, new RegExp(`mode: '${mode.replaceAll('/', '\\/')}'`)) + assert.doesNotMatch(source, /\]\([^)]+\)/, `${language}/${file} must not depend on its location`) + + const generation = source.indexOf(sectionHeadings[language][0]) + const preview = source.indexOf(sectionHeadings[language][1]) + const typing = source.indexOf(sectionHeadings[language][2]) + assert.ok(generation < preview && preview < typing) + } + } +}) + +test('all local documentation links resolve', () => { + for (const file of markdownFiles(docsDir)) { + const source = fs.readFileSync(file, 'utf8') + for (const match of source.matchAll(/\]\(([^)]+)\)/g)) { + const target = match[1] + if (/^(?:https?:|mailto:|#)/.test(target)) continue + const targetPath = decodeURIComponent(target.split('#')[0]) + assert.equal( + fs.existsSync(path.resolve(path.dirname(file), targetPath)), + true, + `${path.relative(rootDir, file)} links to missing ${target}`, + ) + } + } +}) diff --git a/test/mode-contracts.test.mjs b/test/mode-contracts.test.mjs index 048079b..beb973e 100644 --- a/test/mode-contracts.test.mjs +++ b/test/mode-contracts.test.mjs @@ -14,7 +14,6 @@ const GENERATED_FILES = [ 'index.js', 'react', 'sprite.svg', - 'state.json', 'svg-sprite.manifest.d.ts', 'svg-sprite.manifest.js', ] @@ -73,6 +72,7 @@ test('generates the current isolated React Vite contract', async () => { const componentTypes = fs.readFileSync(path.join(generatedDir, 'react', 'react-component.d.ts'), 'utf8') const iconData = fs.readFileSync(path.join(generatedDir, 'icon-data.js'), 'utf8') const manifest = fs.readFileSync(result.manifestPath, 'utf8') + const manifestTypes = fs.readFileSync(path.join(generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8') assert.match(component, /export const FileManagerIcon/) assert.match(component, /sprite\.svg\?no-inline/) @@ -83,23 +83,29 @@ test('generates the current isolated React Vite contract', async () => { assert.match(manifest, /"mode": "react@vite"/) assert.match(manifest, /"target": "vite"/) assert.match(manifest, /"iconCount": 3/) + assert.match(manifestTypes, /export type SpriteManifest/) + assert.match(manifestTypes, /mode: 'react@vite'/) + assert.doesNotMatch(manifestTypes, /@gromlab\/svg-sprites\/react/) - const state = JSON.parse(fs.readFileSync(path.join(generatedDir, 'state.json'), 'utf8')) - assert.deepEqual(state.owner, { mode: 'react@vite', contractVersion: 5 }) - assert.equal(state.files.includes('.svg-sprite/react/react-component.js'), true) }) test('supports Webpack target and a custom icons directory', async () => { const { rootDir, configPath } = createReactFixture({ name: 'documents', - inputFolder: './assets', + input: './assets', generatedNotice: false, }) fs.renameSync(path.join(rootDir, 'icons'), path.join(rootDir, 'assets')) + fs.mkdirSync(path.join(rootDir, 'assets', 'nested')) + fs.writeFileSync( + path.join(rootDir, 'assets', 'nested', 'ignored.svg'), + '', + ) const result = await generateReactSprite(configPath, 'webpack') const component = fs.readFileSync(path.join(result.generatedDir, 'react', 'react-component.js'), 'utf8') const manifest = fs.readFileSync(result.manifestPath, 'utf8') + const manifestTypes = fs.readFileSync(path.join(result.generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8') assert.equal(result.name, 'documents') assert.equal(result.iconCount, 3) @@ -109,9 +115,11 @@ test('supports Webpack target and a custom icons directory', async () => { assert.match(component, /DocumentsIcon/) assert.match(component, /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/) assert.match(manifest, /"target": "webpack"/) + assert.match(manifestTypes, /mode: 'react@webpack'/) + assert.doesNotMatch(manifestTypes, /@gromlab\/svg-sprites\/react/) }) -test('merges inputFolder and inputFiles and deduplicates identical paths', async () => { +test('combines input paths and glob patterns and deduplicates identical files', async () => { const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-inputs-')) const rootDir = path.join(temporaryDir, 'svg-sprite') const iconsDir = path.join(rootDir, 'icons') @@ -120,7 +128,7 @@ test('merges inputFolder and inputFiles and deduplicates identical paths', async fs.writeFileSync(path.join(iconsDir, 'local.svg'), '') fs.writeFileSync(sharedIcon, '') const configPath = path.join(rootDir, 'config.ts') - fs.writeFileSync(configPath, `export default { name: 'mixed-inputs', inputFiles: [${JSON.stringify(path.relative(rootDir, sharedIcon))}, ${JSON.stringify(path.relative(rootDir, sharedIcon))}] }`) + fs.writeFileSync(configPath, `export default { name: 'mixed-inputs', input: ['./icons/*.svg', ${JSON.stringify(path.relative(rootDir, sharedIcon))}, ${JSON.stringify(path.relative(rootDir, sharedIcon))}] }`) const result = await generateReactSprite(configPath, 'vite') const sprite = fs.readFileSync(result.spritePath, 'utf8') @@ -130,7 +138,7 @@ test('merges inputFolder and inputFiles and deduplicates identical paths', async assert.match(sprite, /id="shared"/) }) -test('supports inputFiles without the default icons directory', async () => { +test('supports one input file without the default icons directory', async () => { const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-files-')) const rootDir = path.join(temporaryDir, 'svg-sprite') const sharedIcon = path.join(temporaryDir, 'shared', 'only.svg') @@ -138,14 +146,49 @@ test('supports inputFiles without the default icons directory', async () => { fs.mkdirSync(path.dirname(sharedIcon), { recursive: true }) fs.writeFileSync(sharedIcon, '') const configPath = path.join(rootDir, 'config.ts') - fs.writeFileSync(configPath, `export default { inputFiles: [${JSON.stringify(path.relative(rootDir, sharedIcon))}] }`) + fs.writeFileSync(configPath, `export default { input: ${JSON.stringify(path.relative(rootDir, sharedIcon))} }`) const result = await generateReactSprite(configPath, 'webpack') assert.equal(result.iconCount, 1) assert.match(fs.readFileSync(result.spritePath, 'utf8'), /id="only"/) }) -test('rejects duplicate names and explicitly missing input folders', async () => { +test('supports recursive input globs and exclusions', async () => { + const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-globs-')) + const rootDir = path.join(temporaryDir, 'svg-sprite') + const nestedDir = path.join(rootDir, 'icons', 'nested') + fs.mkdirSync(nestedDir, { recursive: true }) + fs.writeFileSync(path.join(rootDir, 'icons', 'local.svg'), '') + fs.writeFileSync(path.join(nestedDir, 'included.svg'), '') + fs.writeFileSync(path.join(nestedDir, 'draft.svg'), '') + const configPath = path.join(rootDir, 'config.ts') + fs.writeFileSync(configPath, "export default { input: ['icons/**/*.svg', '!icons/**/draft.svg'] }") + + const result = await generateReactSprite(configPath, 'vite') + const sprite = fs.readFileSync(result.spritePath, 'utf8') + assert.equal(result.iconCount, 2) + assert.match(sprite, /id="local"/) + assert.match(sprite, /id="included"/) + assert.doesNotMatch(sprite, /id="draft"/) +}) + +test('treats an existing excluded path as a literal', async () => { + const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-literal-exclusion-')) + const rootDir = path.join(temporaryDir, 'svg-sprite') + const iconsDir = path.join(rootDir, 'icons') + fs.mkdirSync(iconsDir, { recursive: true }) + fs.writeFileSync(path.join(iconsDir, 'icon[1].svg'), '') + fs.writeFileSync(path.join(iconsDir, 'icon1.svg'), '') + const configPath = path.join(rootDir, 'config.ts') + fs.writeFileSync(configPath, "export default { input: ['icons/*.svg', '!icons/icon[1].svg'] }") + + const result = await generateReactSprite(configPath, 'vite') + const sprite = fs.readFileSync(result.spritePath, 'utf8') + assert.equal(result.iconCount, 1) + assert.match(sprite, /id="icon1"/) +}) + +test('rejects invalid, missing, and conflicting inputs', async () => { const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-input-errors-')) const rootDir = path.join(temporaryDir, 'svg-sprite') const first = path.join(temporaryDir, 'one', 'shared.svg') @@ -156,12 +199,24 @@ test('rejects duplicate names and explicitly missing input folders', async () => fs.writeFileSync(first, '') fs.writeFileSync(second, '') const duplicateConfig = path.join(rootDir, 'duplicate.ts') - fs.writeFileSync(duplicateConfig, `export default { inputFiles: [${JSON.stringify(path.relative(rootDir, first))}, ${JSON.stringify(path.relative(rootDir, second))}] }`) + fs.writeFileSync(duplicateConfig, `export default { input: [${JSON.stringify(path.relative(rootDir, first))}, ${JSON.stringify(path.relative(rootDir, second))}] }`) await assert.rejects(generateReactSprite(duplicateConfig, 'vite'), /produce the same SVG id "shared"/) const missingConfig = path.join(rootDir, 'missing.ts') - fs.writeFileSync(missingConfig, `export default { inputFolder: './missing', inputFiles: [${JSON.stringify(path.relative(rootDir, first))}] }`) - await assert.rejects(generateReactSprite(missingConfig, 'vite'), /Input directory does not exist/) + fs.writeFileSync(missingConfig, "export default { input: './missing' }") + await assert.rejects(generateReactSprite(missingConfig, 'vite'), /Input path does not exist/) + + const unmatchedConfig = path.join(rootDir, 'unmatched.ts') + fs.writeFileSync(unmatchedConfig, "export default { input: './missing/*.svg' }") + await assert.rejects(generateReactSprite(unmatchedConfig, 'vite'), /Input matched no SVG files/) + + const emptyConfig = path.join(rootDir, 'empty.ts') + fs.writeFileSync(emptyConfig, 'export default { input: [] }') + await assert.rejects(generateReactSprite(emptyConfig, 'vite'), /"input" must be a non-empty string or an array/) + + const legacyConfig = path.join(rootDir, 'legacy.ts') + fs.writeFileSync(legacyConfig, "export default { inputFolder: './icons' }") + await assert.rejects(generateReactSprite(legacyConfig, 'vite'), /"inputFolder" and "inputFiles" are no longer supported/) }) test('applies transform options to SVG and React styles', async () => { @@ -195,6 +250,7 @@ test('generates every Next.js exact mode without a client boundary', async () => const result = await generateNextSprite(configPath, options) const component = fs.readFileSync(path.join(result.generatedDir, 'react', 'react-component.js'), 'utf8') const manifest = fs.readFileSync(result.manifestPath, 'utf8') + const manifestTypes = fs.readFileSync(path.join(result.generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8') const mode = `next@${options.router}/${options.bundler}` assert.equal(result.target, mode) assert.equal(result.router, options.router) @@ -202,31 +258,33 @@ test('generates every Next.js exact mode without a client boundary', async () => assert.match(component, /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/) assert.doesNotMatch(component, /['"]use client['"]/) assert.match(manifest, new RegExp(`"target": ${JSON.stringify(mode)}`)) + assert.match(manifestTypes, new RegExp(`mode: ${JSON.stringify(mode).replaceAll('"', "'")}`)) + assert.doesNotMatch(manifestTypes, /@gromlab\/svg-sprites\/react/) } }) -test('writer removes only obsolete managed files and preserves user files', async () => { +test('writer replaces the generated directory and preserves root user files', async () => { const { rootDir, configPath } = createReactFixture() await generateReactSprite(configPath, 'vite') - const statePath = path.join(rootDir, '.svg-sprite', 'state.json') - const state = JSON.parse(fs.readFileSync(statePath, 'utf8')) - state.files.push('.svg-sprite/obsolete.js') - fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`) fs.writeFileSync(path.join(rootDir, '.svg-sprite', 'obsolete.js'), '/* @generated by @gromlab/svg-sprites. */\n') - fs.writeFileSync(path.join(rootDir, '.svg-sprite', 'notes.md'), 'Keep me\n') + fs.writeFileSync(path.join(rootDir, '.svg-sprite', 'notes.md'), 'Remove me\n') fs.writeFileSync(path.join(rootDir, 'index.ts'), 'export const userCode = true\n') await generateReactSprite(configPath, 'vite') assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'obsolete.js')), false) - assert.equal(fs.readFileSync(path.join(rootDir, '.svg-sprite', 'notes.md'), 'utf8'), 'Keep me\n') + assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'notes.md')), false) assert.equal(fs.readFileSync(path.join(rootDir, 'index.ts'), 'utf8'), 'export const userCode = true\n') }) -test('writer refuses user content and symbolic links in managed paths', async () => { +test('writer replaces a reserved output directory and refuses symbolic links', async () => { const first = createReactFixture() fs.mkdirSync(path.join(first.rootDir, '.svg-sprite')) fs.writeFileSync(path.join(first.rootDir, '.svg-sprite', 'index.js'), 'export const userCode = true\n') - await assert.rejects(generateReactSprite(first.configPath, 'vite'), /Refusing to overwrite a user file/) + await generateReactSprite(first.configPath, 'vite') + assert.doesNotMatch( + fs.readFileSync(path.join(first.rootDir, '.svg-sprite', 'index.js'), 'utf8'), + /userCode/, + ) const second = createReactFixture() const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-outside-')) @@ -235,6 +293,24 @@ test('writer refuses user content and symbolic links in managed paths', async () assert.deepEqual(fs.readdirSync(outsideDir), []) }) +test('writer recovers an interrupted directory replacement', async () => { + const { rootDir, configPath } = createReactFixture() + await generateReactSprite(configPath, 'vite') + const generatedDir = path.join(rootDir, '.svg-sprite') + const backupDir = path.join(rootDir, '.svg-sprite.interrupted.old') + const stagingDir = path.join(rootDir, '.svg-sprite.interrupted.tmp') + fs.renameSync(generatedDir, backupDir) + fs.mkdirSync(stagingDir) + fs.writeFileSync(path.join(stagingDir, 'partial.js'), 'partial\n') + + await generateReactSprite(configPath, 'vite') + + assert.equal(fs.existsSync(generatedDir), true) + assert.equal(fs.existsSync(backupDir), false) + assert.equal(fs.existsSync(stagingDir), false) + assert.equal(fs.existsSync(path.join(generatedDir, 'partial.js')), false) +}) + test('rejects colliding SVG shape IDs', async () => { const { iconsDir, configPath } = createReactFixture() const id = `icon-${createHash('sha256').update('folder open').digest('hex').slice(0, 16)}` diff --git a/test/standalone-icon-element.test.mjs b/test/standalone-icon-element.test.mjs new file mode 100644 index 0000000..f4b4cb4 --- /dev/null +++ b/test/standalone-icon-element.test.mjs @@ -0,0 +1,104 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import test from 'node:test' +import { pathToFileURL } from 'node:url' + +import { JSDOM } from 'jsdom' + +import { generateSprite } from '../dist/index.js' + +const CHECK_SVG = '' +const UNSAFE_SVG = '' + +test('standalone Webpack icon element renders generated icons without external runtime dependencies', async (context) => { + const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-icon-element-')) + const iconsDir = path.join(rootDir, 'icons') + fs.mkdirSync(iconsDir) + fs.writeFileSync(path.join(rootDir, 'package.json'), '{"type":"module"}') + fs.writeFileSync(path.join(iconsDir, 'check.svg'), CHECK_SVG) + fs.writeFileSync(path.join(iconsDir, 'folder open.svg'), UNSAFE_SVG) + + await generateSprite(rootDir, { + mode: 'standalone@webpack', + name: 'app-icons', + generatedNotice: false, + }) + + const entryUrl = pathToFileURL(path.join(rootDir, '.svg-sprite', 'index.js')).href + const generated = await import(entryUrl) + assert.equal(generated.appIconsIconTagName, 'app-icons-icon') + assert.doesNotThrow(() => generated.defineAppIconsIconElement()) + + const dom = new JSDOM('', { url: 'https://example.test/' }) + const originals = new Map() + for (const [name, value] of Object.entries({ + HTMLElement: dom.window.HTMLElement, + customElements: dom.window.customElements, + })) { + originals.set(name, Object.getOwnPropertyDescriptor(globalThis, name)) + Object.defineProperty(globalThis, name, { configurable: true, writable: true, value }) + } + + const browserErrors = [] + const originalConsoleError = console.error + console.error = (...values) => browserErrors.push(values.join(' ')) + + context.after(() => { + console.error = originalConsoleError + dom.window.close() + for (const [name, descriptor] of originals) { + if (descriptor) Object.defineProperty(globalThis, name, descriptor) + else delete globalThis[name] + } + }) + + const icon = dom.window.document.createElement('app-icons-icon') + icon.icon = 'folder open' + dom.window.document.body.append(icon) + + generated.defineAppIconsIconElement() + generated.defineAppIconsIconElement() + + assert.equal(icon.icon, 'folder open') + assert.equal(icon.getAttribute('icon'), 'folder open') + assert.equal(icon.shadowRoot.querySelector('svg').getAttribute('viewBox'), '0 0 16 16') + assert.equal( + icon.shadowRoot.querySelector('use').getAttribute('href'), + generated.getAppIconsIconHref('folder open'), + ) + assert.match(icon.shadowRoot.querySelector('use').getAttribute('href'), /#icon-[a-f0-9]{16}$/) + + icon.icon = 'check' + assert.equal(icon.shadowRoot.querySelector('svg').getAttribute('viewBox'), '0 0 24 24') + assert.match(icon.shadowRoot.querySelector('use').getAttribute('href'), /sprite\.svg#check$/) + assert.throws(() => { icon.icon = 'missing' }, /Unknown app-icons icon: missing/) + + icon.setAttribute('icon', 'missing') + assert.equal(icon.icon, null) + assert.equal(icon.shadowRoot.querySelector('use').hasAttribute('href'), false) + assert.equal(icon.shadowRoot.querySelector('svg').hasAttribute('hidden'), true) + assert.deepEqual(browserErrors, [': unknown icon "missing".']) + + const collisionDom = new JSDOM('') + Object.defineProperty(globalThis, 'HTMLElement', { + configurable: true, + writable: true, + value: collisionDom.window.HTMLElement, + }) + Object.defineProperty(globalThis, 'customElements', { + configurable: true, + writable: true, + value: collisionDom.window.customElements, + }) + collisionDom.window.customElements.define( + 'app-icons-icon', + class extends collisionDom.window.HTMLElement {}, + ) + assert.throws( + () => generated.defineAppIconsIconElement(), + /Cannot define : this custom element name is already registered/, + ) + collisionDom.window.close() +}) diff --git a/test/standalone-mode.test.mjs b/test/standalone-mode.test.mjs index d44bf48..77de464 100644 --- a/test/standalone-mode.test.mjs +++ b/test/standalone-mode.test.mjs @@ -44,9 +44,9 @@ test('standalone generates an asset and deployment-neutral JSON manifest', async assert.equal(result.manifestPath, path.join(rootDir, '.svg-sprite', 'svg-sprite.manifest.json')) assert.deepEqual(generatedFiles(rootDir), [ 'sprite.svg', - 'state.json', 'svg-sprite.manifest.json', ]) + assert.equal(fs.existsSync(path.join(rootDir, '.gitignore')), false) const source = fs.readFileSync(result.spritePath, 'utf8') assert.match(source, /@generated by @gromlab\/svg-sprites/) @@ -65,8 +65,20 @@ test('standalone generates an asset and deployment-neutral JSON manifest', async /^icon-[a-f0-9]{16}$/, ) - const state = JSON.parse(fs.readFileSync(path.join(rootDir, '.svg-sprite', 'state.json'), 'utf8')) - assert.deepEqual(state.owner, { mode: 'standalone', contractVersion: 2 }) +}) + +test('standalone preserves a user-owned .gitignore', async () => { + const rootDir = fixture() + const gitignorePath = path.join(rootDir, '.gitignore') + fs.writeFileSync(gitignorePath, '/public/cache/\n') + + await generateSprite(rootDir, { + mode: 'standalone', + name: 'app-icons', + generatedNotice: false, + }) + + assert.equal(fs.readFileSync(gitignorePath, 'utf8'), '/public/cache/\n') }) test('standalone Vite generates a typed facade and resolved manifest', async () => { @@ -78,13 +90,13 @@ test('standalone Vite generates a typed facade and resolved manifest', async () }) assert.equal(result.target, 'vite') + assert.equal(fs.existsSync(path.join(rootDir, '.gitignore')), true) assert.deepEqual(generatedFiles(rootDir), [ 'icon-data.d.ts', 'icon-data.js', 'index.d.ts', 'index.js', 'sprite.svg', - 'state.json', 'svg-sprite.manifest.d.ts', 'svg-sprite.manifest.js', ]) @@ -92,13 +104,26 @@ test('standalone Vite generates a typed facade and resolved manifest', async () const entry = fs.readFileSync(path.join(rootDir, '.svg-sprite', 'index.js'), 'utf8') const declarations = fs.readFileSync(path.join(rootDir, '.svg-sprite', 'index.d.ts'), 'utf8') const manifest = fs.readFileSync(result.manifestPath, 'utf8') + const manifestDeclarations = fs.readFileSync(path.join(rootDir, '.svg-sprite', 'svg-sprite.manifest.d.ts'), 'utf8') assert.match(entry, /sprite\.svg\?no-inline/) assert.match(entry, /export const iconsSpriteUrl = spriteUrl/) + assert.match(entry, /export const iconsIconTagName = "icons-icon"/) assert.match(entry, /export function getIconsIconHref\(icon\)/) + assert.match(entry, /export function defineIconsIconElement\(\)/) + assert.match(entry, /class IconsIconElement extends globalThis\.HTMLElement/) + assert.match(entry, /this\.attachShadow\(\{ mode: 'open' \}\)/) + assert.match(entry, /"folder open": "0 0 16 16"/) assert.match(declarations, /getIconsIconHref\(icon: IconsIconName\): string/) + assert.match(declarations, /export interface IconsIconElement extends HTMLElement/) + assert.match(declarations, /icon: IconsIconName \| null/) + assert.match(declarations, /"icons-icon": IconsIconElement/) assert.match(manifest, /"mode": "standalone@vite"/) assert.match(manifest, /export const spriteManifest = createSpriteManifest\(spriteUrl\)/) - assert.doesNotMatch(entry, /react|jsx|\.css/) + assert.match(manifestDeclarations, /export type SpriteManifestData/) + assert.match(manifestDeclarations, /mode: 'standalone@vite'/) + assert.doesNotMatch(manifestDeclarations, /from '@gromlab\/svg-sprites'/) + assert.doesNotMatch(entry, /react|jsx|\.css|\blit\b/i) + }) test('standalone Webpack generates a typed facade and resolved manifest', async () => { @@ -112,31 +137,39 @@ test('standalone Webpack generates a typed facade and resolved manifest', async assert.equal(result.target, 'webpack') const entry = fs.readFileSync(path.join(rootDir, '.svg-sprite', 'index.js'), 'utf8') const manifest = fs.readFileSync(result.manifestPath, 'utf8') + const manifestDeclarations = fs.readFileSync(path.join(rootDir, '.svg-sprite', 'svg-sprite.manifest.d.ts'), 'utf8') assert.match(entry, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/) assert.match(entry, /export function getIconsIconHref\(icon\)/) + assert.match(entry, /export const iconsIconTagName = "icons-icon"/) + assert.match(entry, /export function defineIconsIconElement\(\)/) assert.match(manifest, /"mode": "standalone@webpack"/) assert.match(manifest, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/) - assert.doesNotMatch(entry, /react|jsx|\.css/) + assert.match(manifestDeclarations, /export type SpriteManifestData/) + assert.match(manifestDeclarations, /mode: 'standalone@webpack'/) + assert.doesNotMatch(manifestDeclarations, /from '@gromlab\/svg-sprites'/) + assert.doesNotMatch(entry, /react|jsx|\.css|\blit\b/i) + }) -test('switching standalone modes removes obsolete managed files', async () => { +test('switching standalone modes replaces the generated directory', async () => { const rootDir = fixture() const options = { name: 'icons', generatedNotice: false } await generateSprite(rootDir, { ...options, mode: 'standalone@vite' }) + assert.equal(fs.existsSync(path.join(rootDir, '.gitignore')), true) fs.writeFileSync(path.join(rootDir, '.svg-sprite', 'user.txt'), 'keep') await generateSprite(rootDir, { ...options, mode: 'standalone' }) assert.deepEqual(generatedFiles(rootDir), [ 'sprite.svg', - 'state.json', 'svg-sprite.manifest.json', - 'user.txt', ]) - assert.equal(fs.readFileSync(path.join(rootDir, '.svg-sprite', 'user.txt'), 'utf8'), 'keep') + assert.equal(fs.existsSync(path.join(rootDir, '.gitignore')), false) + assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'user.txt')), false) await generateSprite(rootDir, { ...options, mode: 'standalone@webpack' }) + assert.equal(fs.existsSync(path.join(rootDir, '.gitignore')), true) assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'svg-sprite.manifest.json')), false) assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'index.js')), true) - assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'user.txt')), true) + assert.equal(fs.existsSync(path.join(rootDir, '.svg-sprite', 'user.txt')), false) })