nexu-io/open-design · error · Error

unsupported template binding path: ${rawPath}

Error message

unsupported template binding path: ${rawPath}

What it means

Thrown by readTemplatePath when the first segment of a binding path (before the first '.') is not literally 'data'. All global bindings must root at the data object: {{data.title}}, {{data.items.0.name}}, etc. A path that does not start with 'data' is treated as unsupported at the root scope; loop-variable-scoped bindings are handled by a different resolver (childResolver) and never reach this check.

Source

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

function walkPath(root: unknown, segments: string[], rawPath: string): unknown {
  let current: unknown = root;
  for (const segment of segments) {
    if (current === null || current === undefined) return '';
    if (Array.isArray(current)) {
      if (!/^\d+$/.test(segment)) throw new Error(`invalid array segment in template binding path: ${rawPath}`);
      current = current[Number(segment)];
      continue;
    }
    if (typeof current !== 'object') return '';
    current = (current as Record<string, unknown>)[segment];
  }
  return current ?? '';
}

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);
  };
}

View on GitHub (pinned to 5be4028344)

Solutions

  1. Prefix root bindings with 'data.': {{data.title}} instead of {{title}}.
  2. For loop content use a repeat directive and bind via the loop variable name: {{item.field}}.

Example fix

// before
<div>{{title}}</div>
// after
<div>{{data.title}}</div>
Defensive patterns

Strategy: validation

Validate before calling

function assertRootedAtData(binding: string): void {
  if (binding.split('.')[0] !== 'data') throw new Error(`binding must start with data.: ${binding}`);
}

Type guard

function isDataRootedBinding(b: string): boolean { return b.startsWith('data.') || b === 'data'; }

Prevention

When it happens

Trigger: Binding {{title}} (missing data. prefix); {{config.title}}; {{metadata.tags}}; any root binding whose head segment isn't 'data'. Note: child loop variables like {{item.label}} are valid inside a repeat because childResolver handles them first.

Common situations: Model omits the data. prefix (common in Handlebars/Mustache where the root is implicit); template ported from a framework that allows bare names; misunderstanding that only loop variables (inside a repeat) and 'data.*' are valid heads.

Related errors


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