nexu-io/open-design · error · Error

invalid template binding path: ${binding}

Error message

invalid template binding path: ${binding}

What it means

Thrown by rootResolver when a binding does not match TEMPLATE_PATH (/^(?:data|[A-Za-z_][A-Za-z0-9_]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$/) or does not start with 'data'. The root scope only accepts paths rooted at data.* with segments that are either identifiers (letters, digits, _, trailing - allowed) or pure-numeric array indices. Anything else — empty segments, disallowed characters, leading digits, dotted reserved words — is rejected.

Source

Thrown at apps/daemon/src/live-artifacts/render.ts:87

}

function readTemplatePath(dataJson: BoundedJsonObject, rawPath: string): unknown {
  const segments = rawPath.split('.');
  if (segments.shift() !== 'data') throw new Error(`unsupported template binding path: ${rawPath}`);
  return walkPath(dataJson, segments, rawPath);
}

function scalarOrThrow(value: unknown, binding: string): string {
  if (Array.isArray(value) || (value !== null && typeof value === 'object')) {
    throw new Error(`template binding must resolve to a scalar: ${binding}`);
  }
  return escapeHtmlTemplateValue(value);
}

function rootResolver(dataJson: BoundedJsonObject): BindingResolver {
  return (binding) => {
    if (!TEMPLATE_PATH.test(binding) || !binding.startsWith('data')) {
      throw new Error(`invalid template binding path: ${binding}`);
    }
    return scalarOrThrow(readTemplatePath(dataJson, binding), binding);
  };
}

function childResolver(parent: BindingResolver, varName: string, item: unknown): BindingResolver {
  return (binding) => {
    if (!TEMPLATE_PATH.test(binding)) throw new Error(`invalid template binding path: ${binding}`);
    const segments = binding.split('.');
    if (segments[0] !== varName) return parent(binding);
    return scalarOrThrow(walkPath(item, segments.slice(1), binding), binding);
  };
}

function interpolateScalars(fragment: string, resolve: BindingResolver): string {
  return fragment.replace(TEMPLATE_INTERPOLATION, (_match, rawBinding: string) => resolve(rawBinding.trim()));
}

View on GitHub (pinned to 5be4028344)

Solutions

  1. Keep segment names to identifiers matching [A-Za-z_][A-Za-z0-9_-]*; use numeric segments only for array indices.
  2. If a data key has an unsupported name, project/rename it in the refresh source output to a safe key.
  3. Use dot-only paths — no brackets, no wildcards, no array[n] syntax.

Example fix

// before
<div>{{data.repo.0-full-name}}</div>
// after
<div>{{data.repo.full_name}}</div>
Defensive patterns

Strategy: validation

Validate before calling

const TEMPLATE_PATH = /^(?:data|[A-Za-z_][A-Za-z0-9_]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$/;
function assertValidBinding(b: string): void {
  if (!TEMPLATE_PATH.test(b) || !b.startsWith('data')) throw new Error(`invalid binding: ${b}`);
}

Type guard

function isValidRootBinding(b: string): boolean {
  return /^(?:data|[A-Za-z_][A-Za-z0-9_]*)(?:\.(?:[A-Za-z_][A-Za-z0-9_-]*|\d+))*$/.test(b) && b.startsWith('data');
}

Prevention

When it happens

Trigger: {{data.foo-bar.baz}} where a segment starts with a hyphen; {{data.1x}} (segment starting with digit but not a pure integer); {{data.a.}} (trailing dot, empty segment); {{data..x}}; {{DATA.title}} (case-sensitive head); bindings starting with a reserved word that isn't 'data'.

Common situations: Field names from the refresh source contain characters the DSL doesn't allow (e.g. data keys with leading digits or unusual punctuation); model invents a path syntax (e.g. array[n] bracket syntax); typo introducing an empty segment; uppercase DATA vs data.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/a6eebfc411e997f3. Report an issue: GitHub.