mirror of
https://github.com/gromlab-ru/svg-sprites.git
synced 2026-07-22 04:40:17 +03:00
feat: добавить серверную генерацию спрайтов
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { isSpriteMode } from '../config.js'
|
||||
import type { SpriteConfig, TransformOptions } from '../types.js'
|
||||
import type { SpriteConfig, SpriteSource, TransformOptions } from '../types.js'
|
||||
import type { CliArgs } from './types.js'
|
||||
|
||||
export const CLI_USAGE = [
|
||||
@@ -14,6 +14,7 @@ export const CLI_USAGE = [
|
||||
' standalone',
|
||||
' standalone@vite',
|
||||
' standalone@webpack',
|
||||
' standalone@server',
|
||||
' react@vite',
|
||||
' react@webpack',
|
||||
' vue@vite',
|
||||
@@ -43,6 +44,7 @@ export const CLI_USAGE = [
|
||||
'',
|
||||
'Options:',
|
||||
' --mode <mode>',
|
||||
' --source <local|remote>',
|
||||
' --name <name>',
|
||||
' --description <text>',
|
||||
' --input <path-or-glob> Repeat for multiple inputs',
|
||||
@@ -66,8 +68,18 @@ function optionValue(argv: string[], index: number, option: string): [string, nu
|
||||
return [value, index + 1]
|
||||
}
|
||||
|
||||
type MutableCliOverrides = {
|
||||
mode?: SpriteConfig['mode']
|
||||
source?: SpriteSource
|
||||
name?: string
|
||||
description?: string
|
||||
input?: string | string[]
|
||||
transform?: TransformOptions
|
||||
generatedNotice?: boolean
|
||||
}
|
||||
|
||||
function setTransform(
|
||||
overrides: SpriteConfig,
|
||||
overrides: MutableCliOverrides,
|
||||
option: keyof TransformOptions,
|
||||
value: boolean,
|
||||
): void {
|
||||
@@ -81,7 +93,7 @@ export function parseCliArgs(argv: string[]): CliArgs | { help: true } {
|
||||
if (argv.includes('--help') || argv.includes('-h')) return { help: true }
|
||||
|
||||
const positional: string[] = []
|
||||
const overrides: SpriteConfig = {}
|
||||
const overrides: MutableCliOverrides = {}
|
||||
|
||||
for (let index = 0; index < argv.length; index++) {
|
||||
const argument = argv[index]
|
||||
@@ -93,6 +105,15 @@ export function parseCliArgs(argv: string[]): CliArgs | { help: true } {
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
if (argument === '--source' || argument.startsWith('--source=')) {
|
||||
const [value, nextIndex] = optionValue(argv, index, '--source')
|
||||
if (value !== 'local' && value !== 'remote') {
|
||||
throw new Error(`Unsupported sprite source: ${value}. Expected local or remote.`)
|
||||
}
|
||||
overrides.source = value
|
||||
index = nextIndex
|
||||
continue
|
||||
}
|
||||
if (argument === '--name' || argument.startsWith('--name=')) {
|
||||
const [value, nextIndex] = optionValue(argv, index, '--name')
|
||||
overrides.name = value
|
||||
@@ -157,6 +178,6 @@ export function parseCliArgs(argv: string[]): CliArgs | { help: true } {
|
||||
|
||||
return {
|
||||
path: positional[0],
|
||||
overrides,
|
||||
overrides: overrides as SpriteConfig,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ export type CompileSpriteOptions = {
|
||||
rootViewBox?: boolean
|
||||
}
|
||||
|
||||
export type SpriteSourceContent = {
|
||||
readonly name: string
|
||||
readonly content: string
|
||||
}
|
||||
|
||||
/** Конфигурация режима для svg-sprite. */
|
||||
function getModeConfig(
|
||||
format: SpriteFormat,
|
||||
@@ -74,6 +79,21 @@ export async function compileSpriteContent(
|
||||
folder: SpriteFolder,
|
||||
transform: TransformOptions = {},
|
||||
options: CompileSpriteOptions = {},
|
||||
): Promise<Uint8Array> {
|
||||
const sources = folder.files.map((filePath) => ({
|
||||
name: path.basename(filePath, '.svg'),
|
||||
content: fs.readFileSync(filePath, 'utf-8'),
|
||||
}))
|
||||
return compileSpriteSourceContent(folder.name, folder.format, sources, transform, options)
|
||||
}
|
||||
|
||||
/** Компилирует SVG sources из памяти без промежуточных файлов. */
|
||||
export async function compileSpriteSourceContent(
|
||||
name: string,
|
||||
format: SpriteFormat,
|
||||
sources: readonly SpriteSourceContent[],
|
||||
transform: TransformOptions = {},
|
||||
options: CompileSpriteOptions = {},
|
||||
): Promise<Uint8Array> {
|
||||
const config = {
|
||||
shape: {
|
||||
@@ -86,14 +106,14 @@ export async function compileSpriteContent(
|
||||
transform: buildShapeTransforms(transform),
|
||||
},
|
||||
mode: {
|
||||
[folder.format]: getModeConfig(folder.format, '.', folder.name, options),
|
||||
[format]: getModeConfig(format, '.', name, options),
|
||||
},
|
||||
}
|
||||
|
||||
const spriter = new SVGSpriter(config)
|
||||
|
||||
for (const filePath of folder.files) {
|
||||
spriter.add(filePath, null, fs.readFileSync(filePath, 'utf-8'))
|
||||
for (const source of sources) {
|
||||
spriter.add(`${source.name}.svg`, null, source.content)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -116,7 +136,7 @@ export async function compileSpriteContent(
|
||||
}
|
||||
|
||||
if (!spriteContents) {
|
||||
reject(new Error(`Failed to compile sprite "${folder.name}".`))
|
||||
reject(new Error(`Failed to compile sprite "${name}".`))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
114
src/config.ts
114
src/config.ts
@@ -3,11 +3,19 @@ import path from 'node:path'
|
||||
import { createJiti } from 'jiti'
|
||||
import { toKebabCase, validateSpriteName } from './core/naming.js'
|
||||
import type { SpriteMode } from './targets/types.js'
|
||||
import type { ResolvedSpriteConfig, SpriteConfig, TransformOptions } from './types.js'
|
||||
import type {
|
||||
ResolvedSpriteConfig,
|
||||
ServerSvgInput,
|
||||
SpriteConfig,
|
||||
SpriteInput,
|
||||
SpriteSource,
|
||||
TransformOptions,
|
||||
} from './types.js'
|
||||
|
||||
const CONFIG_EXTENSIONS = new Set(['.js', '.json', '.ts'])
|
||||
const CONFIG_FIELDS = new Set([
|
||||
'mode',
|
||||
'source',
|
||||
'name',
|
||||
'description',
|
||||
'input',
|
||||
@@ -19,6 +27,7 @@ const MODES = new Set<SpriteMode>([
|
||||
'standalone',
|
||||
'standalone@vite',
|
||||
'standalone@webpack',
|
||||
'standalone@server',
|
||||
'react@vite',
|
||||
'react@webpack',
|
||||
'vue@vite',
|
||||
@@ -57,6 +66,52 @@ export function isSpriteMode(value: unknown): value is SpriteMode {
|
||||
return typeof value === 'string' && MODES.has(value as SpriteMode)
|
||||
}
|
||||
|
||||
function isServerSvgInput(value: unknown): value is ServerSvgInput {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
||||
const input = value as Record<string, unknown>
|
||||
if (Object.keys(input).some((field) => !['name', 'url', 'sha256'].includes(field))) return false
|
||||
return typeof input.name === 'string'
|
||||
&& input.name.trim() !== ''
|
||||
&& !/[\\/]/.test(input.name)
|
||||
&& !input.name.endsWith('.svg')
|
||||
&& typeof input.url === 'string'
|
||||
&& input.url.trim() !== ''
|
||||
&& (input.sha256 === undefined || (typeof input.sha256 === 'string' && /^[a-f0-9]{64}$/i.test(input.sha256)))
|
||||
}
|
||||
|
||||
function inputEntries(input: unknown): unknown[] {
|
||||
return Array.isArray(input) ? input : [input]
|
||||
}
|
||||
|
||||
function validateInput(config: Record<string, unknown>): void {
|
||||
if (config.input === undefined) return
|
||||
|
||||
const entries = inputEntries(config.input)
|
||||
if (entries.length === 0) {
|
||||
throw configError('"input" must be a non-empty string or an array of non-empty strings.')
|
||||
}
|
||||
|
||||
if (config.source === 'remote') {
|
||||
if (typeof config.input !== 'string' || config.input.trim() === '') {
|
||||
throw configError('remote "input" must be one manifest path or HTTP(S) URL.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (config.mode === 'standalone@server') {
|
||||
if (entries.some((entry) => (
|
||||
typeof entry === 'string' ? entry.trim() === '' : !isServerSvgInput(entry)
|
||||
))) {
|
||||
throw configError('standalone@server "input" must contain local paths/globs or { name, url, sha256? } entries.')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (entries.some((entry) => typeof entry !== 'string' || entry.trim() === '')) {
|
||||
throw configError('"input" must be a non-empty string or an array of non-empty strings.')
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultName(rootDir: string): string {
|
||||
const rootName = path.basename(rootDir)
|
||||
const source = rootName === 'svg-sprite' || rootName === 'svg-sprites'
|
||||
@@ -92,21 +147,24 @@ export function validateSpriteConfig(value: unknown): asserts value is SpriteCon
|
||||
if (config.mode !== undefined && !isSpriteMode(config.mode)) {
|
||||
throw configError(`unsupported "mode": ${String(config.mode)}.`)
|
||||
}
|
||||
if (config.source !== undefined && config.source !== 'local' && config.source !== 'remote') {
|
||||
throw configError('"source" must be "local" or "remote".')
|
||||
}
|
||||
if (config.mode === 'standalone@server' && config.source === 'remote') {
|
||||
throw configError('standalone@server cannot consume a remote manifest.')
|
||||
}
|
||||
if (config.source === 'remote') {
|
||||
for (const field of ['name', 'description', 'transform', 'generatedNotice']) {
|
||||
if (field in config) throw configError(`remote config does not support "${field}".`)
|
||||
}
|
||||
}
|
||||
if (config.name !== undefined && typeof config.name !== 'string') {
|
||||
throw configError('"name" must be a string.')
|
||||
}
|
||||
if (config.description !== undefined && typeof config.description !== 'string') {
|
||||
throw configError('"description" must be a string.')
|
||||
}
|
||||
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('"input" must be a non-empty string or an array of non-empty strings.')
|
||||
}
|
||||
validateInput(config)
|
||||
if (config.transform !== undefined) {
|
||||
if (
|
||||
config.transform === null
|
||||
@@ -210,27 +268,38 @@ export function resolveSpriteConfig(
|
||||
validateSpriteConfig(config)
|
||||
validateSpriteConfig(overrides)
|
||||
|
||||
const configValues = definedEntries(config)
|
||||
const overrideValues = definedEntries(overrides)
|
||||
const transform: TransformOptions = {
|
||||
...config.transform,
|
||||
...overrides.transform,
|
||||
const configValues = definedEntries(config) as Record<string, unknown>
|
||||
const overrideValues = definedEntries(overrides) as Record<string, unknown>
|
||||
const source = (overrideValues.source ?? configValues.source ?? 'local') as SpriteSource
|
||||
const sourceChangedByOverride = overrideValues.source !== undefined && overrideValues.source !== configValues.source
|
||||
if (sourceChangedByOverride && overrideValues.input === undefined) {
|
||||
throw new Error('Changing "source" through overrides also requires an "input" override.')
|
||||
}
|
||||
const merged: SpriteConfig = {
|
||||
const transform: TransformOptions = {
|
||||
...(source === 'remote' ? {} : config.transform),
|
||||
...(source === 'remote' ? {} : overrides.transform),
|
||||
}
|
||||
const merged = {
|
||||
...configValues,
|
||||
...overrideValues,
|
||||
source,
|
||||
transform,
|
||||
}
|
||||
} as Record<string, unknown>
|
||||
|
||||
if (!merged.mode) {
|
||||
if (!merged.mode || !isSpriteMode(merged.mode)) {
|
||||
throw new Error('Sprite mode is required. Set "mode" in the config or pass it through CLI/API.')
|
||||
}
|
||||
if (source === 'remote' && merged.mode === 'standalone@server') {
|
||||
throw new Error('Mode "standalone@server" cannot consume a remote manifest.')
|
||||
}
|
||||
|
||||
const name = merged.name ?? getDefaultName(rootDir)
|
||||
const name = source === 'remote' ? getDefaultName(rootDir) : (merged.name as string | undefined) ?? getDefaultName(rootDir)
|
||||
validateSpriteName(name)
|
||||
const configuredInput = merged.input ?? 'icons'
|
||||
const configuredInput = (merged.input ?? 'icons') as SpriteInput | SpriteInput[]
|
||||
const input = (Array.isArray(configuredInput) ? configuredInput : [configuredInput])
|
||||
.map((entry) => {
|
||||
if (typeof entry !== 'string') return entry
|
||||
if (source === 'remote' && /^https?:\/\//i.test(entry)) return entry
|
||||
const negated = entry.startsWith('!')
|
||||
const resolved = path.resolve(rootDir, negated ? entry.slice(1) : entry)
|
||||
const normalized = path.sep === '/' ? resolved : resolved.replaceAll(path.sep, '/')
|
||||
@@ -239,14 +308,15 @@ export function resolveSpriteConfig(
|
||||
|
||||
return {
|
||||
mode: merged.mode,
|
||||
source,
|
||||
name,
|
||||
description: merged.description,
|
||||
description: source === 'remote' ? undefined : merged.description as string | undefined,
|
||||
input,
|
||||
transform: {
|
||||
removeSize: transform.removeSize ?? true,
|
||||
replaceColors: transform.replaceColors ?? true,
|
||||
addTransition: transform.addTransition ?? true,
|
||||
},
|
||||
generatedNotice: merged.generatedNotice ?? true,
|
||||
generatedNotice: source === 'remote' ? true : (merged.generatedNotice as boolean | undefined) ?? true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,36 @@ import type {
|
||||
SpriteAssetTarget,
|
||||
} from '../targets/types.js'
|
||||
import type { ResolvedSpriteConfig, SpriteFolder } from '../types.js'
|
||||
import type { SpriteSourceContent } from '../compiler.js'
|
||||
import type { ServerSpriteManifest } from '../release/types.js'
|
||||
|
||||
export type GeneratedFile = {
|
||||
readonly path: string
|
||||
readonly content: string | Uint8Array
|
||||
}
|
||||
|
||||
export type PreparedSprite = {
|
||||
export type PreparedLocalSprite = {
|
||||
readonly kind: 'local'
|
||||
readonly folder: SpriteFolder
|
||||
readonly iconNames: readonly string[]
|
||||
}
|
||||
|
||||
export type PreparedContentSprite = {
|
||||
readonly kind: 'content'
|
||||
readonly name: string
|
||||
readonly sources: readonly SpriteSourceContent[]
|
||||
readonly iconNames: readonly string[]
|
||||
}
|
||||
|
||||
export type PreparedRemoteSprite = {
|
||||
readonly kind: 'remote'
|
||||
readonly manifest: ServerSpriteManifest
|
||||
readonly manifestLocation: string
|
||||
readonly iconNames: readonly string[]
|
||||
}
|
||||
|
||||
export type PreparedSprite = PreparedLocalSprite | PreparedContentSprite | PreparedRemoteSprite
|
||||
|
||||
export type ModeAdapterContext = {
|
||||
readonly rootDir: string
|
||||
readonly config: ResolvedSpriteConfig
|
||||
@@ -60,5 +79,6 @@ export type OutputPlan = {
|
||||
|
||||
export interface ModeAdapter<M extends SpriteMode = SpriteMode> {
|
||||
readonly mode: M
|
||||
prepare?(config: ResolvedSpriteConfig): Promise<PreparedSprite>
|
||||
generate(context: ModeAdapterContext): Promise<OutputPlan>
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getSpriteShapeId } from '../shape-id.js'
|
||||
import type { ResolvedSpriteConfig } from '../types.js'
|
||||
import type { PreparedSprite } from './mode-adapter.js'
|
||||
|
||||
function validateIconIds(iconNames: readonly string[]): void {
|
||||
export function validateIconIds(iconNames: readonly string[]): void {
|
||||
const namesById = new Map<string, string>()
|
||||
|
||||
for (const iconName of iconNames) {
|
||||
@@ -23,15 +23,21 @@ function validateIconIds(iconNames: readonly string[]): void {
|
||||
|
||||
/** Подготавливает mode-neutral набор исходников. */
|
||||
export function prepareSprite(config: ResolvedSpriteConfig): PreparedSprite {
|
||||
if (config.source !== 'local') {
|
||||
throw new Error('Local sprite preparation requires source "local".')
|
||||
}
|
||||
if (config.input.some((entry) => typeof entry !== 'string')) {
|
||||
throw new Error(`Mode "${config.mode}" only accepts local path or glob inputs.`)
|
||||
}
|
||||
const folder = resolveSpriteSources({
|
||||
name: config.name,
|
||||
format: 'stack',
|
||||
input: config.input,
|
||||
input: config.input as string[],
|
||||
})
|
||||
const iconNames = folder.files
|
||||
.map((filePath) => path.basename(filePath, '.svg'))
|
||||
.sort()
|
||||
|
||||
validateIconIds(iconNames)
|
||||
return { folder, iconNames }
|
||||
return { kind: 'local', folder, iconNames }
|
||||
}
|
||||
|
||||
25
src/core/resolve-compiled-artifact.ts
Normal file
25
src/core/resolve-compiled-artifact.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { compileSpriteContent, type CompileSpriteOptions } from '../compiler.js'
|
||||
import { loadRemoteSpriteArtifact } from '../release/load.js'
|
||||
import type { SpriteFolder, TransformOptions } from '../types.js'
|
||||
import { createCompiledSpriteArtifact, type CompiledSpriteArtifact } from './compiled-artifact.js'
|
||||
import type { PreparedSprite } from './mode-adapter.js'
|
||||
|
||||
/** Компилирует local source либо выбирает готовый profile server release. */
|
||||
export async function resolveCompiledSpriteArtifact(
|
||||
prepared: PreparedSprite,
|
||||
transform: TransformOptions,
|
||||
options: CompileSpriteOptions,
|
||||
): Promise<CompiledSpriteArtifact> {
|
||||
if (prepared.kind === 'remote') {
|
||||
return loadRemoteSpriteArtifact(
|
||||
prepared,
|
||||
options.rootViewBox ? 'stack-root-viewbox' : 'stack',
|
||||
)
|
||||
}
|
||||
if (prepared.kind !== 'local' && !('folder' in prepared)) {
|
||||
throw new Error('This mode cannot compile in-memory server inputs.')
|
||||
}
|
||||
const folder = prepared.folder as SpriteFolder
|
||||
const bytes = await compileSpriteContent(folder, transform, options)
|
||||
return createCompiledSpriteArtifact(bytes, prepared.iconNames, 'stack')
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import type { ModeResultMetadata } from './core/mode-adapter.js'
|
||||
import { writeOutputPlan } from './core/output-writer.js'
|
||||
import { prepareSprite } from './core/prepare-sprite.js'
|
||||
import { prepareRemoteSprite } from './release/load.js'
|
||||
import type { SpriteGenerationBaseResult } from './core/result.js'
|
||||
import { log } from './logger.js'
|
||||
import { getModeAdapter } from './mode-registry.js'
|
||||
@@ -25,10 +26,23 @@ export async function generateSprite(
|
||||
overrides,
|
||||
)
|
||||
const adapter = getModeAdapter(config.mode)
|
||||
const prepared = prepareSprite(config)
|
||||
const prepared = config.source === 'remote'
|
||||
? await prepareRemoteSprite(config.input[0] as string)
|
||||
: adapter.prepare
|
||||
? await adapter.prepare(config)
|
||||
: prepareSprite(config)
|
||||
const materializedConfig = prepared.kind === 'remote'
|
||||
? {
|
||||
...config,
|
||||
name: prepared.manifest.name,
|
||||
description: prepared.manifest.description,
|
||||
transform: prepared.manifest.transform,
|
||||
generatedNotice: prepared.manifest.generatedNotice,
|
||||
}
|
||||
: config
|
||||
const plan = await adapter.generate({
|
||||
rootDir: resolvedSource.rootDir,
|
||||
config,
|
||||
config: materializedConfig,
|
||||
prepared,
|
||||
})
|
||||
const plannedPaths = new Set(plan.files.map((file) => file.path.replaceAll('\\', '/')))
|
||||
@@ -51,11 +65,11 @@ export async function generateSprite(
|
||||
|
||||
const generatedDir = path.resolve(resolvedSource.rootDir, plan.paths.generatedDir)
|
||||
const iconLabel = prepared.iconNames.length === 1 ? 'icon' : 'icons'
|
||||
log.success(`✓ ${config.name} · ${prepared.iconNames.length} ${iconLabel} · ${config.mode}`)
|
||||
log.success(`✓ ${materializedConfig.name} · ${prepared.iconNames.length} ${iconLabel} · ${config.mode}`)
|
||||
log.detail(` → ${path.relative(process.cwd(), generatedDir)}`)
|
||||
|
||||
return {
|
||||
name: config.name,
|
||||
name: materializedConfig.name,
|
||||
rootDir: resolvedSource.rootDir,
|
||||
generatedDir,
|
||||
spritePath: path.resolve(resolvedSource.rootDir, plan.paths.sprite),
|
||||
|
||||
13
src/index.ts
13
src/index.ts
@@ -13,8 +13,8 @@ export { generateSprite } from './generate.js'
|
||||
export type { SpriteGenerationResult } from './generate.js'
|
||||
export { generateNextSprite } from './api/next.js'
|
||||
export { generateReactSprite } from './api/react.js'
|
||||
export { compileSprite, compileSpriteContent } from './compiler.js'
|
||||
export type { CompileSpriteOptions } from './compiler.js'
|
||||
export { compileSprite, compileSpriteContent, compileSpriteSourceContent } from './compiler.js'
|
||||
export type { CompileSpriteOptions, SpriteSourceContent } from './compiler.js'
|
||||
export { createShapeTransform } from './transforms.js'
|
||||
|
||||
export type {
|
||||
@@ -34,6 +34,8 @@ export type {
|
||||
SolidSpriteMode,
|
||||
StandaloneAssetTarget,
|
||||
StandaloneSpriteMode,
|
||||
ServerAssetTarget,
|
||||
ServerSpriteMode,
|
||||
StaticAssetTarget,
|
||||
SvelteSpriteMode,
|
||||
SpriteAssetTarget,
|
||||
@@ -54,8 +56,15 @@ export type {
|
||||
SpriteConfig,
|
||||
SpriteFolder,
|
||||
SpriteFormat,
|
||||
SpriteSource,
|
||||
ServerSvgInput,
|
||||
TransformOptions,
|
||||
} from './types.js'
|
||||
export type {
|
||||
ServerSpriteAsset,
|
||||
ServerSpriteManifest,
|
||||
SpriteCompileProfile,
|
||||
} from './release/types.js'
|
||||
export type {
|
||||
NextSpriteConfig,
|
||||
NextSpriteGenerationOptions,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { qwikViteAdapter } from './modes/qwik-vite/adapter.js'
|
||||
import { reactViteAdapter } from './modes/react-vite/adapter.js'
|
||||
import { reactWebpackAdapter } from './modes/react-webpack/adapter.js'
|
||||
import { standaloneAdapter } from './modes/standalone/adapter.js'
|
||||
import { standaloneServerAdapter } from './modes/standalone-server/adapter.js'
|
||||
import { standaloneViteAdapter } from './modes/standalone-vite/adapter.js'
|
||||
import { standaloneWebpackAdapter } from './modes/standalone-webpack/adapter.js'
|
||||
import { solidStartViteAdapter } from './modes/solid-start-vite/adapter.js'
|
||||
@@ -32,6 +33,7 @@ import type { SpriteMode } from './targets/types.js'
|
||||
|
||||
const modeRegistry: Record<SpriteMode, ModeAdapter> = {
|
||||
standalone: standaloneAdapter,
|
||||
'standalone@server': standaloneServerAdapter,
|
||||
'standalone@vite': standaloneViteAdapter,
|
||||
'standalone@webpack': standaloneWebpackAdapter,
|
||||
'react@vite': reactViteAdapter,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const alpineViteAdapter: ModeAdapter<'alpine@vite'> = {
|
||||
mode: 'alpine@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const alpineWebpackAdapter: ModeAdapter<'alpine@webpack'> = {
|
||||
mode: 'alpine@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapterContext, OutputPlan } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
@@ -15,12 +14,11 @@ type AngularApplicationAdapter = {
|
||||
export const angularApplicationAdapter: AngularApplicationAdapter = {
|
||||
mode: 'angular@application',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -74,11 +74,10 @@ function component(config: ResolvedSpriteConfig): string {
|
||||
const ids = `${camel(config.name)}IconIds`
|
||||
return [
|
||||
header(config.generatedNotice),
|
||||
'/// <reference path="../assets.d.ts" />',
|
||||
"import { ChangeDetectionStrategy, Component, input } from '@angular/core'",
|
||||
`import type { ${typeName} } from '../icon-data.js'`,
|
||||
`import { ${ids} } from '../icon-data.js'`,
|
||||
"import spriteUrl from '../sprite.svg'",
|
||||
"import spriteManifest from '../svg-sprite.manifest.js'",
|
||||
'',
|
||||
'@Component({',
|
||||
` selector: '${config.name}-icon',`,
|
||||
@@ -91,7 +90,7 @@ function component(config: ResolvedSpriteConfig): string {
|
||||
` readonly icon = input.required<${typeName}>()`,
|
||||
'',
|
||||
' get href(): string {',
|
||||
` return spriteUrl + '#' + ${ids}[this.icon()]`,
|
||||
` return spriteManifest.spriteUrl + '#' + ${ids}[this.icon()]`,
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
@@ -138,17 +137,6 @@ function indexDeclarations(config: ResolvedSpriteConfig): string {
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function assetDeclarations(config: ResolvedSpriteConfig): string {
|
||||
return [
|
||||
header(config.generatedNotice),
|
||||
"declare module '*.svg' {",
|
||||
' const url: string',
|
||||
' export default url',
|
||||
'}',
|
||||
'',
|
||||
].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;']
|
||||
@@ -229,7 +217,6 @@ export function generateOutputFiles(config: ResolvedSpriteConfig, artifact: Comp
|
||||
return [
|
||||
{ path: path.posix.join(OUTPUT_DIR, 'index.ts'), content: index(config) },
|
||||
{ path: path.posix.join(OUTPUT_DIR, 'index.d.ts'), content: indexDeclarations(config) },
|
||||
{ path: path.posix.join(OUTPUT_DIR, 'assets.d.ts'), content: assetDeclarations(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) },
|
||||
{ path: path.posix.join(OUTPUT_DIR, 'icon-data.d.ts'), content: iconDeclarations(config, artifact) },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapterContext, OutputPlan } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
@@ -15,12 +14,11 @@ type AngularWebpackAdapter = {
|
||||
export const angularWebpackAdapter: AngularWebpackAdapter = {
|
||||
mode: 'angular@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapterContext, OutputPlan } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
@@ -15,12 +14,11 @@ type AstroViteAdapter = {
|
||||
export const astroViteAdapter: AstroViteAdapter = {
|
||||
mode: 'astro@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const litViteAdapter: ModeAdapter<'lit@vite'> = {
|
||||
mode: 'lit@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const litWebpackAdapter: ModeAdapter<'lit@webpack'> = {
|
||||
mode: 'lit@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const nextAppTurbopackAdapter: ModeAdapter<'next@app/turbopack'> = {
|
||||
mode: 'next@app/turbopack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: true,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: true },
|
||||
)
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
paths: {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const nextAppWebpackAdapter: ModeAdapter<'next@app/webpack'> = {
|
||||
mode: 'next@app/webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true })
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: true },
|
||||
)
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', manifest: '.svg-sprite/svg-sprite.manifest.js', entry: '.svg-sprite/index.js' },
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const nextPagesTurbopackAdapter: ModeAdapter<'next@pages/turbopack'> = {
|
||||
mode: 'next@pages/turbopack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true })
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: true },
|
||||
)
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', manifest: '.svg-sprite/svg-sprite.manifest.js', entry: '.svg-sprite/index.js' },
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const nextPagesWebpackAdapter: ModeAdapter<'next@pages/webpack'> = {
|
||||
mode: 'next@pages/webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, { rootViewBox: true })
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: true },
|
||||
)
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
paths: { generatedDir: '.svg-sprite', sprite: '.svg-sprite/sprite.svg', manifest: '.svg-sprite/svg-sprite.manifest.js', entry: '.svg-sprite/index.js' },
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const nuxtViteAdapter: ModeAdapter<'nuxt@vite'> = {
|
||||
mode: 'nuxt@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const nuxtWebpackAdapter: ModeAdapter<'nuxt@webpack'> = {
|
||||
mode: 'nuxt@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const preactViteAdapter: ModeAdapter<'preact@vite'> = {
|
||||
mode: 'preact@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const preactWebpackAdapter: ModeAdapter<'preact@webpack'> = {
|
||||
mode: 'preact@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const qwikViteAdapter: ModeAdapter<'qwik@vite'> = {
|
||||
mode: 'qwik@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const reactViteAdapter: ModeAdapter<'react@vite'> = {
|
||||
mode: 'react@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const reactWebpackAdapter: ModeAdapter<'react@webpack'> = {
|
||||
mode: 'react@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
paths: {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const solidStartViteAdapter: ModeAdapter<'solid-start@vite'> = {
|
||||
mode: 'solid-start@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const solidViteAdapter: ModeAdapter<'solid@vite'> = {
|
||||
mode: 'solid@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const solidWebpackAdapter: ModeAdapter<'solid@webpack'> = {
|
||||
mode: 'solid@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
43
src/modes/standalone-server/adapter.ts
Normal file
43
src/modes/standalone-server/adapter.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { compileSpriteSourceContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
import { prepareStandaloneServerSprite } from './source.js'
|
||||
|
||||
export const standaloneServerAdapter: ModeAdapter<'standalone@server'> = {
|
||||
mode: 'standalone@server',
|
||||
prepare: prepareStandaloneServerSprite,
|
||||
async generate(context) {
|
||||
if (context.prepared.kind !== 'content') {
|
||||
throw new Error('standalone@server requires prepared local and HTTP SVG content.')
|
||||
}
|
||||
const standardBytes = await compileSpriteSourceContent(
|
||||
context.prepared.name,
|
||||
'stack',
|
||||
context.prepared.sources,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const rootViewBoxBytes = await compileSpriteSourceContent(
|
||||
context.prepared.name,
|
||||
'stack',
|
||||
context.prepared.sources,
|
||||
context.config.transform,
|
||||
{ rootViewBox: true },
|
||||
)
|
||||
const standard = createCompiledSpriteArtifact(standardBytes, context.prepared.iconNames, 'stack')
|
||||
const rootViewBox = createCompiledSpriteArtifact(rootViewBoxBytes, context.prepared.iconNames, 'stack')
|
||||
const output = generateOutputFiles(context.config, standard, rootViewBox)
|
||||
|
||||
return {
|
||||
files: output.files,
|
||||
createGitignore: false,
|
||||
paths: {
|
||||
generatedDir: '.svg-sprite',
|
||||
sprite: output.spritePath,
|
||||
manifest: output.manifestPath,
|
||||
},
|
||||
result: { target: 'server' },
|
||||
}
|
||||
},
|
||||
}
|
||||
69
src/modes/standalone-server/output.ts
Normal file
69
src/modes/standalone-server/output.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import type { CompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import type { GeneratedFile } from '../../core/mode-adapter.js'
|
||||
import type { ServerSpriteManifest, ServerSpriteAsset } from '../../release/types.js'
|
||||
import type { ResolvedSpriteConfig } from '../../types.js'
|
||||
|
||||
const OUTPUT_DIR = '.svg-sprite'
|
||||
|
||||
function sha256(bytes: Uint8Array): string {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function asset(fileName: string, bytes: Uint8Array): ServerSpriteAsset {
|
||||
return {
|
||||
href: `./${fileName}`,
|
||||
sha256: sha256(bytes),
|
||||
byteLength: bytes.byteLength,
|
||||
}
|
||||
}
|
||||
|
||||
function spriteFileName(prefix: string, bytes: Uint8Array): string {
|
||||
return `${prefix}.${sha256(bytes).slice(0, 16)}.svg`
|
||||
}
|
||||
|
||||
export type StandaloneServerOutput = {
|
||||
files: GeneratedFile[]
|
||||
spritePath: `.svg-sprite/${string}`
|
||||
manifestPath: '.svg-sprite/svg-sprite.manifest.json'
|
||||
}
|
||||
|
||||
export function generateOutputFiles(
|
||||
config: ResolvedSpriteConfig,
|
||||
standard: CompiledSpriteArtifact,
|
||||
rootViewBox: CompiledSpriteArtifact,
|
||||
): StandaloneServerOutput {
|
||||
if (JSON.stringify(standard.icons) !== JSON.stringify(rootViewBox.icons)) {
|
||||
throw new Error('standalone@server sprite profiles produced different icon metadata.')
|
||||
}
|
||||
const standardName = spriteFileName('sprite', standard.bytes)
|
||||
const rootViewBoxName = spriteFileName('sprite-root-viewbox', rootViewBox.bytes)
|
||||
const manifest: ServerSpriteManifest = {
|
||||
kind: '@gromlab/svg-sprites/server',
|
||||
schemaVersion: 1,
|
||||
generator: '@gromlab/svg-sprites',
|
||||
mode: 'standalone@server',
|
||||
target: 'server',
|
||||
name: config.name,
|
||||
...(config.description === undefined ? {} : { description: config.description }),
|
||||
format: 'stack',
|
||||
generatedNotice: config.generatedNotice,
|
||||
transform: config.transform,
|
||||
iconCount: standard.icons.length,
|
||||
icons: standard.icons,
|
||||
sprites: {
|
||||
stack: asset(standardName, standard.bytes),
|
||||
'stack-root-viewbox': asset(rootViewBoxName, rootViewBox.bytes),
|
||||
},
|
||||
}
|
||||
const spritePath = `${OUTPUT_DIR}/${standardName}` as const
|
||||
return {
|
||||
files: [
|
||||
{ path: spritePath, content: standard.bytes },
|
||||
{ path: `${OUTPUT_DIR}/${rootViewBoxName}`, content: rootViewBox.bytes },
|
||||
{ path: `${OUTPUT_DIR}/svg-sprite.manifest.json`, content: `${JSON.stringify(manifest, null, 2)}\n` },
|
||||
],
|
||||
spritePath,
|
||||
manifestPath: '.svg-sprite/svg-sprite.manifest.json',
|
||||
}
|
||||
}
|
||||
90
src/modes/standalone-server/source.ts
Normal file
90
src/modes/standalone-server/source.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { SpriteSourceContent } from '../../compiler.js'
|
||||
import { validateIconIds } from '../../core/prepare-sprite.js'
|
||||
import type { PreparedContentSprite } from '../../core/mode-adapter.js'
|
||||
import { resolveSpriteSources } from '../../scanner.js'
|
||||
import type { ResolvedSpriteConfig, ServerSvgInput } from '../../types.js'
|
||||
|
||||
const MAX_SOURCE_BYTES = 2 * 1024 * 1024
|
||||
const MAX_TOTAL_BYTES = 25 * 1024 * 1024
|
||||
const REQUEST_TIMEOUT_MS = 15_000
|
||||
|
||||
function sha256(content: string | Uint8Array): string {
|
||||
return createHash('sha256').update(content).digest('hex')
|
||||
}
|
||||
|
||||
function assertSvg(content: string, name: string): void {
|
||||
if (!/<svg\b/i.test(content)) throw new Error(`HTTP input "${name}" is not an SVG document.`)
|
||||
if (/<!DOCTYPE|<script\b|<foreignObject\b|\son[a-z]+\s*=/i.test(content)) {
|
||||
throw new Error(`HTTP input "${name}" contains unsupported active SVG content.`)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHttpInput(input: ServerSvgInput): Promise<SpriteSourceContent> {
|
||||
let url: URL
|
||||
try {
|
||||
url = new URL(input.url)
|
||||
} catch {
|
||||
throw new Error(`Invalid HTTP input URL for icon "${input.name}".`)
|
||||
}
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw new Error(`HTTP input "${input.name}" must use http: or https:.`)
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
throw new Error(`HTTP input "${input.name}" must not contain URL credentials.`)
|
||||
}
|
||||
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Cannot load HTTP input "${input.name}": ${response.status} ${response.statusText}`)
|
||||
}
|
||||
const declaredLength = Number(response.headers.get('content-length'))
|
||||
if (Number.isFinite(declaredLength) && declaredLength > MAX_SOURCE_BYTES) {
|
||||
throw new Error(`HTTP input "${input.name}" exceeds the ${MAX_SOURCE_BYTES} byte limit.`)
|
||||
}
|
||||
const bytes = new Uint8Array(await response.arrayBuffer())
|
||||
if (bytes.byteLength > MAX_SOURCE_BYTES) {
|
||||
throw new Error(`HTTP input "${input.name}" exceeds the ${MAX_SOURCE_BYTES} byte limit.`)
|
||||
}
|
||||
const content = new TextDecoder().decode(bytes)
|
||||
assertSvg(content, input.name)
|
||||
if (input.sha256 && sha256(bytes) !== input.sha256.toLowerCase()) {
|
||||
throw new Error(`HTTP input "${input.name}" SHA-256 does not match the configured value.`)
|
||||
}
|
||||
return { name: input.name, content }
|
||||
}
|
||||
|
||||
function loadLocalInputs(config: ResolvedSpriteConfig, input: string[]): SpriteSourceContent[] {
|
||||
if (input.length === 0) return []
|
||||
const folder = resolveSpriteSources({ name: config.name, format: 'stack', input })
|
||||
return folder.files.map((filePath) => ({
|
||||
name: path.basename(filePath, '.svg'),
|
||||
content: fs.readFileSync(filePath, 'utf8'),
|
||||
}))
|
||||
}
|
||||
|
||||
export async function prepareStandaloneServerSprite(
|
||||
config: ResolvedSpriteConfig,
|
||||
): Promise<PreparedContentSprite> {
|
||||
const localInput = config.input.filter((entry): entry is string => typeof entry === 'string')
|
||||
const httpInput = config.input.filter((entry): entry is ServerSvgInput => typeof entry !== 'string')
|
||||
const sources = [
|
||||
...loadLocalInputs(config, localInput),
|
||||
...await Promise.all(httpInput.map(loadHttpInput)),
|
||||
].sort((left, right) => left.name.localeCompare(right.name))
|
||||
|
||||
if (sources.length === 0) throw new Error('standalone@server requires at least one SVG input.')
|
||||
const totalBytes = sources.reduce((total, source) => total + Buffer.byteLength(source.content), 0)
|
||||
if (totalBytes > MAX_TOTAL_BYTES) {
|
||||
throw new Error(`standalone@server inputs exceed the ${MAX_TOTAL_BYTES} total byte limit.`)
|
||||
}
|
||||
const iconNames = sources.map((source) => source.name)
|
||||
if (new Set(iconNames).size !== iconNames.length) {
|
||||
throw new Error('standalone@server inputs contain duplicate icon names.')
|
||||
}
|
||||
validateIconIds(iconNames)
|
||||
|
||||
return { kind: 'content', name: config.name, sources, iconNames }
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const standaloneViteAdapter: ModeAdapter<'standalone@vite'> = {
|
||||
mode: 'standalone@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const standaloneWebpackAdapter: ModeAdapter<'standalone@webpack'> = {
|
||||
mode: 'standalone@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const standaloneAdapter: ModeAdapter<'standalone'> = {
|
||||
mode: 'standalone',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const svelteViteAdapter: ModeAdapter<'svelte@vite'> = {
|
||||
mode: 'svelte@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const svelteWebpackAdapter: ModeAdapter<'svelte@webpack'> = {
|
||||
mode: 'svelte@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const sveltekitViteAdapter: ModeAdapter<'sveltekit@vite'> = {
|
||||
mode: 'sveltekit@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const vueViteAdapter: ModeAdapter<'vue@vite'> = {
|
||||
mode: 'vue@vite',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(
|
||||
context.prepared.folder,
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { compileSpriteContent } from '../../compiler.js'
|
||||
import { createCompiledSpriteArtifact } from '../../core/compiled-artifact.js'
|
||||
import { resolveCompiledSpriteArtifact } from '../../core/resolve-compiled-artifact.js'
|
||||
import type { ModeAdapter } from '../../core/mode-adapter.js'
|
||||
import { generateOutputFiles } from './output.js'
|
||||
|
||||
export const vueWebpackAdapter: ModeAdapter<'vue@webpack'> = {
|
||||
mode: 'vue@webpack',
|
||||
async generate(context) {
|
||||
const bytes = await compileSpriteContent(context.prepared.folder, context.config.transform, {
|
||||
rootViewBox: false,
|
||||
})
|
||||
const artifact = createCompiledSpriteArtifact(bytes, context.prepared.iconNames, 'stack')
|
||||
const artifact = await resolveCompiledSpriteArtifact(
|
||||
context.prepared,
|
||||
context.config.transform,
|
||||
{ rootViewBox: false },
|
||||
)
|
||||
|
||||
return {
|
||||
files: generateOutputFiles(context.config, artifact),
|
||||
|
||||
164
src/release/load.ts
Normal file
164
src/release/load.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createCompiledSpriteArtifact, type CompiledIcon, type CompiledSpriteArtifact } from '../core/compiled-artifact.js'
|
||||
import type { PreparedRemoteSprite } from '../core/mode-adapter.js'
|
||||
import type { SpriteCompileProfile, ServerSpriteAsset, ServerSpriteManifest } from './types.js'
|
||||
|
||||
const MAX_MANIFEST_BYTES = 1024 * 1024
|
||||
const MAX_SPRITE_BYTES = 25 * 1024 * 1024
|
||||
const REQUEST_TIMEOUT_MS = 15_000
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function sha256(bytes: Uint8Array): string {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function assertHash(value: unknown, field: string): asserts value is string {
|
||||
if (typeof value !== 'string' || !/^[a-f0-9]{64}$/.test(value)) {
|
||||
throw new Error(`Remote sprite manifest: "${field}" must be a lowercase SHA-256 hash.`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateIcon(value: unknown, index: number): asserts value is CompiledIcon {
|
||||
if (!isRecord(value)) throw new Error(`Remote sprite manifest: icons[${index}] must be an object.`)
|
||||
if (typeof value.name !== 'string' || value.name === '') {
|
||||
throw new Error(`Remote sprite manifest: icons[${index}].name must be a non-empty string.`)
|
||||
}
|
||||
if (typeof value.id !== 'string' || value.id === '') {
|
||||
throw new Error(`Remote sprite manifest: icons[${index}].id must be a non-empty string.`)
|
||||
}
|
||||
if (value.viewBox !== null && typeof value.viewBox !== 'string') {
|
||||
throw new Error(`Remote sprite manifest: icons[${index}].viewBox must be a string or null.`)
|
||||
}
|
||||
if (!Array.isArray(value.colors) || value.colors.some((color) => (
|
||||
!isRecord(color)
|
||||
|| typeof color.variable !== 'string'
|
||||
|| typeof color.fallback !== 'string'
|
||||
))) {
|
||||
throw new Error(`Remote sprite manifest: icons[${index}].colors must be valid color metadata.`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateAsset(value: unknown, profile: SpriteCompileProfile): asserts value is ServerSpriteAsset {
|
||||
if (!isRecord(value)) throw new Error(`Remote sprite manifest: missing "${profile}" sprite profile.`)
|
||||
if (typeof value.href !== 'string' || value.href === '') {
|
||||
throw new Error(`Remote sprite manifest: sprites.${profile}.href must be a non-empty string.`)
|
||||
}
|
||||
assertHash(value.sha256, `sprites.${profile}.sha256`)
|
||||
if (!Number.isSafeInteger(value.byteLength) || (value.byteLength as number) <= 0) {
|
||||
throw new Error(`Remote sprite manifest: sprites.${profile}.byteLength must be a positive integer.`)
|
||||
}
|
||||
}
|
||||
|
||||
function validateTransform(value: unknown): asserts value is ServerSpriteManifest['transform'] {
|
||||
if (!isRecord(value)) throw new Error('Remote sprite manifest: "transform" must be an object.')
|
||||
for (const option of ['removeSize', 'replaceColors', 'addTransition']) {
|
||||
if (typeof value[option] !== 'boolean') {
|
||||
throw new Error(`Remote sprite manifest: transform.${option} must be a boolean.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateServerSpriteManifest(value: unknown): asserts value is ServerSpriteManifest {
|
||||
if (!isRecord(value)) throw new Error('Remote sprite manifest must be an object.')
|
||||
if (value.kind !== '@gromlab/svg-sprites/server' || value.schemaVersion !== 1) {
|
||||
throw new Error('Remote sprite manifest has an unsupported kind or schemaVersion.')
|
||||
}
|
||||
if (value.generator !== '@gromlab/svg-sprites' || value.mode !== 'standalone@server' || value.target !== 'server') {
|
||||
throw new Error('Remote sprite manifest was not produced by standalone@server.')
|
||||
}
|
||||
if (typeof value.name !== 'string' || value.name === '') throw new Error('Remote sprite manifest: "name" is required.')
|
||||
if (value.description !== undefined && typeof value.description !== 'string') {
|
||||
throw new Error('Remote sprite manifest: "description" must be a string.')
|
||||
}
|
||||
if (value.format !== 'stack') throw new Error('Remote sprite manifest: unsupported sprite format.')
|
||||
if (typeof value.generatedNotice !== 'boolean') throw new Error('Remote sprite manifest: "generatedNotice" must be a boolean.')
|
||||
validateTransform(value.transform)
|
||||
if (!Array.isArray(value.icons)) throw new Error('Remote sprite manifest: "icons" must be an array.')
|
||||
value.icons.forEach(validateIcon)
|
||||
if (value.iconCount !== value.icons.length) throw new Error('Remote sprite manifest: iconCount does not match icons.')
|
||||
if (new Set(value.icons.map((icon) => icon.name)).size !== value.icons.length) {
|
||||
throw new Error('Remote sprite manifest contains duplicate icon names.')
|
||||
}
|
||||
if (new Set(value.icons.map((icon) => icon.id)).size !== value.icons.length) {
|
||||
throw new Error('Remote sprite manifest contains duplicate icon IDs.')
|
||||
}
|
||||
if (!isRecord(value.sprites)) throw new Error('Remote sprite manifest: "sprites" must be an object.')
|
||||
validateAsset(value.sprites.stack, 'stack')
|
||||
validateAsset(value.sprites['stack-root-viewbox'], 'stack-root-viewbox')
|
||||
}
|
||||
|
||||
function isHttpLocation(location: string): boolean {
|
||||
return /^https?:\/\//i.test(location)
|
||||
}
|
||||
|
||||
async function fetchBytes(url: string, maximumBytes: number, label: string): Promise<{ bytes: Uint8Array; location: string }> {
|
||||
const response = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) })
|
||||
if (!response.ok) throw new Error(`Cannot load ${label}: ${response.status} ${response.statusText}`)
|
||||
const declaredLength = Number(response.headers.get('content-length'))
|
||||
if (Number.isFinite(declaredLength) && declaredLength > maximumBytes) {
|
||||
throw new Error(`${label} exceeds the ${maximumBytes} byte limit.`)
|
||||
}
|
||||
const bytes = new Uint8Array(await response.arrayBuffer())
|
||||
if (bytes.byteLength > maximumBytes) throw new Error(`${label} exceeds the ${maximumBytes} byte limit.`)
|
||||
return { bytes, location: response.url }
|
||||
}
|
||||
|
||||
async function readResource(location: string, maximumBytes: number, label: string): Promise<{ bytes: Uint8Array; location: string }> {
|
||||
if (isHttpLocation(location)) return fetchBytes(location, maximumBytes, label)
|
||||
const bytes = fs.readFileSync(location)
|
||||
if (bytes.byteLength > maximumBytes) throw new Error(`${label} exceeds the ${maximumBytes} byte limit.`)
|
||||
return { bytes, location: path.resolve(location) }
|
||||
}
|
||||
|
||||
function resolveAssetLocation(manifestLocation: string, href: string): string {
|
||||
if (isHttpLocation(href)) return href
|
||||
if (isHttpLocation(manifestLocation)) return new URL(href, manifestLocation).href
|
||||
return path.resolve(path.dirname(manifestLocation), href)
|
||||
}
|
||||
|
||||
export async function prepareRemoteSprite(manifestLocation: string): Promise<PreparedRemoteSprite> {
|
||||
const resource = await readResource(manifestLocation, MAX_MANIFEST_BYTES, 'remote sprite manifest')
|
||||
let manifest: unknown
|
||||
try {
|
||||
manifest = JSON.parse(new TextDecoder().decode(resource.bytes))
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : String(error)
|
||||
throw new Error(`Cannot parse remote sprite manifest: ${reason}`)
|
||||
}
|
||||
validateServerSpriteManifest(manifest)
|
||||
return {
|
||||
kind: 'remote',
|
||||
manifest,
|
||||
manifestLocation: resource.location,
|
||||
iconNames: manifest.icons.map((icon) => icon.name),
|
||||
}
|
||||
}
|
||||
|
||||
function sameIcons(actual: readonly CompiledIcon[], expected: readonly CompiledIcon[]): boolean {
|
||||
return JSON.stringify(actual) === JSON.stringify(expected)
|
||||
}
|
||||
|
||||
export async function loadRemoteSpriteArtifact(
|
||||
prepared: PreparedRemoteSprite,
|
||||
profile: SpriteCompileProfile,
|
||||
): Promise<CompiledSpriteArtifact> {
|
||||
const asset = prepared.manifest.sprites[profile]
|
||||
const location = resolveAssetLocation(prepared.manifestLocation, asset.href)
|
||||
const resource = await readResource(location, MAX_SPRITE_BYTES, `remote sprite profile "${profile}"`)
|
||||
if (resource.bytes.byteLength !== asset.byteLength) {
|
||||
throw new Error(`Remote sprite profile "${profile}" byteLength does not match its manifest.`)
|
||||
}
|
||||
if (sha256(resource.bytes) !== asset.sha256) {
|
||||
throw new Error(`Remote sprite profile "${profile}" SHA-256 does not match its manifest.`)
|
||||
}
|
||||
const artifact = createCompiledSpriteArtifact(resource.bytes, prepared.iconNames, 'stack')
|
||||
if (!sameIcons(artifact.icons, prepared.manifest.icons)) {
|
||||
throw new Error(`Remote sprite profile "${profile}" icon metadata does not match its manifest.`)
|
||||
}
|
||||
return artifact
|
||||
}
|
||||
26
src/release/types.ts
Normal file
26
src/release/types.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { CompiledIcon } from '../core/compiled-artifact.js'
|
||||
import type { TransformOptions } from '../types.js'
|
||||
|
||||
export type SpriteCompileProfile = 'stack' | 'stack-root-viewbox'
|
||||
|
||||
export type ServerSpriteAsset = {
|
||||
readonly href: string
|
||||
readonly sha256: string
|
||||
readonly byteLength: number
|
||||
}
|
||||
|
||||
export type ServerSpriteManifest = {
|
||||
readonly kind: '@gromlab/svg-sprites/server'
|
||||
readonly schemaVersion: 1
|
||||
readonly generator: '@gromlab/svg-sprites'
|
||||
readonly mode: 'standalone@server'
|
||||
readonly target: 'server'
|
||||
readonly name: string
|
||||
readonly description?: string
|
||||
readonly format: 'stack'
|
||||
readonly generatedNotice: boolean
|
||||
readonly transform: Required<TransformOptions>
|
||||
readonly iconCount: number
|
||||
readonly icons: readonly CompiledIcon[]
|
||||
readonly sprites: Readonly<Record<SpriteCompileProfile, ServerSpriteAsset>>
|
||||
}
|
||||
@@ -14,6 +14,9 @@ export type ViteAssetTarget = 'vite'
|
||||
*/
|
||||
export type WebpackAssetTarget = 'webpack'
|
||||
|
||||
/** Target серверной сборки immutable sprite release. */
|
||||
export type ServerAssetTarget = 'server'
|
||||
|
||||
/** Asset target для проекта без сборщика. */
|
||||
export type StaticAssetTarget = 'static'
|
||||
|
||||
@@ -60,8 +63,11 @@ export type StandaloneAssetTarget = StaticAssetTarget | ViteAssetTarget | Webpac
|
||||
/** Полный ключ standalone mode, используемый конфигом, CLI и manifest. */
|
||||
export type StandaloneSpriteMode = 'standalone' | `standalone@${ViteAssetTarget | WebpackAssetTarget}`
|
||||
|
||||
/** Серверный mode, публикующий готовые sprite profiles и manifest. */
|
||||
export type ServerSpriteMode = 'standalone@server'
|
||||
|
||||
/** Любая среда, для которой может быть сгенерирован React sprite-модуль. */
|
||||
export type SpriteAssetTarget = ReactAssetTarget | NextAssetTarget | StaticAssetTarget | AngularAssetTarget
|
||||
export type SpriteAssetTarget = ReactAssetTarget | NextAssetTarget | StaticAssetTarget | AngularAssetTarget | ServerAssetTarget
|
||||
|
||||
/** Режим генерации sprite-модуля. */
|
||||
export type SpriteMode =
|
||||
@@ -78,6 +84,7 @@ export type SpriteMode =
|
||||
| AlpineSpriteMode
|
||||
| NextAssetTarget
|
||||
| StandaloneSpriteMode
|
||||
| ServerSpriteMode
|
||||
|
||||
/** Фрагменты кода, необходимые компоненту для получения URL SVG asset. */
|
||||
export type SpriteAssetUrlCode = {
|
||||
|
||||
20
src/types.ts
20
src/types.ts
@@ -1,5 +1,16 @@
|
||||
import type { SpriteMode } from './targets/types.js'
|
||||
|
||||
export type SpriteSource = 'local' | 'remote'
|
||||
|
||||
/** HTTP SVG input серверного mode. Имя задаёт публичное имя иконки. */
|
||||
export type ServerSvgInput = {
|
||||
name: string
|
||||
url: string
|
||||
sha256?: string
|
||||
}
|
||||
|
||||
export type SpriteInput = string | ServerSvgInput
|
||||
|
||||
/** Формат спрайта: stack или symbol. */
|
||||
export type SpriteFormat = 'stack' | 'symbol'
|
||||
|
||||
@@ -24,16 +35,18 @@ export type TransformOptions = {
|
||||
addTransition?: boolean
|
||||
}
|
||||
|
||||
/** Единая конфигурация локального sprite-модуля. */
|
||||
/** Единая конфигурация sprite-модуля. Exact mode уточняет допустимый input. */
|
||||
export type SpriteConfig = {
|
||||
/** Режим можно определить в конфиге либо передать через CLI/API. */
|
||||
mode?: SpriteMode
|
||||
/** Локальные SVG по умолчанию либо готовый server manifest. */
|
||||
source?: SpriteSource
|
||||
/** Логическое имя спрайта. По умолчанию выводится из каталога модуля. */
|
||||
name?: string
|
||||
/** Описание спрайта для generated types и manifest. */
|
||||
description?: string
|
||||
/** Путь или glob-шаблоны исходных SVG относительно корня модуля. По умолчанию: ./icons. */
|
||||
input?: string | string[]
|
||||
input?: SpriteInput | SpriteInput[]
|
||||
/** Настройки трансформации SVG. По умолчанию все включены. */
|
||||
transform?: TransformOptions
|
||||
/** Добавлять развёрнутое предупреждение в generated-файлы. По умолчанию: true. */
|
||||
@@ -43,9 +56,10 @@ export type SpriteConfig = {
|
||||
/** Полностью разрешённая конфигурация, готовая к генерации. */
|
||||
export type ResolvedSpriteConfig = {
|
||||
mode: SpriteMode
|
||||
source: SpriteSource
|
||||
name: string
|
||||
description?: string
|
||||
input: string[]
|
||||
input: SpriteInput[]
|
||||
transform: Required<TransformOptions>
|
||||
generatedNotice: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user