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-тестов
This commit is contained in:
2026-07-15 16:56:44 +03:00
parent e54ab4991c
commit 8b3365f957
378 changed files with 21188 additions and 650 deletions

View File

@@ -0,0 +1,162 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { createJiti } from 'jiti'
const jiti = createJiti(import.meta.url)
const [
{ angularApplicationAdapter },
{ angularWebpackAdapter },
{ astroViteAdapter },
] = await Promise.all([
jiti.import('../src/modes/angular-application/adapter.ts'),
jiti.import('../src/modes/angular-webpack/adapter.ts'),
jiti.import('../src/modes/astro-vite/adapter.ts'),
])
function createContext(mode) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-framework-family-'))
const iconsDir = path.join(rootDir, 'icons')
const iconPath = path.join(iconsDir, 'check.svg')
fs.mkdirSync(iconsDir)
fs.writeFileSync(
iconPath,
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
return {
rootDir,
config: {
mode,
name: 'app',
description: 'Application icons',
input: [iconsDir],
transform: {
removeSize: true,
replaceColors: true,
addTransition: true,
},
generatedNotice: false,
},
prepared: {
folder: {
name: 'app',
format: 'stack',
path: iconsDir,
files: [iconPath],
},
iconNames: ['check'],
},
}
}
function filesByPath(plan) {
return new Map(plan.files.map((file) => [file.path, String(file.content)]))
}
test('Angular application adapter emits a standalone component for the application builder', async () => {
const plan = await angularApplicationAdapter.generate(createContext('angular@application'))
const files = filesByPath(plan)
assert.equal(angularApplicationAdapter.mode, 'angular@application')
assert.equal(plan.result.target, 'application')
assert.equal(plan.paths.entry, '.svg-sprite/index.ts')
assert.deepEqual([...files.keys()].sort(), [
'.svg-sprite/angular/angular-component.css',
'.svg-sprite/angular/angular-component.d.ts',
'.svg-sprite/angular/angular-component.ts',
'.svg-sprite/assets.d.ts',
'.svg-sprite/icon-data.d.ts',
'.svg-sprite/icon-data.js',
'.svg-sprite/index.d.ts',
'.svg-sprite/index.ts',
'.svg-sprite/sprite.svg',
'.svg-sprite/svg-sprite.manifest.d.ts',
'.svg-sprite/svg-sprite.manifest.js',
])
const component = files.get('.svg-sprite/angular/angular-component.ts')
const declarations = files.get('.svg-sprite/angular/angular-component.d.ts')
const manifest = files.get('.svg-sprite/svg-sprite.manifest.js')
assert.match(component, /@Component\(\{/)
assert.match(component, /standalone: true/)
assert.match(component, /export class AppIcon/)
assert.match(component, /input\.required<AppIconName>\(\)/)
assert.match(component, /reference path="\.\.\/assets\.d\.ts"/)
assert.match(component, /import spriteUrl from '\.\.\/sprite\.svg'/)
assert.match(declarations, /InputSignal<AppIconName>/)
assert.match(declarations, /ɵɵComponentDeclaration/)
assert.match(manifest, /import spriteUrl from '\.\/sprite\.svg'/)
assert.match(manifest, /"mode": "angular@application"/)
assert.match(manifest, /"framework": "angular"/)
assert.doesNotMatch(component, /customElements|HTMLElement|attachShadow/)
})
test('Angular Webpack adapter owns its Webpack asset contract', async () => {
const plan = await angularWebpackAdapter.generate(createContext('angular@webpack'))
const files = filesByPath(plan)
const component = files.get('.svg-sprite/angular/angular-component.ts')
const manifest = files.get('.svg-sprite/svg-sprite.manifest.js')
assert.equal(angularWebpackAdapter.mode, 'angular@webpack')
assert.equal(plan.result.target, 'webpack')
assert.equal(files.has('.svg-sprite/assets.d.ts'), false)
assert.match(component, /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/)
assert.match(manifest, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/)
assert.match(manifest, /"mode": "angular@webpack"/)
assert.doesNotMatch(component, /customElements|HTMLElement|attachShadow/)
})
test('Astro Vite adapter emits an Astro component with JavaScript frontmatter', async () => {
const plan = await astroViteAdapter.generate(createContext('astro@vite'))
const files = filesByPath(plan)
assert.equal(astroViteAdapter.mode, 'astro@vite')
assert.equal(plan.result.target, 'vite')
assert.equal(plan.paths.entry, '.svg-sprite/index.js')
assert.deepEqual([...files.keys()].sort(), [
'.svg-sprite/astro/astro-component.astro',
'.svg-sprite/astro/astro-component.astro.d.ts',
'.svg-sprite/astro/astro-component.css',
'.svg-sprite/icon-data.d.ts',
'.svg-sprite/icon-data.js',
'.svg-sprite/index.d.ts',
'.svg-sprite/index.js',
'.svg-sprite/sprite.svg',
'.svg-sprite/svg-sprite.manifest.d.ts',
'.svg-sprite/svg-sprite.manifest.js',
])
const component = files.get('.svg-sprite/astro/astro-component.astro')
const declarations = files.get('.svg-sprite/astro/astro-component.astro.d.ts')
const manifest = files.get('.svg-sprite/svg-sprite.manifest.js')
assert.match(component, /^---\n/)
assert.match(component, /sprite\.svg\?no-inline/)
assert.match(component, /Astro\.props/)
assert.doesNotMatch(component, /lang=["']ts|interface Props|customElements|HTMLElement|attachShadow/)
assert.match(declarations, /HTMLAttributes<'svg'>/)
assert.match(declarations, /export type AppIconProps/)
assert.match(manifest, /sprite\.svg\?no-inline/)
assert.match(manifest, /"framework": "astro"/)
assert.match(manifest, /"componentName": "AppIcon"/)
assert.match(manifest, /"mode": "astro@vite"/)
})
test('framework-family adapters have no horizontal mode imports', () => {
for (const modeDirectory of ['angular-application', 'angular-webpack', 'astro-vite']) {
for (const fileName of ['adapter.ts', 'output.ts']) {
const source = fs.readFileSync(
path.resolve(import.meta.dirname, '..', 'src', 'modes', modeDirectory, fileName),
'utf8',
)
const imports = [...source.matchAll(/^import .* from ['"]([^'"]+)['"]$/gm)]
for (const [, specifier] of imports) {
if (specifier.startsWith('../')) assert.match(specifier, /^\.\.\/\.\.\//)
assert.doesNotMatch(specifier, /(?:^|\/)modes\//)
}
}
}
})

View File

@@ -11,6 +11,26 @@ const guideModes = new Map([
['standalone-webpack.md', 'standalone@webpack'],
['react-vite.md', 'react@vite'],
['react-webpack.md', 'react@webpack'],
['vue-vite.md', 'vue@vite'],
['vue-webpack.md', 'vue@webpack'],
['nuxt-vite.md', 'nuxt@vite'],
['nuxt-webpack.md', 'nuxt@webpack'],
['svelte-vite.md', 'svelte@vite'],
['svelte-webpack.md', 'svelte@webpack'],
['sveltekit-vite.md', 'sveltekit@vite'],
['angular-application.md', 'angular@application'],
['angular-webpack.md', 'angular@webpack'],
['astro-vite.md', 'astro@vite'],
['solid-vite.md', 'solid@vite'],
['solid-webpack.md', 'solid@webpack'],
['solid-start-vite.md', 'solid-start@vite'],
['preact-vite.md', 'preact@vite'],
['preact-webpack.md', 'preact@webpack'],
['qwik-vite.md', 'qwik@vite'],
['lit-vite.md', 'lit@vite'],
['lit-webpack.md', 'lit@webpack'],
['alpine-vite.md', 'alpine@vite'],
['alpine-webpack.md', 'alpine@webpack'],
['next-app-turbopack.md', 'next@app/turbopack'],
['next-app-webpack.md', 'next@app/webpack'],
['next-pages-turbopack.md', 'next@pages/turbopack'],
@@ -35,6 +55,11 @@ const commandPatterns = {
ru: /npx --yes @gromlab\/svg-sprites(?:\s|$)/,
}
const spriteDirectoryPatterns = {
en: /Choose a directory for the future SVG sprite/,
ru: /Выберите каталог для будущего SVG-спрайта/,
}
function markdownFiles(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const entryPath = path.join(directory, entry.name)
@@ -67,10 +92,12 @@ test('exact-mode guides are reusable by docs and skills', () => {
]
assert.deepEqual(headings, expectedHeadings)
assert.match(source, commandPatterns[language])
assert.match(source, spriteDirectoryPatterns[language])
if (mode !== 'standalone') {
assert.match(source, /npm install --save-dev @gromlab\/svg-sprites/)
}
assert.doesNotMatch(source, /npx --yes[^\n]*@gromlab\/svg-sprites@/)
assert.doesNotMatch(source, /check\.svg|icon="check"|#check|iconName: 'check'/)
const modePattern = new RegExp(`"mode": "${mode.replaceAll('/', '\\/')}"`)
assert.match(source, modePattern)
assert.doesNotMatch(source, /\]\([^)]+\)/, `${language}/${file} must not depend on its location`)

View File

@@ -0,0 +1,90 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
const CASES = [
{ mode: 'lit@vite', framework: 'lit', target: 'vite', folder: 'lit', runtime: 'lit-component.js' },
{ mode: 'lit@webpack', framework: 'lit', target: 'webpack', folder: 'lit', runtime: 'lit-component.js' },
{ mode: 'alpine@vite', framework: 'alpine', target: 'vite', folder: 'alpine', runtime: 'alpine-integration.js' },
{ mode: 'alpine@webpack', framework: 'alpine', target: 'webpack', folder: 'alpine', runtime: 'alpine-integration.js' },
]
test('keeps Lit and Alpine exact modes isolated', () => {
for (const { mode } of CASES) {
const modeDir = path.join(import.meta.dirname, '..', 'src', 'modes', mode.replace('@', '-'))
const source = ['adapter.ts', 'output.ts']
.map((file) => fs.readFileSync(path.join(modeDir, file), 'utf8'))
.join('\n')
assert.doesNotMatch(source, /from ['"][^'"]*\.\.\/(?:lit|alpine)-(?:vite|webpack)/)
assert.match(source, new RegExp(`const MODE = '${mode}'|mode: '${mode}'`))
}
})
test('generates isolated Lit and Alpine exact-mode contracts', async (t) => {
for (const fixture of CASES) {
await t.test(fixture.mode, async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), `svg-sprites-${fixture.framework}-`))
const iconsDir = path.join(rootDir, 'icons')
fs.mkdirSync(iconsDir)
fs.writeFileSync(
path.join(iconsDir, 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
assert.equal(isSpriteMode(fixture.mode), true)
const result = await generateSprite(rootDir, {
mode: fixture.mode,
name: 'app',
generatedNotice: false,
})
const generatedDir = path.join(rootDir, '.svg-sprite')
const runtimePath = path.join(generatedDir, fixture.folder, fixture.runtime)
const runtime = fs.readFileSync(runtimePath, 'utf8')
const runtimeTypes = fs.readFileSync(runtimePath.replace(/\.js$/, '.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
const manifestTypes = fs.readFileSync(path.join(generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8')
assert.equal(result.mode, fixture.mode)
assert.equal(result.target, fixture.target)
assert.equal(result.iconCount, 1)
assert.deepEqual(fs.readdirSync(path.join(generatedDir, fixture.folder)).sort(), [
`${fixture.runtime.replace(/\.js$/, '')}.css`,
`${fixture.runtime.replace(/\.js$/, '')}.d.ts`,
fixture.runtime,
])
assert.match(manifest, new RegExp(`"framework": "${fixture.framework}"`))
assert.match(manifest, new RegExp(`"mode": "${fixture.mode}"`))
assert.match(manifestTypes, new RegExp(`mode: '${fixture.mode}'`))
if (fixture.framework === 'lit') {
assert.match(runtime, /import \{ LitElement, html, nothing, unsafeCSS \} from 'lit'/)
assert.match(runtime, /export class AppIcon extends LitElement/)
assert.match(runtime, /export function defineAppIcon\(\)/)
assert.match(runtimeTypes, /export declare class AppIcon extends LitElement/)
assert.match(runtimeTypes, /"app-icon": AppIcon/)
assert.match(manifest, /"componentName": "AppIcon"/)
} else {
assert.match(runtime, /export const appAlpinePlugin = \(Alpine\) =>/)
assert.match(runtime, /Alpine\.directive\(appIconDirective/)
assert.match(runtime, /Alpine\.magic\(appIconMagic/)
assert.doesNotMatch(runtime, /customElements|extends HTMLElement|LitElement/)
assert.match(runtimeTypes, /export type AppAlpineApi/)
assert.match(runtimeTypes, /export declare const appAlpinePlugin: AppAlpinePluginType/)
assert.doesNotMatch(runtimeTypes, /from ['"]alpinejs['"]|import\(['"]alpinejs['"]\)/)
}
if (fixture.target === 'vite') {
assert.match(runtime, /sprite\.svg\?no-inline/)
assert.match(manifest, /sprite\.svg\?no-inline/)
} else {
assert.match(runtime, /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/)
assert.match(manifest, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/)
}
})
}
})

View File

@@ -0,0 +1,52 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
const MODES = [
{ mode: 'preact@vite', target: 'vite', asset: /sprite\.svg\?no-inline/ },
{ mode: 'preact@webpack', target: 'webpack', asset: /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/ },
]
function fixture() {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-preact-'))
fs.mkdirSync(path.join(rootDir, 'icons'))
fs.writeFileSync(
path.join(rootDir, 'icons', 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
return rootDir
}
test('generates every isolated Preact family contract', async () => {
for (const contract of MODES) {
assert.equal(isSpriteMode(contract.mode), true)
const result = await generateSprite(fixture(), {
mode: contract.mode,
name: 'app-icons',
generatedNotice: false,
})
const component = fs.readFileSync(path.join(result.generatedDir, 'preact', 'preact-component.js'), 'utf8')
const componentTypes = fs.readFileSync(path.join(result.generatedDir, 'preact', 'preact-component.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
assert.equal(result.mode, contract.mode)
assert.equal(result.target, contract.target)
assert.deepEqual(fs.readdirSync(path.join(result.generatedDir, 'preact')).sort(), [
'preact-component.d.ts',
'preact-component.js',
'preact-component.module.css',
])
assert.match(component, /import \{ h \} from 'preact'/)
assert.match(component, /export const AppIconsIcon/)
assert.match(component, contract.asset)
assert.match(componentTypes, /FunctionalComponent<AppIconsIconProps>/)
assert.match(manifest, /"framework": "preact"/)
assert.match(manifest, new RegExp(`"mode": ${JSON.stringify(contract.mode)}`))
assert.doesNotMatch(component, /customElements|HTMLElement|gromlab-sprite-viewer/)
assert.equal(fs.existsSync(path.join(result.generatedDir, 'preact', 'preact-component.tsx')), false)
}
})

View File

@@ -0,0 +1,43 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
test('generates an isolated SSR-safe Qwik Vite contract', async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-qwik-'))
fs.mkdirSync(path.join(rootDir, 'icons'))
fs.writeFileSync(
path.join(rootDir, 'icons', 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
assert.equal(isSpriteMode('qwik@vite'), true)
const result = await generateSprite(rootDir, {
mode: 'qwik@vite',
name: 'app-icons',
generatedNotice: false,
})
const component = fs.readFileSync(path.join(result.generatedDir, 'qwik', 'qwik-component.jsx'), 'utf8')
const componentTypes = fs.readFileSync(path.join(result.generatedDir, 'qwik', 'qwik-component.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
assert.equal(result.mode, 'qwik@vite')
assert.equal(result.target, 'vite')
assert.deepEqual(fs.readdirSync(path.join(result.generatedDir, 'qwik')).sort(), [
'qwik-component.d.ts',
'qwik-component.jsx',
'qwik-component.module.css',
])
assert.match(component, /import \{ component\$ \} from '@builder\.io\/qwik'/)
assert.match(component, /sprite\.svg\?no-inline/)
assert.match(component, /export const AppIconsIcon = component\$/)
assert.match(componentTypes, /Component<AppIconsIconProps>/)
assert.match(manifest, /"framework": "qwik"/)
assert.match(manifest, /"ssr": true/)
assert.match(manifest, /"mode": "qwik@vite"/)
assert.doesNotMatch(component, /\b(?:window|document|navigator|customElements|HTMLElement)\b/)
assert.equal(fs.existsSync(path.join(result.generatedDir, 'qwik', 'qwik-component.tsx')), false)
})

59
test/solid-modes.test.mjs Normal file
View File

@@ -0,0 +1,59 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
const MODES = [
{ mode: 'solid@vite', target: 'vite', asset: /sprite\.svg\?no-inline/ },
{ mode: 'solid@webpack', target: 'webpack', asset: /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/ },
{ mode: 'solid-start@vite', target: 'vite', asset: /sprite\.svg\?no-inline/, ssr: true },
]
function fixture() {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-solid-'))
fs.mkdirSync(path.join(rootDir, 'icons'))
fs.writeFileSync(
path.join(rootDir, 'icons', 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
return rootDir
}
test('generates every isolated Solid family contract', async () => {
for (const contract of MODES) {
assert.equal(isSpriteMode(contract.mode), true)
const result = await generateSprite(fixture(), {
mode: contract.mode,
name: 'app-icons',
generatedNotice: false,
})
const componentPath = path.join(result.generatedDir, 'solid', 'solid-component.jsx')
const component = fs.readFileSync(componentPath, 'utf8')
const componentTypes = fs.readFileSync(path.join(result.generatedDir, 'solid', 'solid-component.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
assert.equal(result.mode, contract.mode)
assert.equal(result.target, contract.target)
assert.deepEqual(fs.readdirSync(path.join(result.generatedDir, 'solid')).sort(), [
'solid-component.d.ts',
'solid-component.jsx',
'solid-component.module.css',
])
assert.match(component, /import \{ Show, splitProps \} from 'solid-js'/)
assert.match(component, /export const AppIconsIcon/)
assert.match(component, contract.asset)
assert.match(componentTypes, /Component<AppIconsIconProps>/)
assert.match(manifest, /"framework": "solid(?:-start)?"/)
assert.match(manifest, new RegExp(`"mode": ${JSON.stringify(contract.mode)}`))
assert.doesNotMatch(component, /customElements|HTMLElement|gromlab-sprite-viewer/)
assert.equal(fs.existsSync(path.join(result.generatedDir, 'solid', 'solid-component.tsx')), false)
if (contract.ssr) {
assert.match(manifest, /"ssr": true/)
assert.doesNotMatch(component, /\b(?:window|document|navigator|customElements)\b/)
}
}
})

View File

@@ -0,0 +1,69 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
const GENERATED_FILES = [
'icon-data.d.ts',
'icon-data.js',
'index.d.ts',
'index.js',
'sprite.svg',
'svelte',
'svg-sprite.manifest.d.ts',
'svg-sprite.manifest.js',
]
test('generates isolated native Svelte contracts for every Svelte exact mode', async () => {
const modes = [
{ mode: 'svelte@vite', target: 'vite', url: /sprite\.svg\?no-inline/ },
{ mode: 'svelte@webpack', target: 'webpack', url: /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/ },
{ mode: 'sveltekit@vite', target: 'vite', url: /sprite\.svg\?no-inline/ },
]
for (const expected of modes) {
assert.equal(isSpriteMode(expected.mode), true)
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-svelte-'))
const iconsDir = path.join(rootDir, 'icons')
fs.mkdirSync(iconsDir)
fs.writeFileSync(
path.join(iconsDir, 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
const result = await generateSprite(rootDir, {
mode: expected.mode,
name: 'app',
generatedNotice: false,
})
const generatedDir = path.join(rootDir, '.svg-sprite')
const component = fs.readFileSync(path.join(generatedDir, 'svelte', 'svelte-component.svelte'), 'utf8')
const componentTypes = fs.readFileSync(path.join(generatedDir, 'svelte', 'svelte-component.svelte.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
const manifestTypes = fs.readFileSync(path.join(generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8')
assert.equal(result.mode, expected.mode)
assert.equal(result.target, expected.target)
assert.equal(result.iconCount, 1)
assert.deepEqual(fs.readdirSync(generatedDir).sort(), GENERATED_FILES)
assert.deepEqual(fs.readdirSync(path.join(generatedDir, 'svelte')).sort(), [
'svelte-component.svelte',
'svelte-component.svelte.d.ts',
])
assert.match(component, /<script>/)
assert.match(component, /\$props\(\)/)
assert.match(component, expected.url)
assert.doesNotMatch(component, /lang=["']ts["']/)
assert.doesNotMatch(component, /window|document|customElements/)
assert.match(componentTypes, /Component<AppIconProps>/)
assert.match(componentTypes, /SVGAttributes<SVGSVGElement>/)
assert.match(manifest, /"framework": "svelte"/)
assert.match(manifest, /"componentName": "AppIcon"/)
assert.match(manifest, new RegExp(`"mode": ${JSON.stringify(expected.mode)}`))
assert.match(manifestTypes, new RegExp(`mode: ${JSON.stringify(expected.mode).replaceAll('"', "'")}`))
assert.doesNotMatch(component, /from ['"].*modes\//)
}
})

View File

@@ -0,0 +1,85 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
const GENERATED_FILES = [
'icon-data.d.ts',
'icon-data.js',
'index.d.ts',
'index.js',
'sprite.svg',
'svg-sprite.manifest.d.ts',
'svg-sprite.manifest.js',
'vue',
]
const MODES = [
{
mode: 'vue@webpack',
target: 'webpack',
componentAsset: /new URL\('\.\.\/sprite\.svg', import\.meta\.url\)\.href/,
manifestAsset: /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/,
},
{
mode: 'nuxt@vite',
target: 'vite',
componentAsset: /sprite\.svg\?no-inline/,
manifestAsset: /sprite\.svg\?no-inline/,
},
{
mode: 'nuxt@webpack',
target: 'webpack',
componentAsset: /import spriteUrl from '\.\.\/sprite\.svg'/,
manifestAsset: /import spriteUrl from '\.\/sprite\.svg'/,
},
]
test('generates isolated Vue-family contracts for Vue Webpack and Nuxt', async () => {
for (const contract of MODES) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), `svg-sprites-${contract.mode.replace('@', '-')}-`))
const iconsDir = path.join(rootDir, 'icons')
fs.mkdirSync(iconsDir)
fs.writeFileSync(
path.join(iconsDir, 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
assert.equal(isSpriteMode(contract.mode), true)
const result = await generateSprite(rootDir, {
mode: contract.mode,
name: 'app',
generatedNotice: false,
})
const generatedDir = path.join(rootDir, '.svg-sprite')
const component = fs.readFileSync(path.join(generatedDir, 'vue', 'vue-component.js'), 'utf8')
const componentTypes = fs.readFileSync(path.join(generatedDir, 'vue', 'vue-component.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
const manifestTypes = fs.readFileSync(path.join(generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8')
assert.equal(result.mode, contract.mode)
assert.equal(result.target, contract.target)
assert.deepEqual(fs.readdirSync(generatedDir).sort(), GENERATED_FILES)
assert.deepEqual(fs.readdirSync(path.join(generatedDir, 'vue')).sort(), [
'vue-component.d.ts',
'vue-component.js',
'vue-component.module.css',
])
assert.match(component, /import \{ defineComponent, h \} from 'vue'/)
assert.match(component, /export const AppIcon = defineComponent/)
assert.match(component, contract.componentAsset)
assert.match(componentTypes, /DefineComponent<AppIconProps>/)
assert.match(componentTypes, /--icon-color-/)
assert.match(manifest, /"framework": "vue"/)
assert.match(manifest, /"componentName": "AppIcon"/)
assert.match(manifest, new RegExp(`"mode": ${JSON.stringify(contract.mode)}`))
assert.match(manifest, contract.manifestAsset)
assert.match(manifestTypes, new RegExp(`mode: ${JSON.stringify(contract.mode).replaceAll('"', "'")}`))
assert.doesNotMatch(component, /\b(?:window|document|customElements|HTMLElement)\b/)
assert.doesNotMatch(manifest, /\b(?:window|document|customElements|HTMLElement)\b/)
assert.doesNotMatch(component, /from '@gromlab\/svg-sprites/)
}
})

View File

@@ -0,0 +1,62 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import test from 'node:test'
import { generateSprite, isSpriteMode } from '../dist/index.js'
const GENERATED_FILES = [
'icon-data.d.ts',
'icon-data.js',
'index.d.ts',
'index.js',
'sprite.svg',
'svg-sprite.manifest.d.ts',
'svg-sprite.manifest.js',
'vue',
]
test('generates the isolated Vue Vite contract', async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-vue-vite-'))
const iconsDir = path.join(rootDir, 'icons')
fs.mkdirSync(iconsDir)
fs.writeFileSync(
path.join(iconsDir, 'check.svg'),
'<svg viewBox="0 0 24 24"><path fill="#16a34a" d="M2 12l6 6L22 4" /></svg>',
)
assert.equal(isSpriteMode('vue@vite'), true)
const result = await generateSprite(rootDir, {
mode: 'vue@vite',
name: 'app',
generatedNotice: false,
})
const generatedDir = path.join(rootDir, '.svg-sprite')
assert.equal(result.mode, 'vue@vite')
assert.equal(result.target, 'vite')
assert.equal(result.iconCount, 1)
assert.deepEqual(fs.readdirSync(generatedDir).sort(), GENERATED_FILES)
assert.deepEqual(fs.readdirSync(path.join(generatedDir, 'vue')).sort(), [
'vue-component.d.ts',
'vue-component.js',
'vue-component.module.css',
])
const component = fs.readFileSync(path.join(generatedDir, 'vue', 'vue-component.js'), 'utf8')
const componentTypes = fs.readFileSync(path.join(generatedDir, 'vue', 'vue-component.d.ts'), 'utf8')
const manifest = fs.readFileSync(result.manifestPath, 'utf8')
const manifestTypes = fs.readFileSync(path.join(generatedDir, 'svg-sprite.manifest.d.ts'), 'utf8')
assert.match(component, /import \{ defineComponent, h \} from 'vue'/)
assert.match(component, /export const AppIcon = defineComponent/)
assert.match(component, /sprite\.svg\?no-inline/)
assert.match(componentTypes, /DefineComponent<AppIconProps>/)
assert.match(componentTypes, /--icon-color-/)
assert.match(manifest, /"framework": "vue"/)
assert.match(manifest, /"componentName": "AppIcon"/)
assert.match(manifest, /"mode": "vue@vite"/)
assert.match(manifestTypes, /mode: 'vue@vite'/)
assert.doesNotMatch(component, /from '@gromlab\/svg-sprites/)
})