diff --git a/src/scan-sprite.js b/src/scan-sprite.js new file mode 100644 index 0000000..c75de14 --- /dev/null +++ b/src/scan-sprite.js @@ -0,0 +1,48 @@ +// src/scan-sprite.js +// Scans a source dir for PNG/WebP/JPG sprite files (no _diffuse/_height +// suffix convention — sprite-mode treats each file as a standalone +// sprite). Normalises filenames to snake_case aliases, detects +// collisions. + +const fs = require('node:fs'); +const path = require('node:path'); +const { aliasFromFilename } = require('./alias'); + +const SUPPORTED_EXT = ['.png', '.webp', '.jpg', '.jpeg']; + +function scanSpriteSourceDir(srcDir, opts = {}) { + if (!fs.existsSync(srcDir)) { + return { sources: [], errors: [`scan-sprite: source dir does not exist: ${srcDir}`] }; + } + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); + const candidates = []; + for (const e of entries) { + if (!e.isFile()) continue; + const ext = path.extname(e.name).toLowerCase(); + if (!SUPPORTED_EXT.includes(ext)) continue; + candidates.push(e.name); + } + candidates.sort(); + + const aliasToFile = new Map(); + const sources = []; + for (const filename of candidates) { + const alias = aliasFromFilename(filename, opts); + if (aliasToFile.has(alias)) { + const prev = aliasToFile.get(alias); + return { + sources: [], + errors: [`scan-sprite: alias collision "${alias}" — both "${prev}" and "${filename}" map to the same alias`], + }; + } + aliasToFile.set(alias, filename); + sources.push({ + alias, + sourceFile: filename, + sourcePath: path.join(srcDir, filename), + }); + } + return { sources, errors: [] }; +} + +module.exports = { scanSpriteSourceDir }; diff --git a/tests/fixtures/sprite-collision/Bed1.png b/tests/fixtures/sprite-collision/Bed1.png new file mode 100644 index 0000000..76ed24b Binary files /dev/null and b/tests/fixtures/sprite-collision/Bed1.png differ diff --git a/tests/fixtures/sprite-collision/bed1.png b/tests/fixtures/sprite-collision/bed1.png new file mode 100644 index 0000000..1d98a7e Binary files /dev/null and b/tests/fixtures/sprite-collision/bed1.png differ diff --git a/tests/fixtures/sprite-fresh/BarrelLarge.png b/tests/fixtures/sprite-fresh/BarrelLarge.png new file mode 100644 index 0000000..7b1c7c0 Binary files /dev/null and b/tests/fixtures/sprite-fresh/BarrelLarge.png differ diff --git a/tests/fixtures/sprite-fresh/Bed1.png b/tests/fixtures/sprite-fresh/Bed1.png new file mode 100644 index 0000000..ec89ea9 Binary files /dev/null and b/tests/fixtures/sprite-fresh/Bed1.png differ diff --git a/tests/fixtures/sprite-fresh/Bench1.png b/tests/fixtures/sprite-fresh/Bench1.png new file mode 100644 index 0000000..d75531a Binary files /dev/null and b/tests/fixtures/sprite-fresh/Bench1.png differ diff --git a/tests/fixtures/sprite-fresh/Sack.png b/tests/fixtures/sprite-fresh/Sack.png new file mode 100644 index 0000000..9d8aec3 Binary files /dev/null and b/tests/fixtures/sprite-fresh/Sack.png differ diff --git a/tests/scan-sprite.test.js b/tests/scan-sprite.test.js new file mode 100644 index 0000000..cb1658a --- /dev/null +++ b/tests/scan-sprite.test.js @@ -0,0 +1,47 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const path = require('node:path'); +const { scanSpriteSourceDir } = require('../src/scan-sprite'); + +const FRESH = path.resolve(__dirname, 'fixtures/sprite-fresh'); +const COLLISION = path.resolve(__dirname, 'fixtures/sprite-collision'); + +test('scan-sprite: 4 PNGs, alphabetical, no errors', () => { + const { sources, errors } = scanSpriteSourceDir(FRESH, {}); + assert.deepStrictEqual(errors, []); + assert.strictEqual(sources.length, 4); + assert.strictEqual(sources[0].alias, 'barrel_large'); + assert.strictEqual(sources[1].alias, 'bed1'); + assert.strictEqual(sources[2].alias, 'bench1'); + assert.strictEqual(sources[3].alias, 'sack'); +}); + +test('scan-sprite: sources carry sourcePath + sourceFile', () => { + const { sources } = scanSpriteSourceDir(FRESH, {}); + assert.ok(sources[1].sourcePath.endsWith('Bed1.png')); + assert.strictEqual(sources[1].sourceFile, 'Bed1.png'); +}); + +test('scan-sprite: alias collision -> loud error', () => { + const { sources, errors } = scanSpriteSourceDir(COLLISION, {}); + assert.strictEqual(sources.length, 0); + assert.strictEqual(errors.length, 1); + assert.match(errors[0], /alias collision/i); + assert.match(errors[0], /bed1/); +}); + +test('scan-sprite: missing dir -> error', () => { + const { sources, errors } = scanSpriteSourceDir('/nonexistent/path', {}); + assert.strictEqual(sources.length, 0); + assert.ok(errors[0].includes('does not exist')); +}); + +test('scan-sprite: applies stripPrefix option', () => { + const { sources, errors } = scanSpriteSourceDir(FRESH, { + stripPrefix: 'Bed', + }); + assert.deepStrictEqual(errors, []); + // Bed1 -> 1, Bench1 not stripped -> bench1, etc. + const aliases = sources.map(s => s.alias).sort(); + assert.ok(aliases.includes('1')); +});