Add build subcommand: DSL spec to v2-map JSON file

Reads the DSL file, parses to AST, loads all --atlas-dir paths into
a registry, runs the builder, and writes pretty-printed JSON. The
output filename's basename becomes the map id when the DSL has no
'id' directive. Parser and builder errors are surfaced as
'<file>:<line>: <message>'. Also reorders the duplicate-atlas warning
so the atlas_id appears before the word 'duplicate'.
This commit is contained in:
Axel Meyer
2026-05-23 16:50:42 +02:00
parent 7adcb407fa
commit f516e2acda
4 changed files with 195 additions and 2 deletions

98
tests/build.test.js Normal file
View File

@@ -0,0 +1,98 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
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_ATLAS = path.join(__dirname, 'fixtures', 'atlases');
const FIX_DSL = path.join(__dirname, 'fixtures', 'dsl');
function tmpOut(name) {
return path.join(os.tmpdir(), `${name}.json`);
}
test('build: minimal DSL → valid v2 JSON file', () => {
const out = new Sink(); const err = new Sink();
const outPath = tmpOut('minimal');
try {
const code = run(['build', path.join(FIX_DSL, 'minimal.txt'), outPath, '--atlas-dir', FIX_ATLAS], { out, err });
assert.equal(code, 0, err.text);
const json = JSON.parse(fs.readFileSync(outPath, 'utf8'));
assert.equal(json.schema_version, 2);
assert.equal(json.id, 'minimal'); // derived from out filename
assert.deepEqual(json.size, { w: 4, h: 4 });
assert.deepEqual(json.atlases, ['atlas_a']);
assert.equal(json.layers.surface.tiles.length, 16);
} finally {
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
}
});
test('build: full DSL with id directive overrides filename-derived id', () => {
const out = new Sink(); const err = new Sink();
const outPath = tmpOut('full');
try {
const code = run(['build', path.join(FIX_DSL, 'full.txt'), outPath, '--atlas-dir', FIX_ATLAS], { out, err });
assert.equal(code, 0, err.text);
const json = JSON.parse(fs.readFileSync(outPath, 'utf8'));
assert.equal(json.id, 'full_demo');
assert.deepEqual(Object.keys(json.layers).sort(), ['surface', 'wall']);
assert.equal(json.roof.length, 16);
} finally {
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
}
});
test('build: out-of-bounds DSL reports file:line on stderr, exit 1', () => {
const out = new Sink(); const err = new Sink();
const outPath = tmpOut('err');
const inPath = path.join(FIX_DSL, 'error_oob.txt');
const code = run(['build', inPath, outPath, '--atlas-dir', FIX_ATLAS], { out, err });
assert.equal(code, 1);
assert.match(err.text, /error_oob\.txt:4:/);
assert.match(err.text, /out of bounds/);
// Output file must NOT exist
assert.equal(fs.existsSync(outPath), false);
});
test('build: missing --atlas-dir for DSL that needs atlases → error', () => {
const out = new Sink(); const err = new Sink();
const outPath = tmpOut('noatlas');
const code = run(['build', path.join(FIX_DSL, 'minimal.txt'), outPath, '--atlas-dir', '/no/such/dir'], { out, err });
assert.equal(code, 1);
assert.match(err.text, /error/i);
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
});
test('build: insufficient positional args prints usage, exit 1', () => {
const out = new Sink(); const err = new Sink();
const code = run(['build', 'only_one_arg.txt'], { out, err });
assert.equal(code, 1);
assert.match(err.text, /usage: sporel-mapper build/);
});
test('build: duplicate atlas across --atlas-dir paths warns on stderr but exits 0', () => {
// Use both fixture dirs (atlases + atlases-dup)
const FIX_DUP = path.join(__dirname, 'fixtures', 'atlases-dup');
const out = new Sink(); const err = new Sink();
const outPath = tmpOut('dup');
try {
const code = run(
['build', path.join(FIX_DSL, 'minimal.txt'), outPath, '--atlas-dir', FIX_ATLAS, '--atlas-dir', FIX_DUP],
{ out, err }
);
assert.equal(code, 0, err.text);
assert.match(err.text, /warning.*atlas_a.*duplicate/i);
} finally {
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
}
});