feat: добавить серверную генерацию спрайтов

This commit is contained in:
2026-07-16 09:14:11 +03:00
parent e9ae91815a
commit 57342fae4e
379 changed files with 3108 additions and 655 deletions

View File

@@ -13,6 +13,7 @@
- `standalone`;
- `standalone@vite`;
- `standalone@webpack`;
- `standalone@server`;
- `vue@vite`;
- `vue@webpack`;
- `nuxt@vite`;
@@ -75,6 +76,8 @@
Core не генерирует JavaScript, declarations, manifest source, CSS или framework-specific exports. Изменение общего compiler может ожидаемо изменить SVG всех modes; изменение generated source должно быть локально одному adapter.
Mode-neutral инфраструктура также может загружать и проверять manifest и готовые SVG profiles, созданные `standalone@server`. Сетевой слой не генерирует framework output и передаёт adapter только проверенный compiled artifact.
## Generated-контракт
Один config разрешается ровно в один mode и один output. Множественные modes не генерируются в один root; orchestration выполняется независимыми config/API/CLI вызовами.
@@ -87,6 +90,8 @@ Core writer полностью владеет каталогом `.svg-sprite`
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-каталог.
`standalone@server` принимает local paths/globs и HTTP(S) descriptors `{ name, url, sha256? }`, создаёт content-addressed SVG profiles и JSON manifest. Он не создаёт consumer runtime или `.gitignore`. Consumer modes с `source: "remote"` скачивают один из этих profiles до codegen и по-прежнему публикуют SVG через собственный asset pipeline.
Adapter возвращает файлы в памяти. Только core writer проверяет paths, полностью заменяет `.svg-sprite` и обновляет управляемый `.gitignore`.
## Зависимости

View File

@@ -42,7 +42,7 @@
`SpriteViewer` рендерит все спрайты проекта в одном месте и показывает, какие иконки вошли в каждый набор и как они выглядят. Для каждой иконки видны созданные CSS-переменные и их fallback-цвета. Значения можно менять прямо в Viewer и сразу наблюдать результат. Здесь же доступны готовые примеры для framework из manifest, `<svg><use>`, `<img>` и CSS.
11. **29 exact modes**
11. **29 consumer exact modes и `standalone@server`**
Изолированные adapters создают нативные компоненты для standalone, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Astro, Solid, SolidStart, Preact, Qwik, Lit и Alpine.js. Каждый exact mode учитывает свой Vite, Webpack, Turbopack или application-builder контракт.

View File

@@ -14,6 +14,7 @@ Mode отмечается поддержанным только после за
- [x] `standalone`
- [x] `standalone@vite`
- [x] `standalone@webpack`
- [x] `standalone@server` (server release + remote `react@vite` integration)
## React

View File

@@ -4,7 +4,7 @@
![npm](https://img.shields.io/npm/v/@gromlab/svg-sprites) ![license](https://img.shields.io/npm/l/@gromlab/svg-sprites)
`@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.
`@gromlab/svg-sprites` is a CLI tool for generating SVG sprites in modern web applications. It combines selected SVG icons into one or more external, cacheable sprites and prepares them for use in the UI.
Each exact mode generates a native typed component for its framework and bundler: Web Component, React, Vue, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit, or Alpine.js. In every case, the SVG remains a separate cacheable asset.
@@ -217,9 +217,19 @@ The Viewer is added only to an internal debug page and does not become part of t
With bare standalone, the application loads the Viewer as a browser script and HTML element. Bundler and framework modes use the npm Web Component entry; React and Next.js may instead import the bridge from `@gromlab/svg-sprites/react`.
## 29 exact modes
## 30 exact modes
The package provides isolated contracts for standalone, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Astro, Solid, SolidStart, Preact, Qwik, Lit, and Alpine.js across their supported Vite, Webpack, Turbopack, and application-builder variants.
The package supports 30 isolated exact modes: `standalone@server` for server-side generation of a universal SVG sprite and 29 consumer modes for modern frameworks and bundlers.
`standalone@server` lets you generate an SVG sprite ahead of time on a server or in CI/CD and publish it for shared use. The resulting sprite is not tied to a specific framework or bundler and works with every consumer mode.
The 29 consumer modes cover standalone, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Astro, Solid, SolidStart, Preact, Qwik, Lit, and Alpine.js across their supported Vite, Webpack, Turbopack, and application-builder variants.
All 29 consumer modes can work with sprites generated locally in the project or with universal sprites generated ahead of time on the server through `standalone@server`. The component API and the way icons are used in the application remain the same in both scenarios.
The integration matrix covers all 30 exact modes. A dedicated producer fixture verifies server-side generation of the universal sprite, while each of the 29 consumer applications generates and renders two independent sprites: one local and one remote.
All consumer applications pass a production build and Playwright tests, while typed modes are additionally checked by their framework-native toolchain. Every E2E test confirms that both local and remote sprites load and render, checks for browser errors, and verifies both groups in SpriteViewer.
## Clean Git history
@@ -237,7 +247,11 @@ Production components use only local generated code, styles, and the external SV
This README introduces the project's capabilities and demonstrates the primary use case. For setup, choose the guide for your stack.
### Quick start
### Server-side generation
- [Standalone + Server](docs/en/guides/standalone-server.md)
### Consumer quick starts
- [Bare standalone](docs/en/guides/standalone.md)
- [Standalone + Vite](docs/en/guides/standalone-vite.md)

View File

@@ -4,7 +4,7 @@
![npm](https://img.shields.io/npm/v/@gromlab/svg-sprites) ![license](https://img.shields.io/npm/l/@gromlab/svg-sprites)
`@gromlab/svg-sprites` — генератор SVG-спрайтов для современных веб-приложений. Он собирает выбранные SVG-иконки в один или несколько внешних кешируемых спрайтов и подготавливает их для использования в интерфейсе.
`@gromlab/svg-sprites` CLI-инструмент для генерации SVG-спрайтов в современных веб-приложениях. Он собирает выбранные SVG-иконки в один или несколько внешних кешируемых спрайтов и подготавливает их для использования в интерфейсе.
Каждый exact mode создаёт нативный типизированный компонент для своего framework и bundler: Web Component, React, Vue, Svelte, Angular, Astro, Solid, Preact, Qwik, Lit или Alpine.js. SVG во всех случаях остаётся отдельным кешируемым asset.
@@ -217,9 +217,19 @@ Viewer подключается только к внутренней debug-ст
Bare standalone подключает Viewer через browser script и HTML element. Bundler и framework modes используют npm entry Web Component; React и Next.js также могут импортировать bridge из `@gromlab/svg-sprites/react`.
## 29 exact modes
## 30 exact modes
Пакет предоставляет изолированные контракты для standalone, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Astro, Solid, SolidStart, Preact, Qwik, Lit и Alpine.js в поддерживаемых вариантах Vite, Webpack, Turbopack и application builder.
Пакет поддерживает 30 изолированных exact modes: `standalone@server` для серверной генерации универсального SVG-спрайта и 29 consumer modes для современных frameworks и bundlers.
`standalone@server` позволяет заранее сгенерировать SVG-спрайт на сервере или в CI/CD и опубликовать его для совместного использования. Такой спрайт не привязан к конкретному framework или bundler и подходит всем consumer modes.
29 consumer modes охватывают standalone, React, Next.js, Vue, Nuxt, Svelte, SvelteKit, Angular, Astro, Solid, SolidStart, Preact, Qwik, Lit и Alpine.js в поддерживаемых вариантах Vite, Webpack, Turbopack и application builder.
Все 29 consumer modes могут работать как со спрайтами, сгенерированными локально в проекте, так и с универсальными спрайтами, заранее сгенерированными на сервере через `standalone@server`. API компонентов и способ использования иконок в приложении в обоих сценариях остаются одинаковыми.
Интеграционная матрица охватывает все 30 exact modes. Отдельный producer-стенд проверяет серверную генерацию универсального спрайта, а каждое из 29 consumer-приложений генерирует и рендерит два независимых спрайта: локальный и удалённый.
Все consumer-приложения проходят production build и Playwright-тесты, а типизированные modes дополнительно проверяются штатным toolchain фреймворка. Каждый E2E-тест подтверждает, что локальный и удалённый спрайты загружаются и отрисовываются, а также проверяет отсутствие browser errors и отображение обеих групп в SpriteViewer.
## Чистый Git
@@ -237,7 +247,11 @@ Production-компоненты используют только локальн
README знакомит с возможностями проекта и показывает основной сценарий использования. Для настройки выберите руководство под свой стек.
### Быстрый старт
### Серверная генерация
- [Standalone + Server](docs/ru/guides/standalone-server.md)
### Быстрый старт для consumer modes
- [Bare standalone](docs/ru/guides/standalone.md)
- [Standalone + Vite](docs/ru/guides/standalone-vite.md)

View File

@@ -5,7 +5,7 @@ can also be used unchanged by AI skills.
The common format for JSON, JavaScript, and TypeScript config files is described in the [configuration guide](configuration.md).
## Quick Start Guides
## Consumer Quick Starts
| Project | Exact mode | Guide |
|---|---|---|
@@ -39,12 +39,16 @@ The common format for JSON, JavaScript, and TypeScript config files is described
| 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:
Every consumer guide follows the same order:
1. Generate the sprite through `npx` without adding the package to the project.
2. Use the sprite in the application.
3. Optionally add the Viewer for debugging and previews.
## Server-Side Generation
Use [`standalone@server`](guides/standalone-server.md) to generate a universal SVG sprite on a server or in CI/CD for all consumer modes.
## Reference
- [Configuration](configuration.md)

View File

@@ -32,6 +32,7 @@ JSON works for most projects and does not require installing the package locally
| Field | Default | Purpose |
|---|---|---|
| `mode` | None | Exact mode matching the framework and bundler |
| `source` | `local` | `local` for source SVG files or `remote` for a `standalone@server` manifest |
| `name` | Kebab-case module directory name; for `svg-sprite` and `svg-sprites`, the parent directory name | Sprite name; in modes with a component, it also determines the component and type names |
| `description` | None | Description used in types and the Viewer |
| `input` | `./icons` | Directory, SVG file, glob pattern, or array of sources |
@@ -40,6 +41,45 @@ JSON works for most projects and does not require installing the package locally
Paths and glob patterns in `input` are resolved relative to the config file's directory. A pattern prefixed with `!` excludes matches.
## Remotely built sprite
A consumer config for a server manifest only contains the mode, source, and input:
```json
{
"mode": "react@vite",
"source": "remote",
"input": "https://assets.example/releases/app/svg-sprite.manifest.json"
}
```
`input` accepts one HTTP(S) URL or local manifest path. The name, description,
transforms, and generated notice come from the manifest. The generator downloads
and verifies the matching SVG profile before the adapter creates its normal local
components, types, and bundler asset.
## Server build
`standalone@server` combines local paths/globs with HTTP(S) SVG descriptors:
```js
export default {
mode: 'standalone@server',
name: 'app',
input: [
'./icons/**/*.svg',
{
name: 'remote-logo',
url: 'https://assets.example/logo.svg',
},
],
}
```
The mode creates two content-addressed SVG profiles and `svg-sprite.manifest.json`.
`sha256` is optional for HTTP inputs; when present, it must be the expected 64-character
hexadecimal SHA-256 digest and the build verifies the received bytes.
## JavaScript
A JavaScript config default-exports a plain object:

View File

@@ -1,5 +1,7 @@
# Quick Start Guides
## Consumer Applications
- `standalone`: [bare standalone](standalone.md)
- `standalone@vite`: [standalone with Vite](standalone-vite.md)
- `standalone@webpack`: [standalone with Webpack](standalone-webpack.md)
@@ -29,3 +31,7 @@
- `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)
## Server-Side Generation
- `standalone@server`: [generate a universal sprite on the server](standalone-server.md)

View File

@@ -0,0 +1,113 @@
# Universal SVG Sprite Generated on a Server
Generate a universal SVG sprite in CI or a server worker for applications that use different frameworks and bundlers.
## Generate the sprite
You do not need to install the package in the worker.
### 1. Prepare the workspace
Place the source SVGs in the current workspace's `icons` directory:
```text
.
└── icons/
├── search.svg
└── settings.svg
```
Each filename without the extension becomes an icon name.
### 2. Run generation
Pass the mode, sprite name, and SVG path through the CLI:
```bash
npx --yes @gromlab/svg-sprites \
--mode standalone@server \
--name app \
--input './icons/**/*.svg' \
.
```
This worker workflow does not need a config file. The result appears in `./.svg-sprite`:
```text
.
├── icons/
│ ├── search.svg
│ └── settings.svg
└── .svg-sprite/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
### 3. Publish the result
Upload the contents of `.svg-sprite` to a dedicated S3 bucket directory:
```bash
aws s3 sync ./.svg-sprite/ s3://my-bucket/app-icons/
```
The same directory can be served through a CDN. The public URL does not contain a `.svg-sprite` segment:
```text
https://cdn.example.com/app-icons/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
You can also run `standalone@server` through a JSON, JavaScript, or TypeScript config. A config is useful for persistent settings, local SVGs from several directories, and SVGs loaded over HTTP(S).
## Use the sprite
Create a regular config in the consumer application. For example, with React and Vite:
```text
src/app-icons/
├── index.ts
└── svg-sprite.config.json
```
Set the consumer mode and the CDN manifest URL:
```json
{
"mode": "react@vite",
"source": "remote",
"input": "https://cdn.example.com/app-icons/svg-sprite.manifest.json"
}
```
Add a user-owned entry point:
```ts
// src/app-icons/index.ts
export * from './.svg-sprite/index.js'
```
Run normal generation:
```bash
npx --yes @gromlab/svg-sprites src/app-icons/svg-sprite.config.json
```
Then use the generated component exactly as with a sprite built from local SVGs:
```tsx
import { AppIcon } from './app-icons'
export function SearchButton() {
return <AppIcon icon="search" aria-label="Search" />
}
```
The same CDN manifest works with all 29 consumer modes. Each one preserves the native API of its selected framework and bundler.
## Debug and preview
`standalone@server` does not create a separate icon preview page. Connect the published sprite to a consumer application and open it in SpriteViewer: the remote set appears in the same way as a local one.

View File

@@ -30,7 +30,8 @@ result.manifestPath
Next.js modes additionally return `router` and `bundler`.
For bare `standalone`, `target` is `static`; standalone bundler and React modes
return `vite` or `webpack`; Next.js modes return their full exact mode as the
target.
target. `standalone@server` returns `server`; its `spritePath` identifies the
standard content-addressed profile and `manifestPath` identifies the server manifest.
For static standalone mode, use `result.spritePath` in a build script to publish the
SVG under an application URL:
@@ -105,6 +106,11 @@ export default defineSpriteConfig({
`defineSpriteConfig` is an identity helper for TypeScript autocomplete. JavaScript can export the same object with `export default`, while JSON contains the object directly.
The public `ServerSvgInput`, `ServerSpriteManifest`, `ServerSpriteAsset`, and
`SpriteCompileProfile` types describe `standalone@server` inputs and release data.
A consumer uses the same API with `source: 'remote'` and one local path or HTTP(S)
manifest URL in `input`.
## Specialized wrappers
The specialized functions are available as wrappers around `generateSprite`:

View File

@@ -69,6 +69,7 @@ svg-sprites [options] <config-file-or-directory>
| Static HTML / custom publishing | `standalone` |
| Standalone + Vite | `standalone@vite` |
| Standalone + Webpack 5 | `standalone@webpack` |
| Server release | `standalone@server` |
| React + Vite | `react@vite` |
| React + Webpack 5 | `react@webpack` |
| Vue + Vite | `vue@vite` |
@@ -100,7 +101,7 @@ The config file may have any name and use the `.ts`, `.js`, or `.json` extension
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`.
`--help` and `-h` print usage information without requiring a path. Generation options are `--mode`, `--name`, `--description`, repeatable `--input <path-or-glob>`, 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.
`--help` and `-h` print usage information without requiring a path. Generation options are `--mode`, `--source <local|remote>`, `--name`, `--description`, repeatable `--input <path-or-glob>`, 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:
@@ -138,12 +139,20 @@ export default defineSpriteConfig({
| Option | Type | Default | Purpose |
|---|---|---|---|
| `mode` | `SpriteMode` | None | Generation mode; may be supplied by CLI/API |
| `source` | `local \| remote` | `local` | Source SVG files or a ready server manifest |
| `name` | `string` | Derived from the directory | Sprite name; in modes with a component, it also determines the component and public type names |
| `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 |
| `input` | `SpriteInput \| SpriteInput[]` | `./icons` | Local SVG sources, server HTTP descriptors, or one remote manifest, depending on mode and source |
| `transform` | `TransformOptions` | All enabled | SVG preparation settings |
| `generatedNotice` | `boolean` | `true` | Full or abbreviated warning in generated files |
With `source: 'remote'`, `input` contains one local path or HTTP(S) URL to a
manifest produced by `standalone@server`. A remote consumer config may contain
only `mode`, `source`, and `input`: the name, description, transforms, and generated
notice are verified and inherited from the server manifest. Generation downloads
the profile required by the exact consumer mode and verifies its SHA-256 and byte
length before codegen. There is no runtime network dependency on the server manifest.
### Sprite name
`name` is written in kebab-case and must start with an ASCII letter:
@@ -176,6 +185,26 @@ Supported glob syntax includes:
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.
### Server SVG inputs
`standalone@server` accepts the same local strings plus HTTP(S) descriptors in its
`input` array:
```ts
{
name: 'brand-logo',
url: 'https://assets.example.com/brand-logo.svg',
sha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
}
```
`name` becomes the public icon name. `sha256` is optional and, when present, is
checked against the downloaded bytes. URL credentials and active SVG content such
as scripts, event handlers, `foreignObject`, or a doctype are rejected. One HTTP
source is limited to 2 MiB, all combined sources to 25 MiB, and requests time out
after 15 seconds. Local and HTTP entries share one namespace, so duplicate icon
names fail generation.
## Generated module
After generation, a React or Next.js sprite directory looks like this:
@@ -225,6 +254,20 @@ runtime asset and deployment-neutral manifest data:
native generated Web Component with no external runtime dependencies. Bare
`standalone` intentionally does not generate a JavaScript component.
`standalone@server` creates a publishable release without JavaScript runtime or
`.gitignore`:
```text
.svg-sprite/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
The manifest identifies both compile profiles by relative `href`, full SHA-256,
and byte length. Publish the complete directory atomically; consumers resolve each
profile relative to the manifest URL or local manifest path.
The generator fully manages `.svg-sprite` and replaces the whole directory on every generation through a staged write with rollback on replacement failure. Any files added inside it are deleted during the next generation. Keep user-owned files alongside it, for example in a root `index.ts` barrel:
```ts

View File

@@ -5,7 +5,7 @@
Общий формат JSON, JavaScript и TypeScript config-файлов описан в [руководстве по конфигурации](configuration.md).
## Гайды быстрого старта
## Быстрый старт для consumer modes
| Проект | Exact mode | Guide |
|---|---|---|
@@ -39,12 +39,16 @@
| 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 используют один порядок:
Все consumer guides используют один порядок:
1. Генерация спрайта через `npx` без добавления package в проект.
2. Использование спрайта в приложении.
3. Необязательное подключение Viewer для дебага и превью.
## Серверная генерация
Используйте [`standalone@server`](guides/standalone-server.md), чтобы сгенерировать на сервере или в CI/CD универсальный SVG-спрайт для всех consumer modes.
## Справочники
- [Конфигурация](configuration.md)

View File

@@ -32,6 +32,7 @@ JSON подходит для большинства проектов и не т
| Поле | По умолчанию | Назначение |
|---|---|---|
| `mode` | Нет | Exact mode, соответствующий framework и сборщику |
| `source` | `local` | `local` для исходных SVG или `remote` для manifest от `standalone@server` |
| `name` | Kebab-case имени каталога модуля; для `svg-sprite` и `svg-sprites` — имени родительского каталога | Имя спрайта; в modes с компонентом также задаёт имя компонента и типов |
| `description` | Нет | Описание для типов и Viewer |
| `input` | `./icons` | Каталог, SVG-файл, glob-шаблон или массив источников |
@@ -40,6 +41,45 @@ JSON подходит для большинства проектов и не т
Пути и glob-шаблоны в `input` считаются от каталога config-файла. Паттерн с префиксом `!` исключает совпадения.
## Удалённо собранный спрайт
Consumer config для server manifest содержит только mode, source и input:
```json
{
"mode": "react@vite",
"source": "remote",
"input": "https://assets.example/releases/app/svg-sprite.manifest.json"
}
```
`input` принимает один HTTP(S) URL или локальный путь к manifest. Имя, описание,
transforms и generated notice берутся из manifest. Генератор скачивает и проверяет
подходящий SVG profile, после чего adapter создаёт обычные локальные компоненты,
типы и asset для сборщика.
## Серверная сборка
`standalone@server` объединяет local paths/globs и HTTP(S) SVG descriptors:
```js
export default {
mode: 'standalone@server',
name: 'app',
input: [
'./icons/**/*.svg',
{
name: 'remote-logo',
url: 'https://assets.example/logo.svg',
},
],
}
```
Mode создаёт два content-addressed SVG profiles и `svg-sprite.manifest.json`.
`sha256` у HTTP input необязателен; если он указан, это должен быть ожидаемый
64-символьный hexadecimal SHA-256 digest, по которому сборка проверит полученные байты.
## JavaScript
JavaScript-конфиг экспортирует обычный объект по умолчанию:

View File

@@ -30,13 +30,19 @@
Пути, имена, команды, импорты и названия сгенерированных API должны соответствовать друг другу и фактическому результату генерации.
Во всех гайдах используются согласованные примеры:
Во всех consumer-гайдах используются согласованные примеры:
- исходные SVG находятся в `assets/svg-icons`;
- спрайт создаётся в `assets/app-icons`;
- конфиг записывается в JSON;
- имя спрайта в конфиге — `app`.
`standalone@server` является исключением: quick start использует config-less CLI,
исходные SVG находятся в `./icons` временного worker workspace, output создаётся в
текущем каталоге, а mode, name и input передаются флагами одной команды. Server
config упоминается только как дополнительный вариант; подключение из consumer
может использовать обычный JSON config соответствующего mode.
## Зависимости
В разделе генерации нужно явно показать ключевое преимущество: для создания и использования спрайта пакет не требуется добавлять в зависимости проекта.
@@ -48,7 +54,7 @@ Viewer описывается отдельно как необязательны
Гайд должен содержать только минимальный рабочий путь:
- структуру проекта;
- конфиг;
- конфиг либо полный config-less CLI-вызов для `standalone@server`;
- команду генерации;
- автоматическую генерацию перед запуском и сборкой, если она необходима;
- подключение иконки;

View File

@@ -1,5 +1,7 @@
# Гайды быстрого старта
## Consumer-приложения
- `standalone`: [bare standalone](standalone.md)
- `standalone@vite`: [standalone с Vite](standalone-vite.md)
- `standalone@webpack`: [standalone с Webpack](standalone-webpack.md)
@@ -29,3 +31,7 @@
- `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)
## Серверная генерация
- `standalone@server`: [сгенерировать универсальный спрайт на сервере](standalone-server.md)

View File

@@ -0,0 +1,113 @@
# Универсальный SVG-спрайт на сервере
Сгенерируйте в CI или server worker универсальный SVG-спрайт, который смогут использовать приложения с разными frameworks и bundlers.
## Генерация спрайта
Устанавливать пакет в worker не нужно.
### 1. Подготовьте рабочий каталог
Поместите исходные SVG в папку `icons` текущего workspace:
```text
.
└── icons/
├── search.svg
└── settings.svg
```
Имена файлов без расширения станут именами иконок.
### 2. Запустите генерацию
Передайте mode, имя спрайта и путь к SVG через CLI:
```bash
npx --yes @gromlab/svg-sprites \
--mode standalone@server \
--name app \
--input './icons/**/*.svg' \
.
```
Config-файл для этого worker-сценария не нужен. Результат появится в `./.svg-sprite`:
```text
.
├── icons/
│ ├── search.svg
│ └── settings.svg
└── .svg-sprite/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
### 3. Опубликуйте результат
Загрузите содержимое `.svg-sprite` в отдельный каталог S3 bucket:
```bash
aws s3 sync ./.svg-sprite/ s3://my-bucket/app-icons/
```
Этот же каталог можно раздавать через CDN. В публичном URL нет сегмента `.svg-sprite`:
```text
https://cdn.example.com/app-icons/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
`standalone@server` также можно запускать через JSON, JavaScript или TypeScript config. Config подходит для постоянных настроек, локальных SVG из нескольких каталогов и SVG, загружаемых по HTTP(S).
## Использование спрайта
В consumer-приложении создайте обычный config. Например, для React с Vite:
```text
src/app-icons/
├── index.ts
└── svg-sprite.config.json
```
Укажите consumer mode и URL manifest из CDN:
```json
{
"mode": "react@vite",
"source": "remote",
"input": "https://cdn.example.com/app-icons/svg-sprite.manifest.json"
}
```
Добавьте пользовательскую точку входа:
```ts
// src/app-icons/index.ts
export * from './.svg-sprite/index.js'
```
Запустите обычную генерацию:
```bash
npx --yes @gromlab/svg-sprites src/app-icons/svg-sprite.config.json
```
После этого используйте generated-компонент так же, как со спрайтом из локальных SVG:
```tsx
import { AppIcon } from './app-icons'
export function SearchButton() {
return <AppIcon icon="search" aria-label="Поиск" />
}
```
Тот же CDN manifest поддерживают все 29 consumer modes. В каждом из них сохраняется нативный API выбранного framework и bundler.
## Дебаг и превью
`standalone@server` не создаёт отдельную страницу для просмотра иконок. Подключите опубликованный спрайт к consumer-приложению и откройте его в SpriteViewer: удалённый набор будет отображаться так же, как локальный.

View File

@@ -27,7 +27,9 @@ result.spritePath
result.manifestPath
```
Next.js modes дополнительно возвращают `router` и `bundler`.
Next.js modes дополнительно возвращают `router` и `bundler`. `standalone@server`
возвращает `target: 'server'`; его `spritePath` указывает на стандартный
content-addressed profile, а `manifestPath` — на server manifest.
Для static standalone mode `result.spritePath` можно использовать в build-скрипте,
чтобы опубликовать SVG по URL приложения:
@@ -102,6 +104,11 @@ export default defineSpriteConfig({
`defineSpriteConfig` является identity helper для TypeScript autocomplete. JS может экспортировать тот же объект через `export default`, а JSON содержит объект непосредственно.
Публичные типы `ServerSvgInput`, `ServerSpriteManifest`, `ServerSpriteAsset` и
`SpriteCompileProfile` описывают inputs и release data для `standalone@server`.
Consumer использует тот же API с `source: 'remote'` и одним local path или HTTP(S)
URL manifest в `input`.
## Специализированные обёртки
Специализированные функции доступны как обёртки над `generateSprite`:

View File

@@ -69,6 +69,7 @@ svg-sprites [options] <config-file-or-directory>
| Static HTML / собственная публикация | `standalone` |
| Standalone + Vite | `standalone@vite` |
| Standalone + Webpack 5 | `standalone@webpack` |
| Server release | `standalone@server` |
| React + Vite | `react@vite` |
| React + Webpack 5 | `react@webpack` |
| Vue + Vite | `vue@vite` |
@@ -100,7 +101,7 @@ Config-файл может иметь любое имя и расширение
Если передан каталог, все настройки берутся из CLI. Если передан config-файл, CLI-параметры перекрывают значения файла. Общий порядок: `defaults → config → CLI`.
`--help` и `-h` выводят справку без обязательного пути. Для генерации доступны `--mode`, `--name`, `--description`, повторяемый `--input <path-or-glob>`, а также пары `--remove-size`/`--no-remove-size`, `--replace-colors`/`--no-replace-colors`, `--add-transition`/`--no-add-transition` и `--generated-notice`/`--no-generated-notice`. Переданные transform-флаги перекрывают отдельные поля, а хотя бы один `--input` полностью заменяет значение `input` из config.
`--help` и `-h` выводят справку без обязательного пути. Для генерации доступны `--mode`, `--source <local|remote>`, `--name`, `--description`, повторяемый `--input <path-or-glob>`, а также пары `--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 не раскрыл их до запуска генератора:
@@ -138,12 +139,20 @@ export default defineSpriteConfig({
| Опция | Тип | По умолчанию | Назначение |
|---|---|---|---|
| `mode` | `SpriteMode` | Нет | Режим генерации; можно передать через CLI/API |
| `source` | `local \| remote` | `local` | Исходные SVG либо готовый server manifest |
| `name` | `string` | Выводится из каталога | Имя спрайта; в modes с компонентом также задаёт имя компонента и публичных типов |
| `description` | `string` | Нет | Описание для типов и debug manifest |
| `input` | `string \| string[]` | `./icons` | Папки, SVG-файлы и glob-паттерны относительно папки конфига |
| `input` | `SpriteInput \| SpriteInput[]` | `./icons` | Локальные SVG sources, server HTTP descriptors либо один remote manifest в зависимости от mode и source |
| `transform` | `TransformOptions` | Все включены | Настройки подготовки SVG |
| `generatedNotice` | `boolean` | `true` | Полное или короткое предупреждение в generated-файлах |
При `source: 'remote'` поле `input` содержит один local path или HTTP(S) URL
manifest, созданного `standalone@server`. Remote consumer config может содержать
только `mode`, `source` и `input`: name, description, transforms и generated notice
проверяются и наследуются из server manifest. До codegen генератор скачивает profile,
необходимый exact consumer mode, и проверяет его SHA-256 и размер. Runtime-зависимости
от server manifest нет.
### Имя спрайта
`name` записывается в kebab-case и должно начинаться с латинской буквы:
@@ -176,6 +185,26 @@ file-manager → FileManagerIcon
Каждый включающий источник или паттерн должен найти хотя бы один SVG, иначе генерация завершается ошибкой. Повторяющиеся пути удаляются, а итоговый список файлов детерминированно сортируется. Разные SVG с одинаковым basename по-прежнему считаются конфликтом, потому что basename задаёт публичное имя иконки.
### Server SVG inputs
`standalone@server` принимает те же local strings и HTTP(S) descriptors в массиве
`input`:
```ts
{
name: 'brand-logo',
url: 'https://assets.example.com/brand-logo.svg',
sha256: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
}
```
`name` становится публичным именем иконки. Необязательный `sha256` проверяется по
скачанным байтам. URL credentials и активное SVG-содержимое, включая scripts,
event handlers, `foreignObject` и doctype, запрещены. Один HTTP source ограничен
2 MiB, все источники вместе — 25 MiB, timeout запроса равен 15 секундам. Local и
HTTP entries используют единое пространство имён, поэтому duplicate icon names
завершают генерацию с ошибкой.
## Generated-модуль
После генерации React- или Next.js-каталог спрайта выглядит так:
@@ -225,6 +254,20 @@ runtime asset и deployment-neutral manifest data:
generated Web Component без внешних runtime-зависимостей. Bare `standalone`
намеренно не создаёт JavaScript-компонент.
`standalone@server` создаёт готовый к публикации release без JavaScript runtime и
`.gitignore`:
```text
.svg-sprite/
├── sprite.<content-hash>.svg
├── sprite-root-viewbox.<content-hash>.svg
└── svg-sprite.manifest.json
```
Manifest описывает оба compile profiles через relative `href`, полный SHA-256 и
размер в байтах. Публикуйте весь каталог атомарно; consumer разрешает каждый profile
относительно URL или local path manifest.
`.svg-sprite` полностью управляется генератором и при каждой генерации заменяется целиком. Любые добавленные в него файлы будут удалены. Пользовательские файлы размещайте рядом, например в корневом `index.ts`:
```ts

View File

@@ -15,3 +15,5 @@ apps/*/public/sprites/
apps/*/static/sprites/
apps/*/*.tsbuildinfo
apps/*/src/sprite/.svg-sprite/
apps/*/src/*-icons/.svg-sprite/
apps/standalone-server/cases/*/.svg-sprite/

View File

@@ -39,6 +39,11 @@ production build и отображения внешнего SVG-спрайта
В verify-матрицу входят все exact modes из корневого `MODES_CHECKLIST.md`.
Отдельный fixture `standalone-server` проверяет mixed local/HTTP inputs, HTTP-only
input, отключённые transforms, два sprite profiles и server manifest. Затем каждое
из 29 consumer-приложений генерирует локальный `app-icons` и удалённо собранный
`remote-app-icons` с `source: "remote"`.
## Первый запуск
Из корня репозитория:
@@ -76,6 +81,8 @@ npm run dev --workspace @svg-sprites-fixtures/react-vite --prefix integration
наличие symbol ID, отсутствие browser errors и зелёные пиксели отрисованной иконки.
Для каждого active mode тест также открывает единый Viewer, проверяет его Shadow DOM,
совпадение sprite URL, карточку `check`, dialog и mode-specific вкладки кода.
В каждом consumer-приложении проверяются два разных SVG assets, две rendered-иконки
и группы `app`/`remote-app` в одном Viewer.
Static fixture копирует managed SVG и JSON manifest в `dist/app-icons/` и использует
literal `<use href="/app-icons/sprite.svg#check">`.

View File

@@ -4,7 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.json",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.json",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "tsc --noEmit",
"build": "npm run sprites && npm run typecheck && vite build",
"dev": "npm run sprites && vite"

View File

@@ -1,6 +1,6 @@
{
"mode": "alpine@vite",
"name": "icons",
"name": "app",
"input": "../../../../fixtures/icons/check.svg",
"generatedNotice": false
}

View File

@@ -1,22 +1,41 @@
import {
iconsAlpinePlugin,
iconsIconDirective,
iconsIconMagic,
iconsIconNames,
type IconsIconName,
} from './sprite/index.js'
appAlpinePlugin,
appIconDirective,
appIconMagic,
appIconNames,
type AppIconName,
} from './app-icons/index.js'
import {
remoteAppAlpinePlugin,
remoteAppIconDirective,
remoteAppIconMagic,
remoteAppIconNames,
type RemoteAppIconName,
} from './remote-app-icons/index.js'
declare const Alpine: { plugin(plugin: typeof iconsAlpinePlugin): void }
declare const Alpine: {
plugin(plugin: typeof appAlpinePlugin | typeof remoteAppAlpinePlugin): void
}
const iconName: IconsIconName = iconsIconNames[0]
const directive: 'icons-icon' = iconsIconDirective
const magic: 'iconsIconHref' = iconsIconMagic
const appIconName: AppIconName = appIconNames[0]
const appDirective: 'app-icon' = appIconDirective
const appMagic: 'appIconHref' = appIconMagic
const remoteAppIconName: RemoteAppIconName = remoteAppIconNames[0]
const remoteAppDirective: 'remote-app-icon' = remoteAppIconDirective
const remoteAppMagic: 'remoteAppIconHref' = remoteAppIconMagic
Alpine.plugin(iconsAlpinePlugin)
void iconName
void directive
void magic
Alpine.plugin(appAlpinePlugin)
Alpine.plugin(remoteAppAlpinePlugin)
void appIconName
void appDirective
void appMagic
void remoteAppIconName
void remoteAppDirective
void remoteAppMagic
// @ts-expect-error Generated icon names form a literal union.
const missingIcon: IconsIconName = 'missing'
const missingIcon: AppIconName = 'missing'
void missingIcon
// @ts-expect-error Generated remote icon names form a literal union.
const missingRemoteIcon: RemoteAppIconName = 'missing'
void missingRemoteIcon

View File

@@ -1,9 +1,12 @@
import '@gromlab/svg-sprites/viewer/element'
import Alpine from 'alpinejs'
import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js'
import { iconsAlpinePlugin } from './sprite/index.js'
import appManifest from './app-icons/.svg-sprite/svg-sprite.manifest.js'
import { appAlpinePlugin } from './app-icons/index.js'
import remoteAppManifest from './remote-app-icons/.svg-sprite/svg-sprite.manifest.js'
import { remoteAppAlpinePlugin } from './remote-app-icons/index.js'
Alpine.plugin(iconsAlpinePlugin)
Alpine.plugin(appAlpinePlugin)
Alpine.plugin(remoteAppAlpinePlugin)
window.Alpine = Alpine
document.querySelector('#app').innerHTML = `
@@ -12,14 +15,22 @@ document.querySelector('#app').innerHTML = `
<svg
data-testid="icon"
data-app="alpine-vite"
x-icons-icon="'check'"
x-app-icon="'check'"
role="img"
aria-label="Check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></svg>
<svg
data-testid="remote-icon"
data-app="alpine-vite-remote"
x-remote-app-icon="'check'"
role="img"
aria-label="Remote check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></svg>
<gromlab-sprite-viewer viewer-title="Alpine Vite Viewer"></gromlab-sprite-viewer>
</main>
`
document.querySelector('gromlab-sprite-viewer').sources = [spriteManifest]
document.querySelector('gromlab-sprite-viewer').sources = [appManifest, remoteAppManifest]
Alpine.start()

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'alpine@vite',
source: 'remote',
input: manifestUrl,
}

View File

@@ -4,7 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.json",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.json",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "tsc --noEmit",
"build": "npm run sprites && npm run typecheck && webpack --mode production",
"dev": "npm run sprites && webpack serve --mode development"

View File

@@ -1,6 +1,6 @@
{
"mode": "alpine@webpack",
"name": "icons",
"name": "app",
"input": "../../../../fixtures/icons/check.svg",
"generatedNotice": false
}

View File

@@ -1,22 +1,41 @@
import {
iconsAlpinePlugin,
iconsIconDirective,
iconsIconMagic,
iconsIconNames,
type IconsIconName,
} from './sprite/index.js'
appAlpinePlugin,
appIconDirective,
appIconMagic,
appIconNames,
type AppIconName,
} from './app-icons/index.js'
import {
remoteAppAlpinePlugin,
remoteAppIconDirective,
remoteAppIconMagic,
remoteAppIconNames,
type RemoteAppIconName,
} from './remote-app-icons/index.js'
declare const Alpine: { plugin(plugin: typeof iconsAlpinePlugin): void }
declare const Alpine: {
plugin(plugin: typeof appAlpinePlugin | typeof remoteAppAlpinePlugin): void
}
const iconName: IconsIconName = iconsIconNames[0]
const directive: 'icons-icon' = iconsIconDirective
const magic: 'iconsIconHref' = iconsIconMagic
const appIconName: AppIconName = appIconNames[0]
const appDirective: 'app-icon' = appIconDirective
const appMagic: 'appIconHref' = appIconMagic
const remoteAppIconName: RemoteAppIconName = remoteAppIconNames[0]
const remoteAppDirective: 'remote-app-icon' = remoteAppIconDirective
const remoteAppMagic: 'remoteAppIconHref' = remoteAppIconMagic
Alpine.plugin(iconsAlpinePlugin)
void iconName
void directive
void magic
Alpine.plugin(appAlpinePlugin)
Alpine.plugin(remoteAppAlpinePlugin)
void appIconName
void appDirective
void appMagic
void remoteAppIconName
void remoteAppDirective
void remoteAppMagic
// @ts-expect-error Generated icon names form a literal union.
const missingIcon: IconsIconName = 'missing'
const missingIcon: AppIconName = 'missing'
void missingIcon
// @ts-expect-error Generated remote icon names form a literal union.
const missingRemoteIcon: RemoteAppIconName = 'missing'
void missingRemoteIcon

View File

@@ -1,9 +1,12 @@
import '@gromlab/svg-sprites/viewer/element'
import Alpine from 'alpinejs'
import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js'
import { iconsAlpinePlugin } from './sprite/index.js'
import appManifest from './app-icons/.svg-sprite/svg-sprite.manifest.js'
import { appAlpinePlugin } from './app-icons/index.js'
import remoteAppManifest from './remote-app-icons/.svg-sprite/svg-sprite.manifest.js'
import { remoteAppAlpinePlugin } from './remote-app-icons/index.js'
Alpine.plugin(iconsAlpinePlugin)
Alpine.plugin(appAlpinePlugin)
Alpine.plugin(remoteAppAlpinePlugin)
window.Alpine = Alpine
document.querySelector('#app').innerHTML = `
@@ -12,14 +15,22 @@ document.querySelector('#app').innerHTML = `
<svg
data-testid="icon"
data-app="alpine-webpack"
x-icons-icon="'check'"
x-app-icon="'check'"
role="img"
aria-label="Check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></svg>
<svg
data-testid="remote-icon"
data-app="alpine-webpack-remote"
x-remote-app-icon="'check'"
role="img"
aria-label="Remote check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></svg>
<gromlab-sprite-viewer viewer-title="Alpine Webpack Viewer"></gromlab-sprite-viewer>
</main>
`
document.querySelector('gromlab-sprite-viewer').sources = [spriteManifest]
document.querySelector('gromlab-sprite-viewer').sources = [appManifest, remoteAppManifest]
Alpine.start()

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'alpine@webpack',
source: 'remote',
input: manifestUrl,
}

View File

@@ -3,7 +3,9 @@
"private": true,
"version": "0.0.0",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.ts",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.ts",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "ngc -p tsconfig.json --noEmit",
"build": "npm run sprites && webpack --mode production",
"dev": "npm run sprites && webpack serve --mode development"

View File

@@ -1,6 +1,6 @@
export default {
mode: 'angular@webpack',
name: 'icons',
name: 'app',
input: '../../../../fixtures/icons/check.svg',
generatedNotice: false,
}

View File

@@ -3,24 +3,32 @@ import '@gromlab/svg-sprites/viewer/element'
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'
import { bootstrapApplication } from '@angular/platform-browser'
import { IconsIcon } from './sprite'
import { AppIcon } from './app-icons'
import { RemoteAppIcon } from './remote-app-icons'
import './type-probe'
@Component({
selector: 'app-root',
standalone: true,
imports: [IconsIcon],
imports: [AppIcon, RemoteAppIcon],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<main>
<h1>Angular + Webpack</h1>
<icons-icon
<app-icon
data-testid="icon"
data-app="angular-webpack"
icon="check"
aria-label="Check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
/>
<remote-app-icon
data-testid="remote-icon"
data-app="angular-webpack-remote"
icon="check"
aria-label="Remote check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
/>
<gromlab-sprite-viewer
[sources]="viewerSources"
viewer-title="Angular Webpack Viewer"
@@ -30,7 +38,8 @@ import './type-probe'
})
class AppComponent {
readonly viewerSources = [
() => import('./sprite/.svg-sprite/svg-sprite.manifest.js'),
() => import('./app-icons/.svg-sprite/svg-sprite.manifest.js'),
() => import('./remote-app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
}

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'angular@webpack',
source: 'remote',
input: manifestUrl,
}

View File

@@ -1,8 +1,11 @@
import type { IconsIconName } from './sprite'
import type { AppIconName } from './app-icons'
import type { RemoteAppIconName } from './remote-app-icons'
const validIcon: IconsIconName = 'check'
const validIcon: AppIconName = 'check'
const validRemoteIcon: RemoteAppIconName = 'check'
void validIcon
void validRemoteIcon
// @ts-expect-error generated icon names are a closed union
const invalidIcon: IconsIconName = 'missing'
const invalidIcon: AppIconName = 'missing'
void invalidIcon

View File

@@ -3,7 +3,9 @@
"private": true,
"version": "0.0.0",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.ts",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.ts",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "ngc -p tsconfig.app.json --noEmit",
"build": "npm run sprites && ng build --configuration production",
"dev": "npm run sprites && ng serve"

View File

@@ -0,0 +1 @@
export * from './.svg-sprite/index'

View File

@@ -1,6 +1,6 @@
export default {
mode: 'angular@application',
name: 'icons',
name: 'app',
input: '../../../../fixtures/icons/check.svg',
generatedNotice: false,
}

View File

@@ -2,22 +2,30 @@ import '@gromlab/svg-sprites/viewer/element'
import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'
import { IconsIcon } from './sprite'
import { AppIcon } from './app-icons'
import { RemoteAppIcon } from './remote-app-icons'
@Component({
selector: 'app-root',
standalone: true,
imports: [IconsIcon],
imports: [AppIcon, RemoteAppIcon],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<main>
<h1>Angular application builder</h1>
<icons-icon
<app-icon
data-testid="icon"
data-app="angular"
icon="check"
aria-label="Check icon"
style="color: #16a34a; --icon-color-1: #16a34a"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
/>
<remote-app-icon
data-testid="remote-icon"
data-app="angular-remote"
icon="check"
aria-label="Remote check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
/>
<gromlab-sprite-viewer
[sources]="viewerSources"
@@ -28,6 +36,7 @@ import { IconsIcon } from './sprite'
})
export class AppComponent {
readonly viewerSources = [
() => import('./sprite/.svg-sprite/svg-sprite.manifest.js'),
() => import('./app-icons/.svg-sprite/svg-sprite.manifest.js'),
() => import('./remote-app-icons/.svg-sprite/svg-sprite.manifest.js'),
]
}

View File

@@ -0,0 +1 @@
export * from './.svg-sprite/index'

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'angular@application',
source: 'remote',
input: manifestUrl,
}

View File

@@ -9,7 +9,8 @@ body {
padding: 40px;
}
icons-icon[data-testid='icon'] {
app-icon[data-testid='icon'],
remote-app-icon[data-testid='remote-icon'] {
width: 64px;
height: 64px;
color: #16a34a;

View File

@@ -1,10 +1,14 @@
import type { IconsIconName } from './sprite'
import type { SpriteManifest } from './sprite/.svg-sprite/svg-sprite.manifest.js'
import type { AppIconName } from './app-icons'
import type { SpriteManifest as AppSpriteManifest } from './app-icons/.svg-sprite/svg-sprite.manifest.js'
import type { RemoteAppIconName } from './remote-app-icons'
import type { SpriteManifest as RemoteAppSpriteManifest } from './remote-app-icons/.svg-sprite/svg-sprite.manifest.js'
const icon: IconsIconName = 'check'
const mode: SpriteManifest['mode'] = 'angular@application'
const icon: AppIconName = 'check'
const remoteIcon: RemoteAppIconName = 'check'
const mode: AppSpriteManifest['mode'] = 'angular@application'
const remoteMode: RemoteAppSpriteManifest['mode'] = 'angular@application'
// @ts-expect-error Unknown source file names are rejected by the generated union.
const unknownIcon: IconsIconName = 'missing'
const unknownIcon: AppIconName = 'missing'
export { icon, mode, unknownIcon }
export { icon, mode, remoteIcon, remoteMode, unknownIcon }

View File

@@ -4,7 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.ts",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.ts",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "astro check",
"build": "npm run sprites && astro check && astro build",
"dev": "npm run sprites && astro dev"

View File

@@ -1,6 +1,6 @@
export default {
mode: 'astro@vite',
name: 'icons',
name: 'app',
input: '../../../../fixtures/icons/check.svg',
generatedNotice: false,
}

View File

@@ -1,5 +1,6 @@
---
import { IconsIcon } from '../sprite/index.js'
import { AppIcon } from '../app-icons/index.js'
import { RemoteAppIcon } from '../remote-app-icons/index.js'
const title = 'Astro sprite fixture'
---
@@ -14,14 +15,23 @@ const title = 'Astro sprite fixture'
<body>
<main>
<h1>Astro</h1>
<IconsIcon
<AppIcon
data-testid="icon"
data-app="astro"
icon="check"
aria-label="Check icon"
width="64"
height="64"
style="--icon-color-1: #16a34a"
style="color: #16a34a; --icon-color-1: #16a34a"
/>
<RemoteAppIcon
data-testid="remote-icon"
data-app="astro-remote"
icon="check"
aria-label="Remote check icon"
width="64"
height="64"
style="color: #16a34a; --icon-color-1: #16a34a"
/>
<gromlab-sprite-viewer id="sprite-viewer"></gromlab-sprite-viewer>
</main>
@@ -34,7 +44,10 @@ const title = 'Astro sprite fixture'
if (!viewer) throw new Error('Sprite Viewer element is missing.')
Object.assign(viewer, {
viewerTitle: 'Astro Vite Viewer',
sources: [() => import('../sprite/.svg-sprite/svg-sprite.manifest.js')],
sources: [
() => import('../app-icons/.svg-sprite/svg-sprite.manifest.js'),
() => import('../remote-app-icons/.svg-sprite/svg-sprite.manifest.js'),
],
})
</script>
</body>
@@ -52,7 +65,8 @@ const title = 'Astro sprite fixture'
padding: 40px;
}
[data-testid='icon'] {
[data-testid='icon'],
[data-testid='remote-icon'] {
width: 64px;
height: 64px;
color: #16a34a;

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'astro@vite',
source: 'remote',
input: manifestUrl,
}

View File

@@ -1,11 +1,16 @@
import type { IconsIconName, IconsIconProps } from './sprite/index.js'
import type { SpriteManifest } from './sprite/.svg-sprite/svg-sprite.manifest.js'
import type { AppIconName, AppIconProps } from './app-icons/index.js'
import type { SpriteManifest } from './app-icons/.svg-sprite/svg-sprite.manifest.js'
import type { RemoteAppIconName, RemoteAppIconProps } from './remote-app-icons/index.js'
import type { SpriteManifest as RemoteSpriteManifest } from './remote-app-icons/.svg-sprite/svg-sprite.manifest.js'
const icon: IconsIconName = 'check'
const props: IconsIconProps = { icon, width: 24, 'aria-label': 'Check icon' }
const icon: AppIconName = 'check'
const remoteIcon: RemoteAppIconName = 'check'
const props: AppIconProps = { icon, width: 24, 'aria-label': 'Check icon' }
const remoteProps: RemoteAppIconProps = { icon: remoteIcon, width: 24, 'aria-label': 'Remote check icon' }
const framework: SpriteManifest['usage']['framework'] = 'astro'
const remoteFramework: RemoteSpriteManifest['usage']['framework'] = 'astro'
// @ts-expect-error Unknown source file names are rejected by the generated union.
const unknownIcon: IconsIconName = 'missing'
const unknownIcon: AppIconName = 'missing'
export { framework, icon, props, unknownIcon }
export { framework, icon, props, remoteFramework, remoteIcon, remoteProps, unknownIcon }

View File

@@ -4,7 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.json",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.json",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "tsc --noEmit",
"build": "npm run sprites && npm run typecheck && vite build",
"dev": "npm run sprites && vite"

View File

@@ -1,6 +1,6 @@
{
"mode": "lit@vite",
"name": "icons",
"name": "app",
"input": "../../../../fixtures/icons/check.svg",
"generatedNotice": false
}

View File

@@ -1,18 +1,33 @@
import {
defineIconsIcon,
IconsIcon,
iconsIconNames,
iconsIconTagName,
type IconsIconName,
} from './sprite/index.js'
AppIcon,
appIconNames,
appIconTagName,
defineAppIcon,
type AppIconName,
} from './app-icons/index.js'
import {
defineRemoteAppIcon,
RemoteAppIcon,
remoteAppIconNames,
remoteAppIconTagName,
type RemoteAppIconName,
} from './remote-app-icons/index.js'
const iconName: IconsIconName = iconsIconNames[0]
const icon = new IconsIcon()
icon.icon = iconName
const appIconName: AppIconName = appIconNames[0]
const appIcon = new AppIcon()
appIcon.icon = appIconName
const remoteAppIconName: RemoteAppIconName = remoteAppIconNames[0]
const remoteAppIcon = new RemoteAppIcon()
remoteAppIcon.icon = remoteAppIconName
const registeredIcon = document.createElement(iconsIconTagName)
registeredIcon.icon = 'check'
defineIconsIcon()
const registeredAppIcon = document.createElement(appIconTagName)
registeredAppIcon.icon = 'check'
const registeredRemoteAppIcon = document.createElement(remoteAppIconTagName)
registeredRemoteAppIcon.icon = 'check'
defineAppIcon()
defineRemoteAppIcon()
// @ts-expect-error Generated icon names form a literal union.
registeredIcon.icon = 'missing'
registeredAppIcon.icon = 'missing'
// @ts-expect-error Generated remote icon names form a literal union.
registeredRemoteAppIcon.icon = 'missing'

View File

@@ -1,20 +1,31 @@
import '@gromlab/svg-sprites/viewer/element'
import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js'
import { defineIconsIcon } from './sprite/index.js'
import appManifest from './app-icons/.svg-sprite/svg-sprite.manifest.js'
import { defineAppIcon } from './app-icons/index.js'
import remoteAppManifest from './remote-app-icons/.svg-sprite/svg-sprite.manifest.js'
import { defineRemoteAppIcon } from './remote-app-icons/index.js'
defineIconsIcon()
defineAppIcon()
defineRemoteAppIcon()
document.querySelector('#app').innerHTML = `
<h1>Lit + Vite</h1>
<icons-icon
<app-icon
data-testid="icon"
data-app="lit-vite"
icon="check"
role="img"
aria-label="Check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></icons-icon>
></app-icon>
<remote-app-icon
data-testid="remote-icon"
data-app="lit-vite-remote"
icon="check"
role="img"
aria-label="Remote check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></remote-app-icon>
<gromlab-sprite-viewer viewer-title="Lit Vite Viewer"></gromlab-sprite-viewer>
`
document.querySelector('gromlab-sprite-viewer').sources = [spriteManifest]
document.querySelector('gromlab-sprite-viewer').sources = [appManifest, remoteAppManifest]

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'lit@vite',
source: 'remote',
input: manifestUrl,
}

View File

@@ -4,7 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.json",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.json",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "tsc --noEmit",
"build": "npm run sprites && npm run typecheck && webpack --mode production",
"dev": "npm run sprites && webpack serve --mode development"

View File

@@ -1,6 +1,6 @@
{
"mode": "lit@webpack",
"name": "icons",
"name": "app",
"input": "../../../../fixtures/icons/check.svg",
"generatedNotice": false
}

View File

@@ -1,18 +1,33 @@
import {
defineIconsIcon,
IconsIcon,
iconsIconNames,
iconsIconTagName,
type IconsIconName,
} from './sprite/index.js'
AppIcon,
appIconNames,
appIconTagName,
defineAppIcon,
type AppIconName,
} from './app-icons/index.js'
import {
defineRemoteAppIcon,
RemoteAppIcon,
remoteAppIconNames,
remoteAppIconTagName,
type RemoteAppIconName,
} from './remote-app-icons/index.js'
const iconName: IconsIconName = iconsIconNames[0]
const icon = new IconsIcon()
icon.icon = iconName
const appIconName: AppIconName = appIconNames[0]
const appIcon = new AppIcon()
appIcon.icon = appIconName
const remoteAppIconName: RemoteAppIconName = remoteAppIconNames[0]
const remoteAppIcon = new RemoteAppIcon()
remoteAppIcon.icon = remoteAppIconName
const registeredIcon = document.createElement(iconsIconTagName)
registeredIcon.icon = 'check'
defineIconsIcon()
const registeredAppIcon = document.createElement(appIconTagName)
registeredAppIcon.icon = 'check'
const registeredRemoteAppIcon = document.createElement(remoteAppIconTagName)
registeredRemoteAppIcon.icon = 'check'
defineAppIcon()
defineRemoteAppIcon()
// @ts-expect-error Generated icon names form a literal union.
registeredIcon.icon = 'missing'
registeredAppIcon.icon = 'missing'
// @ts-expect-error Generated remote icon names form a literal union.
registeredRemoteAppIcon.icon = 'missing'

View File

@@ -1,20 +1,31 @@
import '@gromlab/svg-sprites/viewer/element'
import spriteManifest from './sprite/.svg-sprite/svg-sprite.manifest.js'
import { defineIconsIcon } from './sprite/index.js'
import appManifest from './app-icons/.svg-sprite/svg-sprite.manifest.js'
import { defineAppIcon } from './app-icons/index.js'
import remoteAppManifest from './remote-app-icons/.svg-sprite/svg-sprite.manifest.js'
import { defineRemoteAppIcon } from './remote-app-icons/index.js'
defineIconsIcon()
defineAppIcon()
defineRemoteAppIcon()
document.querySelector('#app').innerHTML = `
<h1>Lit + Webpack</h1>
<icons-icon
<app-icon
data-testid="icon"
data-app="lit-webpack"
icon="check"
role="img"
aria-label="Check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></icons-icon>
></app-icon>
<remote-app-icon
data-testid="remote-icon"
data-app="lit-webpack-remote"
icon="check"
role="img"
aria-label="Remote check icon"
style="width:64px;height:64px;color:#16a34a;--icon-color-1:#16a34a"
></remote-app-icon>
<gromlab-sprite-viewer viewer-title="Lit Webpack Viewer"></gromlab-sprite-viewer>
`
document.querySelector('gromlab-sprite-viewer').sources = [spriteManifest]
document.querySelector('gromlab-sprite-viewer').sources = [appManifest, remoteAppManifest]

View File

@@ -0,0 +1,8 @@
const manifestUrl = process.env.SVG_SPRITE_REMOTE_MANIFEST_URL
?? '../../../standalone-server/cases/mixed-input/.svg-sprite/svg-sprite.manifest.json'
export default {
mode: 'lit@webpack',
source: 'remote',
input: manifestUrl,
}

View File

@@ -1,11 +1,12 @@
import { IconsIcon } from '../src/sprite'
import { AppIcon } from '../src/app-icons'
import { RemoteAppIcon } from '../src/remote-app-icons'
import { AppSpriteViewer } from './sprite-viewer'
export default function Page() {
return (
<main>
<h1>Next.js App Router + Turbopack</h1>
<IconsIcon
<AppIcon
data-testid="icon"
data-app="next-app-turbopack"
icon="check"
@@ -14,6 +15,15 @@ export default function Page() {
height={64}
style={{ '--icon-color-1': '#16a34a' }}
/>
<RemoteAppIcon
data-testid="remote-icon"
data-app="next-app-turbopack-remote"
icon="check"
aria-label="Remote check icon"
width={64}
height={64}
style={{ '--icon-color-1': '#16a34a' }}
/>
<AppSpriteViewer />
</main>
)

View File

@@ -3,7 +3,8 @@
import { SpriteViewer } from '@gromlab/svg-sprites/react'
const viewerSources = [
() => import('../src/sprite/.svg-sprite/svg-sprite.manifest.js'),
() => import('../src/app-icons/.svg-sprite/svg-sprite.manifest.js'),
() => import('../src/remote-app-icons/.svg-sprite/svg-sprite.manifest.js'),
] as const
export function AppSpriteViewer() {

View File

@@ -1,18 +1,28 @@
import { IconsIcon, iconsIconNames } from './src/sprite'
import type { IconsIconName, IconsIconProps, IconsIconStyle } from './src/sprite'
import { IconsIcon as GeneratedIconsIcon } from './src/sprite/.svg-sprite/react/react-component.js'
import type { IconsIconName as GeneratedIconName } from './src/sprite/.svg-sprite/icon-data.js'
import { iconsIconNames as generatedIconNames } from './src/sprite/.svg-sprite/icon-data.js'
import { AppIcon, appIconNames } from './src/app-icons'
import type { AppIconName, AppIconProps, AppIconStyle } from './src/app-icons'
import { AppIcon as GeneratedAppIcon } from './src/app-icons/.svg-sprite/react/react-component.js'
import type { AppIconName as GeneratedAppIconName } from './src/app-icons/.svg-sprite/icon-data.js'
import { appIconNames as generatedAppIconNames } from './src/app-icons/.svg-sprite/icon-data.js'
import { RemoteAppIcon, remoteAppIconNames } from './src/remote-app-icons'
import type { RemoteAppIconName, RemoteAppIconProps } from './src/remote-app-icons'
import { RemoteAppIcon as GeneratedRemoteAppIcon } from './src/remote-app-icons/.svg-sprite/react/react-component.js'
import type { RemoteAppIconName as GeneratedRemoteAppIconName } from './src/remote-app-icons/.svg-sprite/icon-data.js'
import { remoteAppIconNames as generatedRemoteAppIconNames } from './src/remote-app-icons/.svg-sprite/icon-data.js'
const iconName: IconsIconName = iconsIconNames[0]
const generatedIconName: GeneratedIconName = generatedIconNames[0]
const allIconNames: readonly IconsIconName[] = iconsIconNames
const style: IconsIconStyle = { '--icon-color-1': '#16a34a' }
const props: IconsIconProps = { icon: iconName, style }
const appIconName: AppIconName = appIconNames[0]
const generatedAppIconName: GeneratedAppIconName = generatedAppIconNames[0]
const allAppIconNames: readonly AppIconName[] = appIconNames
const style: AppIconStyle = { '--icon-color-1': '#16a34a' }
const appProps: AppIconProps = { icon: appIconName, style }
const remoteAppIconName: RemoteAppIconName = remoteAppIconNames[0]
const generatedRemoteAppIconName: GeneratedRemoteAppIconName = generatedRemoteAppIconNames[0]
const remoteAppProps: RemoteAppIconProps = { icon: remoteAppIconName, style }
void allIconNames
void <IconsIcon {...props} />
void <GeneratedIconsIcon icon={generatedIconName} />
void allAppIconNames
void <AppIcon {...appProps} />
void <GeneratedAppIcon icon={generatedAppIconName} />
void <RemoteAppIcon {...remoteAppProps} />
void <GeneratedRemoteAppIcon icon={generatedRemoteAppIconName} />
// @ts-expect-error Unknown icon names must be rejected by generated declarations.
void <IconsIcon icon="missing" />
void <AppIcon icon="missing" />

View File

@@ -3,7 +3,9 @@
"private": true,
"version": "0.0.0",
"scripts": {
"sprites": "svg-sprites src/sprite/svg-sprite.config.ts",
"sprites": "npm run sprites:local && npm run sprites:remote",
"sprites:local": "svg-sprites src/app-icons/svg-sprite.config.ts",
"sprites:remote": "svg-sprites src/remote-app-icons/svg-sprite.config.js",
"typecheck": "tsc --noEmit",
"build": "npm run sprites && next build --turbopack",
"dev": "npm run sprites && next dev --turbopack",

Some files were not shown because too many files have changed in this diff Show More