From 21ab4cba963838805ec27753c14adf558d14cf96 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 10:20:25 +0000 Subject: [PATCH 1/6] deps: update memfs 4.57.2->4.57.6, @types/node 25.6.0->25.9.2 All other dependencies (aontu, chokidar, shape, typescript) already at latest. Clean rebuild produces byte-identical artifacts; build, test, and coverage all pass. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a24bea7..93d8b5f 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "aontu": "0.44.0", "chokidar": "5.0.0", "shape": "10.1.0", - "memfs": "4.57.2" + "memfs": "4.57.6" }, "peerDependencies": { "@voxgig/util": ">=0", @@ -58,7 +58,7 @@ } }, "devDependencies": { - "@types/node": "25.6.0", + "@types/node": "25.9.2", "typescript": "6.0.3" } } From 9410a6789fd319091c4bea67f46e0c27466d403b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 10:56:48 +0000 Subject: [PATCH 2/6] fix: build error handling, CLI build args, watch lifecycle Fixes found during strict review, each with a test: - bin: pass parsed buildargs to Model. It was sending the raw --build string under the wrong key, so CLI -b/--build args never reached build actions. - build: reset per-run error state and collect aontu errors via the `err` option key (aontu's current API) rather than the dead `errs` key. A reused BuildImpl no longer sticks one failure to every later watch rebuild, and model errors are collected instead of thrown. - model: assign a `() => Build` thunk (matching BuildResult.build) so the model producer can call it on a config-triggered rebuild; stop the config watcher in Model.stop() so start() leaves no open handle; redirect promise-based fs writers under dryrun; de-dup writers list. - local: fail with a clear message when an order entry names an unknown action, or one missing a load path, instead of an opaque TypeError. - watch: fall back to cwd when the require base is unset; correct the misleading start() doc comment. Tests added: error recovery, unknown-action error, dryrun promise writers, watch rebuild-on-change, and CLI build-arg passing. Overall line coverage 82.8% -> 92.0% (watch.ts 50.6% -> 91.5%). https://claude.ai/code/session_01HxXpZNrKj3qonocwAtEP8r --- .gitignore | 3 + bin/voxgig-model | 2 +- dist-test/cli.test.js | 40 +++++++++++++ dist-test/cli.test.js.map | 1 + dist-test/fix.test.js | 86 ++++++++++++++++++++++++++++ dist-test/fix.test.js.map | 1 + dist-test/watch.test.js | 57 +++++++++++++++++++ dist-test/watch.test.js.map | 1 + dist/build.js | 26 ++++++++- dist/build.js.map | 2 +- dist/model.js | 27 +++++++-- dist/model.js.map | 2 +- dist/producer/local.js | 7 +++ dist/producer/local.js.map | 2 +- dist/watch.js | 5 +- dist/watch.js.map | 2 +- src/build.ts | 28 +++++++++- src/model.ts | 27 +++++++-- src/producer/local.ts | 12 ++++ src/watch.ts | 5 +- test/cli.test.ts | 51 +++++++++++++++++ test/fix.test.ts | 108 ++++++++++++++++++++++++++++++++++++ test/watch.test.ts | 74 ++++++++++++++++++++++++ 23 files changed, 546 insertions(+), 23 deletions(-) create mode 100644 dist-test/cli.test.js create mode 100644 dist-test/cli.test.js.map create mode 100644 dist-test/fix.test.js create mode 100644 dist-test/fix.test.js.map create mode 100644 dist-test/watch.test.js create mode 100644 dist-test/watch.test.js.map create mode 100644 test/cli.test.ts create mode 100644 test/fix.test.ts create mode 100644 test/watch.test.ts diff --git a/.gitignore b/.gitignore index 5931ce2..8ee4c30 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,9 @@ typings/ coverage test/cov +# Generated fixtures written by tests at runtime +test/_gen + package-lock.json yarn.lock diff --git a/bin/voxgig-model b/bin/voxgig-model index 165484f..b53af43 100755 --- a/bin/voxgig-model +++ b/bin/voxgig-model @@ -183,7 +183,7 @@ async function generate(options) { require: process.cwd(), debug: options.debug, dryrun: options.dryrun, - build: options.build, + buildargs: options.buildargs, // TODO: read more complex options from elsewhere? watch: { diff --git a/dist-test/cli.test.js b/dist-test/cli.test.js new file mode 100644 index 0000000..682ed35 --- /dev/null +++ b/dist-test/cli.test.js @@ -0,0 +1,40 @@ +"use strict"; +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_child_process_1 = require("node:child_process"); +const promises_1 = require("node:fs/promises"); +const node_test_1 = require("node:test"); +const node_assert_1 = __importDefault(require("node:assert")); +const GEN = __dirname + '/../test/_gen'; +const BIN = __dirname + '/../bin/voxgig-model'; +(0, node_test_1.describe)('cli', () => { + // The -b/--build flag is parsed into buildargs; confirm those args actually + // reach a build action via the real CLI entry point. + (0, node_test_1.test)('passes-build-args', async () => { + const dir = GEN + '/cli01'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir + '/model/.model-config', { recursive: true }); + await (0, promises_1.mkdir)(dir + '/build', { recursive: true }); + await (0, promises_1.writeFile)(dir + '/model/model.jsonic', 'top: 1\n'); + await (0, promises_1.writeFile)(dir + '/model/.model-config/model-config.jsonic', "sys: model: action: { recordargs: load: 'build/recordargs' }\n"); + await (0, promises_1.writeFile)(dir + '/build/recordargs.js', "const Path = require('node:path')\n" + + 'module.exports = async function recordargs(model, build) {\n' + + " const root = Path.resolve(build.path, '..', '..')\n" + + " build.fs.writeFileSync(Path.resolve(root, 'args-out.json'),\n" + + ' JSON.stringify(build.args ?? null))\n' + + ' return { ok: true }\n' + + '}\n'); + const out = dir + '/args-out.json'; + await (0, promises_1.rm)(out, { force: true }); + // No shell: args array avoids cross-platform quoting issues. Barewords + // keep the jsonic free of embedded quotes. + const res = (0, node_child_process_1.spawnSync)(process.execPath, [BIN, dir + '/model/model.jsonic', '-b', '{outer:{inner:VAL}}'], { encoding: 'utf8' }); + node_assert_1.default.strictEqual(res.status, 0, 'cli should exit 0\nstdout:' + res.stdout + '\nstderr:' + res.stderr); + const args = JSON.parse(await (0, promises_1.readFile)(out, 'utf8')); + node_assert_1.default.deepStrictEqual(args, { outer: { inner: 'VAL' } }); + }); +}); +//# sourceMappingURL=cli.test.js.map \ No newline at end of file diff --git a/dist-test/cli.test.js.map b/dist-test/cli.test.js.map new file mode 100644 index 0000000..f46b697 --- /dev/null +++ b/dist-test/cli.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"cli.test.js","sourceRoot":"","sources":["../test/cli.test.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;AAEpD,2DAA8C;AAC9C,+CAAiE;AACjE,yCAA0C;AAC1C,8DAAgC;AAGhC,MAAM,GAAG,GAAG,SAAS,GAAG,eAAe,CAAA;AACvC,MAAM,GAAG,GAAG,SAAS,GAAG,sBAAsB,CAAA;AAG9C,IAAA,oBAAQ,EAAC,KAAK,EAAE,GAAG,EAAE;IAEnB,4EAA4E;IAC5E,qDAAqD;IACrD,IAAA,gBAAI,EAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE;QACnC,MAAM,GAAG,GAAG,GAAG,GAAG,QAAQ,CAAA;QAC1B,MAAM,IAAA,aAAE,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,IAAA,gBAAK,EAAC,GAAG,GAAG,sBAAsB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC9D,MAAM,IAAA,gBAAK,EAAC,GAAG,GAAG,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAEhD,MAAM,IAAA,oBAAS,EAAC,GAAG,GAAG,qBAAqB,EAAE,UAAU,CAAC,CAAA;QACxD,MAAM,IAAA,oBAAS,EAAC,GAAG,GAAG,0CAA0C,EAC9D,gEAAgE,CAAC,CAAA;QACnE,MAAM,IAAA,oBAAS,EAAC,GAAG,GAAG,sBAAsB,EAC1C,qCAAqC;YACrC,8DAA8D;YAC9D,uDAAuD;YACvD,iEAAiE;YACjE,2CAA2C;YAC3C,yBAAyB;YACzB,KAAK,CAAC,CAAA;QAER,MAAM,GAAG,GAAG,GAAG,GAAG,gBAAgB,CAAA;QAClC,MAAM,IAAA,aAAE,EAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAE9B,uEAAuE;QACvE,2CAA2C;QAC3C,MAAM,GAAG,GAAG,IAAA,8BAAS,EAAC,OAAO,CAAC,QAAQ,EACpC,CAAC,GAAG,EAAE,GAAG,GAAG,qBAAqB,EAAE,IAAI,EAAE,qBAAqB,CAAC,EAC/D,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAA;QAEvB,qBAAM,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,EAC9B,4BAA4B,GAAG,GAAG,CAAC,MAAM,GAAG,WAAW,GAAG,GAAG,CAAC,MAAM,CAAC,CAAA;QAEvE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,IAAA,mBAAQ,EAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAA;QACpD,qBAAM,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;AAEJ,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/dist-test/fix.test.js b/dist-test/fix.test.js new file mode 100644 index 0000000..0a785f0 --- /dev/null +++ b/dist-test/fix.test.js @@ -0,0 +1,86 @@ +"use strict"; +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_fs_1 = __importDefault(require("node:fs")); +const promises_1 = require("node:fs/promises"); +const node_test_1 = require("node:test"); +const node_assert_1 = __importDefault(require("node:assert")); +const util_1 = require("@voxgig/util"); +const build_1 = require("../dist/build"); +const model_1 = require("../dist/model"); +const GEN = __dirname + '/../test/_gen'; +(0, node_test_1.describe)('fix', () => { + // A model failure must not stick to later builds. The BuildImpl is reused + // across watch rebuilds, so its error state has to reset every run. This + // also exercises that aontu errors are collected (not thrown) into errs. + (0, node_test_1.test)('recovers-from-model-error', async () => { + const dir = GEN + '/err01'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + const path = dir + '/model.jsonic'; + const log = (0, util_1.prettyPino)('test', {}); + // Conflicting scalar values do not unify -> a collected model error. + await (0, promises_1.writeFile)(path, 'x: 1\nx: 2\n'); + const b = (0, build_1.makeBuild)({ fs: node_fs_1.default, base: dir, path, res: [] }, log); + const bad = await b.run({ watch: false }); + node_assert_1.default.strictEqual(bad.ok, false, 'invalid model should fail'); + node_assert_1.default.ok(0 < bad.errs.length, 'invalid model should report errors'); + // Repair the model and rebuild on the SAME instance. + await (0, promises_1.writeFile)(path, 'x: 1\n'); + const good = await b.run({ watch: false }); + node_assert_1.default.strictEqual(good.ok, true, 'build should recover after repair'); + node_assert_1.default.strictEqual(good.errs.length, 0, 'errors must not leak across runs'); + node_assert_1.default.deepStrictEqual(b.model, { x: 1 }); + }); + // An order entry that names an undefined action should fail with a clear + // message rather than an opaque "cannot read properties of undefined". + (0, node_test_1.test)('clear-error-on-unknown-action', async () => { + const dir = GEN + '/act01'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir + '/model/.model-config', { recursive: true }); + await (0, promises_1.mkdir)(dir + '/build', { recursive: true }); + await (0, promises_1.writeFile)(dir + '/model/model.jsonic', 'top: 1\n'); + await (0, promises_1.writeFile)(dir + '/model/.model-config/model-config.jsonic', "sys: model: action: { real: load: 'build/real' }\n" + + "sys: model: order: action: 'real,ghost'\n"); + await (0, promises_1.writeFile)(dir + '/build/real.js', 'module.exports = async () => ({ ok: true })\n'); + const model = new model_1.Model({ + path: dir + '/model/model.jsonic', + base: dir + '/model', + // The build deliberately errors; silence the expected log noise. + debug: 'silent', + }); + const br = await model.run(); + node_assert_1.default.strictEqual(br.ok, false, 'unknown action should fail the build'); + const msg = String(br.errs[0]?.message ?? br.errs[0] ?? ''); + node_assert_1.default.match(msg, /Unknown model action "ghost"/); + }); + // dryrun must not write to the real filesystem, including via the + // promise-based fs API. + (0, node_test_1.test)('dryrun-readonly-promises', async () => { + const dir = GEN + '/dry01'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + const model = new model_1.Model({ + path: dir + '/model.jsonic', + base: dir, + dryrun: true, + }); + // Parent dir (cwd) exists in the dryrun in-memory volume. + const target = process.cwd() + '/.dryrun-probe-' + Date.now() + '.tmp'; + try { + await model.fs.promises.writeFile(target, 'NOPE'); + } + catch { + // A rejection is also acceptable: nothing reached the real disk. + } + const onDisk = node_fs_1.default.existsSync(target); + if (onDisk) { + node_fs_1.default.unlinkSync(target); + } + node_assert_1.default.strictEqual(onDisk, false, 'dryrun promises.writeFile must not touch the real filesystem'); + }); +}); +//# sourceMappingURL=fix.test.js.map \ No newline at end of file diff --git a/dist-test/fix.test.js.map b/dist-test/fix.test.js.map new file mode 100644 index 0000000..daaa6bd --- /dev/null +++ b/dist-test/fix.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"fix.test.js","sourceRoot":"","sources":["../test/fix.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist-test/watch.test.js b/dist-test/watch.test.js new file mode 100644 index 0000000..a16fe27 --- /dev/null +++ b/dist-test/watch.test.js @@ -0,0 +1,57 @@ +"use strict"; +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const promises_1 = require("node:fs/promises"); +const node_test_1 = require("node:test"); +const node_assert_1 = __importDefault(require("node:assert")); +const model_1 = require("../dist/model"); +const GEN = __dirname + '/../test/_gen'; +async function waitFor(fn, ms = 6000) { + const start = Date.now(); + while (Date.now() - start < ms) { + if (await fn()) { + return true; + } + await new Promise(r => setTimeout(r, 50)); + } + return false; +} +async function readVal(file) { + try { + return JSON.parse(await (0, promises_1.readFile)(file, 'utf8')).val; + } + catch { + return undefined; + } +} +(0, node_test_1.describe)('watch', () => { + // Start watching, then change a dependency file and confirm the model is + // rebuilt. Exercises the watch interval, change handling, drain queue, + // dependency tracking, and clean shutdown of both watchers. + (0, node_test_1.test)('rebuilds-on-change', async () => { + const base = GEN + '/wat01/model'; + await (0, promises_1.rm)(GEN + '/wat01', { recursive: true, force: true }); + await (0, promises_1.mkdir)(base + '/.model-config', { recursive: true }); + await (0, promises_1.writeFile)(base + '/model.jsonic', 'top: 1\nval: @"./zed.jsonic"\n'); + await (0, promises_1.writeFile)(base + '/zed.jsonic', '2'); + // Standalone config (no actions) so we don't depend on package resolution. + await (0, promises_1.writeFile)(base + '/.model-config/model-config.jsonic', 'sys: model: action: {}\n'); + const out = base + '/model.json'; + const model = new model_1.Model({ path: base + '/model.jsonic', base }); + try { + await model.start(); + node_assert_1.default.ok(await waitFor(async () => (await readVal(out)) === 2), 'initial build should produce val:2'); + // Let the dependency watchers attach before mutating zed.jsonic. + await new Promise(r => setTimeout(r, 250)); + await (0, promises_1.writeFile)(base + '/zed.jsonic', '7'); + node_assert_1.default.ok(await waitFor(async () => (await readVal(out)) === 7), 'changing the dependency should rebuild to val:7'); + } + finally { + await model.stop(); + } + }); +}); +//# sourceMappingURL=watch.test.js.map \ No newline at end of file diff --git a/dist-test/watch.test.js.map b/dist-test/watch.test.js.map new file mode 100644 index 0000000..ac89ff9 --- /dev/null +++ b/dist-test/watch.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"watch.test.js","sourceRoot":"","sources":["../test/watch.test.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;AAEpD,+CAAiE;AACjE,yCAA0C;AAC1C,8DAAgC;AAEhC,yCAAqC;AAGrC,MAAM,GAAG,GAAG,SAAS,GAAG,eAAe,CAAA;AAGvC,KAAK,UAAU,OAAO,CAAC,EAA0B,EAAE,EAAE,GAAG,IAAI;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACxB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,EAAE,EAAE,CAAC;QAC/B,IAAI,MAAM,EAAE,EAAE,EAAE,CAAC;YACf,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAGD,KAAK,UAAU,OAAO,CAAC,IAAY;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,IAAA,mBAAQ,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAA;IACrD,CAAC;IACD,MAAM,CAAC;QACL,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAGD,IAAA,oBAAQ,EAAC,OAAO,EAAE,GAAG,EAAE;IAErB,yEAAyE;IACzE,uEAAuE;IACvE,4DAA4D;IAC5D,IAAA,gBAAI,EAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,IAAI,GAAG,GAAG,GAAG,cAAc,CAAA;QACjC,MAAM,IAAA,aAAE,EAAC,GAAG,GAAG,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1D,MAAM,IAAA,gBAAK,EAAC,IAAI,GAAG,gBAAgB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAEzD,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,eAAe,EAAE,gCAAgC,CAAC,CAAA;QACzE,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,aAAa,EAAE,GAAG,CAAC,CAAA;QAC1C,2EAA2E;QAC3E,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,oCAAoC,EACzD,0BAA0B,CAAC,CAAA;QAE7B,MAAM,GAAG,GAAG,IAAI,GAAG,aAAa,CAAA;QAChC,MAAM,KAAK,GAAG,IAAI,aAAK,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,eAAe,EAAE,IAAI,EAAE,CAAC,CAAA;QAE/D,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAA;YAEnB,qBAAM,CAAC,EAAE,CACP,MAAM,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EACrD,oCAAoC,CAAC,CAAA;YAEvC,iEAAiE;YACjE,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YAC1C,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,aAAa,EAAE,GAAG,CAAC,CAAA;YAE1C,qBAAM,CAAC,EAAE,CACP,MAAM,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EACrD,iDAAiD,CAAC,CAAA;QACtD,CAAC;gBACO,CAAC;YACP,MAAM,KAAK,CAAC,IAAI,EAAE,CAAA;QACpB,CAAC;IACH,CAAC,CAAC,CAAA;AAEJ,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/dist/build.js b/dist/build.js index 0705372..6382b60 100644 --- a/dist/build.js +++ b/dist/build.js @@ -35,6 +35,11 @@ class BuildImpl { async run(rspec) { let hasErr = false; let runlog = []; + // Reset per-run error state. The BuildImpl is reused across watch + // rebuilds, so without this a single failure would stick to every + // later build. Reassign (don't clear in place) so a previously + // returned BuildResult keeps its own errors. + this.errs = []; this.ctx = { step: 'pre', state: {}, watch: rspec.watch }; const plog = []; if (!hasErr) { @@ -113,11 +118,26 @@ class BuildImpl { } } if (!hasErr) { - this.opts.errs = this.errs; + // Collect this generation's errors into a fresh array. The option key + // is `err` (not `errs`) — that is what aontu reads, and providing it + // puts aontu in collect mode so model errors are gathered rather than + // thrown. A per-call array avoids leaking errors into later builds. + const modelErrs = []; + this.opts.err = modelErrs; this.opts.deps = this.deps; this.opts.fs = this.fs; - this.model = this.aontu.generate(src, this.opts); - hasErr = this.opts.errs && 0 < this.opts.errs.length; + try { + this.model = this.aontu.generate(src, this.opts); + } + catch (err) { + // collect mode normally prevents throws, but guard the rare cases + // (e.g. unresolved imports) so they surface as build errors. + modelErrs.push(err); + } + if (0 < modelErrs.length) { + hasErr = true; + this.errs.push(...modelErrs); + } } this.cacheSig = hasErr ? null : this.snapshotSig(); return hasErr; diff --git a/dist/build.js.map b/dist/build.js.map index 82e87cb..3fd2170 100644 --- a/dist/build.js.map +++ b/dist/build.js.map @@ -1 +1 @@ -{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"build.js","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/model.js b/dist/model.js index 03edd7c..be43b6c 100644 --- a/dist/model.js +++ b/dist/model.js @@ -74,8 +74,12 @@ class Model { } if (self.trigger_model) { // TODO: better design - if (self.build.use) { - self.build.use.config.watch.last.build = build; + // Point the config's last result at the current build so the model + // producer reads fresh config state. It must be a thunk to satisfy + // BuildResult.build's `() => Build` contract (consumers call it). + const lastConfig = self.build.use?.config?.watch?.last; + if (lastConfig) { + lastConfig.build = () => build; } const br = await self.watch.run('model', true); pres.ok = br.ok; @@ -134,6 +138,9 @@ class Model { return br.ok ? this.watch.start() : br; } async stop() { + // start() also spins up a config-file watcher; stop both so no + // chokidar handle is left open keeping the process alive. + await this.config.stop(); return this.watch.stop(); } } @@ -199,8 +206,6 @@ function makeReadOnly(fsm) { 'unlink', 'unlinkSync', 'write', - 'writeFile', - 'writeFileSync', 'writev', ]; const { fs } = (0, memfs_1.memfs)({ [process.cwd()]: {} }); @@ -209,6 +214,20 @@ function makeReadOnly(fsm) { fsm[w] = fs[w].bind(fs); } } + // Also redirect the promise-based writers. fsm.promises is shared by + // reference with the real fs module, so replace it with a copy rather + // than mutating the caller's fs. + const memPromises = fs.promises; + if (fsm.promises && memPromises) { + const promises = { ...fsm.promises }; + for (let w of writers) { + if ('function' === typeof memPromises[w]) { + promises[w] = memPromises[w].bind(memPromises); + } + } + ; + fsm.promises = promises; + } return fsm; } //# sourceMappingURL=model.js.map \ No newline at end of file diff --git a/dist/model.js.map b/dist/model.js.map index 690edd0..35bd0c0 100644 --- a/dist/model.js.map +++ b/dist/model.js.map @@ -1 +1 @@ -{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGpD,gDAAiC;AAEjC,iCAAsC;AAEtC,uCAAyC;AAezC,qCAAiC;AACjC,mCAA+B;AAE/B,4CAAiD;AACjD,4CAAiD;AAGjD,MAAM,KAAK;IAUT,YAAY,KAAgB;QAL5B,kBAAa,GAAG,KAAK,CAAA;QAMnB,MAAM,IAAI,GAAG,IAAI,CAAA;QAEjB,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,CAAA;QAErC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACjB,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACvB,CAAC;QAED,MAAM,IAAI,GAAG,IAAA,iBAAU,EAAC,OAAO,EAAE,KAAY,CAAC,CAAA;QAE9C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;QAEvC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;QACtC,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBACb,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI;oBACpC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;yBACtD,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;yBACjB,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC;aACpC,CAAC,CAAA;QACJ,CAAC;QAED,oDAAoD;QACpD,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE;YACjD,IAAI,EAAE,GAAG;YACT,KAAK,EAAE,KAAK,UAAU,aAAa,CAAC,KAAY,EAAE,GAAiB;gBACjE,IAAI,IAAI,GAAmB;oBACzB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE;iBACvF,CAAA;gBAED,IAAI,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC;oBACxB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAA;oBACd,OAAO,IAAI,CAAA;gBACb,CAAC;gBAGD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;oBAEvB,sBAAsB;oBACtB,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;wBACnB,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;oBAChD,CAAC;oBAED,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;oBAC9C,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAA;oBACf,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,CAAA;gBACrB,CAAC;qBACI,CAAC;oBACJ,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;oBACzB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAA;gBAChB,CAAC;gBAED,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;oBACd,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,CAAA;oBAE/C,IAAI,QAAQ,EAAE,CAAC;wBACb,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,EAAE;4BAC7C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;wBACtB,CAAC,CAAC,CAAA;oBACJ,CAAC;gBACH,CAAC;gBAED,OAAO,IAAI,CAAA;YACb,CAAC;SACF,CAAC,CAAA;QAEF,oBAAoB;QACpB,IAAI,CAAC,KAAK,GAAG;YACX,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;YAC5B,GAAG,EAAE;gBACH;oBACE,IAAI,EAAE,GAAG;oBACT,KAAK,EAAE,sBAAc;iBACtB;gBACD;oBACE,IAAI,EAAE,GAAG;oBACT,KAAK,EAAE,sBAAc;iBACtB;aACF;YACD,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB,CAAA;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,aAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9C,CAAC;IAGD,YAAY;IACZ,KAAK,CAAC,GAAG;QACP,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;QAC1B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACvC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/D,CAAC;IAGD,uDAAuD;IACvD,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;QAC1B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACtC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACxC,CAAC;IAGD,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IAC1B,CAAC;CACF;AA0FC,sBAAK;AAvFP,SAAS,UAAU,CAAC,KAAgB,EAAE,GAAQ,EAAE,EAAO,EAAE,mBAAgC;IACvF,IAAI,KAAK,GAAG,KAAK,CAAC,IAAI,GAAG,gBAAgB,CAAA;IACzC,IAAI,KAAK,GAAG,KAAK,GAAG,sBAAsB,CAAA;IAE1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE;;;;CAI3B,CAAC,CAAA;IACA,CAAC;IAED,IAAI,KAAK,GAAc;QACrB,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,EAAE;YAEH,iDAAiD;YACjD;gBACE,IAAI,EAAE,GAAG;gBACT,KAAK,EAAE,sBAAc;aACtB;YAED,4BAA4B;YAC5B,mBAAmB;SACpB;QACD,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,GAAG;QACH,EAAE;KACH,CAAA;IAED,OAAO,IAAI,eAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;AAC/B,CAAC;AAGD,SAAS,YAAY,CAAC,GAAQ;IAE5B,sBAAsB;IACtB,yBAAyB;IACzB,MAAM,OAAO,GAAG;QACd,WAAW;QACX,eAAe;QACf,YAAY;QACZ,gBAAgB;QAChB,OAAO;QACP,WAAW;QACX,OAAO;QACP,WAAW;QACX,IAAI;QACJ,QAAQ;QACR,mBAAmB;QACnB,OAAO;QACP,WAAW;QACX,QAAQ;QACR,YAAY;QACZ,IAAI;QACJ,QAAQ;QACR,OAAO;QACP,WAAW;QACX,SAAS;QACT,aAAa;QACb,UAAU;QACV,cAAc;QACd,QAAQ;QACR,YAAY;QACZ,OAAO;QACP,WAAW;QACX,eAAe;QACf,QAAQ;KACT,CAAA;IAED,MAAM,EAAE,EAAE,EAAE,GAAG,IAAA,aAAK,EAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAE7C,KAAK,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QACtB,IAAK,EAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAClB,GAAW,CAAC,CAAC,CAAC,GAAI,EAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC3C,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC"} \ No newline at end of file +{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/producer/local.js b/dist/producer/local.js index 2062f4d..9418931 100644 --- a/dist/producer/local.js +++ b/dist/producer/local.js @@ -28,6 +28,13 @@ const local_producer = async (build, ctx) => { // load actions for (let name of ordering) { let actiondef = actions[name]; + if (null == actiondef) { + throw new Error('Unknown model action "' + name + + '" referenced in sys.model.order.action'); + } + if (null == actiondef.load) { + throw new Error('Model action "' + name + '" is missing a "load" path'); + } let actionpath = path_1.default.join(root, actiondef.load); let action = require(actionpath); if (action instanceof Promise) { diff --git a/dist/producer/local.js.map b/dist/producer/local.js.map index 27e4c92..6c75a61 100644 --- a/dist/producer/local.js.map +++ b/dist/producer/local.js.map @@ -1 +1 @@ -{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/producer/local.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/producer/local.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/watch.js b/dist/watch.js index 3b34840..aa492a6 100644 --- a/dist/watch.js +++ b/dist/watch.js @@ -48,7 +48,8 @@ class Watch { } return this.fsw; } - // Returns first BuildResult + // Begin watching. The initial build, and every subsequent rebuild, is + // enqueued asynchronously once the watcher settles, so nothing is returned. start() { this.ensureFSW(); this.startTime = Date.now(); @@ -116,7 +117,7 @@ class Watch { } async add(path) { if (!node_path_1.default.isAbsolute(path)) { - path = node_path_1.default.join(this.wspec.require, path); + path = node_path_1.default.join(this.wspec.require || process.cwd(), path); } // Ignore if already added if (this.canonPaths.has(path)) { diff --git a/dist/watch.js.map b/dist/watch.js.map index ca1ecd1..3c52c49 100644 --- a/dist/watch.js.map +++ b/dist/watch.js.map @@ -1 +1 @@ -{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/src/build.ts b/src/build.ts index aeafa7b..4d2e0c7 100644 --- a/src/build.ts +++ b/src/build.ts @@ -74,6 +74,12 @@ class BuildImpl implements Build { let hasErr = false let runlog = [] + // Reset per-run error state. The BuildImpl is reused across watch + // rebuilds, so without this a single failure would stick to every + // later build. Reassign (don't clear in place) so a previously + // returned BuildResult keeps its own errors. + this.errs = [] + this.ctx = { step: 'pre', state: {}, watch: rspec.watch } const plog: any[] = [] @@ -169,12 +175,28 @@ class BuildImpl implements Build { } if (!hasErr) { - this.opts.errs = this.errs + // Collect this generation's errors into a fresh array. The option key + // is `err` (not `errs`) — that is what aontu reads, and providing it + // puts aontu in collect mode so model errors are gathered rather than + // thrown. A per-call array avoids leaking errors into later builds. + const modelErrs: any[] = [] + this.opts.err = modelErrs this.opts.deps = this.deps this.opts.fs = this.fs - this.model = this.aontu.generate(src, this.opts) - hasErr = this.opts.errs && 0 < this.opts.errs.length + try { + this.model = this.aontu.generate(src, this.opts) + } + catch (err: any) { + // collect mode normally prevents throws, but guard the rare cases + // (e.g. unresolved imports) so they surface as build errors. + modelErrs.push(err) + } + + if (0 < modelErrs.length) { + hasErr = true + this.errs.push(...modelErrs) + } } this.cacheSig = hasErr ? null : this.snapshotSig() diff --git a/src/model.ts b/src/model.ts index 0d4809a..48a7414 100644 --- a/src/model.ts +++ b/src/model.ts @@ -77,8 +77,12 @@ class Model { if (self.trigger_model) { // TODO: better design - if (self.build.use) { - self.build.use.config.watch.last.build = build + // Point the config's last result at the current build so the model + // producer reads fresh config state. It must be a thunk to satisfy + // BuildResult.build's `() => Build` contract (consumers call it). + const lastConfig = self.build.use?.config?.watch?.last + if (lastConfig) { + lastConfig.build = () => build } const br = await self.watch.run('model', true) @@ -149,6 +153,9 @@ class Model { async stop() { + // start() also spins up a config-file watcher; stop both so no + // chokidar handle is left open keeping the process alive. + await this.config.stop() return this.watch.stop() } } @@ -223,8 +230,6 @@ function makeReadOnly(fsm: FST) { 'unlink', 'unlinkSync', 'write', - 'writeFile', - 'writeFileSync', 'writev', ] @@ -236,6 +241,20 @@ function makeReadOnly(fsm: FST) { } } + // Also redirect the promise-based writers. fsm.promises is shared by + // reference with the real fs module, so replace it with a copy rather + // than mutating the caller's fs. + const memPromises = (fs as any).promises + if ((fsm as any).promises && memPromises) { + const promises: any = { ...(fsm as any).promises } + for (let w of writers) { + if ('function' === typeof memPromises[w]) { + promises[w] = memPromises[w].bind(memPromises) + } + } + ;(fsm as any).promises = promises + } + return fsm } diff --git a/src/producer/local.ts b/src/producer/local.ts index 047c2d0..f2ecceb 100644 --- a/src/producer/local.ts +++ b/src/producer/local.ts @@ -34,6 +34,18 @@ const local_producer: Producer = async (build: Build, ctx: BuildContext) => { // load actions for (let name of ordering) { let actiondef = actions[name] + + if (null == actiondef) { + throw new Error( + 'Unknown model action "' + name + + '" referenced in sys.model.order.action') + } + + if (null == actiondef.load) { + throw new Error( + 'Model action "' + name + '" is missing a "load" path') + } + let actionpath = Path.join(root, actiondef.load) let action = require(actionpath) diff --git a/src/watch.ts b/src/watch.ts index 0919a88..5a24e96 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -95,7 +95,8 @@ class Watch { } - // Returns first BuildResult + // Begin watching. The initial build, and every subsequent rebuild, is + // enqueued asynchronously once the watcher settles, so nothing is returned. start() { this.ensureFSW() this.startTime = Date.now() @@ -180,7 +181,7 @@ class Watch { async add(path: string) { if (!Path.isAbsolute(path)) { - path = Path.join(this.wspec.require, path) + path = Path.join(this.wspec.require || process.cwd(), path) } // Ignore if already added diff --git a/test/cli.test.ts b/test/cli.test.ts new file mode 100644 index 0000000..81d4082 --- /dev/null +++ b/test/cli.test.ts @@ -0,0 +1,51 @@ +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ + +import { spawnSync } from 'node:child_process' +import { mkdir, writeFile, readFile, rm } from 'node:fs/promises' +import { test, describe } from 'node:test' +import assert from 'node:assert' + + +const GEN = __dirname + '/../test/_gen' +const BIN = __dirname + '/../bin/voxgig-model' + + +describe('cli', () => { + + // The -b/--build flag is parsed into buildargs; confirm those args actually + // reach a build action via the real CLI entry point. + test('passes-build-args', async () => { + const dir = GEN + '/cli01' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir + '/model/.model-config', { recursive: true }) + await mkdir(dir + '/build', { recursive: true }) + + await writeFile(dir + '/model/model.jsonic', 'top: 1\n') + await writeFile(dir + '/model/.model-config/model-config.jsonic', + "sys: model: action: { recordargs: load: 'build/recordargs' }\n") + await writeFile(dir + '/build/recordargs.js', + "const Path = require('node:path')\n" + + 'module.exports = async function recordargs(model, build) {\n' + + " const root = Path.resolve(build.path, '..', '..')\n" + + " build.fs.writeFileSync(Path.resolve(root, 'args-out.json'),\n" + + ' JSON.stringify(build.args ?? null))\n' + + ' return { ok: true }\n' + + '}\n') + + const out = dir + '/args-out.json' + await rm(out, { force: true }) + + // No shell: args array avoids cross-platform quoting issues. Barewords + // keep the jsonic free of embedded quotes. + const res = spawnSync(process.execPath, + [BIN, dir + '/model/model.jsonic', '-b', '{outer:{inner:VAL}}'], + { encoding: 'utf8' }) + + assert.strictEqual(res.status, 0, + 'cli should exit 0\nstdout:' + res.stdout + '\nstderr:' + res.stderr) + + const args = JSON.parse(await readFile(out, 'utf8')) + assert.deepStrictEqual(args, { outer: { inner: 'VAL' } }) + }) + +}) diff --git a/test/fix.test.ts b/test/fix.test.ts new file mode 100644 index 0000000..353adc0 --- /dev/null +++ b/test/fix.test.ts @@ -0,0 +1,108 @@ +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ + +import Fs from 'node:fs' +import { mkdir, writeFile, rm } from 'node:fs/promises' +import { test, describe } from 'node:test' +import assert from 'node:assert' + +import { prettyPino } from '@voxgig/util' + +import { makeBuild } from '../dist/build' +import { Model } from '../dist/model' + + +const GEN = __dirname + '/../test/_gen' + + +describe('fix', () => { + + // A model failure must not stick to later builds. The BuildImpl is reused + // across watch rebuilds, so its error state has to reset every run. This + // also exercises that aontu errors are collected (not thrown) into errs. + test('recovers-from-model-error', async () => { + const dir = GEN + '/err01' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + + const path = dir + '/model.jsonic' + const log = prettyPino('test', {}) + + // Conflicting scalar values do not unify -> a collected model error. + await writeFile(path, 'x: 1\nx: 2\n') + + const b = makeBuild({ fs: Fs, base: dir, path, res: [] }, log) + + const bad = await b.run({ watch: false }) + assert.strictEqual(bad.ok, false, 'invalid model should fail') + assert.ok(0 < bad.errs.length, 'invalid model should report errors') + + // Repair the model and rebuild on the SAME instance. + await writeFile(path, 'x: 1\n') + + const good = await b.run({ watch: false }) + assert.strictEqual(good.ok, true, 'build should recover after repair') + assert.strictEqual(good.errs.length, 0, 'errors must not leak across runs') + assert.deepStrictEqual((b as any).model, { x: 1 }) + }) + + + // An order entry that names an undefined action should fail with a clear + // message rather than an opaque "cannot read properties of undefined". + test('clear-error-on-unknown-action', async () => { + const dir = GEN + '/act01' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir + '/model/.model-config', { recursive: true }) + await mkdir(dir + '/build', { recursive: true }) + + await writeFile(dir + '/model/model.jsonic', 'top: 1\n') + await writeFile(dir + '/model/.model-config/model-config.jsonic', + "sys: model: action: { real: load: 'build/real' }\n" + + "sys: model: order: action: 'real,ghost'\n") + await writeFile(dir + '/build/real.js', + 'module.exports = async () => ({ ok: true })\n') + + const model = new Model({ + path: dir + '/model/model.jsonic', + base: dir + '/model', + // The build deliberately errors; silence the expected log noise. + debug: 'silent', + }) + const br = await model.run() + + assert.strictEqual(br.ok, false, 'unknown action should fail the build') + const msg = String(br.errs[0]?.message ?? br.errs[0] ?? '') + assert.match(msg, /Unknown model action "ghost"/) + }) + + + // dryrun must not write to the real filesystem, including via the + // promise-based fs API. + test('dryrun-readonly-promises', async () => { + const dir = GEN + '/dry01' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + + const model: any = new Model({ + path: dir + '/model.jsonic', + base: dir, + dryrun: true, + }) + + // Parent dir (cwd) exists in the dryrun in-memory volume. + const target = process.cwd() + '/.dryrun-probe-' + Date.now() + '.tmp' + try { + await model.fs.promises.writeFile(target, 'NOPE') + } + catch { + // A rejection is also acceptable: nothing reached the real disk. + } + + const onDisk = Fs.existsSync(target) + if (onDisk) { + Fs.unlinkSync(target) + } + assert.strictEqual(onDisk, false, + 'dryrun promises.writeFile must not touch the real filesystem') + }) + +}) diff --git a/test/watch.test.ts b/test/watch.test.ts new file mode 100644 index 0000000..f6b154e --- /dev/null +++ b/test/watch.test.ts @@ -0,0 +1,74 @@ +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ + +import { mkdir, writeFile, readFile, rm } from 'node:fs/promises' +import { test, describe } from 'node:test' +import assert from 'node:assert' + +import { Model } from '../dist/model' + + +const GEN = __dirname + '/../test/_gen' + + +async function waitFor(fn: () => Promise, ms = 6000): Promise { + const start = Date.now() + while (Date.now() - start < ms) { + if (await fn()) { + return true + } + await new Promise(r => setTimeout(r, 50)) + } + return false +} + + +async function readVal(file: string): Promise { + try { + return JSON.parse(await readFile(file, 'utf8')).val + } + catch { + return undefined + } +} + + +describe('watch', () => { + + // Start watching, then change a dependency file and confirm the model is + // rebuilt. Exercises the watch interval, change handling, drain queue, + // dependency tracking, and clean shutdown of both watchers. + test('rebuilds-on-change', async () => { + const base = GEN + '/wat01/model' + await rm(GEN + '/wat01', { recursive: true, force: true }) + await mkdir(base + '/.model-config', { recursive: true }) + + await writeFile(base + '/model.jsonic', 'top: 1\nval: @"./zed.jsonic"\n') + await writeFile(base + '/zed.jsonic', '2') + // Standalone config (no actions) so we don't depend on package resolution. + await writeFile(base + '/.model-config/model-config.jsonic', + 'sys: model: action: {}\n') + + const out = base + '/model.json' + const model = new Model({ path: base + '/model.jsonic', base }) + + try { + await model.start() + + assert.ok( + await waitFor(async () => (await readVal(out)) === 2), + 'initial build should produce val:2') + + // Let the dependency watchers attach before mutating zed.jsonic. + await new Promise(r => setTimeout(r, 250)) + await writeFile(base + '/zed.jsonic', '7') + + assert.ok( + await waitFor(async () => (await readVal(out)) === 7), + 'changing the dependency should rebuild to val:7') + } + finally { + await model.stop() + } + }) + +}) From a9cb988d1c77ca8d2605cef3855db8955e0ec201 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 11:23:00 +0000 Subject: [PATCH 3/6] feat: watch config files for changes in Model.start() start() previously ran the config build once but never started the config watcher's interval, so config-file edits were detected yet never acted on (and the watcher leaked until stop()). It now also watches the config files: a config change rebuilds the config and re-triggers the model build - the path the BuildResult.build thunk fix enables. To keep the initial config build from racing the first model build, Watch.start()/Config.start() take an `initial` flag; start() keeps the synchronous initial config build and begins config watching with initial=false. Tests: config-change-triggers-rebuild (verified it fails without the wiring), plus missing-load and throwing-action coverage for the local producer. Line coverage now 93.3% (config.ts 100%, local.ts 96.5%). Known limitation: a simultaneous config + model file change can still drive two overlapping model builds, because the config trigger calls watch.run() directly rather than via the main queue. The per-run error reset makes this self-healing; unifying the queues is left as follow-up. https://claude.ai/code/session_01HxXpZNrKj3qonocwAtEP8r --- dist-test/fix.test.js | 38 ++++++++++++++++++++++ dist-test/fix.test.js.map | 2 +- dist-test/watch.test.js | 45 ++++++++++++++++++++++++++ dist-test/watch.test.js.map | 2 +- dist/config.d.ts | 2 +- dist/config.js | 4 +-- dist/config.js.map | 2 +- dist/model.js | 12 +++++-- dist/model.js.map | 2 +- dist/watch.d.ts | 2 +- dist/watch.js | 8 +++-- dist/watch.js.map | 2 +- src/config.ts | 4 +-- src/model.ts | 12 +++++-- src/watch.ts | 8 +++-- test/fix.test.ts | 51 +++++++++++++++++++++++++++++ test/watch.test.ts | 64 ++++++++++++++++++++++++++++++++++++- 17 files changed, 240 insertions(+), 20 deletions(-) diff --git a/dist-test/fix.test.js b/dist-test/fix.test.js index 0a785f0..c6795c0 100644 --- a/dist-test/fix.test.js +++ b/dist-test/fix.test.js @@ -57,6 +57,44 @@ const GEN = __dirname + '/../test/_gen'; const msg = String(br.errs[0]?.message ?? br.errs[0] ?? ''); node_assert_1.default.match(msg, /Unknown model action "ghost"/); }); + // An action definition with no load path should also fail clearly. + (0, node_test_1.test)('clear-error-on-missing-load', async () => { + const dir = GEN + '/act02'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir + '/model/.model-config', { recursive: true }); + await (0, promises_1.writeFile)(dir + '/model/model.jsonic', 'top: 1\n'); + await (0, promises_1.writeFile)(dir + '/model/.model-config/model-config.jsonic', 'sys: model: action: { noload: {} }\n' + + "sys: model: order: action: 'noload'\n"); + const model = new model_1.Model({ + path: dir + '/model/model.jsonic', + base: dir + '/model', + debug: 'silent', + }); + const br = await model.run(); + node_assert_1.default.strictEqual(br.ok, false, 'action without load should fail'); + const msg = String(br.errs[0]?.message ?? br.errs[0] ?? ''); + node_assert_1.default.match(msg, /Model action "noload" is missing a "load" path/); + }); + // An action that throws at run time must fail the build and surface the + // error rather than silently passing. + (0, node_test_1.test)('action-error-fails-build', async () => { + const dir = GEN + '/throw01'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir + '/model/.model-config', { recursive: true }); + await (0, promises_1.mkdir)(dir + '/build', { recursive: true }); + await (0, promises_1.writeFile)(dir + '/model/model.jsonic', 'top: 1\n'); + await (0, promises_1.writeFile)(dir + '/model/.model-config/model-config.jsonic', "sys: model: action: { boom: load: 'build/boom' }\n"); + await (0, promises_1.writeFile)(dir + '/build/boom.js', "module.exports = async () => { throw new Error('boom-action') }\n"); + const model = new model_1.Model({ + path: dir + '/model/model.jsonic', + base: dir + '/model', + debug: 'silent', + }); + const br = await model.run(); + node_assert_1.default.strictEqual(br.ok, false, 'a throwing action should fail the build'); + const msg = String(br.errs[0]?.message ?? br.errs[0] ?? ''); + node_assert_1.default.match(msg, /boom-action/); + }); // dryrun must not write to the real filesystem, including via the // promise-based fs API. (0, node_test_1.test)('dryrun-readonly-promises', async () => { diff --git a/dist-test/fix.test.js.map b/dist-test/fix.test.js.map index daaa6bd..804f06a 100644 --- a/dist-test/fix.test.js.map +++ b/dist-test/fix.test.js.map @@ -1 +1 @@ -{"version":3,"file":"fix.test.js","sourceRoot":"","sources":["../test/fix.test.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;AAEpD,sDAAwB;AACxB,+CAAuD;AACvD,yCAA0C;AAC1C,8DAAgC;AAEhC,uCAAyC;AAEzC,yCAAyC;AACzC,yCAAqC;AAGrC,MAAM,GAAG,GAAG,SAAS,GAAG,eAAe,CAAA;AAGvC,IAAA,oBAAQ,EAAC,KAAK,EAAE,GAAG,EAAE;IAEnB,0EAA0E;IAC1E,yEAAyE;IACzE,yEAAyE;IACzE,IAAA,gBAAI,EAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;QAC3C,MAAM,GAAG,GAAG,GAAG,GAAG,QAAQ,CAAA;QAC1B,MAAM,IAAA,aAAE,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,IAAA,gBAAK,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAErC,MAAM,IAAI,GAAG,GAAG,GAAG,eAAe,CAAA;QAClC,MAAM,GAAG,GAAG,IAAA,iBAAU,EAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QAElC,qEAAqE;QACrE,MAAM,IAAA,oBAAS,EAAC,IAAI,EAAE,cAAc,CAAC,CAAA;QAErC,MAAM,CAAC,GAAG,IAAA,iBAAS,EAAC,EAAE,EAAE,EAAE,iBAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,CAAA;QAE9D,MAAM,GAAG,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QACzC,qBAAM,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,2BAA2B,CAAC,CAAA;QAC9D,qBAAM,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,oCAAoC,CAAC,CAAA;QAEpE,qDAAqD;QACrD,MAAM,IAAA,oBAAS,EAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;QAE/B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QAC1C,qBAAM,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,mCAAmC,CAAC,CAAA;QACtE,qBAAM,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,kCAAkC,CAAC,CAAA;QAC3E,qBAAM,CAAC,eAAe,CAAE,CAAS,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IAGF,yEAAyE;IACzE,uEAAuE;IACvE,IAAA,gBAAI,EAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,GAAG,GAAG,GAAG,GAAG,QAAQ,CAAA;QAC1B,MAAM,IAAA,aAAE,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,IAAA,gBAAK,EAAC,GAAG,GAAG,sBAAsB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC9D,MAAM,IAAA,gBAAK,EAAC,GAAG,GAAG,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAEhD,MAAM,IAAA,oBAAS,EAAC,GAAG,GAAG,qBAAqB,EAAE,UAAU,CAAC,CAAA;QACxD,MAAM,IAAA,oBAAS,EAAC,GAAG,GAAG,0CAA0C,EAC9D,oDAAoD;YACpD,2CAA2C,CAAC,CAAA;QAC9C,MAAM,IAAA,oBAAS,EAAC,GAAG,GAAG,gBAAgB,EACpC,+CAA+C,CAAC,CAAA;QAElD,MAAM,KAAK,GAAG,IAAI,aAAK,CAAC;YACtB,IAAI,EAAE,GAAG,GAAG,qBAAqB;YACjC,IAAI,EAAE,GAAG,GAAG,QAAQ;YACpB,iEAAiE;YACjE,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAA;QACF,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,CAAA;QAE5B,qBAAM,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,sCAAsC,CAAC,CAAA;QACxE,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;QAC3D,qBAAM,CAAC,KAAK,CAAC,GAAG,EAAE,8BAA8B,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAGF,kEAAkE;IAClE,wBAAwB;IACxB,IAAA,gBAAI,EAAC,0BAA0B,EAAE,KAAK,IAAI,EAAE;QAC1C,MAAM,GAAG,GAAG,GAAG,GAAG,QAAQ,CAAA;QAC1B,MAAM,IAAA,aAAE,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,IAAA,gBAAK,EAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAErC,MAAM,KAAK,GAAQ,IAAI,aAAK,CAAC;YAC3B,IAAI,EAAE,GAAG,GAAG,eAAe;YAC3B,IAAI,EAAE,GAAG;YACT,MAAM,EAAE,IAAI;SACb,CAAC,CAAA;QAEF,0DAA0D;QAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,iBAAiB,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAA;QACtE,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;QACnD,CAAC;QACD,MAAM,CAAC;YACL,iEAAiE;QACnE,CAAC;QAED,MAAM,MAAM,GAAG,iBAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;QACpC,IAAI,MAAM,EAAE,CAAC;YACX,iBAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;QACvB,CAAC;QACD,qBAAM,CAAC,WAAW,CAAC,MAAM,EAAE,KAAK,EAC9B,8DAA8D,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;AAEJ,CAAC,CAAC,CAAA"} \ No newline at end of file +{"version":3,"file":"fix.test.js","sourceRoot":"","sources":["../test/fix.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist-test/watch.test.js b/dist-test/watch.test.js index a16fe27..2fd4946 100644 --- a/dist-test/watch.test.js +++ b/dist-test/watch.test.js @@ -27,6 +27,14 @@ async function readVal(file) { return undefined; } } +async function read(file) { + try { + return await (0, promises_1.readFile)(file, 'utf8'); + } + catch { + return undefined; + } +} (0, node_test_1.describe)('watch', () => { // Start watching, then change a dependency file and confirm the model is // rebuilt. Exercises the watch interval, change handling, drain queue, @@ -53,5 +61,42 @@ async function readVal(file) { await model.stop(); } }); + // A change to a config file should rebuild the config and re-trigger the + // model build. The `mark` action bumps a counter on every model build, so + // a change in its output proves the config change drove a rebuild. + (0, node_test_1.test)('config-change-triggers-rebuild', async () => { + const dir = GEN + '/wat02'; + const base = dir + '/model'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(base + '/.model-config', { recursive: true }); + await (0, promises_1.mkdir)(dir + '/build', { recursive: true }); + await (0, promises_1.writeFile)(base + '/model.jsonic', 'top: 1\n'); + await (0, promises_1.writeFile)(base + '/.model-config/model-config.jsonic', "sys: model: action: { mark: load: 'build/mark' }\n"); + await (0, promises_1.writeFile)(dir + '/build/mark.js', "const Path = require('node:path')\n" + + 'let n = 0\n' + + 'module.exports = async function mark(model, build) {\n' + + " n++\n" + + " const root = Path.resolve(build.path, '..', '..')\n" + + " build.fs.writeFileSync(Path.resolve(root, 'mark.txt'), String(n))\n" + + ' return { ok: true }\n' + + '}\n'); + const mark = dir + '/mark.txt'; + const model = new model_1.Model({ path: base + '/model.jsonic', base }); + try { + await model.start(); + node_assert_1.default.ok(await waitFor(async () => null != await read(mark)), 'initial build should run the mark action'); + const first = await read(mark); + // Let the config watcher attach, then edit a config file. + await new Promise(r => setTimeout(r, 250)); + await (0, promises_1.appendFile)(base + '/.model-config/model-config.jsonic', '\n# touch to trigger a config rebuild\n'); + node_assert_1.default.ok(await waitFor(async () => { + const cur = await read(mark); + return null != cur && cur !== first; + }), 'config change should re-run the model build (mark should advance)'); + } + finally { + await model.stop(); + } + }); }); //# sourceMappingURL=watch.test.js.map \ No newline at end of file diff --git a/dist-test/watch.test.js.map b/dist-test/watch.test.js.map index ac89ff9..324f4d3 100644 --- a/dist-test/watch.test.js.map +++ b/dist-test/watch.test.js.map @@ -1 +1 @@ -{"version":3,"file":"watch.test.js","sourceRoot":"","sources":["../test/watch.test.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;AAEpD,+CAAiE;AACjE,yCAA0C;AAC1C,8DAAgC;AAEhC,yCAAqC;AAGrC,MAAM,GAAG,GAAG,SAAS,GAAG,eAAe,CAAA;AAGvC,KAAK,UAAU,OAAO,CAAC,EAA0B,EAAE,EAAE,GAAG,IAAI;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACxB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,EAAE,EAAE,CAAC;QAC/B,IAAI,MAAM,EAAE,EAAE,EAAE,CAAC;YACf,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAGD,KAAK,UAAU,OAAO,CAAC,IAAY;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,IAAA,mBAAQ,EAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAA;IACrD,CAAC;IACD,MAAM,CAAC;QACL,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAGD,IAAA,oBAAQ,EAAC,OAAO,EAAE,GAAG,EAAE;IAErB,yEAAyE;IACzE,uEAAuE;IACvE,4DAA4D;IAC5D,IAAA,gBAAI,EAAC,oBAAoB,EAAE,KAAK,IAAI,EAAE;QACpC,MAAM,IAAI,GAAG,GAAG,GAAG,cAAc,CAAA;QACjC,MAAM,IAAA,aAAE,EAAC,GAAG,GAAG,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC1D,MAAM,IAAA,gBAAK,EAAC,IAAI,GAAG,gBAAgB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAEzD,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,eAAe,EAAE,gCAAgC,CAAC,CAAA;QACzE,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,aAAa,EAAE,GAAG,CAAC,CAAA;QAC1C,2EAA2E;QAC3E,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,oCAAoC,EACzD,0BAA0B,CAAC,CAAA;QAE7B,MAAM,GAAG,GAAG,IAAI,GAAG,aAAa,CAAA;QAChC,MAAM,KAAK,GAAG,IAAI,aAAK,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,eAAe,EAAE,IAAI,EAAE,CAAC,CAAA;QAE/D,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAA;YAEnB,qBAAM,CAAC,EAAE,CACP,MAAM,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EACrD,oCAAoC,CAAC,CAAA;YAEvC,iEAAiE;YACjE,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YAC1C,MAAM,IAAA,oBAAS,EAAC,IAAI,GAAG,aAAa,EAAE,GAAG,CAAC,CAAA;YAE1C,qBAAM,CAAC,EAAE,CACP,MAAM,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EACrD,iDAAiD,CAAC,CAAA;QACtD,CAAC;gBACO,CAAC;YACP,MAAM,KAAK,CAAC,IAAI,EAAE,CAAA;QACpB,CAAC;IACH,CAAC,CAAC,CAAA;AAEJ,CAAC,CAAC,CAAA"} \ No newline at end of file +{"version":3,"file":"watch.test.js","sourceRoot":"","sources":["../test/watch.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/config.d.ts b/dist/config.d.ts index 4f0919e..10c5309 100644 --- a/dist/config.d.ts +++ b/dist/config.d.ts @@ -6,7 +6,7 @@ declare class Config { log: Log; constructor(spec: BuildSpec, log: Log); run(watch: boolean): Promise; - start(): Promise; + start(initial?: boolean): Promise; stop(): Promise; } export { Config, BuildSpec }; diff --git a/dist/config.js b/dist/config.js index 28ba4c7..1c97871 100644 --- a/dist/config.js +++ b/dist/config.js @@ -21,8 +21,8 @@ class Config { async run(watch) { return this.watch.run('config', watch, ''); } - async start() { - return this.watch.start(); + async start(initial = true) { + return this.watch.start(initial); } async stop() { return this.watch.stop(); diff --git a/dist/config.js.map b/dist/config.js.map index db47642..8930559 100644 --- a/dist/config.js.map +++ b/dist/config.js.map @@ -1 +1 @@ -{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;AAKpD,mCAA+B;AAI/B,MAAM,MAAM;IAKV,YAAY,IAAe,EAAE,GAAQ;QACnC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QAEd,IAAI,CAAC,KAAK,GAAG;YACX,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,EAAE;gBACH,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;aACpB;YACD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,EAAE,EAAE,IAAI,CAAC,EAAE;SACZ,CAAA;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,aAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9C,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAc;QACtB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;IACpD,CAAC;IAED,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IAC1B,CAAC;CACF;AAGQ,wBAAM"} \ No newline at end of file +{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;AAKpD,mCAA+B;AAI/B,MAAM,MAAM;IAKV,YAAY,IAAe,EAAE,GAAQ;QACnC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QAEd,IAAI,CAAC,KAAK,GAAG;YACX,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,GAAG,EAAE;gBACH,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;aACpB;YACD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,EAAE,EAAE,IAAI,CAAC,EAAE;SACZ,CAAA;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,aAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9C,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAc;QACtB,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,CAAC,CAAA;IACpD,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,UAAmB,IAAI;QACjC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IAC1B,CAAC;CACF;AAGQ,wBAAM"} \ No newline at end of file diff --git a/dist/model.js b/dist/model.js index be43b6c..cae1e73 100644 --- a/dist/model.js +++ b/dist/model.js @@ -131,11 +131,19 @@ class Model { const br = await this.config.run(false); return br.ok ? this.watch.run('model', false, '') : br; } - // Start watching for file changes. Run once initially. + // Start watching for file changes. Runs an initial build, then watches + // both the model files and the config files for ongoing changes. async start() { this.trigger_model = false; const br = await this.config.run(true); - return br.ok ? this.watch.start() : br; + if (!br.ok) { + return br; + } + // Watch config files too. The initial config build is already done + // above, so start without forcing another one; a later config change + // rebuilds the config and re-triggers the model build. + this.config.start(false); + return this.watch.start(); } async stop() { // start() also spins up a config-file watcher; stop both so no diff --git a/dist/model.js.map b/dist/model.js.map index 35bd0c0..65ac2d8 100644 --- a/dist/model.js.map +++ b/dist/model.js.map @@ -1 +1 @@ -{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":";AAAA,oDAAoD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGpD,gDAAiC;AAEjC,iCAAsC;AAEtC,uCAAyC;AAezC,qCAAiC;AACjC,mCAA+B;AAE/B,4CAAiD;AACjD,4CAAiD;AAGjD,MAAM,KAAK;IAUT,YAAY,KAAgB;QAL5B,kBAAa,GAAG,KAAK,CAAA;QAMnB,MAAM,IAAI,GAAG,IAAI,CAAA;QAEjB,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,IAAI,MAAM,CAAC,EAAE,CAAA;QAErC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACjB,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACvB,CAAC;QAED,MAAM,IAAI,GAAG,IAAA,iBAAU,EAAC,OAAO,EAAE,KAAY,CAAC,CAAA;QAE9C,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;QAEvC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;QACtC,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;gBACb,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI;oBACpC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;yBACtD,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC;yBACjB,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC;aACpC,CAAC,CAAA;QACJ,CAAC;QAED,oDAAoD;QACpD,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE;YACjD,IAAI,EAAE,GAAG;YACT,KAAK,EAAE,KAAK,UAAU,aAAa,CAAC,KAAY,EAAE,GAAiB;gBACjE,IAAI,IAAI,GAAmB;oBACzB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE;iBACvF,CAAA;gBAED,IAAI,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,CAAC;oBACxB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAA;oBACd,OAAO,IAAI,CAAA;gBACb,CAAC;gBAGD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;oBAEvB,sBAAsB;oBACtB,mEAAmE;oBACnE,mEAAmE;oBACnE,kEAAkE;oBAClE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAA;oBACtD,IAAI,UAAU,EAAE,CAAC;wBACf,UAAU,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC,KAAK,CAAA;oBAChC,CAAC;oBAED,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;oBAC9C,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAA;oBACf,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC,IAAI,CAAA;gBACrB,CAAC;qBACI,CAAC;oBACJ,IAAI,CAAC,aAAa,GAAG,IAAI,CAAA;oBACzB,IAAI,CAAC,EAAE,GAAG,IAAI,CAAA;gBAChB,CAAC;gBAED,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;oBACd,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,CAAA;oBAE/C,IAAI,QAAQ,EAAE,CAAC;wBACb,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,IAAY,EAAE,EAAE;4BAC7C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;wBACtB,CAAC,CAAC,CAAA;oBACJ,CAAC;gBACH,CAAC;gBAED,OAAO,IAAI,CAAA;YACb,CAAC;SACF,CAAC,CAAA;QAEF,oBAAoB;QACpB,IAAI,CAAC,KAAK,GAAG;YACX,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,GAAG,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;YAC5B,GAAG,EAAE;gBACH;oBACE,IAAI,EAAE,GAAG;oBACT,KAAK,EAAE,sBAAc;iBACtB;gBACD;oBACE,IAAI,EAAE,GAAG;oBACT,KAAK,EAAE,sBAAc;iBACtB;aACF;YACD,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,KAAK,EAAE,KAAK,CAAC,KAAK;SACnB,CAAA;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,aAAK,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;IAC9C,CAAC;IAGD,YAAY;IACZ,KAAK,CAAC,GAAG;QACP,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;QAC1B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACvC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/D,CAAC;IAGD,uDAAuD;IACvD,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,aAAa,GAAG,KAAK,CAAA;QAC1B,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACtC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACxC,CAAC;IAGD,KAAK,CAAC,IAAI;QACR,+DAA+D;QAC/D,0DAA0D;QAC1D,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;QACxB,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;IAC1B,CAAC;CACF;AAsGC,sBAAK;AAnGP,SAAS,UAAU,CAAC,KAAgB,EAAE,GAAQ,EAAE,EAAO,EAAE,mBAAgC;IACvF,IAAI,KAAK,GAAG,KAAK,CAAC,IAAI,GAAG,gBAAgB,CAAA;IACzC,IAAI,KAAK,GAAG,KAAK,GAAG,sBAAsB,CAAA;IAE1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1B,EAAE,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QACxC,EAAE,CAAC,aAAa,CAAC,KAAK,EAAE;;;;CAI3B,CAAC,CAAA;IACA,CAAC;IAED,IAAI,KAAK,GAAc;QACrB,IAAI,EAAE,QAAQ;QACd,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,EAAE;YAEH,iDAAiD;YACjD;gBACE,IAAI,EAAE,GAAG;gBACT,KAAK,EAAE,sBAAc;aACtB;YAED,4BAA4B;YAC5B,mBAAmB;SACpB;QACD,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,GAAG;QACH,EAAE;KACH,CAAA;IAED,OAAO,IAAI,eAAM,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;AAC/B,CAAC;AAGD,SAAS,YAAY,CAAC,GAAQ;IAE5B,sBAAsB;IACtB,yBAAyB;IACzB,MAAM,OAAO,GAAG;QACd,WAAW;QACX,eAAe;QACf,YAAY;QACZ,gBAAgB;QAChB,OAAO;QACP,WAAW;QACX,OAAO;QACP,WAAW;QACX,IAAI;QACJ,QAAQ;QACR,mBAAmB;QACnB,OAAO;QACP,WAAW;QACX,QAAQ;QACR,YAAY;QACZ,IAAI;QACJ,QAAQ;QACR,OAAO;QACP,WAAW;QACX,SAAS;QACT,aAAa;QACb,UAAU;QACV,cAAc;QACd,QAAQ;QACR,YAAY;QACZ,OAAO;QACP,QAAQ;KACT,CAAA;IAED,MAAM,EAAE,EAAE,EAAE,GAAG,IAAA,aAAK,EAAC,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAA;IAE7C,KAAK,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QACtB,IAAK,EAAU,CAAC,CAAC,CAAC,EAAE,CAAC;YAClB,GAAW,CAAC,CAAC,CAAC,GAAI,EAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC3C,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,sEAAsE;IACtE,iCAAiC;IACjC,MAAM,WAAW,GAAI,EAAU,CAAC,QAAQ,CAAA;IACxC,IAAK,GAAW,CAAC,QAAQ,IAAI,WAAW,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAQ,EAAE,GAAI,GAAW,CAAC,QAAQ,EAAE,CAAA;QAClD,KAAK,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;YACtB,IAAI,UAAU,KAAK,OAAO,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC;gBACzC,QAAQ,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAChD,CAAC;QACH,CAAC;QACD,CAAC;QAAC,GAAW,CAAC,QAAQ,GAAG,QAAQ,CAAA;IACnC,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC"} \ No newline at end of file +{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/watch.d.ts b/dist/watch.d.ts index 732f6a9..5b01078 100644 --- a/dist/watch.d.ts +++ b/dist/watch.d.ts @@ -26,7 +26,7 @@ declare class Watch { }; constructor(bspec: BuildSpec, log: Log); ensureFSW(): FSWatcher; - start(): void; + start(initial?: boolean): void; canon(path: string): string; handleChange(path: string): void; drain(): Promise; diff --git a/dist/watch.js b/dist/watch.js index aa492a6..8eeeefc 100644 --- a/dist/watch.js +++ b/dist/watch.js @@ -50,10 +50,14 @@ class Watch { } // Begin watching. The initial build, and every subsequent rebuild, is // enqueued asynchronously once the watcher settles, so nothing is returned. - start() { + // Pass initial=false to start watching without forcing that first build + // (e.g. when the caller has already produced one). + start(initial = true) { this.ensureFSW(); this.startTime = Date.now(); - this.handleChange(''); + if (initial) { + this.handleChange(''); + } // Check if there have been no recent changes, if so, run build. this.intervalId = setInterval(() => { // const start = this.startTime diff --git a/dist/watch.js.map b/dist/watch.js.map index 3c52c49..a14c940 100644 --- a/dist/watch.js.map +++ b/dist/watch.js.map @@ -1 +1 @@ -{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"watch.js","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index 210964a..871fcbb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,8 +33,8 @@ class Config { return this.watch.run('config', watch, '') } - async start() { - return this.watch.start() + async start(initial: boolean = true) { + return this.watch.start(initial) } async stop() { diff --git a/src/model.ts b/src/model.ts index 48a7414..12f7e7a 100644 --- a/src/model.ts +++ b/src/model.ts @@ -144,11 +144,19 @@ class Model { } - // Start watching for file changes. Run once initially. + // Start watching for file changes. Runs an initial build, then watches + // both the model files and the config files for ongoing changes. async start() { this.trigger_model = false const br = await this.config.run(true) - return br.ok ? this.watch.start() : br + if (!br.ok) { + return br + } + // Watch config files too. The initial config build is already done + // above, so start without forcing another one; a later config change + // rebuilds the config and re-triggers the model build. + this.config.start(false) + return this.watch.start() } diff --git a/src/watch.ts b/src/watch.ts index 5a24e96..6fadab3 100644 --- a/src/watch.ts +++ b/src/watch.ts @@ -97,10 +97,14 @@ class Watch { // Begin watching. The initial build, and every subsequent rebuild, is // enqueued asynchronously once the watcher settles, so nothing is returned. - start() { + // Pass initial=false to start watching without forcing that first build + // (e.g. when the caller has already produced one). + start(initial: boolean = true) { this.ensureFSW() this.startTime = Date.now() - this.handleChange('') + if (initial) { + this.handleChange('') + } // Check if there have been no recent changes, if so, run build. this.intervalId = setInterval(() => { diff --git a/test/fix.test.ts b/test/fix.test.ts index 353adc0..6babe5c 100644 --- a/test/fix.test.ts +++ b/test/fix.test.ts @@ -75,6 +75,57 @@ describe('fix', () => { }) + // An action definition with no load path should also fail clearly. + test('clear-error-on-missing-load', async () => { + const dir = GEN + '/act02' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir + '/model/.model-config', { recursive: true }) + + await writeFile(dir + '/model/model.jsonic', 'top: 1\n') + await writeFile(dir + '/model/.model-config/model-config.jsonic', + 'sys: model: action: { noload: {} }\n' + + "sys: model: order: action: 'noload'\n") + + const model = new Model({ + path: dir + '/model/model.jsonic', + base: dir + '/model', + debug: 'silent', + }) + const br = await model.run() + + assert.strictEqual(br.ok, false, 'action without load should fail') + const msg = String(br.errs[0]?.message ?? br.errs[0] ?? '') + assert.match(msg, /Model action "noload" is missing a "load" path/) + }) + + + // An action that throws at run time must fail the build and surface the + // error rather than silently passing. + test('action-error-fails-build', async () => { + const dir = GEN + '/throw01' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir + '/model/.model-config', { recursive: true }) + await mkdir(dir + '/build', { recursive: true }) + + await writeFile(dir + '/model/model.jsonic', 'top: 1\n') + await writeFile(dir + '/model/.model-config/model-config.jsonic', + "sys: model: action: { boom: load: 'build/boom' }\n") + await writeFile(dir + '/build/boom.js', + "module.exports = async () => { throw new Error('boom-action') }\n") + + const model = new Model({ + path: dir + '/model/model.jsonic', + base: dir + '/model', + debug: 'silent', + }) + const br = await model.run() + + assert.strictEqual(br.ok, false, 'a throwing action should fail the build') + const msg = String(br.errs[0]?.message ?? br.errs[0] ?? '') + assert.match(msg, /boom-action/) + }) + + // dryrun must not write to the real filesystem, including via the // promise-based fs API. test('dryrun-readonly-promises', async () => { diff --git a/test/watch.test.ts b/test/watch.test.ts index f6b154e..d87261e 100644 --- a/test/watch.test.ts +++ b/test/watch.test.ts @@ -1,6 +1,6 @@ /* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ -import { mkdir, writeFile, readFile, rm } from 'node:fs/promises' +import { mkdir, writeFile, readFile, appendFile, rm } from 'node:fs/promises' import { test, describe } from 'node:test' import assert from 'node:assert' @@ -32,6 +32,16 @@ async function readVal(file: string): Promise { } +async function read(file: string): Promise { + try { + return await readFile(file, 'utf8') + } + catch { + return undefined + } +} + + describe('watch', () => { // Start watching, then change a dependency file and confirm the model is @@ -71,4 +81,56 @@ describe('watch', () => { } }) + + // A change to a config file should rebuild the config and re-trigger the + // model build. The `mark` action bumps a counter on every model build, so + // a change in its output proves the config change drove a rebuild. + test('config-change-triggers-rebuild', async () => { + const dir = GEN + '/wat02' + const base = dir + '/model' + await rm(dir, { recursive: true, force: true }) + await mkdir(base + '/.model-config', { recursive: true }) + await mkdir(dir + '/build', { recursive: true }) + + await writeFile(base + '/model.jsonic', 'top: 1\n') + await writeFile(base + '/.model-config/model-config.jsonic', + "sys: model: action: { mark: load: 'build/mark' }\n") + await writeFile(dir + '/build/mark.js', + "const Path = require('node:path')\n" + + 'let n = 0\n' + + 'module.exports = async function mark(model, build) {\n' + + " n++\n" + + " const root = Path.resolve(build.path, '..', '..')\n" + + " build.fs.writeFileSync(Path.resolve(root, 'mark.txt'), String(n))\n" + + ' return { ok: true }\n' + + '}\n') + + const mark = dir + '/mark.txt' + const model = new Model({ path: base + '/model.jsonic', base }) + + try { + await model.start() + + assert.ok( + await waitFor(async () => null != await read(mark)), + 'initial build should run the mark action') + const first = await read(mark) + + // Let the config watcher attach, then edit a config file. + await new Promise(r => setTimeout(r, 250)) + await appendFile(base + '/.model-config/model-config.jsonic', + '\n# touch to trigger a config rebuild\n') + + assert.ok( + await waitFor(async () => { + const cur = await read(mark) + return null != cur && cur !== first + }), + 'config change should re-run the model build (mark should advance)') + } + finally { + await model.stop() + } + }) + }) From fd5e2fa1d5326622a62b66f79ea0e908537c2de0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 11:46:53 +0000 Subject: [PATCH 4/6] docs: comprehensive documentation set + AI-agent files Add a four-part documentation set under docs/ and rework the README into a navigable entry point: - tutorial.md learn the tool by building a model step by step - how-to.md task recipes (build args, actions, custom producers, in-memory fs, dry run, watch tuning, ...) - reference.md CLI, project layout, config, actions, the full programmatic API, build lifecycle, modeling-language essentials, logging, scripts, and troubleshooting - explanation.md the problem, why unification, architecture, the build lifecycle, caching/watch design, and known limitations Add agent guides for working on the codebase: - AGENTS.md environment, build/test, repo map, conventions, test patterns, task playbooks, and the key gotchas - CLAUDE.md Claude Code entry point (imports AGENTS.md), essentials up front Every runnable example is verified against the built tool. Two easy-to-hit modeling rules that surfaced while writing are now documented: keys are jsonic identifiers (quote hyphens), and every emitted field must resolve to a concrete value (give it a default or mark it optional). https://claude.ai/code/session_01HxXpZNrKj3qonocwAtEP8r --- AGENTS.md | 216 +++++++++++++++ CLAUDE.md | 37 +++ README.md | 131 ++++++++- docs/explanation.md | 254 ++++++++++++++++++ docs/how-to.md | 378 ++++++++++++++++++++++++++ docs/reference.md | 630 ++++++++++++++++++++++++++++++++++++++++++++ docs/tutorial.md | 264 +++++++++++++++++++ 7 files changed, 1906 insertions(+), 4 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 docs/explanation.md create mode 100644 docs/how-to.md create mode 100644 docs/reference.md create mode 100644 docs/tutorial.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ca91a4b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,216 @@ +# AGENTS.md + +Guidance for AI coding agents (and humans) working **on** the `@voxgig/model` +codebase. For using the tool, start at the [README](./README.md) and +[docs/](./docs/). This file is about developing the package itself. + +Keep this file accurate: if you change the build, test layout, or a convention +below, update it in the same change. + + +## What this project is + +`@voxgig/model` unifies `.jsonic` source into a single JSON model (via the +[aontu](https://github.com/voxgig/aontu) CUE-style engine) and runs generator +"actions" over it. It can build once or watch and rebuild. It ships a library +(`Model`) and a CLI (`voxgig-model`). + +A 90-second tour of how it fits together is in +[docs/explanation.md](./docs/explanation.md#architecture-at-a-glance). Read that +before making structural changes. + + +## Environment + +- **Node.js 24** is the target (CI matrix). Node 20.19+ works; the `shape` + dependency declares `engines.node >= 24`, so older Node prints an + `EBADENGINE` warning — harmless here. +- TypeScript, CommonJS output (`"type": "commonjs"`). +- Install with `npm install`. No lockfile is committed (it is gitignored). + + +## Build, test, run + +```bash +npm install # once +npm run build # tsc --build src test -> dist/ and dist-test/ +npm test # node --test dist-test/**/*.test.js +``` + +```bash +# a single suite / pattern +TEST_PATTERN='watch' npm run test-some + +# coverage (writes coverage/lcov.info) +npm run test-cov + +# recompile on save +npm run watch + +# exercise the CLI against the bundled sample model +npm run test-model # build once on test/sys01/model/model.jsonic +npm run model # watch model/sys.jsonic +``` + +**You must `npm run build` before `npm test`.** Tests are TypeScript compiled to +`dist-test/`, and they import the compiled library from `dist/`. Source edits do +not take effect in tests until rebuilt. For a fully clean rebuild use +`npx tsc --build src test --force`. + + +## Critical gotchas + +1. **Run commands from the repository root.** `tsc --build src test` resolves + `src` and `test` as project paths relative to the current directory. Running + it from a subdirectory fails with `TS5083: Cannot read file '.../src/ + tsconfig.json'` and silently leaves `dist/` stale — so tests then run against + old code. If a build looks like it "passed" but tests don't reflect your + change, check your working directory. + +2. **`dist/` and `dist-test/` are committed.** They are tracked in git (in the + `files` allowlist for publishing). After any source change, rebuild and + commit the regenerated `dist/`/`dist-test/` alongside the `.ts`. CI rebuilds + them, but the repo convention is to commit them. + +3. **The CLI (`bin/voxgig-model`) is plain JavaScript**, not compiled. Edits to + it take effect immediately (no build needed for the bin itself), but it + `require`s `dist/model.js`, so the library must be built. + +4. **`aontu` reads `opts.err`, not `opts.errs`.** When invoking + `aontu.generate(src, opts)`, set `opts.err = ` to enable collect mode + (errors gathered into the array instead of thrown). This is easy to get + wrong — see `src/build.ts: resolveModel`. + +5. **Generated test fixtures go in `test/_gen/`** (gitignored). Tests write their + own fixtures there at runtime; do not commit them. See "Writing tests". + + +## Repository map + +``` +src/ + model.ts Model: public entry; wires config build + model build + config.ts Config: specialized build for .model-config + watch.ts Watch: chokidar, debounce, rebuild queue, dep tracking + build.ts BuildImpl/makeBuild: resolve model (aontu) + run producers + types.ts shared interfaces (Build, BuildSpec, Producer, ...) + producer/ + model.ts model_producer: write the unified model as JSON + local.ts local_producer: load & run actions declared in config + tsconfig.json project config for src + +bin/voxgig-model CLI entry (plain JS): arg parsing -> new Model().run/start + +test/ + *.test.ts node:test suites (compiled to dist-test/) + tsconfig.json project config for tests + sys01/, p01/, e01/, w01/ committed fixtures + _gen/ GITIGNORED scratch fixtures written by tests at runtime + +model/ the package's own sample model (sys.jsonic + config) +dist/, dist-test/ COMMITTED build output +docs/ tutorial / how-to / reference / explanation +``` + +Data flow: `CLI/Model` → `Config` (resolves actions) + `Watch` → `BuildImpl` +(`aontu` unify → producers). The model build runs `model_producer` then +`local_producer`. Full detail: +[docs/explanation.md](./docs/explanation.md). + + +## Code conventions + +Match the surrounding style; in this codebase that means: + +- **2-space indent, no semicolons, single quotes.** +- Prefer `let`; use `const` where the existing file does. +- Null checks as `null == x` / `null != x`. Comparisons are often "yoda" + (`'post' === ctx.step`). Follow the local file. +- CommonJS modules. Keep imports grouped: node builtins, then deps, then local. +- Keep the public type surface in `src/types.ts`. Many internal fields are typed + `any` by design; do not tighten them speculatively. +- Logging goes through the pino logger (`build.log` / `this.log`) with a `point` + (short event name) and a human `note`. Do not `console.log` in library code. +- Copyright header comment at the top of each source file (see existing files). + + +## Writing tests + +- Use `node:test` (`describe`/`test`) and `node:assert`. Import the library from + `../dist/...` (compiled output), mirroring existing suites. +- **Put runtime fixtures under `test/_gen//`** and create them in the test + (write `.jsonic`, config, and any action `.js`). This keeps fixtures + self-contained and out of git. Clean the dir at the start of the test + (`rm -rf` then `mkdir -p`). +- **Watch-mode tests must call `model.stop()` in a `finally`.** `start()` opens + chokidar watchers (for both the model and the config); leaving them open hangs + the test process. Poll for the expected result with a timeout helper rather + than fixed sleeps (see `test/watch.test.ts`). +- **CLI tests** spawn the built bin without a shell, for cross-platform + safety: + ```js + const { spawnSync } = require('node:child_process') + const res = spawnSync(process.execPath, [BIN, modelPath, '-b', '{a:1}'], + { encoding: 'utf8' }) + ``` + Use jsonic barewords (`{a:b}`) to avoid embedded-quote escaping. +- Make tests **meaningful guards**: when adding a test for a fix, confirm it + fails without the fix (temporarily revert, run, restore). + +Existing suites to model new ones on: + +| Suite | Covers | +|-------|--------| +| `test/build.test.ts` | end-to-end builds (`makeBuild`, `Model.run`) with the `p01`/`sys01` fixtures | +| `test/fix.test.ts` | error recovery, unknown/misconfigured actions, dry-run filesystem | +| `test/watch.test.ts` | watch rebuilds on model and config changes | +| `test/cli.test.ts` | the CLI passes build args through to actions | + + +## Common tasks (playbooks) + +### Add a build action (product-level generator) +Actions are user-space, not framework code — declare in a model's +`.model-config/model-config.jsonic` and implement under `build/`. See +[docs/how-to.md](./docs/how-to.md#write-a-build-action). No library change. + +### Add a built-in producer (framework-level) +1. Create `src/producer/.ts` exporting a `Producer` + `(build, ctx) => Promise` (see `src/producer/model.ts`). +2. Add it to the pipeline in `src/model.ts` (`this.build.res`), or document it + for `makeBuild` users. +3. Rebuild, add a test, commit `dist/` too. + +### Add or change a CLI flag +Edit `bin/voxgig-model`: add to `resolveOptions` (`parseArgs` `options`) and +`validateOptions` (the `Shape`), then include it in the `spec` passed to +`new Model(spec)`. Thread the field through `ModelSpec` in `src/types.ts` and +read it in the `Model` constructor. (The CLI maps `--build` → `buildargs`; keep +spec keys aligned with `ModelSpec`.) + +### Change the model/build types +Edit `src/types.ts`. Rebuild so `dist/*.d.ts` regenerates; commit them. + +### Touch the build/error/cache logic +`src/build.ts`. Remember the `opts.err` collect-mode rule (gotcha #4) and that +`this.errs` is reset per run so a reused build instance doesn't accumulate stale +errors. Add a regression test in `test/fix.test.ts`. + + +## Before you commit + +- `npm run build` is clean (`tsc rc 0`) **from the repo root**. +- `npm test` is green; add/adjust tests for behavior changes. +- Regenerated `dist/` and `dist-test/` are staged with the source. +- No `test/_gen/` artifacts are staged (they are gitignored — verify with + `git status`). +- Keep this file and `docs/` in sync with any behavior or convention change. + + +## More documentation + +- [docs/tutorial.md](./docs/tutorial.md) — learn the tool by building a model. +- [docs/how-to.md](./docs/how-to.md) — task recipes. +- [docs/reference.md](./docs/reference.md) — CLI, API, config, language. +- [docs/explanation.md](./docs/explanation.md) — architecture and design + rationale (read before structural changes). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5d4f840 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,37 @@ +# CLAUDE.md + +Guidance for Claude Code (and other AI agents) working in this repository. + +The full contributor/agent guide — environment, repository map, conventions, +test patterns, and task playbooks — is in **@AGENTS.md**. Read it before making +changes. The essentials: + +## Build & test +- Run everything **from the repository root**. +- `npm run build` (`tsc --build src test`) **before** `npm test`; tests run + against compiled output in `dist-test/` and import the library from `dist/`. +- `dist/` and `dist-test/` are **committed** — rebuild and stage them with any + source change. +- Single suite: `TEST_PATTERN='' npm run test-some`. Coverage: + `npm run test-cov`. + +## Watch out for +- Stale `dist/` from a build that failed because it ran in a subdirectory + (`TS5083`) — tests then pass against old code. Verify cwd is the repo root. +- Watch-mode tests must `await model.stop()` in a `finally`, or the process + hangs on open file watchers. +- Runtime test fixtures belong in `test/_gen/` (gitignored); don't commit them. +- `aontu.generate` collects errors only when given `opts.err` (not `opts.errs`). + +## Where things are +- Library: `src/` (`model.ts`, `config.ts`, `watch.ts`, `build.ts`, `types.ts`, + `producer/`). CLI: `bin/voxgig-model` (plain JS). Docs: `docs/`. +- Architecture overview: `docs/explanation.md`. + +## Style +- 2-space indent, no semicolons, single quotes, CommonJS. Match the local file. +- Log via the pino logger (`build.log`/`this.log`), never `console.log` in + library code. + +When you change behavior or a convention, update `@AGENTS.md` and the relevant +file in `docs/` in the same change. diff --git a/README.md b/README.md index 9ce1c78..8d0e013 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,130 @@ -# Model +# @voxgig/model -A framework for universal application modeling. +A framework for **universal application modeling**: describe a system once as a +single declarative model, then generate every downstream artifact — code, +configuration, documentation, infrastructure — from that one source of truth. -PROTOTYPE. +The core tool unifies `.jsonic` source (using [CUE](https://cuelang.org)-style +unification, via [aontu](https://github.com/voxgig/aontu)) into one canonical +JSON model, then hands that model to your generators ("actions"). It can build +once or watch and rebuild on change. -INSPIRED BY: http://cuelang.org +> **Status: prototype.** The concepts are stable; specific APIs and conventions +> may still change. Inspired by [CUE](https://cuelang.org). + + +## Install + +```bash +npm install @voxgig/model pino +``` + +`pino` is a peer dependency (logging). Requires Node.js — CI runs Node 24; +Node 20.19+ generally works. + + +## Quick start + +```bash +# 1. a model +mkdir -p model && cat > model/model.jsonic <<'EOF' +service: name: 'orders' +service: port: *8080 | integer +EOF + +# 2. build it -> writes model/model.json +npx voxgig-model model/model.jsonic + +# 3. or watch and rebuild on change +npx voxgig-model --watch model/model.jsonic +``` + +`model/model.json`: + +```json +{ "service": { "name": "orders", "port": 8080 } } +``` + +Use it from code instead of the CLI: + +```js +const { Model } = require('@voxgig/model') + +const model = new Model({ path: 'model/model.jsonic', base: 'model' }) +const result = await model.run() +if (!result.ok) throw new Error(result.errs.join('; ')) +``` + + +## What it does + +- **Unifies** `.jsonic` source — with types, defaults, references, wildcards, + and imports — into a single validated JSON model. +- **Generates** artifacts from that model through **actions**: small JS modules + you declare in config and that receive the unified model. +- **Watches** source and config files and rebuilds incrementally, tracking + imports as dependencies. +- **Previews** safely with `--dryrun` (writes redirected to an in-memory + filesystem), and can build against any `fs` implementation. + + +## Documentation + +| If you want to… | Read | +|------------------|------| +| Learn by building a model step by step | [Tutorial](./docs/tutorial.md) | +| Accomplish a specific task | [How-to guides](./docs/how-to.md) | +| Look up a flag, type, config key, or language construct | [Reference](./docs/reference.md) | +| Understand how and why it works | [Explanation](./docs/explanation.md) | + +Working **on** this repository (including with an AI coding agent)? +See [AGENTS.md](./AGENTS.md). + + +## A fuller example + +A model that generates an environment file from its services: + +``` +my-project/ +├─ model/ +│ ├─ model.jsonic +│ └─ .model-config/model-config.jsonic +└─ build/envFile.js +``` + +```jsonic +# model/model.jsonic +shape: service: { name?: string, port: *8080 | integer } +service: orders: $.shape.service & { name: 'orders' } +service: web: $.shape.service & { name: 'web', port: 443 } +``` + +```jsonic +# model/.model-config/model-config.jsonic +sys: model: action: { envFile: load: 'build/envFile' } +``` + +```js +// build/envFile.js +const Path = require('node:path') +module.exports = async function envFile(model, build) { + const root = Path.resolve(build.path, '..', '..') + const lines = Object.entries(model.service) + .map(([n, s]) => `PORT_${n.toUpperCase()}=${s.port}`) + build.fs.writeFileSync(Path.resolve(root, 'services.env'), lines.join('\n') + '\n') + return { ok: true } +} +``` + +```bash +npx voxgig-model model/model.jsonic # writes model/model.json and services.env +``` + +See the [tutorial](./docs/tutorial.md) for the same example built up from +scratch. + + +## License + +MIT © Voxgig Ltd. See [LICENSE](./LICENSE). diff --git a/docs/explanation.md b/docs/explanation.md new file mode 100644 index 0000000..79c6695 --- /dev/null +++ b/docs/explanation.md @@ -0,0 +1,254 @@ +# Explanation: concepts and design + +This document explains *why* `@voxgig/model` works the way it does. It is +background reading, not a task list — for steps see the +[tutorial](./tutorial.md) and [how-to guides](./how-to.md), and for exhaustive +detail the [reference](./reference.md). + +- [The problem: one model, many artifacts](#the-problem-one-model-many-artifacts) +- [Why unification](#why-unification) +- [Architecture at a glance](#architecture-at-a-glance) +- [The build lifecycle](#the-build-lifecycle) +- [Producers and actions](#producers-and-actions) +- [Why there are two builds](#why-there-are-two-builds) +- [Watching and incremental rebuilds](#watching-and-incremental-rebuilds) +- [Caching](#caching) +- [Dry run and the filesystem boundary](#dry-run-and-the-filesystem-boundary) +- [Error handling philosophy](#error-handling-philosophy) +- [Known limitations](#known-limitations) +- [Status](#status) + + +## The problem: one model, many artifacts + +Most non-trivial systems describe the *same* concepts in many places: an API is +defined in route code, in an OpenAPI file, in client SDKs, in infrastructure +config, in documentation, in test fixtures. Each copy drifts from the others. +The cost of a change is multiplied by the number of representations, and the +bugs live in the gaps between them. + +`@voxgig/model` is a framework for **universal application modeling**: describe +the system once, as a single declarative model, then *generate* every +downstream artifact from that one source. The model is the truth; the code, +config, and docs are projections of it. + +This is a generative approach. The tool itself is deliberately small: its job is +to turn source into a trustworthy unified model and hand that model to +generators (here called **actions**). The domain knowledge lives in your model +and your actions, not in the framework. + + +## Why unification + +A model needs to express more than values — it needs rules: defaults, +constraints, types, and composition. Plain JSON or YAML cannot; they only carry +data, so the rules end up in code that interprets the data. + +`@voxgig/model` borrows from [CUE](https://cuelang.org) (via the +[aontu](https://github.com/voxgig/aontu) engine) and uses **unification** as its +core operation. Unification merges two descriptions into the single most +specific description consistent with both — or fails if they conflict. A few +consequences make this powerful for modeling: + +- **Types and values are the same kind of thing.** `port: integer` and + `port: 8080` unify to `8080`; `port: 1` and `port: 2` *conflict* and fail. + Validation is not a separate pass — it is what unification does. +- **Order does not matter.** `a & b` is the same as `b & a`. You can layer a + base shape, environment overrides, and per-instance tweaks in any arrangement + and get the same result. +- **Defaults are first-class.** `*true | boolean` says "default to `true`, but + any boolean is allowed." Composition naturally fills in defaults only where + nothing more specific was said. +- **Composition is explicit and safe.** Reusing a shape with `&` cannot quietly + produce something that violates the shape — a conflict is an error, not a + silent overwrite. + +The result of resolving a model is a single canonical data structure (written as +`model.json`) with every default applied and every constraint satisfied. That +structure is what actions consume. + + +## Architecture at a glance + +The pieces, from outside in: + +``` +CLI (bin/voxgig-model) + └─ Model orchestrates the whole thing + ├─ Config ── Watch resolves .model-config, declares actions + └─ Watch resolves the main model, runs actions + └─ Build (BuildImpl) resolve + run the producer pipeline + ├─ aontu unify source into a model + └─ producers + ├─ model_producer write model.json + └─ local_producer load & run your actions +``` + +| Component | File | Responsibility | +|-----------|------|----------------| +| `Model` | `src/model.ts` | Public entry. Wires a config build and a model build together; exposes `run`/`start`/`stop`. | +| `Config` | `src/config.ts` | A specialized build for `.model-config/model-config.jsonic`. | +| `Watch` | `src/watch.ts` | File watching, debouncing, the rebuild queue, dependency tracking. | +| `Build` / `BuildImpl` | `src/build.ts` | One build: resolve the model via aontu, run the producer pipeline, cache by mtime. | +| `model_producer` | `src/producer/model.ts` | Serialize the unified model to JSON. | +| `local_producer` | `src/producer/local.ts` | Load action modules from config and run them. | +| types | `src/types.ts` | The shared interfaces (`Build`, `BuildSpec`, `Producer`, …). | + +`aontu` (unification), `chokidar` (file watching), `memfs` (the dry-run +filesystem), and `pino`/`@voxgig/util` (logging) are the external moving parts. + + +## The build lifecycle + +A single build is a small state machine (`BuildImpl.run`): + +1. **Reset error state.** A build instance is reused across watch rebuilds, so + each run starts with a clean slate. +2. **Resolve the model.** Read the root file and unify it (and its imports) with + aontu into `build.model`. If nothing relevant changed since last time, the + cached model is reused (see [Caching](#caching)). +3. **Pre phase.** Run every producer with `step = 'pre'`. Producers may signal + `reload` if they changed model source. +4. **Reload if asked.** If a pre-producer requested a reload and there were no + errors, resolve the model again so the post phase sees the new source. +5. **Post phase.** Run every producer with `step = 'post'`. +6. **Return a result.** `ok`, the per-producer results, collected `errs`, and a + `runlog` of the phases. + +The `pre` → `reload` → `post` shape exists for a specific need: actions that +*generate model source*. A code generator might emit a `.jsonic` fragment that +the model itself imports; running it in `pre` and reloading lets that generated +source participate in the final model that `post` actions consume. + + +## Producers and actions + +There are two extension layers, and the distinction matters. + +A **producer** is the low-level unit: a function `(build, ctx) => ProducerResult` +that the build runs in both phases. Producers are how the pipeline is assembled. +The framework ships two — one to write the model JSON, one to run actions. + +An **action** is the high-level, user-facing unit: a JS module declared in the +config file. Actions exist because most users do not want to think about the +producer protocol — they want "given the model, write these files." The +`local_producer` is the bridge: it reads the action declarations from the config +model, loads the modules, and runs them at the right phase. + +So: *producers are the framework's plug-in mechanism; actions are the product's +plug-in mechanism.* Use actions for normal generation. Drop to a custom producer +pipeline (via `makeBuild`) when you need control over the whole build. + + +## Why there are two builds + +A model build needs to know which actions to run *before* it can run them — and +that list is itself modeled, in `.model-config/model-config.jsonic`. So a `Model` +resolves **two** models: + +1. The **config build** unifies the config file into a config model and writes + `model-config.json`. Its declarations (`sys.model.action`, `sys.model.order`) + tell the main build what to do. +2. The **model build** unifies your root model, writes `model.json`, and runs + the actions the config declared. + +Treating config as just another model is deliberate: the config benefits from +the same unification, defaults, and imports as everything else. A shared base +config can be imported by package path, and projects layer their own actions on +top. + +An internal trigger producer on the config build kicks off the model build, so +the two stay in step. In watch mode both the model's sources and the config's +sources are watched, and a change to either rebuilds the model. + + +## Watching and incremental rebuilds + +Watch mode is built around three ideas: + +- **Dependency tracking.** aontu records every file a model imports. After each + successful build the watcher adds those files (plus the root) to the set it + watches, so editing an imported fragment rebuilds the whole model. +- **Debouncing.** External tools often write many files in a burst (a compiler + emitting output, a formatter rewriting a tree). The watcher waits for an + `idle` quiet period (default 111 ms) after the last change before queuing a + build, coalescing the burst into one rebuild. +- **A serial queue.** Changes enqueue build requests that drain one at a time, + so rebuilds never overlap within a watcher. + +Because the model build and config build are separate watchers, the model's +sources and the config's sources are tracked independently; a config edit +rebuilds the config, which re-triggers the model build. + + +## Caching + +Re-unifying a large model on every keystroke is wasteful, so a build caches its +last result by a **signature** of file modification times. After a successful +resolve, the build snapshots the mtime of the root file and every recorded +dependency. On the next resolve, if every tracked file still has the same mtime, +the cached model is reused and unification is skipped entirely. + +This is why `model_producer` skips writing when the output is byte-identical: +rewriting an unchanged file would bump its mtime, invalidate caches downstream, +and re-trigger watchers in a loop. Avoiding needless writes keeps incremental +rebuilds cheap and prevents feedback loops between cooperating tools. + + +## Dry run and the filesystem boundary + +Every build carries an `fs` object, and producers/actions are expected to write +through `build.fs` rather than importing `fs` directly. That indirection is the +seam that makes `--dryrun` work: in dry-run mode the build swaps the write +methods (both synchronous and promise-based) for ones backed by an in-memory +[`memfs`](https://github.com/streamich/memfs) volume. Reads still hit the real +disk; writes go nowhere. The same seam lets you supply any `fs` implementation +to build entirely in memory. + +This is a pragmatic, internal-use safeguard — it covers the standard write +methods, not every conceivable path to disk. An action that bypasses `build.fs` +escapes it. + + +## Error handling philosophy + +Builds aim to **fail loudly and recover cleanly**: + +- A build collects errors rather than throwing on the first one where it can, so + a `BuildResult` reports what went wrong. +- Error state is reset at the start of each build. A build instance is reused + across watch rebuilds, so without this a single transient failure would stick + to every later build. Resetting means a watcher self-heals: fix the source and + the next rebuild succeeds. +- Model (unification) errors, missing files, unknown or misconfigured actions, + and actions that throw all converge on the same outcome — `ok: false` with the + cause in `errs` — so callers have one thing to check. + + +## Known limitations + +This is an early framework; some sharp edges are known and documented rather +than hidden: + +- **Concurrent model builds.** A change to a config file triggers a model + rebuild directly, outside the model watcher's serial queue. If a config file + and a model file change at the very same moment, two model builds can overlap. + The per-build error reset makes this self-healing, but unifying the two + watchers' queues is future work. +- **Dry-run coverage.** The read-only filesystem shim covers the common write + methods; it is explicitly "not complete." Treat `--dryrun` as a strong + convenience, not a security boundary. +- **Project layout assumption.** Action `load` paths resolve against the + directory two levels above the root model file, which bakes in the + `project/model/model.jsonic` convention. +- **Error capture in the language layer.** Capturing *all* syntax and model + errors as structured data (rather than some surfacing as thrown exceptions) + depends on continued work in aontu. + + +## Status + +`@voxgig/model` is a **prototype**. The concepts — model-once, generate-many, +unification as the core operation — are stable and intended; the specific APIs, +file conventions, and internals may still change. It is inspired by, and tracks +the ideas of, [CUE](https://cuelang.org). diff --git a/docs/how-to.md b/docs/how-to.md new file mode 100644 index 0000000..02f4252 --- /dev/null +++ b/docs/how-to.md @@ -0,0 +1,378 @@ +# How-to guides + +Focused recipes for specific tasks. Each is self-contained. If you are new to +the tool, do the [tutorial](./tutorial.md) first; for exhaustive detail see the +[reference](./reference.md). + +- [Build once vs. watch](#build-once-vs-watch) +- [Pass build arguments to actions](#pass-build-arguments-to-actions) +- [Write a build action](#write-a-build-action) +- [Order and select actions](#order-and-select-actions) +- [Regenerate model source, then rebuild (pre + reload)](#regenerate-model-source-then-rebuild-pre--reload) +- [Split a model across files and packages](#split-a-model-across-files-and-packages) +- [Define and reuse a shape](#define-and-reuse-a-shape) +- [Embed the model in your own program](#embed-the-model-in-your-own-program) +- [Run a custom producer pipeline](#run-a-custom-producer-pipeline) +- [Preview without writing files (dry run)](#preview-without-writing-files-dry-run) +- [Build against an in-memory filesystem](#build-against-an-in-memory-filesystem) +- [Control logging](#control-logging) +- [Handle and surface build errors](#handle-and-surface-build-errors) +- [Tune watch behavior](#tune-watch-behavior) + + +## Build once vs. watch + +Once, then exit: + +```bash +voxgig-model model/model.jsonic +``` + +```js +const { Model } = require('@voxgig/model') +const model = new Model({ path: 'model/model.jsonic', base: 'model' }) +const result = await model.run() +if (!result.ok) process.exitCode = 1 +``` + +Watch and rebuild until stopped: + +```bash +voxgig-model --watch model/model.jsonic +``` + +```js +const model = new Model({ path: 'model/model.jsonic', base: 'model' }) +await model.start() +// ... later, to release the watchers: +await model.stop() +``` + +> `start()` returns once watching has begun; the first build is enqueued +> asynchronously. Always `stop()` when done — otherwise the file watchers keep +> the process alive. + + +## Pass build arguments to actions + +Build arguments are arbitrary data exposed to every action as `build.args`. + +```bash +voxgig-model -b '{env:prod, region:eu-west-1}' model/model.jsonic +``` + +```js +new Model({ path: 'model/model.jsonic', base: 'model', + buildargs: { env: 'prod', region: 'eu-west-1' } }) +``` + +```js +// build/deploy.js +module.exports = async function deploy(model, build) { + const env = build.args?.env ?? 'dev' + build.log.info({ point: 'deploy', note: 'env=' + env }) + return { ok: true } +} +``` + + +## Write a build action + +1. Declare it in `model/.model-config/model-config.jsonic`: + + ```jsonic + sys: model: action: { + genDocs: load: 'build/genDocs' + } + ``` + + `load` is relative to the **project root** (the directory above `model/`), + without a file extension. Action names are plain identifiers — use `genDocs`, + not `gen-docs` (a hyphenated name would need quoting: `'gen-docs'`). + +2. Implement `build/genDocs.js`: + + ```js + const Path = require('node:path') + + module.exports = async function genDocs(model, build, ctx) { + const root = Path.resolve(build.path, '..', '..') + build.fs.mkdirSync(Path.resolve(root, 'out'), { recursive: true }) + build.fs.writeFileSync( + Path.resolve(root, 'out', 'model.html'), + '
' + JSON.stringify(model, null, 2) + '
' + ) + return { ok: true } + } + ``` + +Use `build.fs` (not `require('fs')`) so the action honors `--dryrun`. Use +`build.log` for logging so output is consistent and respects the log level. + + +## Order and select actions + +Declare several actions and set an explicit order: + +```jsonic +sys: model: action: { + genTypes: load: 'build/genTypes' + genDocs: load: 'build/genDocs' + lint: load: 'build/lint' +} + +# comma-separated; runs in this order +sys: model: order: action: 'genTypes,genDocs,lint' +``` + +Omit `order.action` to run every declared action in declaration order. Naming an +action in `order.action` that has no definition fails the build with a clear +error. + +Control **when** an action runs with its `step` export: + +```js +module.exports = async function genTypes(model, build) { /* ... */ } +module.exports.step = 'post' // 'pre' | 'post' | 'all'; default 'post' +``` + + +## Regenerate model source, then rebuild (pre + reload) + +Sometimes an action must generate `.jsonic` that the model itself consumes. Run +it in the `pre` phase and request a reload: + +```js +// build/genSource.js (declared as `genSource: load: 'build/genSource'`) +const Path = require('node:path') + +module.exports = async function genSource(model, build) { + const root = Path.resolve(build.path, '..', '..') + // write a .jsonic file the root model imports + build.fs.writeFileSync( + Path.resolve(root, 'model', 'generated.jsonic'), + 'generated: ok: true\n' + ) + return { ok: true, reload: true } // re-resolve before the post phase +} + +module.exports.step = 'pre' +``` + +With `reload: true`, the model is re-resolved after the `pre` phase, so `post` +actions and `model_producer` see the regenerated source. + + +## Split a model across files and packages + +Import another file relative to the current one: + +```jsonic +@"./shapes.jsonic" +color: @"./palette.jsonic" +``` + +Import from an installed package by its module path: + +```jsonic +@"@voxgig/model/model/.model-config/model-config.jsonic" +``` + +All imports are tracked as dependencies — editing an imported file rebuilds the +model in watch mode. + + +## Define and reuse a shape + +A "shape" is a reusable structure of defaults and constraints. Define it once, +then unify it into each instance: + +```jsonic +shape: endpoint: { + method: *'GET' | string + auth: *true | boolean + path?: string # optional in the shape; each endpoint sets it +} + +api: list: $.shape.endpoint & { path: '/items' } +api: create: $.shape.endpoint & { path: '/items', method: 'POST' } +``` + +Apply a shape to **every** entry of a map with the `&:` wildcard: + +```jsonic +api: &: $.shape.endpoint # every child of `api` must satisfy endpoint +api: list: { path: '/items' } +api: create: { path: '/items', method: 'POST' } +``` + + +## Embed the model in your own program + +`Model` is a normal class — drive it from your own tooling: + +```js +const { Model } = require('@voxgig/model') + +async function buildModel() { + const model = new Model({ + path: __dirname + '/model/model.jsonic', + base: __dirname + '/model', + require: __dirname, // project root for action resolution + buildargs: { env: process.env.NODE_ENV ?? 'dev' }, + debug: 'warn', // quieter logging + }) + + const result = await model.run() + if (!result.ok) { + throw new Error('model build failed: ' + + result.errs.map(e => e.message ?? e).join('; ')) + } + // the unified model: + return result.build?.().model +} +``` + + +## Run a custom producer pipeline + +Skip the config/action layer entirely and assemble producers yourself with +`makeBuild`. A **producer** runs in both the `pre` and `post` phases and returns +a `ProducerResult`. + +```js +const Fs = require('node:fs') +const { makeBuild } = require('@voxgig/model/dist/build') +const { model_producer } = require('@voxgig/model/dist/producer/model') +const { prettyPino } = require('@voxgig/util') + +const build = makeBuild({ + fs: Fs, + base: __dirname + '/model', + path: __dirname + '/model/model.jsonic', + res: [ + { path: '/', build: model_producer }, // write model.json + { path: '/', build: async function summary(build, ctx) { + if (ctx.step === 'post') { + const n = Object.keys(build.model).length + build.log.info({ point: 'summary', note: n + ' top-level keys' }) + } + return { ok: true, name: 'summary', step: ctx.step, + active: true, reload: false, errs: [], runlog: [] } + } + }, + ], +}, prettyPino('custom', {})) + +const result = await build.run({ watch: false }) +``` + + +## Preview without writing files (dry run) + +```bash +voxgig-model --dryrun model/model.jsonic +``` + +```js +new Model({ path: 'model/model.jsonic', base: 'model', dryrun: true }) +``` + +The build runs fully — the model resolves and actions execute — but every write +(sync or promise-based) is redirected to an in-memory filesystem. Nothing on +disk changes. This is the safe way to test a model or a new action. + + +## Build against an in-memory filesystem + +Provide your own `fs` implementation (for example +[`memfs`](https://github.com/streamich/memfs)) to build entirely in memory — +useful for tests or sandboxed generation: + +```js +const { memfs } = require('memfs') +const { Model } = require('@voxgig/model') + +// Seed the in-memory volume. Include a config file: a pure in-memory build +// cannot resolve the package import that an auto-created config would use. +const { fs, vol } = memfs({ + '/proj/model/model.jsonic': 'service: name: orders\n', + '/proj/model/.model-config/model-config.jsonic': 'sys: model: action: {}\n', +}) + +const model = new Model({ + path: '/proj/model/model.jsonic', + base: '/proj/model', + fs, +}) +await model.run() + +console.log(vol.toJSON()) // includes the generated /proj/model/model.json +``` + +The provided `fs` must support the synchronous methods the build uses +(`readFileSync`, `writeFileSync`, `mkdirSync`, `statSync`, `existsSync`). + + +## Control logging + +Set the level with `--debug`/`debug`: + +```bash +voxgig-model -g debug model/model.jsonic # verbose +voxgig-model -g warn model/model.jsonic # quieter +voxgig-model -g silent model/model.jsonic # no logs +``` + +```js +new Model({ path, base, debug: 'silent' }) // string level +new Model({ path, base, debug: true }) // shorthand for 'debug' +``` + +Levels: `trace`, `debug`, `info` (default), `warn`, `error`, `fatal`, `silent`. +In your actions and producers, log through `build.log` so output stays +consistent: + +```js +build.log.info({ point: 'my-action', note: 'did the thing' }) +``` + + +## Handle and surface build errors + +`run()` resolves with a `BuildResult`; check `ok` and inspect `errs`: + +```js +const result = await model.run() +if (!result.ok) { + for (const err of result.errs) { + console.error(err.message ?? err) + } + process.exitCode = 1 +} +``` + +Errors are reset at the start of each build, so a result reflects only that +build. Model (unification) errors, missing files, unknown actions, and actions +that throw all surface through `result.errs` with `result.ok === false`. + + +## Tune watch behavior + +Choose which filesystem events trigger a rebuild and adjust the debounce: + +```js +new Model({ + path, base, + idle: 250, // debounce window in ms (default 111) + watch: { + mod: true, // rebuild on file modification (default true) + add: true, // rebuild on file addition (default false) + rem: true, // rebuild on file deletion (default false) + }, +}) +``` + +The CLI enables `mod`, `add`, and `rem` by default in watch mode. A larger +`idle` coalesces bursts of changes (e.g. an external tool writing many files) +into a single rebuild. diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..87f1904 --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,630 @@ +# Reference + +Complete technical reference for `@voxgig/model`. For a guided introduction +start with the [tutorial](./tutorial.md); for goal-oriented recipes see the +[how-to guides](./how-to.md); for the ideas behind the design read the +[explanation](./explanation.md). + +- [Command line interface](#command-line-interface) +- [Project layout](#project-layout) +- [The config file](#the-config-file) +- [Actions](#actions) +- [Build arguments](#build-arguments) +- [Programmatic API](#programmatic-api) +- [Producers](#producers) +- [The build lifecycle](#the-build-lifecycle) +- [Modeling language essentials](#modeling-language-essentials) +- [Logging](#logging) +- [npm scripts](#npm-scripts) +- [Troubleshooting](#troubleshooting) +- [Requirements](#requirements) + + +## Command line interface + +``` +voxgig-model [options] +``` + +`` is the root `.jsonic` file of the model. It is also accepted as +`--model `. The directory containing the root file is the model **base**; +generated model JSON is written next to the root file. + +| Flag | Short | Type | Default | Description | +|------|-------|------|---------|-------------| +| `--model ` | `-m` | string | first positional | Root model file path. | +| `--watch` | `-w` | boolean | `false` | Watch source and config files; rebuild on change. Runs until interrupted. | +| `--debug ` | `-g` | string | `info` | Log level: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `silent`. | +| `--dryrun` | `-y` | boolean | `false` | Resolve and run the build, but redirect all file writes to an in-memory filesystem (nothing touches disk). | +| `--build ` | `-b` | string | `''` | A [jsonic](https://github.com/jsonic-lang/jsonic) document of build arguments, exposed to actions as `build.args`. | +| `--help` | `-h` | boolean | | Print usage and exit. | +| `--version` | `-v` | boolean | | Print version and exit. | + +The model path is resolved relative to the current working directory. The +current working directory is also recorded as the project `require` base used +to resolve action modules. + +**Exit codes:** `0` on success or after `--help`/`--version`; `1` on a usage +error, a missing model file, or an uncaught build error. + +```bash +# Build once, writing model/model.json +voxgig-model model/model.jsonic + +# Watch and rebuild, with debug logging +voxgig-model -w -g debug model/model.jsonic + +# Dry run (no files written) +voxgig-model --dryrun model/model.jsonic + +# Pass build arguments to actions +voxgig-model -b '{env:prod, region:eu-west-1}' model/model.jsonic +``` + + +## Project layout + +A model is a directory tree. The conventional shape: + +``` +my-project/ +├─ model/ +│ ├─ model.jsonic # root model file (your entry point) +│ ├─ ...more .jsonic files # imported by the root +│ ├─ model.json # GENERATED: the unified model +│ └─ .model-config/ +│ ├─ model-config.jsonic # config: declares actions +│ └─ model-config.json # GENERATED: the unified config +└─ build/ + ├─ foo.js # an action module + └─ bar.js +``` + +Two paths are derived from the root file `model/model.jsonic`: + +- **base** = `model/` — the directory of the root file. Generated model JSON + (`model.json`) and the `.model-config/` directory live here. +- **project root** = `my-project/` — the directory **two levels above** the + root file (`resolve(rootFile, '..', '..')`). Action `load` paths are + resolved against the project root, so `build/foo` means + `my-project/build/foo.js`. + +This two-levels-up rule means the root model file is expected to sit one +directory below the project root (e.g. `model/model.jsonic`). + + +## The config file + +`/.model-config/model-config.jsonic` declares the **actions** that run +during a build. If it does not exist, the `Model` constructor creates a minimal +one that imports the package's base config. + +The config is itself a model, unified the same way as your main model. The +keys the tool reads: + +```jsonic +# Each action has a load path (relative to the project root, no extension). +# Action names are jsonic identifiers (no hyphens unless quoted). +sys: model: action: { + genDocs: load: 'build/genDocs' + genTypes: load: 'build/genTypes' +} + +# Optional explicit run order (comma-separated action names). +# When omitted, every declared action runs in declaration order. +sys: model: order: action: 'genTypes,genDocs' +``` + +| Config key | Type | Meaning | +|------------|------|---------| +| `sys.model.action..load` | string | Module path for the action, relative to the project root, without file extension. **Required** for each action. | +| `sys.model.order.action` | string | Comma-separated action names defining run order. Names are split on commas and surrounding whitespace; empty entries are ignored. If absent, the order is the key order of `sys.model.action`. | + +> Backwards compatibility: if `sys.model.action` is absent, `sys.model.builders` +> is read instead. + +An `order.action` entry that names an action with no matching `action.` +fails the build with `Unknown model action ""`. An action whose +definition has no `load` fails with `Model action "" is missing a "load" +path`. + +The generated `model-config.json` is written next to `model-config.jsonic`. + + +## Actions + +An action is a CommonJS module that receives the unified model and produces +side effects — typically generating source files, documentation, infrastructure +config, or other artifacts. + +### Module signature + +```js +// build/genDocs.js +module.exports = async function genDocs(model, build, ctx) { + // model — the unified model object (plain data) + // build — the Build instance (see API reference) + // ctx — the BuildContext: { step, watch, state } + + build.fs.writeFileSync('out/docs.html', render(model)) + + return { ok: true, reload: false } +} + +// Optional: when does this action run? 'pre' | 'post' | 'all'. Default 'post'. +module.exports.step = 'post' +``` + +The module's default export may also be a `Promise` that resolves to the +function (it is awaited once at load time). + +### Arguments + +| Argument | Description | +|----------|-------------| +| `model` | The unified model as plain JSON-compatible data. Same value as `build.model`. | +| `build` | The [`Build`](#build) instance. Useful members: `build.fs` (honors `--dryrun`), `build.args` (build arguments), `build.path`, `build.opts.base`, `build.log`, `build.dryrun`. | +| `ctx` | The [`BuildContext`](#buildcontext). `ctx.step` is `'pre'` or `'post'`; `ctx.state` is a per-build scratch object shared across producers. | + +### Return value + +Return `{ ok, reload }`, or nothing: + +| Field | Type | Meaning | +|-------|------|---------| +| `ok` | boolean | `false` fails the build and stops remaining actions. A missing/`null` return is treated as success. | +| `reload` | boolean | When `true` (typically from a `pre` action that rewrote model source files), the model is re-resolved before `post` actions run. | + +A thrown error fails the build; the error is logged once and surfaced in +`BuildResult.errs`. + +### Steps + +| Step | Runs | Use for | +|------|------|---------| +| `pre` | before the model is finalized | Generating or rewriting `.jsonic` source that the model itself depends on; pair with `reload: true`. | +| `post` (default) | after the model is finalized | Emitting artifacts from the finished model. | +| `all` | both phases | Actions that must observe both phases. | + +### Example: a `pre` action that feeds back into the model + +```js +// build/pre.js — writes a source file the model imports, then asks for a reload +const Path = require('node:path') +const Fs = require('node:fs') + +module.exports = async function pre(model, build) { + const root = Path.resolve(build.path, '..', '..') + if (!build.dryrun) { + Fs.writeFileSync(Path.resolve(root, 'model', 'pre.jsonic'), 'OK') + } + return { ok: true, reload: true } +} + +module.exports.step = 'pre' +``` + + +## Build arguments + +Build arguments are arbitrary data passed into a build and exposed to actions as +`build.args`. + +- **CLI:** `-b ''` — parsed with jsonic. e.g. `-b '{env:prod}'`. +- **API:** `new Model({ ..., buildargs: { env: 'prod' } })`. + +```js +module.exports = async function deploy(model, build) { + const env = build.args?.env ?? 'dev' + // ... +} +``` + + +## Programmatic API + +The package main export provides `Model` and the `BuildSpec` type: + +```js +const { Model } = require('@voxgig/model') +// or: import { Model } from '@voxgig/model' +``` + +Lower-level building blocks are available from subpaths (no `exports` map is +defined, so deep imports resolve directly to files): + +```js +const { makeBuild } = require('@voxgig/model/dist/build') +const { model_producer } = require('@voxgig/model/dist/producer/model') +const { local_producer } = require('@voxgig/model/dist/producer/local') +``` + +### Model + +```ts +class Model { + constructor(mspec: ModelSpec) + run(): Promise // build once + start(): Promise // build once, then watch and rebuild + stop(): Promise // stop all watchers + fs: any // the (possibly dryrun) fs in use + log: Log // the pino logger +} +``` + +- `run()` performs a single build (config then model) and resolves with the + model `BuildResult`. If the config build fails, its result is returned + instead. +- `start()` performs the initial build, then watches both the model files and + the config files, rebuilding on change. It resolves once watching has begun + (the initial build is enqueued asynchronously). On a failed initial config + build it resolves with that result. **Always call `stop()`** to release + watchers, or the process stays alive. +- `stop()` clears the watch interval and closes the file watchers for both the + model and config. + +### ModelSpec + +```ts +interface ModelSpec { + path?: string // root model file path + base?: string // model base directory (defaults from path) + require?: any // project root used to resolve action modules + buildargs?: any // build arguments, exposed as build.args + debug?: boolean | string // log level (string) or true => 'debug' + dryrun?: boolean // redirect writes to an in-memory fs + idle?: number // watch debounce in ms (default 111) + fs?: any // a custom fs implementation (e.g. memfs) + log?: Log // a pre-built pino logger + watch?: { // which filesystem events trigger rebuilds + mod?: boolean // modifications (default true) + add?: boolean // additions (default false) + rem?: boolean // deletions (default false) + } +} +``` + +### BuildResult + +```ts +interface BuildResult { + ok: boolean // did the build succeed? + errs: any[] // errors collected this build + runlog: string[] // ordered log of build phases + producers?: ProducerResult[] // per-producer results + build?: () => Build // accessor for the underlying Build + builder?: string + path?: string + step?: string +} +``` + +`errs` is reset at the start of every build, so a `BuildResult` reflects only +that build (a `Build` reused across watch rebuilds does not accumulate stale +errors). + +### makeBuild and BuildSpec + +`makeBuild(spec, log)` constructs a single `Build` you can drive directly, +bypassing `Model`'s config/watch machinery. Useful for embedding a custom +producer pipeline. + +```ts +function makeBuild(spec: BuildSpec, log: Log): Build + +interface BuildSpec { + path?: string // root model file + base?: string // base dir (enables model JSON output) + res?: ProducerDef[] // the producer pipeline + fs: FST // filesystem (required) + use?: { [name: string]: any } // shared services made available on build.use + require?: any // project root for action resolution + buildargs?: any // build.args + debug?: boolean | string + dryrun?: boolean + idle?: number // watch debounce (ms) + name?: string // build name (default 'model') + watch?: { mod?: boolean; add?: boolean; rem?: boolean } + log?: Log +} +``` + +```js +const Fs = require('node:fs') +const { makeBuild } = require('@voxgig/model/dist/build') +const { model_producer } = require('@voxgig/model/dist/producer/model') +const { prettyPino } = require('@voxgig/util') + +const build = makeBuild({ + fs: Fs, + base: __dirname + '/model', + path: __dirname + '/model/model.jsonic', + res: [ + { path: '/', build: model_producer }, + { path: '/', build: async (build, ctx) => { + if (ctx.step === 'post') console.log('model:', build.model) + return { ok: true, name: 'inspect', step: ctx.step, + active: true, reload: false, errs: [], runlog: [] } + } + }, + ], +}, prettyPino('demo', {})) + +const result = await build.run({ watch: false }) +``` + +### Build + +The `Build` instance passed to producers and actions. + +```ts +interface Build { + id: string // random build id + path: string // root model file + base: string // base directory + model: any // the unified model (after resolveModel) + opts: { [key: string]: any } // aontu options; opts.base is the output base + fs: FST // filesystem (honors dryrun) + args: any // build arguments + dryrun: boolean + log: Log + use: { [name: string]: any } // shared services (e.g. use.config) + deps: any // import dependency graph recorded by aontu + errs: any[] + ctx: BuildContext + aontu: Aontu // the unification engine instance + run: (rspec: RunSpec) => Promise + spec: BuildSpec + pdef: ProducerDef[] +} +``` + +### BuildContext + +```ts +interface BuildContext { + step: 'pre' | 'post' // current build phase + watch: boolean // is this a watch-mode build? + state: Record // scratch shared across producers in one build +} +``` + +### Producer and ProducerResult + +```ts +type Producer = (build: Build, ctx: BuildContext) => Promise + +interface ProducerDef { path: string; build: Producer } + +interface ProducerResult { + ok: boolean + name: string + step: string + active: boolean + reload: boolean // request a model re-resolve before the post phase + errs: any[] + runlog: string[] +} +``` + + +## Producers + +A **producer** is a function run by a build during both the `pre` and `post` +phases. Producers are the extension point; **actions** are a convenience layer +built on top of the `local_producer`. + +The default `Model` pipeline is, in order: + +1. `model_producer` — serializes and writes the unified model. +2. `local_producer` — loads and runs project actions. + +### model_producer + +- **Runs:** `post` phase only. +- **Effect:** writes `build.model` as pretty JSON to + `/.json` (the root file's basename with its + extension replaced by `.json`). +- **Idempotent:** if the target file already contains identical JSON, the write + is skipped to avoid mtime churn that would invalidate caches and re-trigger + watchers. + +### local_producer + +- **Runs:** both phases. On first call it loads the action modules declared in + the config (resolving each `load` against the project root) and caches them in + `ctx.state.local`. +- **Effect:** runs the actions whose `step` matches the current phase (`pre` + actions in `pre`, `post` actions in `post`, `all` actions in both), in the + configured order. Propagates each action's `ok` and `reload`. + +### Custom producers + +Provide your own pipeline via `makeBuild({ res: [...] })`, or compose with the +built-ins. A producer should return a `ProducerResult`; set `reload: true` to +request the model be re-resolved before the post phase. + + +## The build lifecycle + +A single build (`Build.run`) proceeds as: + +1. **Reset** — clear per-build error state. +2. **resolveModel** — read the root file and unify it (and its imports) with + `aontu`, producing `build.model`. Results are cached by file modification + time; an unchanged model is reused. +3. **pre phase** — run every producer with `ctx.step = 'pre'`. Collect `reload` + flags. Stop on the first failure. +4. **reload** — if any `pre` producer requested a reload (and there were no + errors), run `resolveModel` again to pick up regenerated source. +5. **post phase** — run every producer with `ctx.step = 'post'`. +6. **result** — return a `BuildResult` with `ok`, `producers`, `errs`, `runlog`. + +A `Model` orchestrates **two** builds: + +- The **config build** resolves `.model-config/model-config.jsonic` (writing + `model-config.json`) and, via an internal trigger producer, drives the main + model build. +- The **model build** resolves your root model (writing `model.json`) and runs + your actions. + +In watch mode both the model's source files and the config's source files are +watched; a change to either rebuilds the model. + +See [explanation](./explanation.md) for the reasoning and the caching and watch +designs in depth. + + +## Modeling language essentials + +Models are written in **[jsonic](https://github.com/jsonic-lang/jsonic)** +(a relaxed JSON superset) and unified with **[aontu](https://github.com/voxgig/aontu)**, +which implements [CUE](https://cuelang.org)-style unification. This is a +practical summary; consult the CUE and aontu docs for the full language. + +### jsonic basics + +```jsonic +# Comments start with # +foo: 1 # quotes are optional for keys and simple strings +bar: 'a string' +list: [1, 2, 3] +a: b: c: 1 # dotted/nested keys: same as a:{b:{c:1}} +obj: { + x: 10 + y: 20 # commas between entries are optional +} +``` + +The top level is an implicit object; you do not wrap the file in `{ }`. + +> **Key syntax.** Bareword keys are identifiers — letters, digits, and +> underscores (`envFile`, `gen_docs`, `srv2`). Quote any key containing a +> hyphen, dot, space, or other punctuation: `'env-file': …`. An unquoted +> `env-file:` is a syntax error. + +### Unification and types + +| Construct | Meaning | +|-----------|---------| +| `string` `number` `integer` `boolean` | Type constraints (a value must be of that type). | +| `x: number` then `x: 2` | Unify the constraint with the value → `2`. Conflicting concrete values (`x: 1` and `x: 2`) fail to unify. | +| `A & B` | Unify two structures/values into one. | +| `*V \| T` | A **default**: value `V` if nothing more specific is provided, otherwise constrained to type `T`. e.g. `active: *true \| boolean`. | +| `field?: T` | An **optional** field (no error if absent). | +| `type({})` | Declare a struct type. | + +> **Every emitted field must be concrete.** The generated model is plain data, +> so each field that survives into it must resolve to a value: a literal, a +> default (`*v | T`), or an optional field (`f?: T`) that is simply absent. A +> bare type constraint such as `name: string`, left with nothing to satisfy it, +> fails to generate. Give reusable shapes defaults or optional fields and let +> concrete instances supply the rest. (Constraints applied through a `&:` +> wildcard are fine — they are not emitted directly; the children that satisfy +> them are.) + +### References + +| Reference | Meaning | +|-----------|---------| +| `$.a.b.c` | **Absolute** reference from the model root. | +| `.a.b` | **Relative** reference. | + +```jsonic +sys: shape: srv: std: { api: web: active: *true | boolean } + +# Reuse a shape by unifying with an absolute reference: +main: srv: foo: $.sys.shape.srv.std & { api: web: method: 'GET' } +``` + +### Wildcards and `key()` + +`&:` defines a constraint applied to **every** child of a map. `key()` resolves +to the current map key. + +```jsonic +color: &: { + name: key() # each entry's name becomes its key + value: string +} +color: red: { value: 'f00' } +color: blue: { value: '00f' } +# => color.red.name == 'red', color.blue.name == 'blue' +``` + +### Imports + +Pull in another file with `@"..."`: + +```jsonic +# relative to the importing file +color: @"./color.jsonic" + +# a path inside an installed package +@"@voxgig/model/model/.model-config/model-config.jsonic" +``` + +Imported files are tracked as dependencies, so changing one triggers a rebuild +in watch mode. + + +## Logging + +Logging uses [pino](https://github.com/pinojs/pino) via +`@voxgig/util`'s `prettyPino`. The level comes from `debug` +(`ModelSpec.debug` / `--debug`): + +- absent → `info` +- `true` → `debug` +- a string → used as the level verbatim (`trace`…`fatal`, or `silent`) + +Each log line carries a `point` (a short event name such as `build-start`, +`write-model`, `pre-actions`, `build-end`) and a human-readable `note`. Paths in +notes are shown relative to the current working directory. + + +## npm scripts + +For working **on** this repository (see [AGENTS.md](../AGENTS.md) for the full +contributor/agent guide): + +| Script | What it does | +|--------|--------------| +| `npm run build` | Compile `src` → `dist` and `test` → `dist-test` (`tsc --build src test`). | +| `npm test` | Run the compiled tests (`node --test dist-test/**/*.test.js`). | +| `npm run test-some` | Run tests matching `TEST_PATTERN` (env var). | +| `npm run test-cov` | Run tests with coverage, writing `coverage/lcov.info`. | +| `npm run watch` | Recompile on change (`tsc --build -w`). | +| `npm run model` | Run the CLI in watch mode on the package's own `model/sys.jsonic`. | +| `npm run test-model` | Run the CLI once on `test/sys01/model/model.jsonic`. | +| `npm run clean` | Remove `node_modules`, `dist`, `dist-test`, lockfiles. | +| `npm run reset` | `clean` + install + build + test. | + +> Always `build` before `test`: tests run against compiled output in +> `dist-test/`, importing the compiled library from `dist/`. Run these scripts +> from the repository root. + + +## Troubleshooting + +| Symptom | Likely cause and fix | +|---------|----------------------| +| Build fails with a `syntax` / parse error | An unquoted key contains a hyphen or other punctuation (`env-file:`). Use an identifier (`envFile`) or quote it (`'env-file':`). | +| Model "fails to generate", or a field is reported as nil/incomplete | A bare type constraint (`name: string`) survived into the output with nothing to satisfy it. Give it a default (`name: *'' \| string`) or make it optional (`name?: string`), or provide a concrete value. | +| `Unknown model action "X"` | `sys.model.order.action` names an action that has no `sys.model.action.X` definition. Add the action or fix the order list. | +| `Model action "X" is missing a "load" path` | An action definition has no `load`. Add `load: 'build/X'`. | +| A required action does not run | Check its `step` (`pre`/`post`/`all`) and that it appears in `order.action` (or that `order.action` is absent so all run). | +| In-memory (`fs`) build fails to resolve `@voxgig/model/...` | The auto-created config imports a package path that is not in your volume. Seed a self-contained `.model-config/model-config.jsonic` (e.g. `sys: model: action: {}`). | +| Watch process never exits | This is expected for `--watch` (runs until interrupted). For the API, call `model.stop()`. | +| A change does not trigger a rebuild | Only tracked files rebuild: the root model, its imports, and the config files. Editing an unrelated file does nothing. Also note `add`/`rem` events are off by default in the API (`watch: { add, rem }`). | +| Edits seem ignored after a failed build | Errors reset each build; fix the source and the next rebuild should succeed. If running the repo's own tooling, ensure you rebuilt (`dist/` can go stale — see [AGENTS.md](../AGENTS.md)). | + + +## Requirements + +- **Node.js.** CI tests on Node 24 (recommended). Node 20.19+ generally works; + the `shape` dependency declares `engines.node >= 24`, so older versions emit + an `EBADENGINE` warning. +- **Peer dependencies:** `pino` (`>=10`) and `@voxgig/util`. Install them in the + host project. +- **Module system:** CommonJS (`"type": "commonjs"`). diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..285e13f --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,264 @@ +# Tutorial: your first model + +This walkthrough takes you from an empty folder to a working model that +generates a file, with live rebuilds. It assumes only that you can run `node` +and `npm`. Follow it top to bottom — each step builds on the last. + +By the end you will have: + +- written a model in `.jsonic`, +- unified it into a single JSON document, +- generated an artifact from it with an action, +- and watched it all rebuild on save. + +> New to the ideas here? You can do the tutorial first and read the +> [explanation](./explanation.md) afterwards — the concepts will land better +> once you have seen them work. + + +## 1. Set up a project + +```bash +mkdir hello-model && cd hello-model +npm init -y +npm install @voxgig/model pino +``` + +`pino` is a peer dependency used for logging. Installing it now avoids a warning +later. + +Create the conventional layout: + +```bash +mkdir -p model build +``` + + +## 2. Write a model + +Create `model/model.jsonic`: + +```jsonic +# model/model.jsonic +service: name: 'orders' +service: port: 8080 +``` + +Build it once: + +```bash +npx voxgig-model model/model.jsonic +``` + +You will see a few log lines, and a new file `model/model.json`: + +```json +{ + "service": { + "name": "orders", + "port": 8080 + } +} +``` + +That is the whole job of the core tool: take your `.jsonic` source, **unify** it, +and write the result as a single canonical JSON model. Everything else builds on +that model. + + +## 3. Add structure with types and defaults + +The point of a modeling language is to capture rules, not just values. Replace +`model/model.jsonic` with: + +```jsonic +# model/model.jsonic + +# A reusable "shape" for a service: defaults plus type constraints. +shape: service: { + name?: string # optional in the shape; each service supplies it + port: *8080 | integer # default 8080, but must be an integer + public: *false | boolean # default false +} + +# Two services, each built from the shape. +service: orders: $.shape.service & { name: 'orders' } +service: web: $.shape.service & { name: 'web', public: true, port: 443 } +``` + +Rebuild: + +```bash +npx voxgig-model model/model.jsonic +``` + +`model/model.json` now contains: + +```json +{ + "shape": { "service": { "port": 8080, "public": false } }, + "service": { + "orders": { "name": "orders", "port": 8080, "public": false }, + "web": { "name": "web", "port": 443, "public": true } + } +} +``` + +Notice what happened: + +- `orders` inherited the default `port: 8080` and `public: false`. +- `web` overrode both. The override **unifies** with the shape — if you tried + `port: 'https'` it would fail, because `port` must be an `integer`. + +`$.shape.service` is an **absolute reference** (from the model root) and `&` +**unifies** it with the per-service overrides. This is how you keep one source +of truth for structure and reuse it everywhere. + +> Why is `name` optional (`name?`)? The shape itself appears in the output, and +> every field in the generated model must resolve to a concrete value. A bare +> `name: string` with nothing to satisfy it would fail to generate — so the +> shape leaves `name` optional (it may be absent) and each concrete service +> supplies its own. + + +## 4. Split the model across files + +Real models grow. Move the shape into its own file. + +`model/shapes.jsonic`: + +```jsonic +# model/shapes.jsonic +shape: service: { + name?: string + port: *8080 | integer + public: *false | boolean +} +``` + +`model/model.jsonic`: + +```jsonic +# model/model.jsonic +@"./shapes.jsonic" + +service: orders: $.shape.service & { name: 'orders' } +service: web: $.shape.service & { name: 'web', public: true, port: 443 } +``` + +Rebuild — the output is identical. `@"./shapes.jsonic"` **imports** the file and +unifies it into the model. Imports are tracked as dependencies, which matters in +step 6. + + +## 5. Generate something with an action + +A model is only useful if it drives output. An **action** is a small JS module +that receives the unified model and produces an artifact. + +First, declare the action in the config file +`model/.model-config/model-config.jsonic`: + +```jsonic +# model/.model-config/model-config.jsonic +sys: model: action: { + envFile: load: 'build/envFile' +} +``` + +The `load` path is relative to the **project root** (the folder above `model/`) +and has no extension, so this points at `build/envFile.js`. (Action names are +plain identifiers — use `envFile`, not `env-file`; a hyphen would need quoting.) + +Now write the action, `build/envFile.js`: + +```js +// build/envFile.js +const Path = require('node:path') + +module.exports = async function envFile(model, build) { + // project root is two levels up from the model root file + const root = Path.resolve(build.path, '..', '..') + + const lines = Object.entries(model.service).map( + ([name, svc]) => `# ${name}\nPORT_${name.toUpperCase()}=${svc.port}` + ) + + build.fs.writeFileSync( + Path.resolve(root, 'services.env'), + lines.join('\n') + '\n' + ) + + return { ok: true } +} +``` + +Build again: + +```bash +npx voxgig-model model/model.jsonic +``` + +You now have a generated `services.env`: + +``` +# orders +PORT_ORDERS=8080 +# web +PORT_WEB=443 +``` + +Using `build.fs` (rather than `require('fs')` directly) means your action +automatically respects `--dryrun`. + + +## 6. Watch and iterate + +Generating on demand is fine; generating as you type is better. Start watch +mode: + +```bash +npx voxgig-model --watch model/model.jsonic +``` + +Leave it running. In another terminal (or your editor), change a value — set +`orders` to `port: 9090` in `model/model.jsonic`, or edit `model/shapes.jsonic`. +Within a moment the tool rebuilds and `services.env` updates. Editing the +imported `shapes.jsonic` works too, because imports are tracked dependencies. + +Press `Ctrl-C` to stop. + + +## 7. Try a dry run + +Before wiring a model into anything destructive, preview it without writing +files: + +```bash +npx voxgig-model --dryrun model/model.jsonic +``` + +The build runs exactly as normal — actions execute, the model resolves — but +every write is redirected to an in-memory filesystem. Nothing on disk changes. + + +## What you learned + +- A model is `.jsonic` source unified into one JSON document. +- **Types** (`integer`, `string`, …) and **defaults** (`*value | type`) let the + model enforce its own rules. +- **References** (`$.path`) and **unification** (`&`) reuse structure. +- **Imports** (`@"./file"`) split a model across files. +- **Actions** turn the model into artifacts, and respect `--dryrun` via + `build.fs`. +- **Watch mode** rebuilds on every change, including changes to imports. + + +## Where to next + +- [How-to guides](./how-to.md) — focused recipes (build args, custom producers, + embedding the API, in-memory filesystems, and more). +- [Reference](./reference.md) — every CLI flag, config key, API type, and + language construct. +- [Explanation](./explanation.md) — why unification, how the build lifecycle and + watcher actually work, and the current limitations. From fb215d1ec9f046be5ebda0af66edecb3d161ef57 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 18:15:45 +0000 Subject: [PATCH 5/6] restructure: split into ts/ and go/, add Go port, bump aontu Mirror the voxgig/util layout: the TypeScript package now lives in ts/ (canonical) and a new Go port lives in go/ (kept in parity). A root Makefile builds and tests both. - Move the TypeScript project (src, test, dist, dist-test, bin, model, package.json) into ts/. Relative paths inside are unchanged, so the npm scripts work as before when run from ts/. - Bump aontu 0.44.0 -> 0.45.1 (ts). Build clean; 11/11 tests pass; the compiled dist is byte-identical (runtime-only change). - Add go/ - module github.com/voxgig/model/go, package model. Ports the build lifecycle (pre/reload/post), producers, dryrun and watch semantics, using the real Go aontu engine (github.com/rjrodger/aontu/go) for unification. Go adaptations: actions are a programmatic registry (no runtime require), watching polls mtimes (no chokidar), and imports resolve via a base chdir (Go aontu Generate takes no base param). gofmt/vet clean; go test passes. - Add root Makefile (build/test/clean/publish-go) and go/README.md. - Update README, AGENTS.md, CLAUDE.md and docs/ for the dual-language layout and the canonical-TS / parity-Go workflow. Note: .github/workflows/build.yml still needs updating for the new layout; it could not be pushed from this session (token lacks GitHub 'workflow' scope). https://claude.ai/code/session_01HxXpZNrKj3qonocwAtEP8r --- .gitignore | 6 +- AGENTS.md | 265 ++++++++---------- CLAUDE.md | 47 ++-- Makefile | 51 ++++ README.md | 12 + docs/explanation.md | 14 +- docs/reference.md | 7 +- go/README.md | 72 +++++ go/build.go | 251 +++++++++++++++++ go/build_test.go | 87 ++++++ go/cmd/voxgig-model/main.go | 78 ++++++ go/fs.go | 68 +++++ go/go.mod | 13 + go/go.sum | 12 + go/log.go | 79 ++++++ go/model.go | 87 ++++++ go/model_test.go | 108 +++++++ go/producer.go | 107 +++++++ go/producer_test.go | 102 +++++++ go/types.go | 119 ++++++++ go/watch.go | 166 +++++++++++ {bin => ts/bin}/voxgig-model | 0 {dist-test => ts/dist-test}/build.test.js | 0 {dist-test => ts/dist-test}/build.test.js.map | 0 {dist-test => ts/dist-test}/cli.test.js | 0 {dist-test => ts/dist-test}/cli.test.js.map | 0 {dist-test => ts/dist-test}/fix.test.js | 0 {dist-test => ts/dist-test}/fix.test.js.map | 0 {dist-test => ts/dist-test}/model.test.js | 0 {dist-test => ts/dist-test}/model.test.js.map | 0 {dist-test => ts/dist-test}/watch.test.js | 0 {dist-test => ts/dist-test}/watch.test.js.map | 0 {dist => ts/dist}/build.d.ts | 0 {dist => ts/dist}/build.js | 0 {dist => ts/dist}/build.js.map | 0 {dist => ts/dist}/config.d.ts | 0 {dist => ts/dist}/config.js | 0 {dist => ts/dist}/config.js.map | 0 {dist => ts/dist}/model.d.ts | 0 {dist => ts/dist}/model.js | 0 {dist => ts/dist}/model.js.map | 0 {dist => ts/dist}/producer/local.d.ts | 0 {dist => ts/dist}/producer/local.js | 0 {dist => ts/dist}/producer/local.js.map | 0 {dist => ts/dist}/producer/model.d.ts | 0 {dist => ts/dist}/producer/model.js | 0 {dist => ts/dist}/producer/model.js.map | 0 {dist => ts/dist}/types.d.ts | 0 {dist => ts/dist}/types.js | 0 {dist => ts/dist}/types.js.map | 0 {dist => ts/dist}/watch.d.ts | 0 {dist => ts/dist}/watch.js | 0 {dist => ts/dist}/watch.js.map | 0 .../model}/.model-config/model-config.json | 0 .../model}/.model-config/model-config.jsonic | 0 {model => ts/model}/sys.json | 0 {model => ts/model}/sys.jsonic | 0 package.json => ts/package.json | 2 +- {src => ts/src}/build.ts | 0 {src => ts/src}/config.ts | 0 {src => ts/src}/model.ts | 0 {src => ts/src}/producer/local.ts | 0 {src => ts/src}/producer/model.ts | 0 {src => ts/src}/tsconfig.json | 0 {src => ts/src}/types.ts | 0 {src => ts/src}/watch.ts | 0 {test => ts/test}/build.test.ts | 0 {test => ts/test}/cli.test.ts | 0 {test => ts/test}/e01/doc.html | 0 .../test}/e01/model/config/config.jsonic | 0 {test => ts/test}/e01/model/config/model.json | 0 {test => ts/test}/e01/model/model.json | 0 {test => ts/test}/e01/model/model.jsonic | 0 {test => ts/test}/fix.test.ts | 0 {test => ts/test}/model.test.ts | 0 {test => ts/test}/p01/doc.html | 0 {test => ts/test}/p01/model/model.json | 0 {test => ts/test}/p01/model/model.jsonic | 0 {test => ts/test}/p01/model/zed.jsonic | 0 {test => ts/test}/perf-bench.js | 0 {test => ts/test}/quick.js | 0 {test => ts/test}/sys01/build/foo.js | 0 {test => ts/test}/sys01/build/pre.js | 0 {test => ts/test}/sys01/foo.txt | 0 .../model/.model-config/model-config.json | 0 .../model/.model-config/model-config.jsonic | 0 {test => ts/test}/sys01/model/color.jsonic | 0 {test => ts/test}/sys01/model/model.json | 54 ++-- {test => ts/test}/sys01/model/model.jsonic | 0 {test => ts/test}/sys01/model/pre.jsonic | 0 {test => ts/test}/sys01/pre.txt | 0 {test => ts/test}/t01.jsonic | 0 {test => ts/test}/t02.jsonic | 0 {test => ts/test}/t03.jsonic | 0 {test => ts/test}/tsconfig.json | 0 {test => ts/test}/util.js | 0 {test => ts/test}/w01/doc.html | 0 {test => ts/test}/w01/model/model.json | 0 {test => ts/test}/w01/model/model.jsonic | 0 {test => ts/test}/w01/model/zed.jsonic | 0 {test => ts/test}/watch-w01.js | 0 {test => ts/test}/watch.test.ts | 0 102 files changed, 1593 insertions(+), 214 deletions(-) create mode 100644 Makefile create mode 100644 go/README.md create mode 100644 go/build.go create mode 100644 go/build_test.go create mode 100644 go/cmd/voxgig-model/main.go create mode 100644 go/fs.go create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/log.go create mode 100644 go/model.go create mode 100644 go/model_test.go create mode 100644 go/producer.go create mode 100644 go/producer_test.go create mode 100644 go/types.go create mode 100644 go/watch.go rename {bin => ts/bin}/voxgig-model (100%) rename {dist-test => ts/dist-test}/build.test.js (100%) rename {dist-test => ts/dist-test}/build.test.js.map (100%) rename {dist-test => ts/dist-test}/cli.test.js (100%) rename {dist-test => ts/dist-test}/cli.test.js.map (100%) rename {dist-test => ts/dist-test}/fix.test.js (100%) rename {dist-test => ts/dist-test}/fix.test.js.map (100%) rename {dist-test => ts/dist-test}/model.test.js (100%) rename {dist-test => ts/dist-test}/model.test.js.map (100%) rename {dist-test => ts/dist-test}/watch.test.js (100%) rename {dist-test => ts/dist-test}/watch.test.js.map (100%) rename {dist => ts/dist}/build.d.ts (100%) rename {dist => ts/dist}/build.js (100%) rename {dist => ts/dist}/build.js.map (100%) rename {dist => ts/dist}/config.d.ts (100%) rename {dist => ts/dist}/config.js (100%) rename {dist => ts/dist}/config.js.map (100%) rename {dist => ts/dist}/model.d.ts (100%) rename {dist => ts/dist}/model.js (100%) rename {dist => ts/dist}/model.js.map (100%) rename {dist => ts/dist}/producer/local.d.ts (100%) rename {dist => ts/dist}/producer/local.js (100%) rename {dist => ts/dist}/producer/local.js.map (100%) rename {dist => ts/dist}/producer/model.d.ts (100%) rename {dist => ts/dist}/producer/model.js (100%) rename {dist => ts/dist}/producer/model.js.map (100%) rename {dist => ts/dist}/types.d.ts (100%) rename {dist => ts/dist}/types.js (100%) rename {dist => ts/dist}/types.js.map (100%) rename {dist => ts/dist}/watch.d.ts (100%) rename {dist => ts/dist}/watch.js (100%) rename {dist => ts/dist}/watch.js.map (100%) rename {model => ts/model}/.model-config/model-config.json (100%) rename {model => ts/model}/.model-config/model-config.jsonic (100%) rename {model => ts/model}/sys.json (100%) rename {model => ts/model}/sys.jsonic (100%) rename package.json => ts/package.json (98%) rename {src => ts/src}/build.ts (100%) rename {src => ts/src}/config.ts (100%) rename {src => ts/src}/model.ts (100%) rename {src => ts/src}/producer/local.ts (100%) rename {src => ts/src}/producer/model.ts (100%) rename {src => ts/src}/tsconfig.json (100%) rename {src => ts/src}/types.ts (100%) rename {src => ts/src}/watch.ts (100%) rename {test => ts/test}/build.test.ts (100%) rename {test => ts/test}/cli.test.ts (100%) rename {test => ts/test}/e01/doc.html (100%) rename {test => ts/test}/e01/model/config/config.jsonic (100%) rename {test => ts/test}/e01/model/config/model.json (100%) rename {test => ts/test}/e01/model/model.json (100%) rename {test => ts/test}/e01/model/model.jsonic (100%) rename {test => ts/test}/fix.test.ts (100%) rename {test => ts/test}/model.test.ts (100%) rename {test => ts/test}/p01/doc.html (100%) rename {test => ts/test}/p01/model/model.json (100%) rename {test => ts/test}/p01/model/model.jsonic (100%) rename {test => ts/test}/p01/model/zed.jsonic (100%) rename {test => ts/test}/perf-bench.js (100%) rename {test => ts/test}/quick.js (100%) rename {test => ts/test}/sys01/build/foo.js (100%) rename {test => ts/test}/sys01/build/pre.js (100%) rename {test => ts/test}/sys01/foo.txt (100%) rename {test => ts/test}/sys01/model/.model-config/model-config.json (100%) rename {test => ts/test}/sys01/model/.model-config/model-config.jsonic (100%) rename {test => ts/test}/sys01/model/color.jsonic (100%) rename {test => ts/test}/sys01/model/model.json (85%) rename {test => ts/test}/sys01/model/model.jsonic (100%) rename {test => ts/test}/sys01/model/pre.jsonic (100%) rename {test => ts/test}/sys01/pre.txt (100%) rename {test => ts/test}/t01.jsonic (100%) rename {test => ts/test}/t02.jsonic (100%) rename {test => ts/test}/t03.jsonic (100%) rename {test => ts/test}/tsconfig.json (100%) rename {test => ts/test}/util.js (100%) rename {test => ts/test}/w01/doc.html (100%) rename {test => ts/test}/w01/model/model.json (100%) rename {test => ts/test}/w01/model/model.jsonic (100%) rename {test => ts/test}/w01/model/zed.jsonic (100%) rename {test => ts/test}/watch-w01.js (100%) rename {test => ts/test}/watch.test.ts (100%) diff --git a/.gitignore b/.gitignore index 8ee4c30..d95345e 100644 --- a/.gitignore +++ b/.gitignore @@ -105,10 +105,10 @@ typings/ coverage -test/cov +ts/test/cov # Generated fixtures written by tests at runtime -test/_gen +ts/test/_gen package-lock.json yarn.lock @@ -116,5 +116,5 @@ yarn.lock *~ -test/coverage.html +ts/test/coverage.html diff --git a/AGENTS.md b/AGENTS.md index ca91a4b..bfb3d41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,207 +4,172 @@ Guidance for AI coding agents (and humans) working **on** the `@voxgig/model` codebase. For using the tool, start at the [README](./README.md) and [docs/](./docs/). This file is about developing the package itself. -Keep this file accurate: if you change the build, test layout, or a convention -below, update it in the same change. +This repository holds **two implementations** of the same tool: +- **`ts/`** — the TypeScript package `@voxgig/model` (npm library + CLI). + **TypeScript is canonical.** +- **`go/`** — the Go module `github.com/voxgig/model/go`, kept in architectural + parity with the TypeScript version. -## What this project is +Keep this file accurate: if you change the build, test layout, or a convention, +update it in the same change. -`@voxgig/model` unifies `.jsonic` source into a single JSON model (via the -[aontu](https://github.com/voxgig/aontu) CUE-style engine) and runs generator -"actions" over it. It can build once or watch and rebuild. It ships a library -(`Model`) and a CLI (`voxgig-model`). -A 90-second tour of how it fits together is in -[docs/explanation.md](./docs/explanation.md#architecture-at-a-glance). Read that -before making structural changes. +## Layout +``` +ts/ TypeScript implementation (canonical) + src/ library source (model.ts, build.ts, watch.ts, ...) + test/ node:test suites + dist/ dist-test/ COMMITTED build output + bin/voxgig-model CLI entry (plain JS) + model/ the package's own sample model + package.json +go/ Go implementation (parity) + *.go package `model` (build.go, producer.go, watch.go, ...) + cmd/voxgig-model/ Go CLI entry + *_test.go go test suites + go.mod go.sum +docs/ tutorial / how-to / reference / explanation +Makefile orchestrates both: `make build`, `make test` +.github/workflows/ CI (a `ts` job and a `go` job) +``` -## Environment - -- **Node.js 24** is the target (CI matrix). Node 20.19+ works; the `shape` - dependency declares `engines.node >= 24`, so older Node prints an - `EBADENGINE` warning — harmless here. -- TypeScript, CommonJS output (`"type": "commonjs"`). -- Install with `npm install`. No lockfile is committed (it is gitignored). +## Build & test -## Build, test, run +From the repository root, `make` drives both implementations: ```bash -npm install # once -npm run build # tsc --build src test -> dist/ and dist-test/ -npm test # node --test dist-test/**/*.test.js +make build # build-ts + build-go +make test # test-ts + test-go +make # all: build then test ``` -```bash -# a single suite / pattern -TEST_PATTERN='watch' npm run test-some +### TypeScript (run from `ts/`) -# coverage (writes coverage/lcov.info) +```bash +cd ts +npm install +npm run build # tsc --build src test -> dist/ and dist-test/ +npm test # node --test dist-test/**/*.test.js +TEST_PATTERN=watch npm run test-some npm run test-cov +``` + +**You must `npm run build` before `npm test`** — tests run against compiled +output in `dist-test/` and import the library from `dist/`. -# recompile on save -npm run watch +### Go (run from `go/`) -# exercise the CLI against the bundled sample model -npm run test-model # build once on test/sys01/model/model.jsonic -npm run model # watch model/sys.jsonic +```bash +cd go +go build ./... +go vet ./... +gofmt -l . # must print nothing +go test ./... ``` -**You must `npm run build` before `npm test`.** Tests are TypeScript compiled to -`dist-test/`, and they import the compiled library from `dist/`. Source edits do -not take effect in tests until rebuilt. For a fully clean rebuild use -`npx tsc --build src test --force`. +Go **1.24+** is required (the `aontu/go` dependency declares `go 1.24.7`). -## Critical gotchas +## Critical gotchas (TypeScript) -1. **Run commands from the repository root.** `tsc --build src test` resolves - `src` and `test` as project paths relative to the current directory. Running - it from a subdirectory fails with `TS5083: Cannot read file '.../src/ - tsconfig.json'` and silently leaves `dist/` stale — so tests then run against - old code. If a build looks like it "passed" but tests don't reflect your - change, check your working directory. +1. **Run TS commands from `ts/`.** `tsc --build src test` resolves `src` and + `test` as project paths relative to the current directory; from the wrong + directory it fails with `TS5083` and silently leaves `dist/` stale — so + tests then run against old code. -2. **`dist/` and `dist-test/` are committed.** They are tracked in git (in the - `files` allowlist for publishing). After any source change, rebuild and - commit the regenerated `dist/`/`dist-test/` alongside the `.ts`. CI rebuilds - them, but the repo convention is to commit them. +2. **`ts/dist/` and `ts/dist-test/` are committed.** After any source change, + rebuild and commit the regenerated output alongside the `.ts`. -3. **The CLI (`bin/voxgig-model`) is plain JavaScript**, not compiled. Edits to - it take effect immediately (no build needed for the bin itself), but it +3. **The CLI (`ts/bin/voxgig-model`) is plain JavaScript**, not compiled. It `require`s `dist/model.js`, so the library must be built. -4. **`aontu` reads `opts.err`, not `opts.errs`.** When invoking - `aontu.generate(src, opts)`, set `opts.err = ` to enable collect mode - (errors gathered into the array instead of thrown). This is easy to get - wrong — see `src/build.ts: resolveModel`. +4. **`aontu` (npm) reads `opts.err`, not `opts.errs`** — collect mode. See + `ts/src/build.ts: resolveModel`. -5. **Generated test fixtures go in `test/_gen/`** (gitignored). Tests write their - own fixtures there at runtime; do not commit them. See "Writing tests". +5. **Generated test fixtures go in `ts/test/_gen/`** (gitignored). Tests write + their own fixtures there at runtime; do not commit them. -## Repository map +## The Go port -``` -src/ - model.ts Model: public entry; wires config build + model build - config.ts Config: specialized build for .model-config - watch.ts Watch: chokidar, debounce, rebuild queue, dep tracking - build.ts BuildImpl/makeBuild: resolve model (aontu) + run producers - types.ts shared interfaces (Build, BuildSpec, Producer, ...) - producer/ - model.ts model_producer: write the unified model as JSON - local.ts local_producer: load & run actions declared in config - tsconfig.json project config for src - -bin/voxgig-model CLI entry (plain JS): arg parsing -> new Model().run/start - -test/ - *.test.ts node:test suites (compiled to dist-test/) - tsconfig.json project config for tests - sys01/, p01/, e01/, w01/ committed fixtures - _gen/ GITIGNORED scratch fixtures written by tests at runtime - -model/ the package's own sample model (sys.jsonic + config) -dist/, dist-test/ COMMITTED build output -docs/ tutorial / how-to / reference / explanation -``` +The Go module ports the **architecture**, not every mechanism. The build +lifecycle (pre → reload → post), producers, model output, dryrun, and watch +semantics match TypeScript. Two things differ by necessity: + +- **Actions are a registry, not dynamic modules.** TypeScript declares actions + in a config file and loads them with `require()`. Go cannot load code at + runtime, so actions are registered programmatically via `ModelSpec.Actions` + (`map[string]ActionDef`) — see `go/producer.go`. +- **Watching polls modification times** (`go/watch.go`) rather than using + chokidar. -Data flow: `CLI/Model` → `Config` (resolves actions) + `Watch` → `BuildImpl` -(`aontu` unify → producers). The model build runs `model_producer` then -`local_producer`. Full detail: -[docs/explanation.md](./docs/explanation.md). +Other notes: +- **Unification** uses the real Go aontu engine + (`github.com/rjrodger/aontu/go`). Its `Generate(src)` has no base parameter, + so `AontuResolver` briefly `chdir`s to the model base (guarded by a mutex) + so `@"..."` imports resolve. aontu/go does not report import deps, so the + watcher tracks `*.jsonic` files under the base directory. +- **JSON key order** differs: Go's `encoding/json` sorts object keys; + TypeScript preserves insertion order. Content is otherwise equivalent — an + accepted cross-language difference. +- **`const Version`** lives in `go/model.go`; `make publish-go V=x.y.z` + rewrites it and tags `go/vx.y.z`. +- The Go port depends on **`aontu/go` only**; it does not use `util/go` (the + TypeScript `@voxgig/util` dependency is for pino logging, replaced here by a + minimal `Log` interface). -## Code conventions -Match the surrounding style; in this codebase that means: +## Maintaining parity -- **2-space indent, no semicolons, single quotes.** -- Prefer `let`; use `const` where the existing file does. -- Null checks as `null == x` / `null != x`. Comparisons are often "yoda" - (`'post' === ctx.step`). Follow the local file. -- CommonJS modules. Keep imports grouped: node builtins, then deps, then local. -- Keep the public type surface in `src/types.ts`. Many internal fields are typed - `any` by design; do not tighten them speculatively. -- Logging goes through the pino logger (`build.log` / `this.log`) with a `point` - (short event name) and a human `note`. Do not `console.log` in library code. -- Copyright header comment at the top of each source file (see existing files). +TypeScript is canonical. When changing behavior: + +1. Change TypeScript first, with a test (`ts/test/*.test.ts`). +2. Apply the equivalent change to Go, with a test (`go/*_test.go`). +3. Rebuild TypeScript and commit the regenerated `ts/dist/`. +4. Run both suites (`make test`); keep `gofmt`/`go vet` clean. +5. Update `docs/` if the API changed. ## Writing tests -- Use `node:test` (`describe`/`test`) and `node:assert`. Import the library from - `../dist/...` (compiled output), mirroring existing suites. -- **Put runtime fixtures under `test/_gen//`** and create them in the test - (write `.jsonic`, config, and any action `.js`). This keeps fixtures - self-contained and out of git. Clean the dir at the start of the test - (`rm -rf` then `mkdir -p`). -- **Watch-mode tests must call `model.stop()` in a `finally`.** `start()` opens - chokidar watchers (for both the model and the config); leaving them open hangs - the test process. Poll for the expected result with a timeout helper rather - than fixed sleeps (see `test/watch.test.ts`). -- **CLI tests** spawn the built bin without a shell, for cross-platform - safety: - ```js - const { spawnSync } = require('node:child_process') - const res = spawnSync(process.execPath, [BIN, modelPath, '-b', '{a:1}'], - { encoding: 'utf8' }) - ``` - Use jsonic barewords (`{a:b}`) to avoid embedded-quote escaping. -- Make tests **meaningful guards**: when adding a test for a fix, confirm it - fails without the fix (temporarily revert, run, restore). - -Existing suites to model new ones on: - -| Suite | Covers | -|-------|--------| -| `test/build.test.ts` | end-to-end builds (`makeBuild`, `Model.run`) with the `p01`/`sys01` fixtures | -| `test/fix.test.ts` | error recovery, unknown/misconfigured actions, dry-run filesystem | -| `test/watch.test.ts` | watch rebuilds on model and config changes | -| `test/cli.test.ts` | the CLI passes build args through to actions | +**TypeScript:** `node:test` (`describe`/`test`), import from `../dist/...`. +Runtime fixtures under `ts/test/_gen//`, created in the test. Watch tests +must `await model.stop()` in a `finally`. CLI tests spawn the built bin without +a shell. + +**Go:** standard `testing` with `t.TempDir()` fixtures. Watch tests must +`defer m.Stop()`. Do **not** `t.Parallel()` resolver-using tests — +`AontuResolver` changes the working directory. ## Common tasks (playbooks) ### Add a build action (product-level generator) -Actions are user-space, not framework code — declare in a model's -`.model-config/model-config.jsonic` and implement under `build/`. See -[docs/how-to.md](./docs/how-to.md#write-a-build-action). No library change. +User-space, not framework code. TypeScript: declare in +`.model-config/model-config.jsonic`, implement under `build/`. Go: register an +`ActionDef` in `ModelSpec.Actions`. See +[docs/how-to.md](./docs/how-to.md#write-a-build-action). ### Add a built-in producer (framework-level) -1. Create `src/producer/.ts` exporting a `Producer` - `(build, ctx) => Promise` (see `src/producer/model.ts`). -2. Add it to the pipeline in `src/model.ts` (`this.build.res`), or document it - for `makeBuild` users. -3. Rebuild, add a test, commit `dist/` too. - -### Add or change a CLI flag -Edit `bin/voxgig-model`: add to `resolveOptions` (`parseArgs` `options`) and -`validateOptions` (the `Shape`), then include it in the `spec` passed to -`new Model(spec)`. Thread the field through `ModelSpec` in `src/types.ts` and -read it in the `Model` constructor. (The CLI maps `--build` → `buildargs`; keep -spec keys aligned with `ModelSpec`.) +TypeScript: add `ts/src/producer/.ts`, wire it into `ts/src/model.ts`. +Go: add a `Producer` func in `go/producer.go`, wire it into `go/model.go`. +Rebuild and test both; commit `ts/dist/`. ### Change the model/build types -Edit `src/types.ts`. Rebuild so `dist/*.d.ts` regenerates; commit them. - -### Touch the build/error/cache logic -`src/build.ts`. Remember the `opts.err` collect-mode rule (gotcha #4) and that -`this.errs` is reset per run so a reused build instance doesn't accumulate stale -errors. Add a regression test in `test/fix.test.ts`. +TypeScript: `ts/src/types.ts`. Go: `go/types.go`. Keep the two aligned. ## Before you commit -- `npm run build` is clean (`tsc rc 0`) **from the repo root**. -- `npm test` is green; add/adjust tests for behavior changes. -- Regenerated `dist/` and `dist-test/` are staged with the source. -- No `test/_gen/` artifacts are staged (they are gitignored — verify with - `git status`). -- Keep this file and `docs/` in sync with any behavior or convention change. +- `make build` is clean and `make test` is green (both languages). +- Regenerated `ts/dist/` and `ts/dist-test/` are staged with the source. +- `gofmt -l go` prints nothing; `cd go && go vet ./...` is clean. +- No `ts/test/_gen/` artifacts are staged. +- This file and `docs/` reflect any behavior or convention change. ## More documentation diff --git a/CLAUDE.md b/CLAUDE.md index 5d4f840..a14a818 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,36 +2,37 @@ Guidance for Claude Code (and other AI agents) working in this repository. -The full contributor/agent guide — environment, repository map, conventions, -test patterns, and task playbooks — is in **@AGENTS.md**. Read it before making -changes. The essentials: +This repo has **two implementations**: `ts/` (TypeScript, canonical) and `go/` +(Go, in parity). The full contributor/agent guide is in **@AGENTS.md** — read +it before making changes. The essentials: ## Build & test -- Run everything **from the repository root**. -- `npm run build` (`tsc --build src test`) **before** `npm test`; tests run - against compiled output in `dist-test/` and import the library from `dist/`. -- `dist/` and `dist-test/` are **committed** — rebuild and stage them with any - source change. -- Single suite: `TEST_PATTERN='' npm run test-some`. Coverage: - `npm run test-cov`. +- From the root, `make build` and `make test` drive both languages. +- **TypeScript** (`cd ts`): `npm run build` (`tsc --build src test`) **before** + `npm test`. `ts/dist/` and `ts/dist-test/` are **committed** — rebuild and + stage them with any source change. +- **Go** (`cd go`): `go build ./...`, `go vet ./...`, `gofmt -l .`, + `go test ./...`. Go 1.24+. ## Watch out for -- Stale `dist/` from a build that failed because it ran in a subdirectory - (`TS5083`) — tests then pass against old code. Verify cwd is the repo root. -- Watch-mode tests must `await model.stop()` in a `finally`, or the process - hangs on open file watchers. -- Runtime test fixtures belong in `test/_gen/` (gitignored); don't commit them. -- `aontu.generate` collects errors only when given `opts.err` (not `opts.errs`). +- Run TS commands **from `ts/`**: `tsc --build src test` resolves project paths + relative to cwd (`TS5083` otherwise), and a failed build leaves stale `dist/`. +- Watch tests must release watchers (`await model.stop()` / `defer m.Stop()`), + or the process hangs. +- TS runtime fixtures belong in `ts/test/_gen/` (gitignored). +- TS: `aontu.generate` collects errors only with `opts.err` (not `opts.errs`). +- Go: `AontuResolver` `chdir`s to the model base — don't `t.Parallel()` tests + that resolve. -## Where things are -- Library: `src/` (`model.ts`, `config.ts`, `watch.ts`, `build.ts`, `types.ts`, - `producer/`). CLI: `bin/voxgig-model` (plain JS). Docs: `docs/`. -- Architecture overview: `docs/explanation.md`. +## Parity +TypeScript is canonical. Change TS (with a test) first, then mirror in Go (with +a test), rebuild `ts/dist/`, and run `make test`. Keep the two in step. ## Style -- 2-space indent, no semicolons, single quotes, CommonJS. Match the local file. -- Log via the pino logger (`build.log`/`this.log`), never `console.log` in - library code. +- TS: 2-space indent, no semicolons, single quotes, CommonJS. Match the file. +- Go: standard `gofmt`; package `model`; `/* Copyright © ... */` header. +- Never `console.log` in TS library code (use the pino logger); in Go log + through the `Log` interface. When you change behavior or a convention, update `@AGENTS.md` and the relevant file in `docs/` in the same change. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5b1e801 --- /dev/null +++ b/Makefile @@ -0,0 +1,51 @@ +.PHONY: all build test clean build-ts build-go test-ts test-go vet-go clean-ts clean-go publish-go tags-go reset + +all: build test + +build: build-ts build-go + +test: test-ts test-go + +clean: clean-ts clean-go + +# TypeScript (the npm package lives in ts/) — the canonical implementation. +build-ts: + cd ts && npm run build + +test-ts: + cd ts && npm test + +cov-ts: + cd ts && npm run test-cov + +clean-ts: + rm -rf ts/dist ts/dist-test ts/node_modules + +# Go (the module lives in go/) — kept in parity with TypeScript. +build-go: + cd go && go build ./... + +test-go: + cd go && go test ./... + +vet-go: + cd go && go vet ./... && gofmt -l . + +clean-go: + cd go && go clean + +# Publish the Go module: make publish-go V=0.1.1 +publish-go: vet-go test-go + @test -n "$(V)" || (echo "Usage: make publish-go V=x.y.z" && exit 1) + sed -i 's/^const Version = ".*"/const Version = "$(V)"/' go/model.go + git add go/model.go + git commit -m "go: v$(V)" + git tag go/v$(V) + git push origin HEAD go/v$(V) + +tags-go: + git tag -l 'go/v*' --sort=-version:refname + +reset: + cd ts && npm run reset + cd go && go clean -cache && go build ./... && go test ./... diff --git a/README.md b/README.md index 8d0e013..481ff7e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ once or watch and rebuild on change. ## Install +The TypeScript package (npm): + ```bash npm install @voxgig/model pino ``` @@ -22,6 +24,16 @@ npm install @voxgig/model pino `pino` is a peer dependency (logging). Requires Node.js — CI runs Node 24; Node 20.19+ generally works. +The Go module: + +```bash +go get github.com/voxgig/model/go +``` + +Both implementations live in this repository — `ts/` (TypeScript, canonical) +and `go/` (Go, kept in parity). See [go/README.md](./go/README.md) for Go usage +and [AGENTS.md](./AGENTS.md) for working on the repo. + ## Quick start diff --git a/docs/explanation.md b/docs/explanation.md index 79c6695..05d3e1a 100644 --- a/docs/explanation.md +++ b/docs/explanation.md @@ -86,13 +86,13 @@ CLI (bin/voxgig-model) | Component | File | Responsibility | |-----------|------|----------------| -| `Model` | `src/model.ts` | Public entry. Wires a config build and a model build together; exposes `run`/`start`/`stop`. | -| `Config` | `src/config.ts` | A specialized build for `.model-config/model-config.jsonic`. | -| `Watch` | `src/watch.ts` | File watching, debouncing, the rebuild queue, dependency tracking. | -| `Build` / `BuildImpl` | `src/build.ts` | One build: resolve the model via aontu, run the producer pipeline, cache by mtime. | -| `model_producer` | `src/producer/model.ts` | Serialize the unified model to JSON. | -| `local_producer` | `src/producer/local.ts` | Load action modules from config and run them. | -| types | `src/types.ts` | The shared interfaces (`Build`, `BuildSpec`, `Producer`, …). | +| `Model` | `ts/src/model.ts` | Public entry. Wires a config build and a model build together; exposes `run`/`start`/`stop`. | +| `Config` | `ts/src/config.ts` | A specialized build for `.model-config/model-config.jsonic`. | +| `Watch` | `ts/src/watch.ts` | File watching, debouncing, the rebuild queue, dependency tracking. | +| `Build` / `BuildImpl` | `ts/src/build.ts` | One build: resolve the model via aontu, run the producer pipeline, cache by mtime. | +| `model_producer` | `ts/src/producer/model.ts` | Serialize the unified model to JSON. | +| `local_producer` | `ts/src/producer/local.ts` | Load action modules from config and run them. | +| types | `ts/src/types.ts` | The shared interfaces (`Build`, `BuildSpec`, `Producer`, …). | `aontu` (unification), `chokidar` (file watching), `memfs` (the dry-run filesystem), and `pino`/`@voxgig/util` (logging) are the external moving parts. diff --git a/docs/reference.md b/docs/reference.md index 87f1904..416fc11 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -585,8 +585,9 @@ notes are shown relative to the current working directory. ## npm scripts -For working **on** this repository (see [AGENTS.md](../AGENTS.md) for the full -contributor/agent guide): +For working **on** the TypeScript package — run these from the `ts/` directory +(see [AGENTS.md](../AGENTS.md) for the full contributor/agent guide, including +the Go module and the root `Makefile` that builds and tests both): | Script | What it does | |--------|--------------| @@ -602,7 +603,7 @@ contributor/agent guide): > Always `build` before `test`: tests run against compiled output in > `dist-test/`, importing the compiled library from `dist/`. Run these scripts -> from the repository root. +> from the `ts/` directory. ## Troubleshooting diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..c33b384 --- /dev/null +++ b/go/README.md @@ -0,0 +1,72 @@ +# @voxgig/model (Go) + +A Go port of [@voxgig/model](https://github.com/voxgig/model): unify `.jsonic` +source into a single model (via [aontu](https://github.com/rjrodger/aontu)) and +run generator "actions" over it, once or in a rebuild-on-change watch loop. The +TypeScript implementation in [`../ts`](../ts) is canonical; this module is kept +in architectural parity. + +```bash +go get github.com/voxgig/model/go +``` + +## Usage + +```go +package main + +import ( + "fmt" + + model "github.com/voxgig/model/go" +) + +func main() { + m := model.New(model.ModelSpec{ + Path: "model/model.jsonic", + Base: "model", + Actions: map[string]model.ActionDef{ + "summary": {Run: func(mod map[string]any, b *model.Build, ctx *model.BuildContext) model.ActionResult { + fmt.Printf("model has %d top-level keys\n", len(mod)) + return model.ActionResult{OK: true} + }}, + }, + }) + + br := m.Run() // or m.Start() to watch, then m.Stop() + if !br.OK { + for _, e := range br.Errs { + fmt.Println("ERROR:", e) + } + } +} +``` + +`model.New(...).Run()` resolves the model (applying aontu defaults, types and +`@"..."` imports), writes `/.json`, then runs the registered +actions whose step matches each build phase. + +## Differences from the TypeScript version + +This port mirrors the architecture — the build lifecycle (pre → reload → +post), producers, dryrun, and watch semantics — but adapts a few mechanisms to +Go: + +- **Actions are registered programmatically** (`ModelSpec.Actions`) instead of + being loaded from a config file: Go cannot `require()` code at runtime. +- **Watching polls modification times** instead of using chokidar. +- **Imports** resolve relative to the model base directory; the resolver + briefly changes the working directory because the Go aontu `Generate(src)` + API takes no base parameter. +- **JSON object keys are emitted in sorted order** (Go's `encoding/json`), + where TypeScript preserves insertion order. The content is otherwise the + same. + +## CLI + +```bash +go run github.com/voxgig/model/go/cmd/voxgig-model -w model/model.jsonic +``` + +Flags: `-w` watch, `-y` dryrun, `-g ` log level. The CLI writes the +model JSON; for custom actions, embed the package and register them. diff --git a/go/build.go b/go/build.go new file mode 100644 index 0000000..66a06e9 --- /dev/null +++ b/go/build.go @@ -0,0 +1,251 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "fmt" + "os" + "sync" + + aontu "github.com/rjrodger/aontu/go" +) + +// Resolver turns model source text into a unified model. The default +// implementation uses the aontu CUE engine; callers may supply their own +// (for example to unify from a non-file source, or in tests). +type Resolver interface { + Resolve(src string) (model map[string]any, errs []error) +} + +// chdirMu serializes the working-directory changes AontuResolver needs to +// resolve @"..." imports relative to a model's base directory. +var chdirMu sync.Mutex + +// AontuResolver resolves source with the aontu engine. The Go aontu API has +// no base-directory parameter, so imports are resolved relative to Base by +// briefly changing the working directory (serialized across builds). +type AontuResolver struct { + Base string +} + +// Resolve implements Resolver. +func (r AontuResolver) Resolve(src string) (map[string]any, []error) { + chdirMu.Lock() + defer chdirMu.Unlock() + + if r.Base != "" { + if prev, err := os.Getwd(); err == nil { + if cerr := os.Chdir(r.Base); cerr == nil { + defer func() { _ = os.Chdir(prev) }() + } + } + } + + out, err := aontu.New().Generate(src) + if err != nil { + return nil, []error{err} + } + if out == nil { + return map[string]any{}, nil + } + m, ok := out.(map[string]any) + if !ok { + return nil, []error{fmt.Errorf("model did not resolve to an object (got %T)", out)} + } + return m, nil +} + +// Build resolves a model and runs a producer pipeline over it. +type Build struct { + ID string + Path string + Base string + Model map[string]any + Args map[string]any + Dryrun bool + FS FS + Log Log + Use map[string]any + Errs []error + Ctx BuildContext + + spec BuildSpec + pdef []ProducerDef + resolver Resolver + cacheSig map[string]int64 +} + +var ( + buildSeq int64 + buildSeqMu sync.Mutex +) + +// NewBuild creates a Build from a spec, filling in defaults (an os- or +// dryrun-backed FS, a no-op logger, and the aontu resolver). +func NewBuild(spec BuildSpec) *Build { + buildSeqMu.Lock() + buildSeq++ + id := buildSeq + buildSeqMu.Unlock() + + fs := spec.FS + if fs == nil { + if spec.Dryrun { + fs = newDryFS() + } else { + fs = OSFS{} + } + } + log := spec.Log + if log == nil { + log = NopLog{} + } + resolver := spec.Resolver + if resolver == nil { + resolver = AontuResolver{Base: spec.Base} + } + + return &Build{ + ID: fmt.Sprintf("%06d", id), + Path: spec.Path, + Base: spec.Base, + Args: spec.Args, + Dryrun: spec.Dryrun, + FS: fs, + Log: log, + Use: map[string]any{}, + spec: spec, + pdef: spec.Res, + resolver: resolver, + } +} + +// Run performs one build: resolve the model, run the producer pipeline in +// the pre phase, re-resolve if a producer requested a reload, then run the +// pipeline in the post phase. Per-run error state is reset so a reused Build +// does not carry failures between runs. +func (b *Build) Run(watch bool) *BuildResult { + b.Errs = nil + b.Ctx = BuildContext{Step: StepPre, Watch: watch, State: map[string]any{}} + + runlog := []string{} + var plog []ProducerResult + hasErr := false + + runlog = append(runlog, "model:initial") + if b.resolveModel() { + hasErr = true + } + + forceReload := false + if !hasErr { + for _, pd := range b.pdef { + pr := b.runProducer(pd) + plog = append(plog, pr) + forceReload = forceReload || pr.Reload + runlog = append(runlog, "producer:pre:"+pr.Name) + if !pr.OK { + hasErr = true + break + } + } + } + + if forceReload && !hasErr { + runlog = append(runlog, "model:full") + if b.resolveModel() { + hasErr = true + } + } + + if !hasErr { + b.Ctx.Step = StepPost + for _, pd := range b.pdef { + pr := b.runProducer(pd) + plog = append(plog, pr) + runlog = append(runlog, "producer:post:"+pr.Name) + if !pr.OK { + hasErr = true + break + } + } + } + + return &BuildResult{ + OK: !hasErr, + Errs: b.Errs, + Runlog: runlog, + Producers: plog, + build: b, + } +} + +// runProducer runs a producer, converting a panic into a failed result so a +// single misbehaving producer cannot crash the build. +func (b *Build) runProducer(pd ProducerDef) (pr ProducerResult) { + defer func() { + if r := recover(); r != nil { + err, ok := r.(error) + if !ok { + err = fmt.Errorf("%v", r) + } + b.Errs = append(b.Errs, err) + pr = ProducerResult{OK: false, Step: b.Ctx.Step, Errs: []error{err}} + } + }() + pr = pd.Build(b, &b.Ctx) + if !pr.OK && len(pr.Errs) > 0 { + b.Errs = append(b.Errs, pr.Errs...) + } + return pr +} + +// resolveModel reads and unifies the root model. A successful result is +// cached by the root file's modification time and reused while unchanged. +// +// Note: the Go aontu engine does not report the imports it followed, so the +// cache tracks only the root file. Watchers call InvalidateCache when any +// watched source changes, so imported-file edits still rebuild. +func (b *Build) resolveModel() (hasErr bool) { + if b.Model != nil && b.cacheSig != nil && b.cacheHit() { + return false + } + + src, err := b.FS.ReadFile(b.Path) + if err != nil { + b.Errs = append(b.Errs, err) + b.cacheSig = nil + return true + } + + model, errs := b.resolver.Resolve(string(src)) + if len(errs) > 0 { + b.Errs = append(b.Errs, errs...) + b.cacheSig = nil + return true + } + + b.Model = model + b.cacheSig = map[string]int64{b.Path: mtime(b.FS, b.Path)} + return false +} + +// InvalidateCache forces the next resolveModel to re-read and re-unify. +func (b *Build) InvalidateCache() { b.cacheSig = nil } + +func (b *Build) cacheHit() bool { + for path, prev := range b.cacheSig { + if mtime(b.FS, path) != prev { + return false + } + } + return true +} + +func mtime(fs FS, path string) int64 { + info, err := fs.Stat(path) + if err != nil { + return -1 + } + return info.ModTime().UnixNano() +} diff --git a/go/build_test.go b/go/build_test.go new file mode 100644 index 0000000..65e34f6 --- /dev/null +++ b/go/build_test.go @@ -0,0 +1,87 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "os" + "path/filepath" + "testing" +) + +// A happy build resolves the model and exposes it to producers, with aontu +// applying defaults and types (port defaults to 8080 as an integer). +func TestRunHappy(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "service: { name: 'orders', port: *8080 | integer }\n") + + var seen map[string]any + spec := BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), + Base: dir, + Res: []ProducerDef{ + {Path: "/", Build: func(b *Build, ctx *BuildContext) ProducerResult { + if ctx.Step == StepPost { + seen = b.Model + } + return ProducerResult{OK: true, Name: "capture", Step: ctx.Step, Active: true} + }}, + }, + } + + br := NewBuild(spec).Run(false) + if !br.OK { + t.Fatalf("build failed: %v", br.Errs) + } + svc, _ := seen["service"].(map[string]any) + if svc["name"] != "orders" { + t.Fatalf("name = %#v", svc["name"]) + } + if svc["port"] != int64(8080) { + t.Fatalf("port = %#v", svc["port"]) + } +} + +// A failed build must not stick to the next run on a reused Build. +func TestErrorRecovery(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "m.jsonic") + writeFile(t, dir, "m.jsonic", "x: 1\nx: 2\n") + + b := NewBuild(BuildSpec{Path: path, Base: dir}) + + bad := b.Run(false) + if bad.OK { + t.Fatal("expected failure on conflicting values") + } + if len(bad.Errs) == 0 { + t.Fatal("expected errors") + } + + writeFile(t, dir, "m.jsonic", "x: 1\n") + good := b.Run(false) + if !good.OK { + t.Fatalf("expected recovery after fix: %v", good.Errs) + } + if len(good.Errs) != 0 { + t.Fatalf("errors leaked across runs: %v", good.Errs) + } +} + +// A dryrun build runs fully but writes nothing to disk. +func TestDryrunWritesNothing(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "m.jsonic") + writeFile(t, dir, "m.jsonic", "a: 1\n") + + b := NewBuild(BuildSpec{ + Path: path, Base: dir, Dryrun: true, + Res: []ProducerDef{{Path: "/", Build: ModelProducer}}, + }) + br := b.Run(false) + if !br.OK { + t.Fatalf("dryrun build failed: %v", br.Errs) + } + if _, err := os.Stat(filepath.Join(dir, "m.json")); !os.IsNotExist(err) { + t.Fatal("dryrun wrote m.json to disk") + } +} diff --git a/go/cmd/voxgig-model/main.go b/go/cmd/voxgig-model/main.go new file mode 100644 index 0000000..21dffb5 --- /dev/null +++ b/go/cmd/voxgig-model/main.go @@ -0,0 +1,78 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +// Command voxgig-model unifies a .jsonic model and writes the resulting +// model JSON. It mirrors the core of the TypeScript CLI. Custom build +// actions — which the TypeScript CLI loads dynamically from a config file — +// are not available here, because Go cannot load code at runtime; embed the +// model package and register actions programmatically to use them. +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + + model "github.com/voxgig/model/go" +) + +func main() { + watch := flag.Bool("w", false, "watch and rebuild on change") + dryrun := flag.Bool("y", false, "dry run (write nothing to disk)") + level := flag.String("g", "info", "log level: trace|debug|info|warn|error|silent") + flag.Usage = func() { + fmt.Fprintln(os.Stderr, "usage: voxgig-model [-w] [-y] [-g level] ") + flag.PrintDefaults() + } + flag.Parse() + + path := flag.Arg(0) + if path == "" { + flag.Usage() + os.Exit(1) + } + + abs, err := filepath.Abs(path) + if err != nil { + fmt.Fprintln(os.Stderr, "ERROR:", err) + os.Exit(1) + } + if _, serr := os.Stat(abs); serr != nil { + fmt.Fprintln(os.Stderr, "ERROR: model file does not exist:", path) + os.Exit(1) + } + + m := model.New(model.ModelSpec{ + Path: abs, + Base: filepath.Dir(abs), + Dryrun: *dryrun, + Log: model.NewLog(*level), + }) + + if *watch { + br := m.Start() + reportErrs(br) + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + <-sig + m.Stop() + return + } + + br := m.Run() + if !br.OK { + reportErrs(br) + os.Exit(1) + } +} + +func reportErrs(br *model.BuildResult) { + if br == nil { + return + } + for _, e := range br.Errs { + fmt.Fprintln(os.Stderr, "ERROR:", e) + } +} diff --git a/go/fs.go b/go/fs.go new file mode 100644 index 0000000..ab05bba --- /dev/null +++ b/go/fs.go @@ -0,0 +1,68 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "os" + "path/filepath" + "sync" +) + +// FS is the filesystem seam used by builds, producers and actions. Writing +// through it (rather than the os package directly) lets a dryrun build +// redirect writes away from disk. It mirrors the role of the swappable fs in +// the TypeScript implementation. +type FS interface { + ReadFile(name string) ([]byte, error) + WriteFile(name string, data []byte, perm os.FileMode) error + MkdirAll(path string, perm os.FileMode) error + Stat(name string) (os.FileInfo, error) +} + +// OSFS is the real, disk-backed filesystem. +type OSFS struct{} + +// ReadFile reads a file from disk. +func (OSFS) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } + +// WriteFile writes a file to disk. +func (OSFS) WriteFile(name string, data []byte, perm os.FileMode) error { + return os.WriteFile(name, data, perm) +} + +// MkdirAll creates a directory and any parents. +func (OSFS) MkdirAll(path string, perm os.FileMode) error { return os.MkdirAll(path, perm) } + +// Stat returns file info from disk. +func (OSFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } + +// dryFS reads from the real filesystem but keeps writes in memory, so a +// dryrun build runs normally without touching disk. +type dryFS struct { + mu sync.Mutex + mem map[string][]byte +} + +func newDryFS() *dryFS { return &dryFS{mem: map[string][]byte{}} } + +func (d *dryFS) ReadFile(name string) ([]byte, error) { + key := filepath.Clean(name) + d.mu.Lock() + data, ok := d.mem[key] + d.mu.Unlock() + if ok { + return append([]byte(nil), data...), nil + } + return os.ReadFile(name) +} + +func (d *dryFS) WriteFile(name string, data []byte, _ os.FileMode) error { + d.mu.Lock() + d.mem[filepath.Clean(name)] = append([]byte(nil), data...) + d.mu.Unlock() + return nil +} + +func (d *dryFS) MkdirAll(string, os.FileMode) error { return nil } + +func (d *dryFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..cc8f74c --- /dev/null +++ b/go/go.mod @@ -0,0 +1,13 @@ +module github.com/voxgig/model/go + +go 1.24.7 + +require github.com/rjrodger/aontu/go v0.1.2 + +require ( + github.com/jsonicjs/directive/go v0.1.4 // indirect + github.com/jsonicjs/expr/go v0.1.3 // indirect + github.com/jsonicjs/jsonic/go v0.1.22 // indirect + github.com/jsonicjs/multisource/go v0.1.4 // indirect + github.com/jsonicjs/path/go v0.1.2 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..ccc88e4 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,12 @@ +github.com/jsonicjs/directive/go v0.1.4 h1:K7ai2ccoh8d23mjCK8/YMjEDbzb6+jWcepOL3YubpKo= +github.com/jsonicjs/directive/go v0.1.4/go.mod h1:PMjJ3EulIhdo1R0o9bOUvSUOTCPvM/xCzo1O35VBiXs= +github.com/jsonicjs/expr/go v0.1.3 h1:D7x+5AIM/CLX6A6VUiHBzfvRxufuGzXYB6gmPb1lv+A= +github.com/jsonicjs/expr/go v0.1.3/go.mod h1:6kmd1o4p4/FL4DVuCxwYAqX5YRYjF20hbIrHQM81Qoc= +github.com/jsonicjs/jsonic/go v0.1.22 h1:sam238fTyjDq0nby9TYS+aCCHprLl91ArQPWLCg2O0Y= +github.com/jsonicjs/jsonic/go v0.1.22/go.mod h1:ObNKlCG7esWoi4AHCpdgkILvPINV8bpvkbCd4llGGUg= +github.com/jsonicjs/multisource/go v0.1.4 h1:vTfr1pHNuKV6XBSErZpi2fA4SjVvX5DfoQs7nEoQzko= +github.com/jsonicjs/multisource/go v0.1.4/go.mod h1:XzgM6nuW2MOAsVySsGtpp7fWSx9PTCXd4eIRSCAA/6o= +github.com/jsonicjs/path/go v0.1.2 h1:Pk7PZIiRq64iodssEVtaJw+4jHiJe84aFtmKDUasXAo= +github.com/jsonicjs/path/go v0.1.2/go.mod h1:ffXuSMg950pdfU3wapeK6QnyvZrV5yHFSj4ifwmsgmU= +github.com/rjrodger/aontu/go v0.1.2 h1:vTStp1UIbTxGovcPH+29FwZ3eNwzaJUMV8PXEtz6Lmk= +github.com/rjrodger/aontu/go v0.1.2/go.mod h1:Ejv19nAWPMFlzqc1rdkvIEjh1dihKf8S2uiGigyQyYE= diff --git a/go/log.go b/go/log.go new file mode 100644 index 0000000..7ac4a94 --- /dev/null +++ b/go/log.go @@ -0,0 +1,79 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "fmt" + "io" + "os" + "strings" +) + +// Log is a minimal leveled logger. Each entry carries a short point (event +// name) and a human-readable note. It stands in for the pino logger used by +// the TypeScript implementation. +type Log interface { + Info(point, note string) + Debug(point, note string) + Error(point string, err error, note string) +} + +const ( + levelDebug = iota + levelInfo + levelError + levelSilent +) + +// NewLog returns a Log writing to stderr at the given level: "debug", +// "info" (the default), "error", or "silent". +func NewLog(level string) Log { + return &stdLog{level: parseLevel(level), w: os.Stderr} +} + +func parseLevel(level string) int { + switch strings.ToLower(level) { + case "debug", "trace": + return levelDebug + case "warn", "error", "fatal": + return levelError + case "silent": + return levelSilent + default: + return levelInfo + } +} + +type stdLog struct { + level int + w io.Writer +} + +func (l *stdLog) Info(point, note string) { l.emit(levelInfo, "INFO", point, note) } +func (l *stdLog) Debug(point, note string) { l.emit(levelDebug, "DEBUG", point, note) } + +func (l *stdLog) Error(point string, err error, note string) { + if err != nil { + note = strings.TrimSpace(note + " " + err.Error()) + } + l.emit(levelError, "ERROR", point, note) +} + +func (l *stdLog) emit(level int, label, point, note string) { + if level < l.level { + return + } + fmt.Fprintf(l.w, "%-5s %-18s %s\n", label, point, note) +} + +// NopLog discards all log entries. +type NopLog struct{} + +// Info discards the entry. +func (NopLog) Info(string, string) {} + +// Debug discards the entry. +func (NopLog) Debug(string, string) {} + +// Error discards the entry. +func (NopLog) Error(string, error, string) {} diff --git a/go/model.go b/go/model.go new file mode 100644 index 0000000..cf111c0 --- /dev/null +++ b/go/model.go @@ -0,0 +1,87 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +// Package model is a Go port of @voxgig/model. It unifies .jsonic source +// into a single model (via the aontu engine) and runs generator "actions" +// over it, once or in a rebuild-on-change watch loop. +// +// The TypeScript implementation in ts/ is canonical; this package is kept in +// architectural parity. Two mechanisms differ by necessity: actions are +// registered programmatically (Go cannot require() code at runtime), and +// watching polls modification times (rather than using chokidar). +package model + +import ( + "path/filepath" + "time" +) + +// Version is the released version of the Go module. It is rewritten by +// `make publish-go V=x.y.z` to match the git tag (go/vx.y.z). +const Version = "0.1.0" + +// DefaultIdle is the default watch debounce period. +const DefaultIdle = 111 * time.Millisecond + +// Model unifies a .jsonic model and runs producers (the model writer and any +// registered actions) over it. It can build once or watch and rebuild. +type Model struct { + build *Build + watch *Watch + log Log +} + +// New creates a Model from a spec. +func New(spec ModelSpec) *Model { + log := spec.Log + if log == nil { + log = NopLog{} + } + + base := spec.Base + if base == "" && spec.Path != "" { + base = filepath.Dir(spec.Path) + } + + idle := spec.Idle + if idle <= 0 { + idle = DefaultIdle + } + + bspec := BuildSpec{ + Name: "model", + Path: spec.Path, + Base: base, + Args: spec.Args, + Dryrun: spec.Dryrun, + Resolver: spec.Resolver, + Actions: spec.Actions, + Order: spec.Order, + Idle: idle, + Watch: spec.Watch, + Log: log, + Res: []ProducerDef{ + {Path: "/", Build: ModelProducer}, + {Path: "/", Build: LocalProducer}, + }, + } + + build := NewBuild(bspec) + return &Model{ + build: build, + watch: NewWatch(build, "model", idle), + log: log, + } +} + +// Run builds the model once and returns the result. +func (m *Model) Run() *BuildResult { return m.watch.Run(false) } + +// Start builds once, then watches and rebuilds until Stop is called. It +// returns the initial build result. +func (m *Model) Start() *BuildResult { return m.watch.Start() } + +// Stop ends watching and releases the watcher. +func (m *Model) Stop() { m.watch.Stop() } + +// Build returns the underlying Build (valid after Run or Start). +func (m *Model) Build() *Build { return m.build } diff --git a/go/model_test.go b/go/model_test.go new file mode 100644 index 0000000..9b29b95 --- /dev/null +++ b/go/model_test.go @@ -0,0 +1,108 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" +) + +func writeFile(t *testing.T, dir, name, content string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func containsErr(errs []error, sub string) bool { + for _, e := range errs { + if strings.Contains(e.Error(), sub) { + return true + } + } + return false +} + +// End-to-end: New().Run() unifies the model, writes the JSON, and runs a +// registered action that sees the resolved model. +func TestModelRun(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "service: { name: 'orders', port: *8080 | integer }\n") + + var saw string + m := New(ModelSpec{ + Path: filepath.Join(dir, "m.jsonic"), + Base: dir, + Actions: map[string]ActionDef{ + "env": {Run: func(model map[string]any, b *Build, ctx *BuildContext) ActionResult { + svc := model["service"].(map[string]any) + saw = fmt.Sprintf("PORT=%v", svc["port"]) + return ActionResult{OK: true} + }}, + }, + }) + br := m.Run() + if !br.OK { + t.Fatalf("model run failed: %v", br.Errs) + } + if saw != "PORT=8080" { + t.Fatalf("action saw %q", saw) + } + if _, err := os.Stat(filepath.Join(dir, "m.json")); err != nil { + t.Fatalf("model JSON not written: %v", err) + } +} + +// Watch mode rebuilds when an imported source file changes. +func TestWatchRebuild(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "val: @\"./zed.jsonic\"\n") + writeFile(t, dir, "zed.jsonic", "2") + + var mu sync.Mutex + var last any + m := New(ModelSpec{ + Path: filepath.Join(dir, "m.jsonic"), + Base: dir, + Idle: 60 * time.Millisecond, + Actions: map[string]ActionDef{ + "capture": {Step: StepPost, Run: func(model map[string]any, b *Build, ctx *BuildContext) ActionResult { + mu.Lock() + last = model["val"] + mu.Unlock() + return ActionResult{OK: true} + }}, + }, + }) + + br := m.Start() + defer m.Stop() + if !br.OK { + t.Fatalf("initial build failed: %v", br.Errs) + } + + read := func() any { mu.Lock(); defer mu.Unlock(); return last } + if read() != int64(2) { + t.Fatalf("initial val = %#v", read()) + } + + // Let the watcher settle, then change the imported file. + time.Sleep(120 * time.Millisecond) + writeFile(t, dir, "zed.jsonic", "7") + + deadline := time.Now().Add(4 * time.Second) + for time.Now().Before(deadline) { + if read() == int64(7) { + break + } + time.Sleep(30 * time.Millisecond) + } + if read() != int64(7) { + t.Fatalf("after change val = %#v", read()) + } +} diff --git a/go/producer.go b/go/producer.go new file mode 100644 index 0000000..8a5b11f --- /dev/null +++ b/go/producer.go @@ -0,0 +1,107 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "bytes" + "encoding/json" + "fmt" + "path/filepath" + "sort" + "strings" +) + +// ModelProducer writes the unified model to /.json. It runs +// in the post phase only, and skips the write when the output is byte-for-byte +// unchanged (avoiding mtime churn that would re-trigger watchers). +// +// Go's encoding/json emits object keys in sorted order, where the TypeScript +// implementation preserves insertion order; the JSON content is otherwise +// equivalent. This is a known, accepted cross-language difference. +func ModelProducer(b *Build, ctx *BuildContext) ProducerResult { + pr := ProducerResult{OK: true, Name: "model", Step: ctx.Step, Active: true} + if ctx.Step != StepPost { + return pr + } + + data, err := json.MarshalIndent(b.Model, "", " ") + if err != nil { + return fail("model", ctx.Step, err) + } + + name := filepath.Base(b.Path) + if ext := filepath.Ext(name); ext != "" { + name = strings.TrimSuffix(name, ext) + } + file := filepath.Join(b.Base, name+".json") + + if existing, rerr := b.FS.ReadFile(file); rerr == nil && bytes.Equal(existing, data) { + b.Log.Debug("write-model-skip", file+" (unchanged)") + return pr + } + + if merr := b.FS.MkdirAll(filepath.Dir(file), 0o755); merr != nil { + return fail("model", ctx.Step, merr) + } + if werr := b.FS.WriteFile(file, data, 0o644); werr != nil { + return fail("model", ctx.Step, werr) + } + b.Log.Info("write-model", file) + return pr +} + +// LocalProducer runs the registered actions whose step matches the current +// phase, in the configured order (or sorted action names when no order is +// given). An order entry naming an unregistered action fails the build. +func LocalProducer(b *Build, ctx *BuildContext) ProducerResult { + pr := ProducerResult{OK: true, Name: "local", Step: ctx.Step, Active: true} + + order := b.spec.Order + if len(order) == 0 { + for name := range b.spec.Actions { + order = append(order, name) + } + sort.Strings(order) + } + + var ran []string + for _, name := range order { + def, ok := b.spec.Actions[name] + if !ok { + pr.OK = false + pr.Errs = append(pr.Errs, + fmt.Errorf("unknown model action %q referenced in order", name)) + return pr + } + + step := def.Step + if step == "" { + step = StepPost + } + if step != ctx.Step && step != StepAll { + continue + } + if def.Run == nil { + pr.OK = false + pr.Errs = append(pr.Errs, fmt.Errorf("model action %q has no Run function", name)) + return pr + } + + res := def.Run(b.Model, b, ctx) + ran = append(ran, name) + if res.Reload { + pr.Reload = true + } + if !res.OK { + pr.OK = false + break + } + } + + b.Log.Info(string(ctx.Step)+"-actions", strings.Join(ran, ";")) + return pr +} + +func fail(name string, step Step, err error) ProducerResult { + return ProducerResult{Name: name, Step: step, Active: true, OK: false, Errs: []error{err}} +} diff --git a/go/producer_test.go b/go/producer_test.go new file mode 100644 index 0000000..3115ada --- /dev/null +++ b/go/producer_test.go @@ -0,0 +1,102 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// ModelProducer writes the model JSON and skips the write when unchanged. +func TestModelProducerWritesAndSkips(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "m.jsonic") + writeFile(t, dir, "m.jsonic", "a: 1\nb: 2\n") + + build := func() *Build { + return NewBuild(BuildSpec{Path: path, Base: dir, + Res: []ProducerDef{{Path: "/", Build: ModelProducer}}}) + } + + if br := build().Run(false); !br.OK { + t.Fatalf("build failed: %v", br.Errs) + } + out := filepath.Join(dir, "m.json") + data, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + var got map[string]any + if err := json.Unmarshal(data, &got); err != nil { + t.Fatal(err) + } + if got["a"] != float64(1) || got["b"] != float64(2) { + t.Fatalf("unexpected model.json: %s", data) + } + + info1, _ := os.Stat(out) + time.Sleep(15 * time.Millisecond) + if br := build().Run(false); !br.OK { + t.Fatalf("second build failed: %v", br.Errs) + } + info2, _ := os.Stat(out) + if !info1.ModTime().Equal(info2.ModTime()) { + t.Fatal("model.json rewritten despite unchanged content") + } +} + +// Actions run in the configured order. +func TestLocalProducerOrder(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "m.jsonic") + writeFile(t, dir, "m.jsonic", "x: 1\n") + + var order []string + mk := func(name string) ActionDef { + return ActionDef{Run: func(model map[string]any, b *Build, ctx *BuildContext) ActionResult { + order = append(order, name) + return ActionResult{OK: true} + }} + } + b := NewBuild(BuildSpec{ + Path: path, Base: dir, + Actions: map[string]ActionDef{"a": mk("a"), "b": mk("b"), "c": mk("c")}, + Order: []string{"c", "a", "b"}, + Res: []ProducerDef{{Path: "/", Build: LocalProducer}}, + }) + if br := b.Run(false); !br.OK { + t.Fatalf("build failed: %v", br.Errs) + } + if strings.Join(order, ",") != "c,a,b" { + t.Fatalf("action order = %v", order) + } +} + +// An order entry naming an unregistered action fails clearly. +func TestUnknownAction(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "m.jsonic") + writeFile(t, dir, "m.jsonic", "x: 1\n") + + b := NewBuild(BuildSpec{ + Path: path, Base: dir, + Actions: map[string]ActionDef{ + "real": {Run: func(map[string]any, *Build, *BuildContext) ActionResult { + return ActionResult{OK: true} + }}, + }, + Order: []string{"real", "ghost"}, + Res: []ProducerDef{{Path: "/", Build: LocalProducer}}, + }) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure for unknown action") + } + if !containsErr(br.Errs, "ghost") { + t.Fatalf("errors = %v", br.Errs) + } +} diff --git a/go/types.go b/go/types.go new file mode 100644 index 0000000..d000d80 --- /dev/null +++ b/go/types.go @@ -0,0 +1,119 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import "time" + +// Step is a build phase. +type Step string + +const ( + // StepPre runs before the model is finalized. + StepPre Step = "pre" + // StepPost runs after the model is finalized. + StepPost Step = "post" + // StepAll runs in both phases. + StepAll Step = "all" +) + +// BuildContext carries the current phase and shared state through one build. +type BuildContext struct { + Step Step + Watch bool + State map[string]any +} + +// ProducerResult is what a producer reports for one phase. +type ProducerResult struct { + OK bool + Name string + Step Step + Active bool + Reload bool + Errs []error + Runlog []string +} + +// Producer transforms or emits output from the model during a build phase. +type Producer func(b *Build, ctx *BuildContext) ProducerResult + +// ProducerDef pairs a producer with a (currently informational) path scope. +type ProducerDef struct { + Path string + Build Producer +} + +// BuildResult summarizes one build. +type BuildResult struct { + OK bool + Errs []error + Runlog []string + Producers []ProducerResult + + build *Build +} + +// Build returns the underlying Build (may be nil for a synthesized result). +func (r *BuildResult) Build() *Build { + if r == nil { + return nil + } + return r.build +} + +// Action is a project-local generator run by LocalProducer. It receives the +// unified model and the build, and reports whether it succeeded and whether +// the model should be re-resolved. +type Action func(model map[string]any, b *Build, ctx *BuildContext) ActionResult + +// ActionResult is what an Action reports. +type ActionResult struct { + OK bool + Reload bool +} + +// ActionDef registers an Action to run in a given step. Unlike the +// TypeScript port, which loads actions dynamically from a config file, Go +// actions are registered programmatically (Go cannot load code at runtime). +type ActionDef struct { + Run Action + Step Step // StepPre, StepPost (the zero value defaults to post) or StepAll +} + +// WatchModes selects which filesystem events trigger a rebuild. +type WatchModes struct { + Mod bool + Add bool + Rem bool +} + +// BuildSpec configures a Build. +type BuildSpec struct { + Name string + Path string + Base string + Res []ProducerDef + Resolver Resolver + Args map[string]any + Dryrun bool + FS FS + Actions map[string]ActionDef + Order []string + Idle time.Duration + Watch WatchModes + Log Log +} + +// ModelSpec configures a Model. +type ModelSpec struct { + Path string + Base string + Args map[string]any + Dryrun bool + Resolver Resolver + Actions map[string]ActionDef + Order []string + Idle time.Duration + Watch WatchModes + Log Log +} diff --git a/go/watch.go b/go/watch.go new file mode 100644 index 0000000..fa9cb83 --- /dev/null +++ b/go/watch.go @@ -0,0 +1,166 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "io/fs" + "os" + "path/filepath" + "sync" + "time" +) + +// modelExts are the source extensions a watcher tracks. Generated output +// (e.g. .json) is deliberately excluded so writing it cannot loop. +var modelExts = map[string]bool{".jsonic": true, ".aon": true, ".aontu": true} + +// Watch rebuilds a Build when its source files change. It polls modification +// times (stdlib only, no external dependency where the TypeScript port uses +// chokidar) and debounces bursts of changes by an idle quiet period. +type Watch struct { + build *Build + name string + idle time.Duration + + mu sync.Mutex + last *BuildResult + sig map[string]int64 + stop chan struct{} + done chan struct{} + running bool +} + +// NewWatch creates a watcher for a build. A non-positive idle defaults to +// 111ms (matching the TypeScript default). +func NewWatch(b *Build, name string, idle time.Duration) *Watch { + if idle <= 0 { + idle = 111 * time.Millisecond + } + if name == "" { + name = "model" + } + return &Watch{build: b, name: name, idle: idle} +} + +// Run performs a single build and records the result. +func (w *Watch) Run(watch bool) *BuildResult { + br := w.build.Run(watch) + w.mu.Lock() + w.last = br + w.mu.Unlock() + return br +} + +// Last returns the most recent build result. +func (w *Watch) Last() *BuildResult { + w.mu.Lock() + defer w.mu.Unlock() + return w.last +} + +// Start runs an initial build, then watches the model's base directory and +// rebuilds after each quiet period. Call Stop to release it. +func (w *Watch) Start() *BuildResult { + br := w.Run(true) + + w.mu.Lock() + w.sig = w.snapshot() + w.stop = make(chan struct{}) + w.done = make(chan struct{}) + w.running = true + w.mu.Unlock() + + go w.loop() + return br +} + +// Stop ends watching and waits for the watch loop to exit. +func (w *Watch) Stop() { + w.mu.Lock() + running := w.running + w.running = false + stop := w.stop + done := w.done + w.mu.Unlock() + if !running { + return + } + close(stop) + <-done +} + +func (w *Watch) loop() { + defer close(w.done) + + tick := w.idle / 2 + if tick <= 0 { + tick = 50 * time.Millisecond + } + t := time.NewTicker(tick) + defer t.Stop() + + var changedAt time.Time + for { + select { + case <-w.stop: + return + case <-t.C: + now := w.snapshot() + + w.mu.Lock() + prev := w.sig + w.mu.Unlock() + + if !sameSig(now, prev) { + w.mu.Lock() + w.sig = now + w.mu.Unlock() + changedAt = time.Now() + continue + } + + if !changedAt.IsZero() && time.Since(changedAt) >= w.idle { + changedAt = time.Time{} + w.build.InvalidateCache() + w.Run(true) + w.mu.Lock() + w.sig = w.snapshot() + w.mu.Unlock() + } + } + } +} + +// snapshot records the modification times of model source files under the +// build's base directory. +func (w *Watch) snapshot() map[string]int64 { + sig := map[string]int64{} + base := w.build.Base + if base == "" { + base = filepath.Dir(w.build.Path) + } + _ = filepath.WalkDir(base, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + if modelExts[filepath.Ext(path)] { + if info, serr := os.Stat(path); serr == nil { + sig[path] = info.ModTime().UnixNano() + } + } + return nil + }) + return sig +} + +func sameSig(a, b map[string]int64) bool { + if len(a) != len(b) { + return false + } + for k, v := range a { + if b[k] != v { + return false + } + } + return true +} diff --git a/bin/voxgig-model b/ts/bin/voxgig-model similarity index 100% rename from bin/voxgig-model rename to ts/bin/voxgig-model diff --git a/dist-test/build.test.js b/ts/dist-test/build.test.js similarity index 100% rename from dist-test/build.test.js rename to ts/dist-test/build.test.js diff --git a/dist-test/build.test.js.map b/ts/dist-test/build.test.js.map similarity index 100% rename from dist-test/build.test.js.map rename to ts/dist-test/build.test.js.map diff --git a/dist-test/cli.test.js b/ts/dist-test/cli.test.js similarity index 100% rename from dist-test/cli.test.js rename to ts/dist-test/cli.test.js diff --git a/dist-test/cli.test.js.map b/ts/dist-test/cli.test.js.map similarity index 100% rename from dist-test/cli.test.js.map rename to ts/dist-test/cli.test.js.map diff --git a/dist-test/fix.test.js b/ts/dist-test/fix.test.js similarity index 100% rename from dist-test/fix.test.js rename to ts/dist-test/fix.test.js diff --git a/dist-test/fix.test.js.map b/ts/dist-test/fix.test.js.map similarity index 100% rename from dist-test/fix.test.js.map rename to ts/dist-test/fix.test.js.map diff --git a/dist-test/model.test.js b/ts/dist-test/model.test.js similarity index 100% rename from dist-test/model.test.js rename to ts/dist-test/model.test.js diff --git a/dist-test/model.test.js.map b/ts/dist-test/model.test.js.map similarity index 100% rename from dist-test/model.test.js.map rename to ts/dist-test/model.test.js.map diff --git a/dist-test/watch.test.js b/ts/dist-test/watch.test.js similarity index 100% rename from dist-test/watch.test.js rename to ts/dist-test/watch.test.js diff --git a/dist-test/watch.test.js.map b/ts/dist-test/watch.test.js.map similarity index 100% rename from dist-test/watch.test.js.map rename to ts/dist-test/watch.test.js.map diff --git a/dist/build.d.ts b/ts/dist/build.d.ts similarity index 100% rename from dist/build.d.ts rename to ts/dist/build.d.ts diff --git a/dist/build.js b/ts/dist/build.js similarity index 100% rename from dist/build.js rename to ts/dist/build.js diff --git a/dist/build.js.map b/ts/dist/build.js.map similarity index 100% rename from dist/build.js.map rename to ts/dist/build.js.map diff --git a/dist/config.d.ts b/ts/dist/config.d.ts similarity index 100% rename from dist/config.d.ts rename to ts/dist/config.d.ts diff --git a/dist/config.js b/ts/dist/config.js similarity index 100% rename from dist/config.js rename to ts/dist/config.js diff --git a/dist/config.js.map b/ts/dist/config.js.map similarity index 100% rename from dist/config.js.map rename to ts/dist/config.js.map diff --git a/dist/model.d.ts b/ts/dist/model.d.ts similarity index 100% rename from dist/model.d.ts rename to ts/dist/model.d.ts diff --git a/dist/model.js b/ts/dist/model.js similarity index 100% rename from dist/model.js rename to ts/dist/model.js diff --git a/dist/model.js.map b/ts/dist/model.js.map similarity index 100% rename from dist/model.js.map rename to ts/dist/model.js.map diff --git a/dist/producer/local.d.ts b/ts/dist/producer/local.d.ts similarity index 100% rename from dist/producer/local.d.ts rename to ts/dist/producer/local.d.ts diff --git a/dist/producer/local.js b/ts/dist/producer/local.js similarity index 100% rename from dist/producer/local.js rename to ts/dist/producer/local.js diff --git a/dist/producer/local.js.map b/ts/dist/producer/local.js.map similarity index 100% rename from dist/producer/local.js.map rename to ts/dist/producer/local.js.map diff --git a/dist/producer/model.d.ts b/ts/dist/producer/model.d.ts similarity index 100% rename from dist/producer/model.d.ts rename to ts/dist/producer/model.d.ts diff --git a/dist/producer/model.js b/ts/dist/producer/model.js similarity index 100% rename from dist/producer/model.js rename to ts/dist/producer/model.js diff --git a/dist/producer/model.js.map b/ts/dist/producer/model.js.map similarity index 100% rename from dist/producer/model.js.map rename to ts/dist/producer/model.js.map diff --git a/dist/types.d.ts b/ts/dist/types.d.ts similarity index 100% rename from dist/types.d.ts rename to ts/dist/types.d.ts diff --git a/dist/types.js b/ts/dist/types.js similarity index 100% rename from dist/types.js rename to ts/dist/types.js diff --git a/dist/types.js.map b/ts/dist/types.js.map similarity index 100% rename from dist/types.js.map rename to ts/dist/types.js.map diff --git a/dist/watch.d.ts b/ts/dist/watch.d.ts similarity index 100% rename from dist/watch.d.ts rename to ts/dist/watch.d.ts diff --git a/dist/watch.js b/ts/dist/watch.js similarity index 100% rename from dist/watch.js rename to ts/dist/watch.js diff --git a/dist/watch.js.map b/ts/dist/watch.js.map similarity index 100% rename from dist/watch.js.map rename to ts/dist/watch.js.map diff --git a/model/.model-config/model-config.json b/ts/model/.model-config/model-config.json similarity index 100% rename from model/.model-config/model-config.json rename to ts/model/.model-config/model-config.json diff --git a/model/.model-config/model-config.jsonic b/ts/model/.model-config/model-config.jsonic similarity index 100% rename from model/.model-config/model-config.jsonic rename to ts/model/.model-config/model-config.jsonic diff --git a/model/sys.json b/ts/model/sys.json similarity index 100% rename from model/sys.json rename to ts/model/sys.json diff --git a/model/sys.jsonic b/ts/model/sys.jsonic similarity index 100% rename from model/sys.jsonic rename to ts/model/sys.jsonic diff --git a/package.json b/ts/package.json similarity index 98% rename from package.json rename to ts/package.json index 93d8b5f..a4bad04 100644 --- a/package.json +++ b/ts/package.json @@ -43,7 +43,7 @@ "LICENSE" ], "dependencies": { - "aontu": "0.44.0", + "aontu": "0.45.1", "chokidar": "5.0.0", "shape": "10.1.0", "memfs": "4.57.6" diff --git a/src/build.ts b/ts/src/build.ts similarity index 100% rename from src/build.ts rename to ts/src/build.ts diff --git a/src/config.ts b/ts/src/config.ts similarity index 100% rename from src/config.ts rename to ts/src/config.ts diff --git a/src/model.ts b/ts/src/model.ts similarity index 100% rename from src/model.ts rename to ts/src/model.ts diff --git a/src/producer/local.ts b/ts/src/producer/local.ts similarity index 100% rename from src/producer/local.ts rename to ts/src/producer/local.ts diff --git a/src/producer/model.ts b/ts/src/producer/model.ts similarity index 100% rename from src/producer/model.ts rename to ts/src/producer/model.ts diff --git a/src/tsconfig.json b/ts/src/tsconfig.json similarity index 100% rename from src/tsconfig.json rename to ts/src/tsconfig.json diff --git a/src/types.ts b/ts/src/types.ts similarity index 100% rename from src/types.ts rename to ts/src/types.ts diff --git a/src/watch.ts b/ts/src/watch.ts similarity index 100% rename from src/watch.ts rename to ts/src/watch.ts diff --git a/test/build.test.ts b/ts/test/build.test.ts similarity index 100% rename from test/build.test.ts rename to ts/test/build.test.ts diff --git a/test/cli.test.ts b/ts/test/cli.test.ts similarity index 100% rename from test/cli.test.ts rename to ts/test/cli.test.ts diff --git a/test/e01/doc.html b/ts/test/e01/doc.html similarity index 100% rename from test/e01/doc.html rename to ts/test/e01/doc.html diff --git a/test/e01/model/config/config.jsonic b/ts/test/e01/model/config/config.jsonic similarity index 100% rename from test/e01/model/config/config.jsonic rename to ts/test/e01/model/config/config.jsonic diff --git a/test/e01/model/config/model.json b/ts/test/e01/model/config/model.json similarity index 100% rename from test/e01/model/config/model.json rename to ts/test/e01/model/config/model.json diff --git a/test/e01/model/model.json b/ts/test/e01/model/model.json similarity index 100% rename from test/e01/model/model.json rename to ts/test/e01/model/model.json diff --git a/test/e01/model/model.jsonic b/ts/test/e01/model/model.jsonic similarity index 100% rename from test/e01/model/model.jsonic rename to ts/test/e01/model/model.jsonic diff --git a/test/fix.test.ts b/ts/test/fix.test.ts similarity index 100% rename from test/fix.test.ts rename to ts/test/fix.test.ts diff --git a/test/model.test.ts b/ts/test/model.test.ts similarity index 100% rename from test/model.test.ts rename to ts/test/model.test.ts diff --git a/test/p01/doc.html b/ts/test/p01/doc.html similarity index 100% rename from test/p01/doc.html rename to ts/test/p01/doc.html diff --git a/test/p01/model/model.json b/ts/test/p01/model/model.json similarity index 100% rename from test/p01/model/model.json rename to ts/test/p01/model/model.json diff --git a/test/p01/model/model.jsonic b/ts/test/p01/model/model.jsonic similarity index 100% rename from test/p01/model/model.jsonic rename to ts/test/p01/model/model.jsonic diff --git a/test/p01/model/zed.jsonic b/ts/test/p01/model/zed.jsonic similarity index 100% rename from test/p01/model/zed.jsonic rename to ts/test/p01/model/zed.jsonic diff --git a/test/perf-bench.js b/ts/test/perf-bench.js similarity index 100% rename from test/perf-bench.js rename to ts/test/perf-bench.js diff --git a/test/quick.js b/ts/test/quick.js similarity index 100% rename from test/quick.js rename to ts/test/quick.js diff --git a/test/sys01/build/foo.js b/ts/test/sys01/build/foo.js similarity index 100% rename from test/sys01/build/foo.js rename to ts/test/sys01/build/foo.js diff --git a/test/sys01/build/pre.js b/ts/test/sys01/build/pre.js similarity index 100% rename from test/sys01/build/pre.js rename to ts/test/sys01/build/pre.js diff --git a/test/sys01/foo.txt b/ts/test/sys01/foo.txt similarity index 100% rename from test/sys01/foo.txt rename to ts/test/sys01/foo.txt diff --git a/test/sys01/model/.model-config/model-config.json b/ts/test/sys01/model/.model-config/model-config.json similarity index 100% rename from test/sys01/model/.model-config/model-config.json rename to ts/test/sys01/model/.model-config/model-config.json diff --git a/test/sys01/model/.model-config/model-config.jsonic b/ts/test/sys01/model/.model-config/model-config.jsonic similarity index 100% rename from test/sys01/model/.model-config/model-config.jsonic rename to ts/test/sys01/model/.model-config/model-config.jsonic diff --git a/test/sys01/model/color.jsonic b/ts/test/sys01/model/color.jsonic similarity index 100% rename from test/sys01/model/color.jsonic rename to ts/test/sys01/model/color.jsonic diff --git a/test/sys01/model/model.json b/ts/test/sys01/model/model.json similarity index 85% rename from test/sys01/model/model.json rename to ts/test/sys01/model/model.json index 314b859..cab1b47 100644 --- a/test/sys01/model/model.json +++ b/ts/test/sys01/model/model.json @@ -2,15 +2,18 @@ "main": { "srv": { "foo": { + "in": {}, + "out": {}, + "deps": {}, "api": { "web": { "active": true, + "path": { + "prefix": "/api/" + }, "method": "POST", "cors": { "active": false - }, - "path": { - "prefix": "/api/" } } }, @@ -20,46 +23,43 @@ "timeout": 30, "handler": { "path": { - "suffix": ".handler", - "prefix": "src/handler/lambda/" + "prefix": "src/handler/lambda/", + "suffix": ".handler" } }, "kind": "standard" } - }, - "in": {}, - "out": {}, - "deps": {} + } }, "bar": { + "in": {}, + "out": {}, + "deps": {}, + "api": { + "web": { + "active": true, + "path": { + "prefix": "/api/" + }, + "method": "POST", + "cors": { + "active": false + } + } + }, "env": { "lambda": { "active": true, "timeout": 30, "handler": { "path": { - "suffix": ".handler", - "prefix": "src/handler/lambda/" + "prefix": "src/handler/lambda/", + "suffix": ".handler" } }, "kind": "standard" } - }, - "api": { - "web": { - "active": true, - "method": "POST", - "cors": { - "active": false - }, - "path": { - "prefix": "/api/" - } - } - }, - "in": {}, - "out": {}, - "deps": {} + } } } }, diff --git a/test/sys01/model/model.jsonic b/ts/test/sys01/model/model.jsonic similarity index 100% rename from test/sys01/model/model.jsonic rename to ts/test/sys01/model/model.jsonic diff --git a/test/sys01/model/pre.jsonic b/ts/test/sys01/model/pre.jsonic similarity index 100% rename from test/sys01/model/pre.jsonic rename to ts/test/sys01/model/pre.jsonic diff --git a/test/sys01/pre.txt b/ts/test/sys01/pre.txt similarity index 100% rename from test/sys01/pre.txt rename to ts/test/sys01/pre.txt diff --git a/test/t01.jsonic b/ts/test/t01.jsonic similarity index 100% rename from test/t01.jsonic rename to ts/test/t01.jsonic diff --git a/test/t02.jsonic b/ts/test/t02.jsonic similarity index 100% rename from test/t02.jsonic rename to ts/test/t02.jsonic diff --git a/test/t03.jsonic b/ts/test/t03.jsonic similarity index 100% rename from test/t03.jsonic rename to ts/test/t03.jsonic diff --git a/test/tsconfig.json b/ts/test/tsconfig.json similarity index 100% rename from test/tsconfig.json rename to ts/test/tsconfig.json diff --git a/test/util.js b/ts/test/util.js similarity index 100% rename from test/util.js rename to ts/test/util.js diff --git a/test/w01/doc.html b/ts/test/w01/doc.html similarity index 100% rename from test/w01/doc.html rename to ts/test/w01/doc.html diff --git a/test/w01/model/model.json b/ts/test/w01/model/model.json similarity index 100% rename from test/w01/model/model.json rename to ts/test/w01/model/model.json diff --git a/test/w01/model/model.jsonic b/ts/test/w01/model/model.jsonic similarity index 100% rename from test/w01/model/model.jsonic rename to ts/test/w01/model/model.jsonic diff --git a/test/w01/model/zed.jsonic b/ts/test/w01/model/zed.jsonic similarity index 100% rename from test/w01/model/zed.jsonic rename to ts/test/w01/model/zed.jsonic diff --git a/test/watch-w01.js b/ts/test/watch-w01.js similarity index 100% rename from test/watch-w01.js rename to ts/test/watch-w01.js diff --git a/test/watch.test.ts b/ts/test/watch.test.ts similarity index 100% rename from test/watch.test.ts rename to ts/test/watch.test.ts From 127934bd465b78accee9d7e94cfa54be28bacbe3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 8 Jun 2026 18:42:30 +0000 Subject: [PATCH 6/6] test: add cases for both languages; make the Go CLI testable Go (model package 78% -> 94%, total 70% -> 91%): - Refactor cmd/voxgig-model into a testable run() with a local FlagSet; add main_test.go (write, dryrun, missing file, bad model, no args). - Add log_test.go (level parsing/filtering, silent, NopLog). - Add extra_test.go: reload re-resolves, resolver error, missing root, non-object model, action steps (pre/post/all), failing action, action with no Run, recovered producer panic, write error, dryrun via Model, Build accessor, NewWatch defaults + Last. TypeScript (93.3% -> 94.3%): - extra.test.ts: producer throws (pre/post), producer returns ok:false (pre/post), missing root file, Promise-exported action, unresolved import. - watch.test.ts: recover from a failed rebuild while watching. make test is green for both languages (19 TS, 30 Go test cases). https://claude.ai/code/session_01HxXpZNrKj3qonocwAtEP8r --- go/cmd/voxgig-model/main.go | 57 ++++--- go/cmd/voxgig-model/main_test.go | 79 ++++++++++ go/extra_test.go | 254 +++++++++++++++++++++++++++++++ go/log_test.go | 68 +++++++++ ts/dist-test/extra.test.js | 151 ++++++++++++++++++ ts/dist-test/extra.test.js.map | 1 + ts/dist-test/watch.test.js | 25 +++ ts/dist-test/watch.test.js.map | 2 +- ts/test/extra.test.ts | 181 ++++++++++++++++++++++ ts/test/watch.test.ts | 36 +++++ 10 files changed, 830 insertions(+), 24 deletions(-) create mode 100644 go/cmd/voxgig-model/main_test.go create mode 100644 go/extra_test.go create mode 100644 go/log_test.go create mode 100644 ts/dist-test/extra.test.js create mode 100644 ts/dist-test/extra.test.js.map create mode 100644 ts/test/extra.test.ts diff --git a/go/cmd/voxgig-model/main.go b/go/cmd/voxgig-model/main.go index 21dffb5..e4e8f05 100644 --- a/go/cmd/voxgig-model/main.go +++ b/go/cmd/voxgig-model/main.go @@ -10,6 +10,7 @@ package main import ( "flag" "fmt" + "io" "os" "os/signal" "path/filepath" @@ -19,29 +20,40 @@ import ( ) func main() { - watch := flag.Bool("w", false, "watch and rebuild on change") - dryrun := flag.Bool("y", false, "dry run (write nothing to disk)") - level := flag.String("g", "info", "log level: trace|debug|info|warn|error|silent") - flag.Usage = func() { - fmt.Fprintln(os.Stderr, "usage: voxgig-model [-w] [-y] [-g level] ") - flag.PrintDefaults() + os.Exit(run(os.Args[1:], os.Stderr)) +} + +// run parses args and builds the model, returning a process exit code. It is +// separated from main (which only adds os.Exit and signal handling) so the +// behavior can be tested directly. +func run(args []string, stderr io.Writer) int { + fs := flag.NewFlagSet("voxgig-model", flag.ContinueOnError) + fs.SetOutput(stderr) + watch := fs.Bool("w", false, "watch and rebuild on change") + dryrun := fs.Bool("y", false, "dry run (write nothing to disk)") + level := fs.String("g", "info", "log level: trace|debug|info|warn|error|silent") + fs.Usage = func() { + fmt.Fprintln(stderr, "usage: voxgig-model [-w] [-y] [-g level] ") + fs.PrintDefaults() + } + if err := fs.Parse(args); err != nil { + return 2 } - flag.Parse() - path := flag.Arg(0) + path := fs.Arg(0) if path == "" { - flag.Usage() - os.Exit(1) + fs.Usage() + return 1 } abs, err := filepath.Abs(path) if err != nil { - fmt.Fprintln(os.Stderr, "ERROR:", err) - os.Exit(1) + fmt.Fprintln(stderr, "ERROR:", err) + return 1 } if _, serr := os.Stat(abs); serr != nil { - fmt.Fprintln(os.Stderr, "ERROR: model file does not exist:", path) - os.Exit(1) + fmt.Fprintln(stderr, "ERROR: model file does not exist:", path) + return 1 } m := model.New(model.ModelSpec{ @@ -52,27 +64,26 @@ func main() { }) if *watch { - br := m.Start() - reportErrs(br) + reportErrs(m.Start(), stderr) sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) <-sig m.Stop() - return + return 0 } - br := m.Run() - if !br.OK { - reportErrs(br) - os.Exit(1) + if br := m.Run(); !br.OK { + reportErrs(br, stderr) + return 1 } + return 0 } -func reportErrs(br *model.BuildResult) { +func reportErrs(br *model.BuildResult, stderr io.Writer) { if br == nil { return } for _, e := range br.Errs { - fmt.Fprintln(os.Stderr, "ERROR:", e) + fmt.Fprintln(stderr, "ERROR:", e) } } diff --git a/go/cmd/voxgig-model/main_test.go b/go/cmd/voxgig-model/main_test.go new file mode 100644 index 0000000..a04d807 --- /dev/null +++ b/go/cmd/voxgig-model/main_test.go @@ -0,0 +1,79 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package main + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" +) + +func write(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } +} + +func TestCLIWritesModel(t *testing.T) { + dir := t.TempDir() + root := filepath.Join(dir, "m.jsonic") + write(t, root, "a: 1\nb: 2\n") + + var out bytes.Buffer + if code := run([]string{"-g", "silent", root}, &out); code != 0 { + t.Fatalf("exit %d: %s", code, out.String()) + } + if _, err := os.Stat(filepath.Join(dir, "m.json")); err != nil { + t.Fatalf("model JSON not written: %v", err) + } +} + +func TestCLIDryrunWritesNothing(t *testing.T) { + dir := t.TempDir() + root := filepath.Join(dir, "m.jsonic") + write(t, root, "a: 1\n") + + var out bytes.Buffer + if code := run([]string{"-y", "-g", "silent", root}, &out); code != 0 { + t.Fatalf("exit %d: %s", code, out.String()) + } + if _, err := os.Stat(filepath.Join(dir, "m.json")); !os.IsNotExist(err) { + t.Fatal("dryrun wrote m.json to disk") + } +} + +func TestCLIMissingFile(t *testing.T) { + var out bytes.Buffer + code := run([]string{filepath.Join(t.TempDir(), "nope.jsonic")}, &out) + if code == 0 { + t.Fatal("expected non-zero exit for missing file") + } + if !strings.Contains(out.String(), "does not exist") { + t.Fatalf("stderr = %q", out.String()) + } +} + +func TestCLINoArgs(t *testing.T) { + var out bytes.Buffer + if code := run(nil, &out); code != 1 { + t.Fatalf("exit %d, want 1", code) + } +} + +func TestCLIBadModel(t *testing.T) { + dir := t.TempDir() + root := filepath.Join(dir, "m.jsonic") + write(t, root, "x: 1\nx: 2\n") + + var out bytes.Buffer + code := run([]string{"-g", "silent", root}, &out) + if code != 1 { + t.Fatalf("exit %d, want 1", code) + } + if !strings.Contains(out.String(), "ERROR") { + t.Fatalf("stderr = %q", out.String()) + } +} diff --git a/go/extra_test.go b/go/extra_test.go new file mode 100644 index 0000000..31fcfa8 --- /dev/null +++ b/go/extra_test.go @@ -0,0 +1,254 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +// countResolver records how many times it was asked to resolve, and returns +// a fixed model. It exercises the Resolver seam without aontu. +type countResolver struct { + model map[string]any + errs []error + calls int +} + +func (c *countResolver) Resolve(string) (map[string]any, []error) { + c.calls++ + out := map[string]any{} + for k, v := range c.model { + out[k] = v + } + return out, c.errs +} + +// A pre-action that invalidates the cache and requests a reload causes the +// model to be re-resolved before the post phase. +func TestReloadReResolves(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "a: 1\n") + + r := &countResolver{model: map[string]any{"a": int64(1)}} + b := NewBuild(BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), Base: dir, Resolver: r, + Actions: map[string]ActionDef{ + "regen": {Step: StepPre, Run: func(_ map[string]any, b *Build, _ *BuildContext) ActionResult { + b.InvalidateCache() // a real pre-action would have rewritten source + return ActionResult{OK: true, Reload: true} + }}, + }, + Res: []ProducerDef{{Path: "/", Build: LocalProducer}}, + }) + + br := b.Run(false) + if !br.OK { + t.Fatalf("build failed: %v", br.Errs) + } + if r.calls != 2 { + t.Fatalf("resolve calls = %d, want 2 (initial + reload)", r.calls) + } +} + +// A custom resolver returning errors fails the build. +func TestResolverError(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "a: 1\n") + r := &countResolver{errs: []error{errors.New("nope")}} + b := NewBuild(BuildSpec{Path: filepath.Join(dir, "m.jsonic"), Base: dir, Resolver: r}) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure from resolver error") + } + if !containsErr(br.Errs, "nope") { + t.Fatalf("errors = %v", br.Errs) + } +} + +// A missing root file fails the build with the read error. +func TestMissingRootFile(t *testing.T) { + dir := t.TempDir() + b := NewBuild(BuildSpec{Path: filepath.Join(dir, "nope.jsonic"), Base: dir}) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure for missing root file") + } + if len(br.Errs) == 0 { + t.Fatal("expected an error") + } +} + +// A model that resolves to a non-object is rejected by AontuResolver. +func TestNonObjectModel(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "5\n") + b := NewBuild(BuildSpec{Path: filepath.Join(dir, "m.jsonic"), Base: dir}) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure for non-object model") + } + if len(br.Errs) == 0 { + t.Fatal("expected an error") + } +} + +// Actions run only in their declared step; StepAll runs in both phases. +func TestActionSteps(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "x: 1\n") + + var log []string + rec := func(name string) Action { + return func(_ map[string]any, _ *Build, ctx *BuildContext) ActionResult { + log = append(log, name+":"+string(ctx.Step)) + return ActionResult{OK: true} + } + } + b := NewBuild(BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), Base: dir, + Actions: map[string]ActionDef{ + "pre": {Step: StepPre, Run: rec("pre")}, + "post": {Step: StepPost, Run: rec("post")}, + "all": {Step: StepAll, Run: rec("all")}, + }, + Order: []string{"pre", "post", "all"}, + Res: []ProducerDef{{Path: "/", Build: LocalProducer}}, + }) + if br := b.Run(false); !br.OK { + t.Fatalf("build failed: %v", br.Errs) + } + got := strings.Join(log, ",") + want := "pre:pre,all:pre,post:post,all:post" + if got != want { + t.Fatalf("action run log = %q, want %q", got, want) + } +} + +// A failing action stops the build and skips later actions. +func TestActionFailsBuild(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "x: 1\n") + + var ran []string + mk := func(name string, ok bool) ActionDef { + return ActionDef{Run: func(_ map[string]any, _ *Build, _ *BuildContext) ActionResult { + ran = append(ran, name) + return ActionResult{OK: ok} + }} + } + b := NewBuild(BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), Base: dir, + Actions: map[string]ActionDef{"a": mk("a", true), "b": mk("b", false), "c": mk("c", true)}, + Order: []string{"a", "b", "c"}, + Res: []ProducerDef{{Path: "/", Build: LocalProducer}}, + }) + br := b.Run(false) + if br.OK { + t.Fatal("expected build failure from failing action") + } + if strings.Join(ran, ",") != "a,b" { + t.Fatalf("ran %v, want [a b] (c skipped)", ran) + } +} + +// An action whose step matches but has no Run function fails the build. +func TestActionMissingRun(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "x: 1\n") + b := NewBuild(BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), Base: dir, + Actions: map[string]ActionDef{"empty": {Step: StepPost}}, + Order: []string{"empty"}, + Res: []ProducerDef{{Path: "/", Build: LocalProducer}}, + }) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure for action with no Run") + } + if !containsErr(br.Errs, "no Run") { + t.Fatalf("errors = %v", br.Errs) + } +} + +// A producer that panics is converted to a failed build, not a crash. +func TestProducerPanicRecovered(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "x: 1\n") + b := NewBuild(BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), Base: dir, + Res: []ProducerDef{{Path: "/", Build: func(_ *Build, _ *BuildContext) ProducerResult { + panic("boom in producer") + }}}, + }) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure from panicking producer") + } + if !containsErr(br.Errs, "boom in producer") { + t.Fatalf("errors = %v", br.Errs) + } +} + +// A filesystem write error fails the model producer. +type failWriteFS struct{ OSFS } + +func (failWriteFS) WriteFile(string, []byte, os.FileMode) error { + return errors.New("simulated write failure") +} + +func TestModelProducerWriteError(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "a: 1\n") + b := NewBuild(BuildSpec{ + Path: filepath.Join(dir, "m.jsonic"), Base: dir, FS: failWriteFS{}, + Res: []ProducerDef{{Path: "/", Build: ModelProducer}}, + }) + br := b.Run(false) + if br.OK { + t.Fatal("expected failure when write fails") + } + if !containsErr(br.Errs, "write failure") { + t.Fatalf("errors = %v", br.Errs) + } +} + +// Model.New honors dryrun and exposes the resolved build. +func TestModelDryrunAndBuild(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "a: 1\n") + m := New(ModelSpec{Path: filepath.Join(dir, "m.jsonic"), Base: dir, Dryrun: true}) + + br := m.Run() + if !br.OK { + t.Fatalf("run failed: %v", br.Errs) + } + if _, err := os.Stat(filepath.Join(dir, "m.json")); !os.IsNotExist(err) { + t.Fatal("dryrun wrote m.json") + } + if m.Build() == nil || m.Build().Model["a"] != int64(1) { + t.Fatalf("Build() model = %#v", m.Build()) + } + if br.Build() != m.Build() { + t.Fatal("BuildResult.Build() did not match Model.Build()") + } +} + +// NewWatch applies defaults; Last returns the most recent result. +func TestWatchRunAndLast(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "m.jsonic", "a: 1\n") + b := NewBuild(BuildSpec{Path: filepath.Join(dir, "m.jsonic"), Base: dir}) + w := NewWatch(b, "", 0) // defaults: name "model", idle 111ms + + br := w.Run(false) + if !br.OK { + t.Fatalf("run failed: %v", br.Errs) + } + if w.Last() != br { + t.Fatal("Last did not return the latest result") + } +} diff --git a/go/log_test.go b/go/log_test.go new file mode 100644 index 0000000..811a4c3 --- /dev/null +++ b/go/log_test.go @@ -0,0 +1,68 @@ +/* Copyright © 2021-2026 Voxgig Ltd, MIT License. */ + +package model + +import ( + "bytes" + "errors" + "strings" + "testing" +) + +func TestParseLevel(t *testing.T) { + cases := map[string]int{ + "debug": levelDebug, "trace": levelDebug, + "info": levelInfo, "": levelInfo, "nonsense": levelInfo, + "warn": levelError, "error": levelError, "fatal": levelError, + "silent": levelSilent, + } + for in, want := range cases { + if got := parseLevel(in); got != want { + t.Errorf("parseLevel(%q) = %d, want %d", in, got, want) + } + } +} + +func TestStdLogRespectsLevel(t *testing.T) { + var buf bytes.Buffer + l := &stdLog{level: levelInfo, w: &buf} + + l.Debug("dbg", "hidden") // below info -> suppressed + l.Info("inf", "shown") + l.Error("err", errors.New("boom"), "context") + + out := buf.String() + if strings.Contains(out, "hidden") { + t.Error("debug entry leaked at info level") + } + if !strings.Contains(out, "inf") || !strings.Contains(out, "shown") { + t.Error("info entry not logged") + } + if !strings.Contains(out, "ERROR") || !strings.Contains(out, "boom") { + t.Errorf("error not logged with cause: %q", out) + } +} + +func TestStdLogSilent(t *testing.T) { + var buf bytes.Buffer + l := &stdLog{level: levelSilent, w: &buf} + l.Info("a", "b") + l.Debug("a", "b") + l.Error("a", errors.New("x"), "b") + if buf.Len() != 0 { + t.Fatalf("silent log produced output: %q", buf.String()) + } +} + +func TestNewLogConstructs(t *testing.T) { + if NewLog("debug") == nil { + t.Fatal("NewLog returned nil") + } +} + +func TestNopLog(t *testing.T) { + var l Log = NopLog{} // discards everything, must not panic + l.Info("a", "b") + l.Debug("a", "b") + l.Error("a", errors.New("x"), "b") +} diff --git a/ts/dist-test/extra.test.js b/ts/dist-test/extra.test.js new file mode 100644 index 0000000..45be782 --- /dev/null +++ b/ts/dist-test/extra.test.js @@ -0,0 +1,151 @@ +"use strict"; +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const node_fs_1 = __importDefault(require("node:fs")); +const promises_1 = require("node:fs/promises"); +const node_test_1 = require("node:test"); +const node_assert_1 = __importDefault(require("node:assert")); +const util_1 = require("@voxgig/util"); +const build_1 = require("../dist/build"); +const model_1 = require("../dist/model"); +const GEN = __dirname + '/../test/_gen'; +function silentLog() { + return (0, util_1.prettyPino)('test', { debug: 'silent' }); +} +function okResult(name) { + return { ok: true, name, step: '', active: true, reload: false, errs: [], runlog: [] }; +} +(0, node_test_1.describe)('extra', () => { + // A producer that throws in the pre phase fails the build, and the error is + // collected rather than escaping. + (0, node_test_1.test)('producer-throws-in-pre', async () => { + const dir = GEN + '/ex-pre'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + await (0, promises_1.writeFile)(dir + '/m.jsonic', 'a: 1\n'); + const b = (0, build_1.makeBuild)({ + fs: node_fs_1.default, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function boom(_build, ctx) { + if ('pre' === ctx.step) { + throw new Error('pre-boom'); + } + return okResult('boom'); + }, + }], + }, silentLog()); + const v = await b.run({ watch: false }); + node_assert_1.default.strictEqual(v.ok, false); + node_assert_1.default.ok(v.errs.some((e) => String(e.message ?? e).includes('pre-boom'))); + }); + // A producer that throws in the post phase fails the build. + (0, node_test_1.test)('producer-throws-in-post', async () => { + const dir = GEN + '/ex-post'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + await (0, promises_1.writeFile)(dir + '/m.jsonic', 'a: 1\n'); + const b = (0, build_1.makeBuild)({ + fs: node_fs_1.default, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function boom(_build, ctx) { + if ('post' === ctx.step) { + throw new Error('post-boom'); + } + return okResult('boom'); + }, + }], + }, silentLog()); + const v = await b.run({ watch: false }); + node_assert_1.default.strictEqual(v.ok, false); + node_assert_1.default.ok(v.errs.some((e) => String(e.message ?? e).includes('post-boom'))); + }); + // A missing root file fails the build with the read error. + (0, node_test_1.test)('missing-root-file', async () => { + const b = (0, build_1.makeBuild)({ + fs: node_fs_1.default, base: GEN, path: GEN + '/does-not-exist.jsonic', res: [], + }, silentLog()); + const v = await b.run({ watch: false }); + node_assert_1.default.strictEqual(v.ok, false); + node_assert_1.default.ok(0 < v.errs.length); + }); + // An action module may export a Promise resolving to the action function; + // the local producer awaits it before running. + (0, node_test_1.test)('promise-exported-action', async () => { + const dir = GEN + '/ex-promise'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir + '/model/.model-config', { recursive: true }); + await (0, promises_1.mkdir)(dir + '/build', { recursive: true }); + await (0, promises_1.writeFile)(dir + '/model/m.jsonic', 'a: 1\n'); + await (0, promises_1.writeFile)(dir + '/model/.model-config/model-config.jsonic', "sys: model: action: { p: load: 'build/p' }\n"); + await (0, promises_1.writeFile)(dir + '/build/p.js', "const Path = require('node:path')\n" + + 'module.exports = Promise.resolve(async function p(model, build) {\n' + + " const root = Path.resolve(build.path, '..', '..')\n" + + " build.fs.writeFileSync(Path.resolve(root, 'p.txt'), 'OK')\n" + + ' return { ok: true }\n' + + '})\n'); + const model = new model_1.Model({ + path: dir + '/model/m.jsonic', base: dir + '/model', debug: 'silent', + }); + const br = await model.run(); + node_assert_1.default.ok(br.ok, 'build failed: ' + JSON.stringify(br.errs)); + node_assert_1.default.strictEqual(await (0, promises_1.readFile)(dir + '/p.txt', 'utf8'), 'OK'); + }); + // A producer that returns ok:false in the pre phase fails the build. + (0, node_test_1.test)('producer-returns-not-ok-pre', async () => { + const dir = GEN + '/ex-nokpre'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + await (0, promises_1.writeFile)(dir + '/m.jsonic', 'a: 1\n'); + const b = (0, build_1.makeBuild)({ + fs: node_fs_1.default, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function bad(_build, ctx) { + return { + ok: 'pre' !== ctx.step, name: 'bad', step: ctx.step, + active: true, reload: false, errs: [], runlog: [], + }; + }, + }], + }, silentLog()); + const v = await b.run({ watch: false }); + node_assert_1.default.strictEqual(v.ok, false); + }); + // A producer that returns ok:false in the post phase fails the build. + (0, node_test_1.test)('producer-returns-not-ok-post', async () => { + const dir = GEN + '/ex-nokpost'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + await (0, promises_1.writeFile)(dir + '/m.jsonic', 'a: 1\n'); + const b = (0, build_1.makeBuild)({ + fs: node_fs_1.default, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function bad(_build, ctx) { + return { + ok: 'post' !== ctx.step, name: 'bad', step: ctx.step, + active: true, reload: false, errs: [], runlog: [], + }; + }, + }], + }, silentLog()); + const v = await b.run({ watch: false }); + node_assert_1.default.strictEqual(v.ok, false); + }); + // An unresolved import makes aontu throw; the build collects it as an error + // rather than letting it escape. + (0, node_test_1.test)('unresolved-import-fails', async () => { + const dir = GEN + '/ex-import'; + await (0, promises_1.rm)(dir, { recursive: true, force: true }); + await (0, promises_1.mkdir)(dir, { recursive: true }); + await (0, promises_1.writeFile)(dir + '/m.jsonic', 'top: @"./missing.jsonic"\n'); + const b = (0, build_1.makeBuild)({ + fs: node_fs_1.default, base: dir, path: dir + '/m.jsonic', res: [], + }, silentLog()); + const v = await b.run({ watch: false }); + node_assert_1.default.strictEqual(v.ok, false); + node_assert_1.default.ok(0 < v.errs.length); + }); +}); +//# sourceMappingURL=extra.test.js.map \ No newline at end of file diff --git a/ts/dist-test/extra.test.js.map b/ts/dist-test/extra.test.js.map new file mode 100644 index 0000000..51cd058 --- /dev/null +++ b/ts/dist-test/extra.test.js.map @@ -0,0 +1 @@ +{"version":3,"file":"extra.test.js","sourceRoot":"","sources":["../test/extra.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/ts/dist-test/watch.test.js b/ts/dist-test/watch.test.js index 2fd4946..af330a4 100644 --- a/ts/dist-test/watch.test.js +++ b/ts/dist-test/watch.test.js @@ -98,5 +98,30 @@ async function read(file) { await model.stop(); } }); + // A failed rebuild while watching is reported, and the watcher recovers when + // the model is fixed. + (0, node_test_1.test)('recovers-from-error-while-watching', async () => { + const base = GEN + '/wat03/model'; + await (0, promises_1.rm)(GEN + '/wat03', { recursive: true, force: true }); + await (0, promises_1.mkdir)(base + '/.model-config', { recursive: true }); + await (0, promises_1.writeFile)(base + '/model.jsonic', 'val: 1\n'); + await (0, promises_1.writeFile)(base + '/.model-config/model-config.jsonic', 'sys: model: action: {}\n'); + const out = base + '/model.json'; + const model = new model_1.Model({ path: base + '/model.jsonic', base, debug: 'silent' }); + try { + await model.start(); + node_assert_1.default.ok(await waitFor(async () => (await readVal(out)) === 1), 'initial build should produce val:1'); + // Break the model: conflicting scalar values do not unify. + await new Promise(r => setTimeout(r, 200)); + await (0, promises_1.writeFile)(base + '/model.jsonic', 'val: 1\nval: 2\n'); + await new Promise(r => setTimeout(r, 400)); // let the failed rebuild run + // Fix it; the watcher should recover. + await (0, promises_1.writeFile)(base + '/model.jsonic', 'val: 9\n'); + node_assert_1.default.ok(await waitFor(async () => (await readVal(out)) === 9), 'watcher should recover to val:9 after the model is fixed'); + } + finally { + await model.stop(); + } + }); }); //# sourceMappingURL=watch.test.js.map \ No newline at end of file diff --git a/ts/dist-test/watch.test.js.map b/ts/dist-test/watch.test.js.map index 324f4d3..ed18a1d 100644 --- a/ts/dist-test/watch.test.js.map +++ b/ts/dist-test/watch.test.js.map @@ -1 +1 @@ -{"version":3,"file":"watch.test.js","sourceRoot":"","sources":["../test/watch.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"watch.test.js","sourceRoot":"","sources":["../test/watch.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/ts/test/extra.test.ts b/ts/test/extra.test.ts new file mode 100644 index 0000000..282f54b --- /dev/null +++ b/ts/test/extra.test.ts @@ -0,0 +1,181 @@ +/* Copyright © 2021-2025 Voxgig Ltd, MIT License. */ + +import Fs from 'node:fs' +import { mkdir, writeFile, readFile, rm } from 'node:fs/promises' +import { test, describe } from 'node:test' +import assert from 'node:assert' + +import { prettyPino } from '@voxgig/util' + +import { makeBuild } from '../dist/build' +import { Model } from '../dist/model' +import type { Build, BuildContext } from '../dist/types' + + +const GEN = __dirname + '/../test/_gen' + +function silentLog() { + return prettyPino('test', { debug: 'silent' }) +} + +function okResult(name: string) { + return { ok: true, name, step: '', active: true, reload: false, errs: [], runlog: [] } +} + + +describe('extra', () => { + + // A producer that throws in the pre phase fails the build, and the error is + // collected rather than escaping. + test('producer-throws-in-pre', async () => { + const dir = GEN + '/ex-pre' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + await writeFile(dir + '/m.jsonic', 'a: 1\n') + + const b = makeBuild({ + fs: Fs, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function boom(_build: Build, ctx: BuildContext) { + if ('pre' === ctx.step) { throw new Error('pre-boom') } + return okResult('boom') + }, + }], + }, silentLog()) + + const v = await b.run({ watch: false }) + assert.strictEqual(v.ok, false) + assert.ok(v.errs.some((e: any) => String(e.message ?? e).includes('pre-boom'))) + }) + + + // A producer that throws in the post phase fails the build. + test('producer-throws-in-post', async () => { + const dir = GEN + '/ex-post' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + await writeFile(dir + '/m.jsonic', 'a: 1\n') + + const b = makeBuild({ + fs: Fs, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function boom(_build: Build, ctx: BuildContext) { + if ('post' === ctx.step) { throw new Error('post-boom') } + return okResult('boom') + }, + }], + }, silentLog()) + + const v = await b.run({ watch: false }) + assert.strictEqual(v.ok, false) + assert.ok(v.errs.some((e: any) => String(e.message ?? e).includes('post-boom'))) + }) + + + // A missing root file fails the build with the read error. + test('missing-root-file', async () => { + const b = makeBuild({ + fs: Fs, base: GEN, path: GEN + '/does-not-exist.jsonic', res: [], + }, silentLog()) + + const v = await b.run({ watch: false }) + assert.strictEqual(v.ok, false) + assert.ok(0 < v.errs.length) + }) + + + // An action module may export a Promise resolving to the action function; + // the local producer awaits it before running. + test('promise-exported-action', async () => { + const dir = GEN + '/ex-promise' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir + '/model/.model-config', { recursive: true }) + await mkdir(dir + '/build', { recursive: true }) + + await writeFile(dir + '/model/m.jsonic', 'a: 1\n') + await writeFile(dir + '/model/.model-config/model-config.jsonic', + "sys: model: action: { p: load: 'build/p' }\n") + await writeFile(dir + '/build/p.js', + "const Path = require('node:path')\n" + + 'module.exports = Promise.resolve(async function p(model, build) {\n' + + " const root = Path.resolve(build.path, '..', '..')\n" + + " build.fs.writeFileSync(Path.resolve(root, 'p.txt'), 'OK')\n" + + ' return { ok: true }\n' + + '})\n') + + const model = new Model({ + path: dir + '/model/m.jsonic', base: dir + '/model', debug: 'silent', + }) + const br = await model.run() + + assert.ok(br.ok, 'build failed: ' + JSON.stringify(br.errs)) + assert.strictEqual(await readFile(dir + '/p.txt', 'utf8'), 'OK') + }) + + + // A producer that returns ok:false in the pre phase fails the build. + test('producer-returns-not-ok-pre', async () => { + const dir = GEN + '/ex-nokpre' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + await writeFile(dir + '/m.jsonic', 'a: 1\n') + + const b = makeBuild({ + fs: Fs, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function bad(_build: Build, ctx: BuildContext) { + return { + ok: 'pre' !== ctx.step, name: 'bad', step: ctx.step, + active: true, reload: false, errs: [], runlog: [], + } + }, + }], + }, silentLog()) + + const v = await b.run({ watch: false }) + assert.strictEqual(v.ok, false) + }) + + + // A producer that returns ok:false in the post phase fails the build. + test('producer-returns-not-ok-post', async () => { + const dir = GEN + '/ex-nokpost' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + await writeFile(dir + '/m.jsonic', 'a: 1\n') + + const b = makeBuild({ + fs: Fs, base: dir, path: dir + '/m.jsonic', + res: [{ + path: '/', build: async function bad(_build: Build, ctx: BuildContext) { + return { + ok: 'post' !== ctx.step, name: 'bad', step: ctx.step, + active: true, reload: false, errs: [], runlog: [], + } + }, + }], + }, silentLog()) + + const v = await b.run({ watch: false }) + assert.strictEqual(v.ok, false) + }) + + + // An unresolved import makes aontu throw; the build collects it as an error + // rather than letting it escape. + test('unresolved-import-fails', async () => { + const dir = GEN + '/ex-import' + await rm(dir, { recursive: true, force: true }) + await mkdir(dir, { recursive: true }) + await writeFile(dir + '/m.jsonic', 'top: @"./missing.jsonic"\n') + + const b = makeBuild({ + fs: Fs, base: dir, path: dir + '/m.jsonic', res: [], + }, silentLog()) + + const v = await b.run({ watch: false }) + assert.strictEqual(v.ok, false) + assert.ok(0 < v.errs.length) + }) + +}) diff --git a/ts/test/watch.test.ts b/ts/test/watch.test.ts index d87261e..3fabf21 100644 --- a/ts/test/watch.test.ts +++ b/ts/test/watch.test.ts @@ -133,4 +133,40 @@ describe('watch', () => { } }) + + // A failed rebuild while watching is reported, and the watcher recovers when + // the model is fixed. + test('recovers-from-error-while-watching', async () => { + const base = GEN + '/wat03/model' + await rm(GEN + '/wat03', { recursive: true, force: true }) + await mkdir(base + '/.model-config', { recursive: true }) + await writeFile(base + '/model.jsonic', 'val: 1\n') + await writeFile(base + '/.model-config/model-config.jsonic', + 'sys: model: action: {}\n') + + const out = base + '/model.json' + const model = new Model({ path: base + '/model.jsonic', base, debug: 'silent' }) + + try { + await model.start() + assert.ok( + await waitFor(async () => (await readVal(out)) === 1), + 'initial build should produce val:1') + + // Break the model: conflicting scalar values do not unify. + await new Promise(r => setTimeout(r, 200)) + await writeFile(base + '/model.jsonic', 'val: 1\nval: 2\n') + await new Promise(r => setTimeout(r, 400)) // let the failed rebuild run + + // Fix it; the watcher should recover. + await writeFile(base + '/model.jsonic', 'val: 9\n') + assert.ok( + await waitFor(async () => (await readVal(out)) === 9), + 'watcher should recover to val:9 after the model is fixed') + } + finally { + await model.stop() + } + }) + })