mirror of
https://github.com/gromlab-ru/svg-sprites.git
synced 2026-07-22 04:40:17 +03:00
- добавлены отдельные режимы генерации для React, Next.js и legacy - добавлены SpriteViewer, типизированные компоненты и безопасный codegen - перенесена сборка AI-скила и обновлена документация - добавлены migration guide, лицензии и проверки публикации
591 lines
21 KiB
JavaScript
591 lines
21 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { createHash } from 'node:crypto'
|
|
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import test from 'node:test'
|
|
import ts from 'typescript'
|
|
|
|
import { generateNextSprite, generateReactSprite } from '../dist/index.js'
|
|
|
|
const GENERATED_FILES = [
|
|
'.svg-sprites.manifest.json',
|
|
'react-component.tsx',
|
|
'sprite.svg',
|
|
'styles.module.css',
|
|
'types.ts',
|
|
]
|
|
|
|
function createReactFixture() {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-'))
|
|
const rootDir = path.join(temporaryDir, 'src', 'widgets', 'file-manager', 'svg-sprite')
|
|
const iconsDir = path.join(rootDir, 'icons')
|
|
|
|
fs.mkdirSync(iconsDir, { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default { description: 'File manager icons' }\n`,
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, 'check.svg'),
|
|
'<svg viewBox="0 0 16 16"><path fill="none" stroke="#123456" d="M1 8l4 4L15 2" /></svg>',
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, 'folder.svg'),
|
|
'<svg viewBox="0 0 16 16"><path fill="#000" d="M1 2h6l2 2h6v10H1z" /></svg>',
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, 'folder open.svg'),
|
|
'<svg viewBox="0 0 16 16"><path fill="#000" d="M1 3h14v11H1z" /></svg>',
|
|
)
|
|
|
|
return { rootDir, iconsDir }
|
|
}
|
|
|
|
test('generates an isolated React sprite module', async () => {
|
|
const { rootDir } = createReactFixture()
|
|
const result = await generateReactSprite(rootDir, 'vite')
|
|
const generatedDir = path.join(rootDir, 'generated')
|
|
|
|
assert.equal(result.name, 'file-manager')
|
|
assert.equal(result.iconCount, 3)
|
|
assert.equal(result.target, 'vite')
|
|
assert.equal(result.manifestPath, path.join(rootDir, 'manifest.ts'))
|
|
assert.deepEqual(fs.readdirSync(generatedDir).sort(), GENERATED_FILES)
|
|
|
|
const gitignore = fs.readFileSync(path.join(rootDir, '.gitignore'), 'utf-8')
|
|
assert.match(gitignore, /@generated by @gromlab\/svg-sprites/)
|
|
assert.match(gitignore, /^\/generated\/$/m)
|
|
assert.match(gitignore, /^\/index\.ts$/m)
|
|
assert.match(gitignore, /^\/manifest\.ts$/m)
|
|
|
|
const sprite = fs.readFileSync(path.join(generatedDir, 'sprite.svg'), 'utf-8')
|
|
assert.match(sprite, /АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ/)
|
|
assert.match(sprite, /Генератор: @gromlab\/svg-sprites/)
|
|
assert.match(sprite, /<svg[^>]+id="check"/)
|
|
assert.match(sprite, /<svg[^>]+id="folder"/)
|
|
assert.match(sprite, /<svg[^>]*>\n <style>:root>svg/)
|
|
assert.match(sprite, /\n <path[^>]+\/>\n <\/svg>/)
|
|
assert.doesNotMatch(sprite, /<symbol/)
|
|
assert.ok(sprite.endsWith('\n'))
|
|
const unsafeId = sprite.match(/<svg[^>]+id="(icon-[a-f0-9]{16})"/)?.[1]
|
|
assert.ok(unsafeId)
|
|
|
|
const component = fs.readFileSync(
|
|
path.join(generatedDir, 'react-component.tsx'),
|
|
'utf-8',
|
|
)
|
|
assert.match(component, /АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ/)
|
|
assert.match(component, /export const FileManagerIcon/)
|
|
assert.match(component, /export type FileManagerIconStyle = CSSProperties/)
|
|
assert.match(component, /Record<`--icon-color-\$\{number\}`, string \| number>/)
|
|
assert.match(component, /Omit<SVGAttributes<SVGSVGElement>, 'style'>/)
|
|
assert.match(component, /import spriteUrl from '\.\/sprite\.svg\?no-inline'/)
|
|
assert.doesNotMatch(component, /SvgSpriteProvider/)
|
|
assert.match(component, /iconIds\[icon\]/)
|
|
|
|
const transpiled = ts.transpileModule(component, {
|
|
compilerOptions: {
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
module: ts.ModuleKind.ESNext,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
reportDiagnostics: true,
|
|
})
|
|
assert.deepEqual(transpiled.diagnostics, [])
|
|
|
|
const metadata = fs.readFileSync(
|
|
path.join(generatedDir, 'types.ts'),
|
|
'utf-8',
|
|
)
|
|
assert.match(metadata, /export type FileManagerIconName/)
|
|
assert.match(metadata, /File manager icons/)
|
|
assert.match(metadata, /"check"/)
|
|
assert.match(metadata, /"folder"/)
|
|
assert.ok(component.includes(`"folder open": "${unsafeId}"`))
|
|
|
|
const spriteManifest = fs.readFileSync(path.join(rootDir, 'manifest.ts'), 'utf-8')
|
|
assert.match(spriteManifest, /import spriteUrl from '\.\/generated\/sprite\.svg\?no-inline'/)
|
|
assert.match(spriteManifest, /export const spriteManifest/)
|
|
assert.match(spriteManifest, /componentName: "FileManagerIcon"/)
|
|
assert.match(spriteManifest, /target: "vite"/)
|
|
assert.match(spriteManifest, /format: "stack"/)
|
|
assert.match(spriteManifest, /iconCount: 3/)
|
|
assert.match(spriteManifest, /name: "check"/)
|
|
assert.match(spriteManifest, /viewBox: "0 0 16 16"/)
|
|
assert.match(spriteManifest, /variable: "--icon-color-1"/)
|
|
assert.match(spriteManifest, /fallback: "currentColor"/)
|
|
assert.ok(spriteManifest.includes(`id: "${unsafeId}"`))
|
|
|
|
const transpiledManifest = ts.transpileModule(spriteManifest, {
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.ESNext,
|
|
target: ts.ScriptTarget.ES2022,
|
|
},
|
|
reportDiagnostics: true,
|
|
})
|
|
assert.deepEqual(transpiledManifest.diagnostics, [])
|
|
|
|
const rootIndex = fs.readFileSync(path.join(rootDir, 'index.ts'), 'utf-8')
|
|
assert.match(rootIndex, /АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ/)
|
|
assert.match(rootIndex, /export \{ FileManagerIcon \} from '\.\/generated\/react-component'/)
|
|
assert.match(rootIndex, /FileManagerIconProps, FileManagerIconStyle/)
|
|
assert.match(rootIndex, /export \{ fileManagerIconNames \} from '\.\/generated\/types'/)
|
|
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(path.join(generatedDir, '.svg-sprites.manifest.json'), 'utf-8'),
|
|
)
|
|
assert.match(manifest.warning, /АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ/)
|
|
})
|
|
|
|
test('supports Webpack target, explicit name and custom icons directory', async () => {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-config-'))
|
|
const rootDir = path.join(temporaryDir, 'svg-sprite')
|
|
const iconsDir = path.join(rootDir, 'assets')
|
|
|
|
fs.mkdirSync(iconsDir, { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default { name: 'documents', inputFolder: './assets', generatedNotice: false }\n`,
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, 'file.svg'),
|
|
'<svg viewBox="0 0 16 16"><path d="M2 1h8l4 4v10H2z" /></svg>',
|
|
)
|
|
|
|
const result = await generateReactSprite(rootDir, 'webpack')
|
|
const componentPath = path.join(
|
|
rootDir,
|
|
'generated',
|
|
'react-component.tsx',
|
|
)
|
|
|
|
assert.equal(result.name, 'documents')
|
|
assert.equal(result.iconCount, 1)
|
|
assert.equal(result.target, 'webpack')
|
|
const component = fs.readFileSync(componentPath, 'utf-8')
|
|
assert.match(component, /@generated by @gromlab\/svg-sprites/)
|
|
assert.doesNotMatch(component, /АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ/)
|
|
assert.match(component, /DocumentsIcon/)
|
|
assert.match(component, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/)
|
|
assert.doesNotMatch(component, /SvgSpriteProvider/)
|
|
|
|
const spriteManifest = fs.readFileSync(path.join(rootDir, 'manifest.ts'), 'utf-8')
|
|
assert.match(spriteManifest, /@generated by @gromlab\/svg-sprites/)
|
|
assert.match(spriteManifest, /new URL\('\.\/generated\/sprite\.svg', import\.meta\.url\)\.href/)
|
|
assert.match(spriteManifest, /target: "webpack"/)
|
|
assert.match(spriteManifest, /format: "stack"/)
|
|
assert.match(spriteManifest, /componentName: "DocumentsIcon"/)
|
|
|
|
const manifest = JSON.parse(
|
|
fs.readFileSync(path.join(rootDir, 'generated', '.svg-sprites.manifest.json'), 'utf-8'),
|
|
)
|
|
assert.equal(manifest.warning, undefined)
|
|
})
|
|
|
|
test('merges inputFolder and inputFiles and deduplicates identical paths', async () => {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-inputs-'))
|
|
const rootDir = path.join(temporaryDir, 'svg-sprite')
|
|
const iconsDir = path.join(rootDir, 'icons')
|
|
const sharedDir = path.join(temporaryDir, 'shared')
|
|
const localIcon = path.join(iconsDir, 'local.svg')
|
|
const sharedIcon = path.join(sharedDir, 'shared.svg')
|
|
|
|
fs.mkdirSync(iconsDir, { recursive: true })
|
|
fs.mkdirSync(sharedDir, { recursive: true })
|
|
fs.writeFileSync(localIcon, '<svg viewBox="0 0 16 16"><path d="M0 0h16v16H0z" /></svg>')
|
|
fs.writeFileSync(sharedIcon, '<svg viewBox="0 0 16 16"><path d="M1 1h14v14H1z" /></svg>')
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default {
|
|
name: 'mixed-inputs',
|
|
description: 'Local and shared icons',
|
|
inputFiles: [
|
|
'./icons/local.svg',
|
|
${JSON.stringify(path.relative(rootDir, sharedIcon))},
|
|
],
|
|
}\n`,
|
|
)
|
|
|
|
const result = await generateReactSprite(rootDir, 'vite')
|
|
const sprite = fs.readFileSync(result.spritePath, 'utf-8')
|
|
const manifest = fs.readFileSync(result.manifestPath, 'utf-8')
|
|
|
|
assert.equal(result.name, 'mixed-inputs')
|
|
assert.equal(result.iconCount, 2)
|
|
assert.match(sprite, /id="local"/)
|
|
assert.match(sprite, /id="shared"/)
|
|
assert.match(manifest, /description: "Local and shared icons"/)
|
|
})
|
|
|
|
test('supports inputFiles without the default icons directory', async () => {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-files-'))
|
|
const rootDir = path.join(temporaryDir, 'svg-sprite')
|
|
const sharedIcon = path.join(temporaryDir, 'shared', 'only.svg')
|
|
|
|
fs.mkdirSync(rootDir, { recursive: true })
|
|
fs.mkdirSync(path.dirname(sharedIcon), { recursive: true })
|
|
fs.writeFileSync(sharedIcon, '<svg viewBox="0 0 16 16"><path d="M0 0h16v16H0z" /></svg>')
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default { inputFiles: [${JSON.stringify(path.relative(rootDir, sharedIcon))}] }\n`,
|
|
)
|
|
|
|
const result = await generateReactSprite(rootDir, 'webpack')
|
|
const sprite = fs.readFileSync(result.spritePath, 'utf-8')
|
|
|
|
assert.equal(result.iconCount, 1)
|
|
assert.match(sprite, /id="only"/)
|
|
})
|
|
|
|
test('rejects different input files with the same icon name', async () => {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-names-'))
|
|
const rootDir = path.join(temporaryDir, 'svg-sprite')
|
|
const firstIcon = path.join(temporaryDir, 'first', 'shared.svg')
|
|
const secondIcon = path.join(temporaryDir, 'second', 'shared.svg')
|
|
|
|
fs.mkdirSync(rootDir, { recursive: true })
|
|
fs.mkdirSync(path.dirname(firstIcon), { recursive: true })
|
|
fs.mkdirSync(path.dirname(secondIcon), { recursive: true })
|
|
fs.writeFileSync(firstIcon, '<svg viewBox="0 0 16 16"><path d="M0 0h16v16H0z" /></svg>')
|
|
fs.writeFileSync(secondIcon, '<svg viewBox="0 0 16 16"><path d="M1 1h14v14H1z" /></svg>')
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default {
|
|
inputFiles: [
|
|
${JSON.stringify(path.relative(rootDir, firstIcon))},
|
|
${JSON.stringify(path.relative(rootDir, secondIcon))},
|
|
],
|
|
}\n`,
|
|
)
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/produce the same SVG id "shared"/,
|
|
)
|
|
})
|
|
|
|
test('rejects an explicitly configured missing inputFolder', async () => {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-folder-'))
|
|
const rootDir = path.join(temporaryDir, 'svg-sprite')
|
|
const sharedIcon = path.join(temporaryDir, 'shared.svg')
|
|
|
|
fs.mkdirSync(rootDir, { recursive: true })
|
|
fs.writeFileSync(sharedIcon, '<svg viewBox="0 0 16 16"><path d="M0 0h16v16H0z" /></svg>')
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default {
|
|
inputFolder: './missing',
|
|
inputFiles: [${JSON.stringify(path.relative(rootDir, sharedIcon))}],
|
|
}\n`,
|
|
)
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/Input directory does not exist/,
|
|
)
|
|
})
|
|
|
|
test('applies React transform options', async () => {
|
|
const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-react-transform-'))
|
|
const rootDir = path.join(temporaryDir, 'svg-sprite')
|
|
const iconsDir = path.join(rootDir, 'icons')
|
|
|
|
fs.mkdirSync(iconsDir, { recursive: true })
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprite.config.ts'),
|
|
`export default {
|
|
transform: {
|
|
removeSize: false,
|
|
replaceColors: false,
|
|
addTransition: false,
|
|
},
|
|
}\n`,
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, 'plain.svg'),
|
|
'<svg width="24" height="24" viewBox="0 0 24 24"><path fill="#123456" d="M0 0h24v24H0z" /></svg>',
|
|
)
|
|
|
|
const result = await generateReactSprite(rootDir, 'vite')
|
|
const sprite = fs.readFileSync(result.spritePath, 'utf-8')
|
|
const styles = fs.readFileSync(path.join(result.generatedDir, 'styles.module.css'), 'utf-8')
|
|
|
|
assert.match(sprite, /width="24"/)
|
|
assert.match(sprite, /height="24"/)
|
|
assert.match(sprite, /fill="#(?:123456|123)"/)
|
|
assert.doesNotMatch(sprite, /--icon-color-/)
|
|
assert.doesNotMatch(sprite, /transition:/)
|
|
assert.doesNotMatch(styles, /transition-/)
|
|
})
|
|
|
|
test('generates App and Pages Router modules for Webpack and Turbopack', async () => {
|
|
const targets = [
|
|
{ router: 'app', bundler: 'turbopack' },
|
|
{ router: 'app', bundler: 'webpack' },
|
|
{ router: 'pages', bundler: 'turbopack' },
|
|
{ router: 'pages', bundler: 'webpack' },
|
|
]
|
|
|
|
for (const options of targets) {
|
|
const { rootDir } = createReactFixture()
|
|
const result = await generateNextSprite(rootDir, options)
|
|
const component = fs.readFileSync(
|
|
path.join(result.generatedDir, 'react-component.tsx'),
|
|
'utf-8',
|
|
)
|
|
const manifest = fs.readFileSync(result.manifestPath, 'utf-8')
|
|
const target = `next@${options.router}/${options.bundler}`
|
|
|
|
assert.equal(result.target, target)
|
|
assert.equal(result.router, options.router)
|
|
assert.equal(result.bundler, options.bundler)
|
|
assert.match(component, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/)
|
|
assert.doesNotMatch(component, /['"]use client['"]/)
|
|
assert.match(manifest, new RegExp(`target: ${JSON.stringify(target)}`))
|
|
}
|
|
})
|
|
|
|
test('regeneration removes only files listed in the manifest', async () => {
|
|
const { rootDir } = createReactFixture()
|
|
await generateReactSprite(rootDir, 'vite')
|
|
|
|
const generatedDir = path.join(rootDir, 'generated')
|
|
const manifestPath = path.join(generatedDir, '.svg-sprites.manifest.json')
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
const obsoleteRelativePath = 'generated/obsolete.generated.ts'
|
|
|
|
manifest.files.push('generated\\obsolete.generated.ts')
|
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
fs.writeFileSync(
|
|
path.join(rootDir, obsoleteRelativePath),
|
|
'/** @generated by @gromlab/svg-sprites. Do not edit. */\n',
|
|
)
|
|
fs.writeFileSync(path.join(generatedDir, 'notes.md'), 'Keep me\n')
|
|
fs.writeFileSync(path.join(rootDir, 'README.md'), 'Keep me too\n')
|
|
|
|
await generateReactSprite(rootDir, 'vite')
|
|
|
|
assert.equal(fs.existsSync(path.join(rootDir, obsoleteRelativePath)), false)
|
|
assert.equal(fs.readFileSync(path.join(generatedDir, 'notes.md'), 'utf-8'), 'Keep me\n')
|
|
assert.equal(fs.readFileSync(path.join(rootDir, 'README.md'), 'utf-8'), 'Keep me too\n')
|
|
})
|
|
|
|
test('does not overwrite a user index.ts without a manifest', async () => {
|
|
const { rootDir } = createReactFixture()
|
|
const userIndex = 'export const userCode = true\n'
|
|
fs.writeFileSync(path.join(rootDir, 'index.ts'), userIndex)
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/Refusing to overwrite a user file/,
|
|
)
|
|
assert.equal(fs.readFileSync(path.join(rootDir, 'index.ts'), 'utf-8'), userIndex)
|
|
})
|
|
|
|
test('does not overwrite a generated file replaced with user content', async () => {
|
|
const { rootDir } = createReactFixture()
|
|
await generateReactSprite(rootDir, 'vite')
|
|
|
|
const userIndex = 'export const userCode = true\n'
|
|
fs.writeFileSync(path.join(rootDir, 'index.ts'), userIndex)
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/Refusing to overwrite a user file/,
|
|
)
|
|
assert.equal(fs.readFileSync(path.join(rootDir, 'index.ts'), 'utf-8'), userIndex)
|
|
})
|
|
|
|
test('rejects paths outside the generated area from the manifest', async () => {
|
|
const { rootDir } = createReactFixture()
|
|
await generateReactSprite(rootDir, 'vite')
|
|
|
|
const manifestPath = path.join(rootDir, 'generated', '.svg-sprites.manifest.json')
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
manifest.files.push('svg-sprite.config.ts')
|
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/Invalid generated file path/,
|
|
)
|
|
assert.equal(fs.existsSync(path.join(rootDir, 'svg-sprite.config.ts')), true)
|
|
})
|
|
|
|
test('rejects symbolic links in generated paths', async () => {
|
|
const { rootDir } = createReactFixture()
|
|
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-outside-'))
|
|
fs.symlinkSync(outsideDir, path.join(rootDir, 'generated'), 'dir')
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/Symbolic links are not allowed/,
|
|
)
|
|
assert.deepEqual(fs.readdirSync(outsideDir), [])
|
|
})
|
|
|
|
test('rejects colliding SVG shape IDs', async () => {
|
|
const { rootDir, iconsDir } = createReactFixture()
|
|
const collidingId = `icon-${createHash('sha256').update('folder open').digest('hex').slice(0, 16)}`
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, `${collidingId}.svg`),
|
|
'<svg viewBox="0 0 16 16"><path d="M0 0h16v16H0z" /></svg>',
|
|
)
|
|
|
|
await assert.rejects(
|
|
generateReactSprite(rootDir, 'vite'),
|
|
/produce the same SVG id/,
|
|
)
|
|
})
|
|
|
|
test('CLI requires an explicit mode', () => {
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(process.execPath, [cliPath], { encoding: 'utf-8' })
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /Missing required argument: --mode/)
|
|
})
|
|
|
|
test('CLI does not expose the future standalone mode', () => {
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', 'standalone', '.'],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /Unknown mode: standalone/)
|
|
})
|
|
|
|
test('CLI requires a target for React mode', () => {
|
|
const { rootDir } = createReactFixture()
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', 'react', rootDir],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /React mode requires a target/)
|
|
})
|
|
|
|
test('CLI rejects unsupported React target', () => {
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', 'react@next', '.'],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /Unsupported React target: next/)
|
|
})
|
|
|
|
test('CLI requires a complete Next.js target', () => {
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', 'next@app', '.'],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 1)
|
|
assert.match(result.stderr, /Unsupported Next\.js target: next@app/)
|
|
})
|
|
|
|
test('CLI runs the React Vite mode', () => {
|
|
const { rootDir } = createReactFixture()
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', 'react@vite', rootDir],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 0, result.stderr)
|
|
const component = fs.readFileSync(
|
|
path.join(rootDir, 'generated', 'react-component.tsx'),
|
|
'utf-8',
|
|
)
|
|
assert.match(component, /sprite\.svg\?no-inline/)
|
|
assert.match(result.stdout, /✓ file-manager · 3 icons · react@vite/)
|
|
assert.match(result.stdout, /→ .*generated/)
|
|
assert.doesNotMatch(result.stdout, /Generating sprite|\[stack\]/)
|
|
})
|
|
|
|
test('CLI runs the React Webpack mode', () => {
|
|
const { rootDir } = createReactFixture()
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', 'react@webpack', rootDir],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 0, result.stderr)
|
|
const component = fs.readFileSync(
|
|
path.join(rootDir, 'generated', 'react-component.tsx'),
|
|
'utf-8',
|
|
)
|
|
assert.match(component, /new URL\('\.\/sprite\.svg', import\.meta\.url\)\.href/)
|
|
})
|
|
|
|
test('CLI runs all Next.js modes', () => {
|
|
const modes = [
|
|
'next@app/turbopack',
|
|
'next@app/webpack',
|
|
'next@pages/turbopack',
|
|
'next@pages/webpack',
|
|
]
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
|
|
for (const mode of modes) {
|
|
const { rootDir } = createReactFixture()
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode', mode, rootDir],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 0, result.stderr)
|
|
const manifest = fs.readFileSync(path.join(rootDir, 'manifest.ts'), 'utf-8')
|
|
assert.match(manifest, new RegExp(`target: ${JSON.stringify(mode)}`))
|
|
}
|
|
})
|
|
|
|
test('CLI runs the legacy mode relative to its target directory', () => {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'svg-sprites-legacy-'))
|
|
const iconsDir = path.join(rootDir, 'icons')
|
|
const cliPath = path.resolve('dist/cli.js')
|
|
|
|
fs.mkdirSync(iconsDir)
|
|
fs.writeFileSync(
|
|
path.join(iconsDir, 'check.svg'),
|
|
'<svg viewBox="0 0 16 16"><path d="M1 8l4 4L15 2" /></svg>',
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(rootDir, 'svg-sprites.config.ts'),
|
|
[
|
|
'export default {',
|
|
" output: 'sprites',",
|
|
' preview: false,',
|
|
" sprites: [{ name: 'icons', input: 'icons', format: 'symbol' }],",
|
|
'}',
|
|
'',
|
|
].join('\n'),
|
|
)
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cliPath, '--mode=legacy', rootDir],
|
|
{ encoding: 'utf-8' },
|
|
)
|
|
|
|
assert.equal(result.status, 0, result.stderr)
|
|
assert.equal(fs.existsSync(path.join(rootDir, 'sprites', 'icons.sprite.svg')), true)
|
|
})
|