Add inspect subcommand: read-only stats for v2 map files

Reads the map JSON, optionally loads --atlas-dir paths for name
resolution, and prints the inspector report to stdout. Missing
file, bad JSON, and missing --atlas-dir paths all produce
labelled stderr errors with exit 1.
This commit is contained in:
Axel Meyer
2026-05-23 16:59:07 +02:00
parent 530bceaddd
commit eb255480a1
3 changed files with 131 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ function run(argv, { out, err }) {
encode: require('./commands/encode'),
decode: require('./commands/decode'),
build: require('./commands/build'),
inspect: require('./commands/inspect'),
};
const handler = handlers[head];
if (!handler) {

77
src/commands/inspect.js Normal file
View File

@@ -0,0 +1,77 @@
'use strict';
const fs = require('node:fs');
const { loadAtlasDirs } = require('../atlas-loader');
const { inspectMap } = require('../inspector');
const USAGE = 'usage: sporel-mapper inspect <map.json> [--atlas-dir <path> ...]\n';
function parseArgs(args) {
const positional = [];
const atlasDirs = [];
let i = 0;
while (i < args.length) {
const a = args[i];
if (a === '--atlas-dir') {
if (i + 1 >= args.length) {
throw new Error(`--atlas-dir requires a path`);
}
atlasDirs.push(args[i + 1]);
i += 2;
} else {
positional.push(a);
i += 1;
}
}
return { positional, atlasDirs };
}
function run(args, { out, err }) {
let parsed;
try {
parsed = parseArgs(args);
} catch (e) {
err.write(`error: ${e.message}\n${USAGE}`);
return 1;
}
if (parsed.positional.length !== 1) {
err.write(USAGE);
return 1;
}
const [mapPath] = parsed.positional;
let raw;
try {
raw = fs.readFileSync(mapPath, 'utf8');
} catch (e) {
err.write(`error: cannot read ${mapPath}: ${e.message}\n`);
return 1;
}
let map;
try {
map = JSON.parse(raw);
} catch (e) {
err.write(`error: ${mapPath} is not valid JSON: ${e.message}\n`);
return 1;
}
let registry = {};
if (parsed.atlasDirs.length > 0) {
try {
const result = loadAtlasDirs(parsed.atlasDirs);
registry = result.registry;
for (const w of result.warnings) {
err.write(`warning: ${w}\n`);
}
} catch (e) {
err.write(`error: ${e.message}\n`);
return 1;
}
}
out.write(inspectMap(map, registry));
return 0;
}
module.exports = { run };

53
tests/inspect.test.js Normal file
View File

@@ -0,0 +1,53 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { run } = require('../src/cli');
class Sink {
constructor() { this.chunks = []; }
write(s) { this.chunks.push(s); }
get text() { return this.chunks.join(''); }
}
const FIX_MAP = path.join(__dirname, 'fixtures', 'maps', 'inspect_sample.map.json');
const FIX_ATLAS = path.join(__dirname, 'fixtures', 'atlases');
test('inspect: prints report to stdout, exit 0', () => {
const out = new Sink(); const err = new Sink();
const code = run(['inspect', FIX_MAP], { out, err });
assert.equal(code, 0, err.text);
assert.match(out.text, /Map: inspect_sample/);
assert.match(out.text, /Roof: 4 cells flagged/);
});
test('inspect: --atlas-dir enables name resolution', () => {
const out = new Sink(); const err = new Sink();
const code = run(['inspect', FIX_MAP, '--atlas-dir', FIX_ATLAS], { out, err });
assert.equal(code, 0);
assert.match(out.text, /atlas_a:grass/);
});
test('inspect: missing positional arg prints usage, exit 1', () => {
const out = new Sink(); const err = new Sink();
const code = run(['inspect'], { out, err });
assert.equal(code, 1);
assert.match(err.text, /usage: sporel-mapper inspect/);
});
test('inspect: missing map file prints error, exit 1', () => {
const out = new Sink(); const err = new Sink();
const code = run(['inspect', '/no/such/map.json'], { out, err });
assert.equal(code, 1);
assert.match(err.text, /error: cannot read/);
});
test('inspect: invalid JSON prints error, exit 1', () => {
const out = new Sink(); const err = new Sink();
// Use a non-JSON file as bogus input
const reallyNotJson = path.join(__dirname, '..', 'src', 'cli.js');
const code = run(['inspect', reallyNotJson], { out, err });
assert.equal(code, 1);
assert.match(err.text, /error/i);
});