mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
Two lint rules + render-time validation built on top of the existing
data-composition-variables schema.
Lint rules (packages/core/src/lint/rules/composition.ts):
- invalid_variable_values_json — host's data-variable-values must parse as
a JSON object. Today the runtime swallows parse failures silently and
falls back to declared defaults, masking typos.
- invalid_composition_variables_declaration — root <html>'s
data-composition-variables must parse as an array of objects with
`id` (string), `type` (one of string/number/color/boolean/enum), `label`
(string), and `default`. Per-entry findings report which fields are
missing or invalid.
Both rules read attributes via a new `readJsonAttr` helper in lint/utils.ts.
The existing `readAttr` regex `["']([^"']+)["']` truncates JSON-in-attribute
values at the first internal quote (e.g. `data-variable-values='{"x":"y"}'`
captures only `{`); `readJsonAttr` alternates double-vs-single-quoted
branches with quote-specific char classes so JSON values round-trip cleanly.
A second helper `findHtmlTag` returns the actual <html> open tag (where
data-composition-variables lives) — distinct from `findRootTag` which
returns the first in-body composition element.
Render-time validation (packages/core/src/runtime/validateVariables.ts):
- validateVariables(values, declarations) returns a structured array of
issues: undeclared keys, type mismatches, enum-out-of-range values.
Pure / sync; works in any environment.
- formatVariableValidationIssue(issue) renders a one-line user-facing
string for CLI output.
- Both exported from @hyperframes/core for studio/tooling reuse.
CLI integration (packages/cli/src/commands/render.ts):
- New --strict-variables flag. Default behavior: print warnings and
continue. With --strict-variables: print warnings then exit 1.
- New `validateVariablesAgainstProject(indexPath, values)` helper:
reads the project's index.html, runs extractCompositionMetadata to
pull the declared schema, validates the CLI's --variables payload
against it. ensureDOMParser polyfill for Node-side parsing (same
pattern as compositions.ts).
Tests:
- 11 new validateVariables unit tests covering happy path, undeclared
keys, type mismatches (string/number/boolean/color/enum), enum range,
multiple-issue aggregation, and formatter output.
- 11 new composition.test.ts cases for both lint rules: parse errors,
shape errors, per-entry validation, unknown types, missing fields,
positive cases.
- 5 new render.test.ts cases for validateVariablesAgainstProject:
no-declarations, happy path, undeclared, type-mismatch, missing-file.
- All 646 core tests + 213 cli tests still green.
Docs:
- docs/packages/cli.mdx — added --strict-variables flag row.
This is PR 3 of a 4-PR stack. PR 4 ships skill/scaffold distribution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.0 KiB
TypeScript
130 lines
4.0 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { validateVariables, formatVariableValidationIssue } from "./validateVariables";
|
|
import type { CompositionVariable } from "../core.types";
|
|
|
|
const DECLS: readonly CompositionVariable[] = [
|
|
{ id: "title", type: "string", label: "Title", default: "Hello" },
|
|
{ id: "count", type: "number", label: "Count", default: 0 },
|
|
{ id: "active", type: "boolean", label: "Active", default: true },
|
|
{ id: "color", type: "color", label: "Color", default: "#000000" },
|
|
{
|
|
id: "theme",
|
|
type: "enum",
|
|
label: "Theme",
|
|
default: "light",
|
|
options: [
|
|
{ value: "light", label: "Light" },
|
|
{ value: "dark", label: "Dark" },
|
|
],
|
|
},
|
|
];
|
|
|
|
describe("validateVariables", () => {
|
|
it("returns no issues when every value matches its declaration", () => {
|
|
expect(
|
|
validateVariables(
|
|
{ title: "Q4", count: 3, active: false, color: "#abcdef", theme: "dark" },
|
|
DECLS,
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("returns no issues for an empty values map", () => {
|
|
expect(validateVariables({}, DECLS)).toEqual([]);
|
|
});
|
|
|
|
it("flags undeclared keys", () => {
|
|
expect(validateVariables({ title: "x", extra: 1 }, DECLS)).toEqual([
|
|
{ kind: "undeclared", variableId: "extra" },
|
|
]);
|
|
});
|
|
|
|
it("flags string vs number mismatches", () => {
|
|
expect(validateVariables({ count: "three" }, DECLS)).toEqual([
|
|
{ kind: "type-mismatch", variableId: "count", expected: "number", actual: "string" },
|
|
]);
|
|
});
|
|
|
|
it("flags non-finite numbers as type mismatches", () => {
|
|
expect(validateVariables({ count: Number.NaN }, DECLS)).toEqual([
|
|
{ kind: "type-mismatch", variableId: "count", expected: "number", actual: "number" },
|
|
]);
|
|
});
|
|
|
|
it("flags boolean mismatches", () => {
|
|
expect(validateVariables({ active: "true" }, DECLS)).toEqual([
|
|
{ kind: "type-mismatch", variableId: "active", expected: "boolean", actual: "string" },
|
|
]);
|
|
});
|
|
|
|
it("flags non-string color values", () => {
|
|
expect(validateVariables({ color: 0xff0000 }, DECLS)).toEqual([
|
|
{ kind: "type-mismatch", variableId: "color", expected: "color", actual: "number" },
|
|
]);
|
|
});
|
|
|
|
it("flags enum values not in the allowed set", () => {
|
|
expect(validateVariables({ theme: "midnight" }, DECLS)).toEqual([
|
|
{
|
|
kind: "enum-out-of-range",
|
|
variableId: "theme",
|
|
allowed: ["light", "dark"],
|
|
actual: "midnight",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("flags non-string enum values as type mismatches", () => {
|
|
expect(validateVariables({ theme: 1 }, DECLS)).toEqual([
|
|
{ kind: "type-mismatch", variableId: "theme", expected: "enum (string)", actual: "number" },
|
|
]);
|
|
});
|
|
|
|
it("returns multiple issues at once", () => {
|
|
const issues = validateVariables({ title: 42, theme: "neon", extra: true }, DECLS);
|
|
expect(issues).toContainEqual({
|
|
kind: "type-mismatch",
|
|
variableId: "title",
|
|
expected: "string",
|
|
actual: "number",
|
|
});
|
|
expect(issues).toContainEqual({
|
|
kind: "enum-out-of-range",
|
|
variableId: "theme",
|
|
allowed: ["light", "dark"],
|
|
actual: "neon",
|
|
});
|
|
expect(issues).toContainEqual({ kind: "undeclared", variableId: "extra" });
|
|
});
|
|
});
|
|
|
|
describe("formatVariableValidationIssue", () => {
|
|
it("formats undeclared issues", () => {
|
|
expect(formatVariableValidationIssue({ kind: "undeclared", variableId: "extra" })).toBe(
|
|
'Variable "extra" is not declared in data-composition-variables.',
|
|
);
|
|
});
|
|
|
|
it("formats type-mismatch issues", () => {
|
|
expect(
|
|
formatVariableValidationIssue({
|
|
kind: "type-mismatch",
|
|
variableId: "count",
|
|
expected: "number",
|
|
actual: "string",
|
|
}),
|
|
).toBe('Variable "count" expected number, got string.');
|
|
});
|
|
|
|
it("formats enum-out-of-range issues", () => {
|
|
expect(
|
|
formatVariableValidationIssue({
|
|
kind: "enum-out-of-range",
|
|
variableId: "theme",
|
|
allowed: ["light", "dark"],
|
|
actual: "neon",
|
|
}),
|
|
).toBe('Variable "theme" must be one of "light", "dark" (got "neon").');
|
|
});
|
|
});
|