feat: Add CLI integration for LiquidJS template rendering
Integrates LiquidJS template rendering into the CLI render command. When a template is registered for a node's schema type via the variable system (@ucas/template/text/<schema-hash>), the CLI will use the template for rendering. Otherwise, it falls back to YAML output. Changes: - Modified cmdRender in index.ts to use renderAsync with variable store - Added Suite 6: CLI Integration with Templates (5 comprehensive tests) - Fixed template file format: templates must be JSON-encoded strings - Removed unused render import from index.ts - Renamed unused globalDecay parameter in liquid-render.ts Test coverage increased from 336 to 341 tests, all passing. Fixes #40 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -314,3 +314,286 @@ describe("ucas render command", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Suite 6: CLI Integration with Templates", () => {
|
||||
test("6.1 CLI with Template (Default Parameters)", async () => {
|
||||
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
|
||||
try {
|
||||
// Initialize store
|
||||
await runCli(["init"], tmpStore);
|
||||
|
||||
// Create schema
|
||||
const schemaFile = join(tmpStore, "schema.json");
|
||||
writeFileSync(
|
||||
schemaFile,
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
}),
|
||||
);
|
||||
const { stdout: schemaHash } = await runCli(
|
||||
["schema", "put", schemaFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Create node
|
||||
const nodeFile = join(tmpStore, "node.json");
|
||||
writeFileSync(nodeFile, JSON.stringify({ name: "Alice" }));
|
||||
const { stdout: nodeHash } = await runCli(
|
||||
["put", schemaHash.trim(), nodeFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Create template file (JSON-encoded string)
|
||||
const templateFile = join(tmpStore, "template.json");
|
||||
writeFileSync(templateFile, JSON.stringify("Hello {{ payload.name }}!"));
|
||||
const { stdout: tmplHash } = await runCli(
|
||||
["put", "@string", templateFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Register template
|
||||
await runCli(
|
||||
[
|
||||
"var",
|
||||
"set",
|
||||
`@ucas/template/text/${schemaHash.trim()}`,
|
||||
tmplHash.trim(),
|
||||
],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Render with template
|
||||
const { stdout: output, exitCode } = await runCli(
|
||||
["render", nodeHash.trim()],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toBe("Hello Alice!");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("6.2 CLI with Template + Custom Decay", async () => {
|
||||
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
|
||||
try {
|
||||
await runCli(["init"], tmpStore);
|
||||
|
||||
// Create schema with child ref
|
||||
const schemaFile = join(tmpStore, "schema.json");
|
||||
writeFileSync(
|
||||
schemaFile,
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
properties: {
|
||||
value: { type: "string" },
|
||||
child: {
|
||||
anyOf: [{ type: "string", format: "cas_ref" }, { type: "null" }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const { stdout: schemaHash } = await runCli(
|
||||
["schema", "put", schemaFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Create child node
|
||||
const childFile = join(tmpStore, "child.json");
|
||||
writeFileSync(childFile, JSON.stringify({ value: "child", child: null }));
|
||||
const { stdout: childHash } = await runCli(
|
||||
["put", schemaHash.trim(), childFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Create parent node
|
||||
const parentFile = join(tmpStore, "parent.json");
|
||||
writeFileSync(
|
||||
parentFile,
|
||||
JSON.stringify({ value: "parent", child: childHash.trim() }),
|
||||
);
|
||||
const { stdout: parentHash } = await runCli(
|
||||
["put", schemaHash.trim(), parentFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Create template showing resolution (JSON-encoded string)
|
||||
const templateFile = join(tmpStore, "template.json");
|
||||
writeFileSync(
|
||||
templateFile,
|
||||
JSON.stringify("{{ payload.value }}(res={{ resolution }})"),
|
||||
);
|
||||
const { stdout: tmplHash } = await runCli(
|
||||
["put", "@string", templateFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Register template
|
||||
await runCli(
|
||||
[
|
||||
"var",
|
||||
"set",
|
||||
`@ucas/template/text/${schemaHash.trim()}`,
|
||||
tmplHash.trim(),
|
||||
],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Render with custom decay
|
||||
const { stdout: output, exitCode } = await runCli(
|
||||
["render", parentHash.trim(), "--decay", "0.7"],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain("parent(res=1)");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("6.3 CLI with Template + All Parameters", async () => {
|
||||
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
|
||||
try {
|
||||
await runCli(["init"], tmpStore);
|
||||
|
||||
const schemaFile = join(tmpStore, "schema.json");
|
||||
writeFileSync(
|
||||
schemaFile,
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
}),
|
||||
);
|
||||
const { stdout: schemaHash } = await runCli(
|
||||
["schema", "put", schemaFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
const nodeFile = join(tmpStore, "node.json");
|
||||
writeFileSync(nodeFile, JSON.stringify({ name: "Bob" }));
|
||||
const { stdout: nodeHash } = await runCli(
|
||||
["put", schemaHash.trim(), nodeFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// Create template (JSON-encoded string)
|
||||
const templateFile = join(tmpStore, "template.json");
|
||||
writeFileSync(
|
||||
templateFile,
|
||||
JSON.stringify("Greetings {{ payload.name }}!"),
|
||||
);
|
||||
const { stdout: tmplHash } = await runCli(
|
||||
["put", "@string", templateFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
await runCli(
|
||||
[
|
||||
"var",
|
||||
"set",
|
||||
`@ucas/template/text/${schemaHash.trim()}`,
|
||||
tmplHash.trim(),
|
||||
],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
const { stdout: output, exitCode } = await runCli(
|
||||
[
|
||||
"render",
|
||||
nodeHash.trim(),
|
||||
"--resolution",
|
||||
"0.8",
|
||||
"--decay",
|
||||
"0.6",
|
||||
"--epsilon",
|
||||
"0.005",
|
||||
],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toBe("Greetings Bob!");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("6.4 CLI with Non-templated Node (YAML Fallback)", async () => {
|
||||
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
|
||||
try {
|
||||
await runCli(["init"], tmpStore);
|
||||
|
||||
const schemaFile = join(tmpStore, "schema.json");
|
||||
writeFileSync(
|
||||
schemaFile,
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
}),
|
||||
);
|
||||
const { stdout: schemaHash } = await runCli(
|
||||
["schema", "put", schemaFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
const nodeFile = join(tmpStore, "node.json");
|
||||
writeFileSync(nodeFile, JSON.stringify({ name: "Charlie" }));
|
||||
const { stdout: nodeHash } = await runCli(
|
||||
["put", schemaHash.trim(), nodeFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
// No template registered - should fall back to YAML
|
||||
const { stdout: output, exitCode } = await runCli(
|
||||
["render", nodeHash.trim()],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(output).toContain("name:");
|
||||
expect(output).toContain("Charlie");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("6.5 CLI Error: Invalid Decay Value", async () => {
|
||||
const tmpStore = mkdtempSync(join(tmpdir(), "json-cas-test-"));
|
||||
try {
|
||||
await runCli(["init"], tmpStore);
|
||||
|
||||
const schemaFile = join(tmpStore, "schema.json");
|
||||
writeFileSync(
|
||||
schemaFile,
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
properties: { name: { type: "string" } },
|
||||
}),
|
||||
);
|
||||
const { stdout: schemaHash } = await runCli(
|
||||
["schema", "put", schemaFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
const nodeFile = join(tmpStore, "node.json");
|
||||
writeFileSync(nodeFile, JSON.stringify({ name: "Test" }));
|
||||
const { stdout: nodeHash } = await runCli(
|
||||
["put", schemaHash.trim(), nodeFile],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
const { exitCode, stderr } = await runCli(
|
||||
["render", nodeHash.trim(), "--decay", "1.5"],
|
||||
tmpStore,
|
||||
);
|
||||
|
||||
expect(exitCode).not.toBe(0);
|
||||
expect(stderr).toContain("decay");
|
||||
} finally {
|
||||
rmSync(tmpStore, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
InvalidVariableNameError,
|
||||
putSchema,
|
||||
refs,
|
||||
render,
|
||||
renderAsync,
|
||||
TagLabelConflictError,
|
||||
VariableNotFoundError,
|
||||
validate,
|
||||
@@ -421,8 +421,14 @@ async function cmdRender(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
const output = render(store, hash, { resolution, decay, epsilon });
|
||||
// Output to stdout without JSON wrapping (raw YAML)
|
||||
const varStore = openVarStore();
|
||||
const output = await renderAsync(store, hash, {
|
||||
resolution,
|
||||
decay,
|
||||
epsilon,
|
||||
varStore,
|
||||
});
|
||||
// Output to stdout without JSON wrapping (raw output)
|
||||
process.stdout.write(output);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -167,7 +167,7 @@ async function renderNode(
|
||||
varStore: VariableStore,
|
||||
hash: Hash,
|
||||
currentResolution: number,
|
||||
globalDecay: number,
|
||||
_globalDecay: number,
|
||||
epsilon: number,
|
||||
visited: Set<Hash>,
|
||||
): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user