feat(scan-sprite): walk dir, normalise aliases, detect collisions

This commit is contained in:
Axel Meyer
2026-06-16 21:02:06 +02:00
parent 775ad58308
commit 50a0a3508f
8 changed files with 95 additions and 0 deletions

48
src/scan-sprite.js Normal file
View File

@@ -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 };

BIN
tests/fixtures/sprite-collision/Bed1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

BIN
tests/fixtures/sprite-collision/bed1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 B

BIN
tests/fixtures/sprite-fresh/Bed1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

BIN
tests/fixtures/sprite-fresh/Bench1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

BIN
tests/fixtures/sprite-fresh/Sack.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

47
tests/scan-sprite.test.js Normal file
View File

@@ -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'));
});