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

View File

@@ -51,8 +51,8 @@ function loadAtlasDirs(dirs) {
const spec = loadAtlasSpec(specPath);
if (registry[spec.atlas_id]) {
warnings.push(
`duplicate atlas_id '${spec.atlas_id}' at ${specPath}; ` +
`keeping earlier registration from ${registry[spec.atlas_id].specPath}`
`atlas_id '${spec.atlas_id}' is a duplicate; ` +
`${specPath} ignored, keeping earlier registration from ${registry[spec.atlas_id].specPath}`
);
continue;
}

View File

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

94
src/commands/build.js Normal file
View File

@@ -0,0 +1,94 @@
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { parseDsl } = require('../dsl-parser');
const { loadAtlasDirs } = require('../atlas-loader');
const { buildMap } = require('../builder');
const USAGE = 'usage: sporel-mapper build <spec.txt> <out.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 !== 2) {
err.write(USAGE);
return 1;
}
const [specPath, outPath] = parsed.positional;
let dslText;
try {
dslText = fs.readFileSync(specPath, 'utf8');
} catch (e) {
err.write(`error: cannot read ${specPath}: ${e.message}\n`);
return 1;
}
let ast;
try {
ast = parseDsl(dslText);
} catch (e) {
const line = e.line || '?';
err.write(`${specPath}:${line}: ${e.message}\n`);
return 1;
}
let registry;
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;
}
const defaultId = path.basename(outPath, path.extname(outPath));
let map;
try {
map = buildMap(ast, registry, { defaultId });
} catch (e) {
const line = e.line || '?';
err.write(`${specPath}:${line}: ${e.message}\n`);
return 1;
}
try {
fs.writeFileSync(outPath, JSON.stringify(map, null, 2) + '\n');
} catch (e) {
err.write(`error: cannot write ${outPath}: ${e.message}\n`);
return 1;
}
return 0;
}
module.exports = { run };