c90f8f1e5c
- scripts/compile-ink.mjs: build-time inklecate runner using bundled binary (BLOCKER 4 — uses node_modules/inklecate/bin, not stale -windows/-mac path strings). Assumption A6 verified first-try on Windows; the same binary path resolution works on macOS + Linux per the wrapper's own getInklecatePath convention. - scripts/compile-ink.test.mjs: 3 Vitest cases proving the compiler runs + emits valid JSON with inkVersion. wipe=false for the test path so it can run in parallel with the ink-loader test without racing on the wipe step. - 4 Season-1 .ink files authored in voice (Lura warmth-anchor, gardener-keeper for compost): lura-arrival.ink, lura-mid.ink, lura-farewell.ink, compost-acknowledgements.ink (rewrite of Plan 02-03 scaffolded version into VAR-driven branch shape consumable by the runtime). - src/content/ink-loader.ts: loadInkStory + bindGardenStateToInk + INK_VARIABLE_MAP. Centralized snake_case slot mapping per Pitfall 4. UTF-8 BOM stripped before Story instantiation. - src/content/ink-loader.test.ts: 8 cases — Story instantiation for all 4 beats, fragment_count binding, Pitfall 4 snake_case enforcement, silent skip for stories missing declared vars. - package.json: build now runs compile:ink first; ci chain runs compile:ink before test so ink-loader.test.ts's precondition check passes. - .gitignore: src/content/compiled-ink/ excluded (regenerated on every build). npm run ci exits 0; 11 new tests green (228 total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
6.2 KiB
JavaScript
162 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Phase 2 Plan 02-04 — compile content/dialogue/**\/*.ink → src/content/compiled-ink/**\/*.ink.json
|
|
*
|
|
* Per RESEARCH Pattern 5 + Assumption A6 (verified on this run).
|
|
*
|
|
* Approach (chosen after reading node_modules/inklecate/index.js +
|
|
* getInklecatePath.js + executableHandler.js):
|
|
*
|
|
* The npm wrapper for inklecate exposes a CommonJS module shape:
|
|
* `module.exports = { ArgsEnum, DEBUG, getBinDir, getCacheFilepath,
|
|
* getInklecatePath, inklecate }`.
|
|
*
|
|
* The wrapper's `inklecate` function spawns the inklecate.exe / inklecate
|
|
* binary under node_modules/inklecate/bin/ asynchronously and resolves
|
|
* when the child exits — but as of inklecate@1.8.1, the wrapper's
|
|
* `executableHandler` swallows non-zero exit codes silently and the
|
|
* API surface is undocumented for stderr. To keep failure modes loud
|
|
* AND to keep this script cross-platform, we invoke the binary
|
|
* DIRECTLY via `child_process.execFileSync`. The wrapper's bin/ folder
|
|
* is the canonical home for both Windows (inklecate.exe) and POSIX
|
|
* (inklecate) executables; the wrapper handles platform selection
|
|
* internally via `process.platform === 'darwin' ? 'inklecate' :
|
|
* 'inklecate.exe'` (see node_modules/inklecate/getInklecatePath.js).
|
|
*
|
|
* On Linux the same `inklecate` binary applies (it's a single .NET
|
|
* self-contained executable that ships alongside the .dll runtime),
|
|
* matching what `executableHandler` does internally.
|
|
*/
|
|
|
|
import {
|
|
mkdirSync,
|
|
existsSync,
|
|
readdirSync,
|
|
statSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
import { execFileSync } from 'node:child_process';
|
|
|
|
const INK_ROOT = resolve(process.cwd(), 'content/dialogue');
|
|
const OUT_ROOT = resolve(process.cwd(), 'src/content/compiled-ink');
|
|
|
|
function findInkFiles(root) {
|
|
const out = [];
|
|
if (!existsSync(root)) return out;
|
|
for (const entry of readdirSync(root)) {
|
|
const full = join(root, entry);
|
|
const st = statSync(full);
|
|
if (st.isDirectory()) out.push(...findInkFiles(full));
|
|
else if (entry.endsWith('.ink')) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Resolve the bundled inklecate binary path.
|
|
*
|
|
* BLOCKER 4 mitigation — DO NOT use stale path strings like
|
|
* `node_modules/inklecate/inklecate-windows/inklecate.exe`. The wrapper
|
|
* ships a single `bin/` directory containing both inklecate (POSIX) and
|
|
* inklecate.exe (Windows). Verified during Plan 02-04 first run:
|
|
* ls node_modules/inklecate/bin/
|
|
* ink-engine-runtime.dll inklecate.exe inklecate
|
|
* ink_compiler.dll libhostpolicy.so
|
|
*
|
|
* Platform selection follows the wrapper's own
|
|
* getInklecatePath.js convention: anything-not-darwin uses .exe — but
|
|
* that's a quirk of the .NET self-contained build. On Linux the .exe
|
|
* file is the actual ELF executable (Mono-style multi-platform .NET);
|
|
* on macOS the no-extension `inklecate` is used. We replicate that
|
|
* behavior here so this script works on Windows + macOS + Linux dev
|
|
* machines without modification (Assumption A6).
|
|
*/
|
|
function inklecateBinary() {
|
|
const binDir = resolve(process.cwd(), 'node_modules/inklecate/bin');
|
|
// Match the wrapper's own platform-selection logic.
|
|
const name = process.platform === 'darwin' ? 'inklecate' : 'inklecate.exe';
|
|
return join(binDir, name);
|
|
}
|
|
|
|
export async function compileAllInk(options = {}) {
|
|
const { wipe = true } = options;
|
|
const files = findInkFiles(INK_ROOT);
|
|
if (files.length === 0) {
|
|
console.log('[compile:ink] no .ink files under content/dialogue/ — skipping');
|
|
return { compiled: 0, files: [] };
|
|
}
|
|
|
|
// Optionally wipe stale output. The CLI path passes wipe=true (default)
|
|
// so deleted .ink files don't leave stale .ink.json files behind. The
|
|
// Vitest test passes wipe=false so it doesn't race with parallel test
|
|
// files (e.g., src/content/ink-loader.test.ts) reading the compiled
|
|
// artefacts.
|
|
if (wipe && existsSync(OUT_ROOT)) {
|
|
rmSync(OUT_ROOT, { recursive: true, force: true });
|
|
}
|
|
|
|
const binary = inklecateBinary();
|
|
if (!existsSync(binary)) {
|
|
throw new Error(
|
|
`[compile:ink] inklecate binary not found at ${binary}. ` +
|
|
`Did 'npm install' run? Expected node_modules/inklecate/bin/{inklecate,inklecate.exe}.`,
|
|
);
|
|
}
|
|
|
|
const compiled = [];
|
|
for (const inkPath of files) {
|
|
const rel = relative(INK_ROOT, inkPath);
|
|
const outPath = resolve(OUT_ROOT, rel.replace(/\.ink$/, '.ink.json'));
|
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
|
|
// Inklecate CLI shape: `inklecate -o <outFile> <inFile>`.
|
|
// The binary writes a JSON file at the given path. Stderr is captured
|
|
// and surfaced if the exit code is non-zero.
|
|
try {
|
|
execFileSync(binary, ['-o', outPath, inkPath], { stdio: 'pipe' });
|
|
} catch (err) {
|
|
const stderr = err && err.stderr ? err.stderr.toString() : '';
|
|
const stdout = err && err.stdout ? err.stdout.toString() : '';
|
|
throw new Error(
|
|
`[compile:ink] FAILED compiling ${rel}\n` +
|
|
(stderr ? `stderr:\n${stderr}\n` : '') +
|
|
(stdout ? `stdout:\n${stdout}\n` : ''),
|
|
);
|
|
}
|
|
|
|
if (!existsSync(outPath)) {
|
|
throw new Error(
|
|
`[compile:ink] inklecate exit code 0 but no output at ${outPath} for input ${inkPath}`,
|
|
);
|
|
}
|
|
compiled.push({ in: inkPath, out: outPath });
|
|
console.log(`[compile:ink] ${rel} -> ${relative(process.cwd(), outPath)}`);
|
|
}
|
|
console.log(`[compile:ink] compiled ${compiled.length} files`);
|
|
return { compiled: compiled.length, files: compiled };
|
|
}
|
|
|
|
// CLI invocation (gated so Vitest can `import` this module without firing).
|
|
const isDirectCli = (() => {
|
|
try {
|
|
const argvUrl = `file://${resolve(process.argv[1] ?? '').replace(/\\/g, '/')}`;
|
|
return import.meta.url === argvUrl || import.meta.url.endsWith('/compile-ink.mjs') && process.argv[1]?.endsWith('compile-ink.mjs');
|
|
} catch {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
if (isDirectCli) {
|
|
compileAllInk().catch((err) => {
|
|
console.error('[compile:ink] FAILED:', err && err.stack ? err.stack : err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
// Suppress unused-import lint for writeFileSync — kept available for
|
|
// future inline-write paths if the binary path approach ever needs to
|
|
// fall back to a wrapper-only-mode that returns JSON via stdout.
|
|
void writeFileSync;
|