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(')\s*/, `$1\n\n`) : `\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``', ' }', '}', '', `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) }, ] }