Files
svg-sprites/src/modes/lit-vite/output.ts
S.Gromov 8b3365f957 feat: добавить поддержку 20 framework exact modes
Добавлены exact modes:
- vue@vite и vue@webpack
- nuxt@vite и nuxt@webpack
- svelte@vite, svelte@webpack и sveltekit@vite
- angular@application и angular@webpack
- astro@vite
- solid@vite, solid@webpack и solid-start@vite
- preact@vite и preact@webpack
- qwik@vite
- lit@vite и lit@webpack
- alpine@vite и alpine@webpack

Для каждого mode реализованы изолированный adapter, нативный
framework-компонент, declarations, manifest, CSS и внешний asset URL.

Добавлены production integration-стенды с генерацией, typecheck,
сборкой и Playwright-проверкой рендера спрайта и Viewer.

Обновлены Viewer, RU/EN-гайды, README, technical reference и AI skills.
Итоговая матрица включает 29 exact modes.

Проверки:
- 48 unit-тестов
- 29 integration E2E-тестов
2026-07-15 16:56:44 +03:00

259 lines
9.0 KiB
TypeScript

import path from 'node:path'
import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js'
import { GENERATED_MARKER } from '../../core/generated-markers.js'
import type { GeneratedFile } from '../../core/mode-adapter.js'
import type { ResolvedSpriteConfig } from '../../types.js'
const MODE = 'lit@vite'
const TARGET = 'vite'
const OUTPUT_DIR = '.svg-sprite'
const NOTICE = 'AUTOMATICALLY GENERATED FILE. Do not edit manually.'
function pascal(value: string): string {
return value.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
}
function camel(value: string): string {
return value.replace(/-([a-z0-9])/g, (_, char: string) => char.toUpperCase())
}
function header(enabled: boolean): string {
return enabled
? `/*\n * ${NOTICE}\n * ${GENERATED_MARKER}.\n */`
: `/* ${GENERATED_MARKER}. Do not edit. */`
}
function markedSvg(bytes: Uint8Array, enabled: boolean): string {
const marker = enabled ? `${NOTICE} ${GENERATED_MARKER}.` : `${GENERATED_MARKER}. Do not edit.`
const content = new TextDecoder().decode(bytes)
return content.startsWith('<?xml')
? content.replace(/^(<\?xml[^?]*\?>)\s*/, `$1\n<!-- ${marker} -->\n`)
: `<!-- ${marker} -->\n${content}`
}
function description(config: ResolvedSpriteConfig): string[] {
const text = config.description ?? `Icon names for the "${config.name}" SVG sprite.`
return ['/**', ...text.split(/\r?\n/).map((line) => ` * ${line.replace(/\*\//g, '* /')}`), ' */']
}
function iconData(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string {
const prefix = camel(config.name)
const names = artifact.icons.map((icon) => icon.name)
const ids = Object.fromEntries(artifact.icons.map((icon) => [icon.name, icon.id]))
return [
header(config.generatedNotice),
'',
...description(config),
`export const ${prefix}IconNames = ${JSON.stringify(names, null, 2)}`,
'',
`export const ${prefix}IconIds = ${JSON.stringify(ids, null, 2)}`,
'',
].join('\n')
}
function iconTypes(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string {
const component = `${pascal(config.name)}Icon`
const names = `${camel(config.name)}IconNames`
const ids = `${camel(config.name)}IconIds`
return [
header(config.generatedNotice),
'',
...description(config),
`export declare const ${names}: readonly [`,
...artifact.icons.map((icon) => ` ${JSON.stringify(icon.name)},`),
']',
`export type ${component}Name = typeof ${names}[number]`,
`export declare const ${ids}: {`,
...artifact.icons.map((icon) => ` readonly ${JSON.stringify(icon.name)}: ${JSON.stringify(icon.id)}`),
'}',
'',
].join('\n')
}
function component(config: ResolvedSpriteConfig): string {
const className = `${pascal(config.name)}Icon`
const prefix = camel(config.name)
const tagName = `${config.name}-icon`
return [
header(config.generatedNotice),
"import { LitElement, html, nothing, unsafeCSS } from 'lit'",
"import componentStyles from './lit-component.css?inline'",
`import { ${prefix}IconIds } from '../icon-data.js'`,
"import spriteUrl from '../sprite.svg?no-inline'",
'',
`export const ${prefix}IconTagName = ${JSON.stringify(tagName)}`,
'',
`export class ${className} extends LitElement {`,
' static properties = {',
" icon: { type: String },",
' }',
'',
' static styles = unsafeCSS(componentStyles)',
'',
" icon = ''",
'',
' render() {',
` const id = ${prefix}IconIds[this.icon]`,
' if (!id) return nothing',
'',
" const href = spriteUrl + '#' + id",
' return html`<svg class="root" part="svg" aria-hidden="true"><use href=${href}></use></svg>`',
' }',
'}',
'',
`export function define${className}() {`,
` if (!customElements.get(${prefix}IconTagName)) {`,
` customElements.define(${prefix}IconTagName, ${className})`,
' }',
'}',
'',
].join('\n')
}
function componentTypes(config: ResolvedSpriteConfig): string {
const className = `${pascal(config.name)}Icon`
const prefix = camel(config.name)
const tagName = `${config.name}-icon`
return [
header(config.generatedNotice),
"import { LitElement } from 'lit'",
`import type { ${className}Name } from '../icon-data.js'`,
'',
`export declare const ${prefix}IconTagName: ${JSON.stringify(tagName)}`,
`export declare class ${className} extends LitElement {`,
` icon: ${className}Name | ''`,
'}',
`export declare function define${className}(): void`,
'',
'declare global {',
' interface HTMLElementTagNameMap {',
` ${JSON.stringify(tagName)}: ${className}`,
' }',
'}',
'',
].join('\n')
}
function styles(config: ResolvedSpriteConfig): string {
const transition = config.transform.addTransition
? [' transition-property: fill, stroke, color;', ' transition-duration: 0.3s;', ' transition-timing-function: ease;']
: []
return [
header(config.generatedNotice),
'',
':host {',
' display: inline-flex;',
' width: 1em;',
' height: 1em;',
' vertical-align: middle;',
'}',
'',
'.root {',
' width: 100%;',
' height: 100%;',
...transition,
'}',
'',
].join('\n')
}
function index(config: ResolvedSpriteConfig): string {
const className = `${pascal(config.name)}Icon`
const prefix = camel(config.name)
return [
header(config.generatedNotice),
`export { ${className}, define${className}, ${prefix}IconTagName } from './lit/lit-component.js'`,
`export { ${prefix}IconNames } from './icon-data.js'`,
'',
].join('\n')
}
function indexTypes(config: ResolvedSpriteConfig): string {
const className = `${pascal(config.name)}Icon`
const prefix = camel(config.name)
return [
header(config.generatedNotice),
`export { ${className}, define${className}, ${prefix}IconTagName } from './lit/lit-component.js'`,
`export { ${prefix}IconNames } from './icon-data.js'`,
`export type { ${className}Name } from './icon-data.js'`,
'',
].join('\n')
}
function manifest(config: ResolvedSpriteConfig, artifact: CompiledSpriteArtifact): string {
const className = `${pascal(config.name)}Icon`
const data = {
schemaVersion: 1,
generator: '@gromlab/svg-sprites',
name: config.name,
...(config.description === undefined ? {} : { description: config.description }),
usage: {
framework: 'lit',
componentName: className,
tagName: `${config.name}-icon`,
defineFunction: `define${className}`,
},
mode: MODE,
target: TARGET,
format: artifact.format,
iconCount: artifact.icons.length,
spriteUrl: '__SPRITE_URL__',
icons: artifact.icons,
}
const source = JSON.stringify(data, null, 2).replace('"__SPRITE_URL__"', 'spriteUrl')
return [
header(config.generatedNotice),
"import spriteUrl from './sprite.svg?no-inline'",
'',
`export const spriteManifest = ${source}`,
'',
'export default spriteManifest',
'',
].join('\n')
}
function manifestTypes(config: ResolvedSpriteConfig): string {
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',
" usage: { framework: 'lit'; componentName: string; tagName: string; defineFunction: string }",
" mode: 'lit@vite'",
" target: 'vite'",
" 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 [
{ path: path.posix.join(OUTPUT_DIR, 'index.js'), content: index(config) },
{ path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexTypes(config) },
{ path: path.posix.join(OUTPUT_DIR, 'sprite.svg'), content: markedSvg(artifact.bytes, config.generatedNotice) },
{ path: path.posix.join(OUTPUT_DIR, 'icon-data.js'), content: iconData(config, artifact) },
{ path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconTypes(config, artifact) },
{ path: path.posix.join(OUTPUT_DIR, 'lit', 'lit-component.js'), content: component(config) },
{ path: path.posix.join(OUTPUT_DIR, 'lit', 'lit-component.d.ts'), content: componentTypes(config) },
{ path: path.posix.join(OUTPUT_DIR, 'lit', 'lit-component.css'), content: styles(config) },
{ path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.js'), content: manifest(config, artifact) },
{ path: path.posix.join(OUTPUT_DIR, 'svg-sprite.manifest.d.ts'), content: manifestTypes(config) },
]
}