diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2a3480f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ +# Sundrop SVG Sprite Builder - AI Instructions + +## Available Scripts + +Scan package.json and refer to the `scripts` property. Execute scripts using `bun run`. Relevant scripts for the dev workflow are: + +- `format` - runs `prettier` on all source files +- `typecheck` - runs a project-wide static type check +- `lint` - runs eslint + +### Unit Test Suite + +Run unit test using `bun test` + +## Workflow + +- Run a static type check after you're done making any TypeScript changes. +- After making changes to any TypeScript or JavaScript file, run unit tests and verify passing status. +- Prior to committing anything to revision control, run the format script. + +## Git Conventions + +Commit messages should have a short, single-line summary of the changes made as the first line, followed by a blank line, followed by a markdown formatted bulleted list summarizing the most relevant changes made. Each bullet should also be a brief, single-line sentence. + +Pull requests should always be submitted to the main upstream repository, `mgreen-simplethread/sundrop`, if you're working from a fork (check the value of `git remote show origin`). diff --git a/bin/sundrop.js b/bin/sundrop.ts similarity index 51% rename from bin/sundrop.js rename to bin/sundrop.ts index c139b07..89bb5f4 100755 --- a/bin/sundrop.js +++ b/bin/sundrop.ts @@ -7,6 +7,49 @@ import { hideBin } from 'yargs/helpers'; import { bundleSprites } from '../index'; import packageJSON from '../package.json'; +const options = { + path: { + alias: 'p', + describe: 'Relative path or node module to search for icons', + type: 'array' as const, + }, + out: { + alias: 'o', + describe: 'Output file', + type: 'string' as const, + demandOption: true, + }, + files: { + alias: 'f', + describe: 'Glob of files to search for icon names', + type: 'string' as const, + default: './**/*.{html,css}', + }, + alias: { + alias: 'a', + describe: 'Alias for icon name (alias_name:real_icon_name)', + type: 'array' as const, + coerce: (arg: string[]) => + arg.reduce((obj: Record, val: string) => { + const [alias, icon] = val.split(':'); + if (!alias || !icon) return obj; + obj[alias] = icon; + return obj; + }, {}), + }, + idPrefix: { + alias: 'i', + describe: 'Prefix string to add to icon symbol IDs', + type: 'string' as const, + default: 'icon-', + }, + watch: { + alias: 'w', + describe: 'Watch for changes and rebuild sprite sheet', + type: 'boolean' as const, + }, +}; + const argv = yargs(hideBin(Bun.argv)) .usage(`$0 --path PATH_TO_ICONS --out OUTPUT_PATH --files GLOB`) .scriptName('sundrop') @@ -14,56 +57,29 @@ const argv = yargs(hideBin(Bun.argv)) .config( 'config', 'Path to JSON config file. Values set there are overridden by CLI flags.', - (file) => JSON.parse(readFileSync(file)), + (file) => JSON.parse(readFileSync(file).toString()), ) .alias('config', 'c') - .alias('p', 'path') - .describe('p', 'Relative path or node module to search for icons') - .array('p') - .alias('o', 'out') - .describe('o', 'Output file') - .string('o') - .alias('f', 'files') - .string('f') - .describe('f', 'Glob of files to search for icon names') - .default('f', './**/*.{html,css}') - .alias('a', 'alias') - .array('a') - .describe('a', 'alias for icon name (alias_name:real_icon_name)') - .coerce('a', (arg) => - arg.reduce((obj, val) => { - const [alias, icon] = val.split(':'); - if (!alias || !icon) return obj; - obj[alias] = icon; - return obj; - }, {}), - ) - .string('i') - .alias('i', 'idPrefix') - .describe('i', 'Prefix string to add to icon symbol IDs') - .default('i', 'icon-') - .boolean('w') - .alias('w', 'watch') - .describe('w', 'Watch for changes and rebuild sprite sheet') + .options(options) .help() .alias('help', 'h') .version() .alias('version', 'v') - .parse(); + .parseSync(); + +const { out, path, alias: aliases = {}, files: searchPattern, idPrefix = '', watch = false } = argv; -const { - out, - path: paths, - alias: aliases = {}, - files: searchPattern, - idPrefix = '', - watch = false, -} = argv; +const paths = path as string[] | undefined; + +if (!out) { + console.error('Error: --out is required'); + process.exit(1); +} if (watch) { console.log('Sundrop v%s started - watching project for changes.', packageJSON.version); - let timeout; + let timeout: ReturnType | null; const debouncedBundler = () => { if (timeout) clearTimeout(timeout); diff --git a/bun.lock b/bun.lock index 689e4ce..8f90a48 100644 --- a/bun.lock +++ b/bun.lock @@ -5,15 +5,15 @@ "": { "name": "sundrop", "dependencies": { - "fast-xml-parser": "^5.3.3", "svgo": "^4.0.0", "yargs": "^18.0.0", }, "devDependencies": { "@eslint/js": "^9.39.1", - "@types/bun": "latest", - "@types/node": "^25.0.3", + "@types/bun": "^1.3.6", + "@types/node": "^25.0.8", "eslint": "^9.39.1", + "globals": "^17.0.0", "prettier": "^3.6.2", "typescript-eslint": "^8.47.0", }, @@ -49,13 +49,13 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="], @@ -95,7 +95,7 @@ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -163,8 +163,6 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-xml-parser": ["fast-xml-parser@5.3.3", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -181,7 +179,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -263,8 +261,6 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "svgo": ["svgo@4.0.0", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.4.1" }, "bin": "./bin/svgo.js" }, "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw=="], @@ -299,6 +295,8 @@ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..d6ed242 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,18 @@ +import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; + +export default defineConfig([ + eslint.configs.recommended, + tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.bunBuiltin, + ...globals.node, + }, + }, + }, +]); + diff --git a/index.test.ts b/index.test.ts new file mode 100644 index 0000000..f619d8b --- /dev/null +++ b/index.test.ts @@ -0,0 +1,321 @@ +import { describe, test, expect, beforeAll, afterAll, spyOn } from 'bun:test'; +import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { bundleSprites } from './index'; +import { IconSearch } from './lib/icon-search'; +import { SpriteGenerator } from './lib/sprite-generator'; + +const ARROW_SVG = ''; +const CLOSE_SVG = ''; + +describe('bundleSprites', () => { + let tempDir: string; + let iconsDir: string; + let srcDir: string; + let outFile: string; + + beforeAll(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'sundrop-bundle-test-')); + iconsDir = join(tempDir, 'icons'); + srcDir = join(tempDir, 'src'); + outFile = join(tempDir, 'sprites.svg'); + + await mkdir(iconsDir); + await mkdir(srcDir); + + await writeFile(join(iconsDir, 'arrow.svg'), ARROW_SVG); + await writeFile(join(iconsDir, 'close.svg'), CLOSE_SVG); + }); + + afterAll(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + describe('IconSearch integration', () => { + test('creates IconSearch with correct config', async () => { + await writeFile(join(srcDir, 'page.html'), ''); + + const config = { + cwd: tempDir, + paths: ['./icons'], + aliases: { back: 'arrow' }, + searchPattern: 'src/**/*.html', + idPrefix: 'icon-', + out: outFile, + }; + + const searchSpy = spyOn(IconSearch.prototype, 'search'); + + await bundleSprites(config); + + expect(searchSpy).toHaveBeenCalled(); + searchSpy.mockRestore(); + }); + + test('calls search() on IconSearch instance', async () => { + await writeFile(join(srcDir, 'test.html'), ''); + + const searchSpy = spyOn(IconSearch.prototype, 'search'); + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/test.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(searchSpy).toHaveBeenCalledTimes(1); + searchSpy.mockRestore(); + }); + }); + + describe('early exit on no matches', () => { + test('returns early when no icons are found', async () => { + await writeFile(join(srcDir, 'empty.html'), '
no icons
'); + + const renderSpy = spyOn(SpriteGenerator.prototype, 'render'); + const errorSpy = spyOn(console, 'error'); + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/empty.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(renderSpy).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith('bummer. no icon matches found.'); + + renderSpy.mockRestore(); + errorSpy.mockRestore(); + }); + }); + + describe('SpriteGenerator integration', () => { + test('creates SpriteGenerator with matches and idPrefix', async () => { + await writeFile(join(srcDir, 'icons.html'), ''); + + const renderSpy = spyOn(SpriteGenerator.prototype, 'render'); + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/icons.html', + idPrefix: 'custom-', + out: outFile, + }); + + expect(renderSpy).toHaveBeenCalled(); + renderSpy.mockRestore(); + }); + + test('calls render() on SpriteGenerator', async () => { + await writeFile(join(srcDir, 'render.html'), ''); + + const renderSpy = spyOn(SpriteGenerator.prototype, 'render'); + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/render.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(renderSpy).toHaveBeenCalledTimes(1); + renderSpy.mockRestore(); + }); + }); + + describe('file output', () => { + test('writes sprite sheet to output file', async () => { + await writeFile(join(srcDir, 'output.html'), ''); + const outputPath = join(tempDir, 'output-test.svg'); + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/output.html', + idPrefix: 'icon-', + out: outputPath, + }); + + const outputFile = Bun.file(outputPath); + expect(await outputFile.exists()).toBe(true); + + const content = await outputFile.text(); + expect(content).toContain(' { + await writeFile( + join(srcDir, 'multi.html'), + '', + ); + const outputPath = join(tempDir, 'multi-test.svg'); + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/multi.html', + idPrefix: 'icon-', + out: outputPath, + }); + + const content = await Bun.file(outputPath).text(); + + // Should have wrapper SVG with hidden positioning + expect(content).toContain('width="0"'); + expect(content).toContain('height="0"'); + expect(content).toContain('position:absolute'); + + // Should contain symbols for both icons + expect(content).toContain(' { + test('passes cwd to IconSearch', async () => { + await writeFile(join(srcDir, 'cwd.html'), ''); + + let capturedCwd: string | undefined; + const originalSearch = IconSearch.prototype.search; + + IconSearch.prototype.search = async function () { + capturedCwd = this.options.cwd; + return originalSearch.call(this); + }; + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/cwd.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(capturedCwd).toBe(tempDir); + IconSearch.prototype.search = originalSearch; + }); + + test('passes paths to IconSearch', async () => { + await writeFile(join(srcDir, 'paths.html'), ''); + + let capturedPaths: string[] | undefined; + const originalSearch = IconSearch.prototype.search; + + IconSearch.prototype.search = async function () { + capturedPaths = this.options.paths; + return originalSearch.call(this); + }; + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/paths.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(capturedPaths).toEqual(['./icons']); + IconSearch.prototype.search = originalSearch; + }); + + test('passes aliases to IconSearch', async () => { + await writeFile(join(srcDir, 'alias.html'), ''); + + let capturedAliases: Record | undefined; + const originalSearch = IconSearch.prototype.search; + + IconSearch.prototype.search = async function () { + capturedAliases = this.options.aliases; + return originalSearch.call(this); + }; + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: { back: 'arrow' }, + searchPattern: 'src/**/alias.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(capturedAliases).toEqual({ back: 'arrow' }); + IconSearch.prototype.search = originalSearch; + }); + + test('passes searchPattern to IconSearch', async () => { + await writeFile(join(srcDir, 'pattern.html'), ''); + + let capturedPattern: string | undefined; + const originalSearch = IconSearch.prototype.search; + + IconSearch.prototype.search = async function () { + capturedPattern = this.options.searchPattern; + return originalSearch.call(this); + }; + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/pattern.html', + idPrefix: 'icon-', + out: outFile, + }); + + expect(capturedPattern).toBe('src/**/pattern.html'); + IconSearch.prototype.search = originalSearch; + }); + + test('passes idPrefix to both IconSearch and SpriteGenerator', async () => { + await writeFile(join(srcDir, 'prefix.html'), ''); + + let searchPrefix: string | string[] | undefined; + let generatorPrefix: string | undefined; + + const originalSearch = IconSearch.prototype.search; + const originalRender = SpriteGenerator.prototype.render; + + IconSearch.prototype.search = async function () { + searchPrefix = this.options.idPrefix; + return originalSearch.call(this); + }; + + SpriteGenerator.prototype.render = async function () { + generatorPrefix = this.options.idPrefix; + return originalRender.call(this); + }; + + await bundleSprites({ + cwd: tempDir, + paths: ['./icons'], + aliases: {}, + searchPattern: 'src/**/prefix.html', + idPrefix: 'custom-', + out: outFile, + }); + + expect(searchPrefix).toBe('custom-'); + expect(generatorPrefix).toBe('custom-'); + + IconSearch.prototype.search = originalSearch; + SpriteGenerator.prototype.render = originalRender; + }); + }); +}); diff --git a/lib/icon-search.test.ts b/lib/icon-search.test.ts index 9fc1369..297c7fa 100644 --- a/lib/icon-search.test.ts +++ b/lib/icon-search.test.ts @@ -254,7 +254,7 @@ describe('IconSearch', () => { // Reference same icon by base name and prefixed name await writeFile( join(srcDir, 'dupes.html'), - '' + '', ); const searcher = new IconSearch({ diff --git a/lib/icon-search.ts b/lib/icon-search.ts index 3fd9496..5ecd8d7 100644 --- a/lib/icon-search.ts +++ b/lib/icon-search.ts @@ -134,7 +134,7 @@ export class IconSearch { const isNamespaced = name.startsWith('@'); const nameParts = name.split('/'); - const pkgName = isNamespaced ? nameParts.slice(0, 2).join('/') : nameParts[0]; + const pkgName = isNamespaced ? nameParts.slice(0, 2).join('/') : nameParts[0]!; const pkgSubpath = isNamespaced ? nameParts.slice(2) : nameParts.slice(1); try { diff --git a/lib/sprite-generator.test.ts b/lib/sprite-generator.test.ts index 4dc26bd..6d07acd 100644 --- a/lib/sprite-generator.test.ts +++ b/lib/sprite-generator.test.ts @@ -267,21 +267,21 @@ describe('SpriteGenerator', () => { describe('DEFAULT_SVGO_PLUGINS', () => { test('includes convertSvgToSymbol plugin', () => { const hasPlugin = DEFAULT_SVGO_PLUGINS.some( - (p) => typeof p === 'object' && p.name === 'convertSvgToSymbol' + (p) => typeof p === 'object' && p.name === 'convertSvgToSymbol', ); expect(hasPlugin).toBe(true); }); test('includes addCurrentColorFill plugin', () => { const hasPlugin = DEFAULT_SVGO_PLUGINS.some( - (p) => typeof p === 'object' && p.name === 'addCurrentColorFillAttr' + (p) => typeof p === 'object' && p.name === 'addCurrentColorFillAttr', ); expect(hasPlugin).toBe(true); }); test('includes removeAttrs plugin for width/height', () => { const removeAttrsPlugin = DEFAULT_SVGO_PLUGINS.find( - (p) => typeof p === 'object' && p.name === 'removeAttrs' + (p) => typeof p === 'object' && p.name === 'removeAttrs', ); expect(removeAttrsPlugin).toBeDefined(); expect((removeAttrsPlugin as any).params.attrs).toContain('width'); diff --git a/package.json b/package.json index a187854..7249c78 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,13 @@ "sundrop": "bin/sundrop.js" }, "devDependencies": { - "@types/bun": "latest", - "@types/node": "^25.0.3", + "@eslint/js": "^9.39.1", + "@types/bun": "^1.3.6", + "@types/node": "^25.0.8", + "@types/yargs": "^17.0.35", "eslint": "^9.39.1", + "globals": "^17.0.0", "prettier": "^3.6.2", - "@eslint/js": "^9.39.1", "typescript-eslint": "^8.47.0" }, "peerDependencies": { @@ -30,6 +32,6 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "format": "prettier -w ./lib/**/*.ts ./bin/*.ts" + "format": "prettier -w **/*.ts" } }