remotion-dev/remotion · error

Expected the JSX name attribute to be a string

Error message

Expected the JSX name attribute to be a string

What it means

duplicateJsxNodes (Remotion Studio codemod that duplicates a JSX element in a composition's source) rewrites a Sequence's `name` prop to `<old>-copy` when duplicating. When a `name` attribute exists, the library expects its value to be a string literal (optionally wrapped in `{}`); anything else cannot be uniquely renamed, so it throws. In practice the attribute filter already requires a StringLiteral, so this is an internal invariant that fires only on unexpected AST shapes (e.g. a JSXExpressionContainer with an empty or non-string expression).

Solutions

  1. Change the `name` prop to a static string literal, e.g. name="MySeq", before duplicating
  2. Rename the attribute so it is not exactly `name` if a dynamic value is intentional
  3. Parse your input and assert the name attribute value type before calling the API
  4. Report the AST shape if you believe a valid string name triggered this — it indicates an invariant bug

Example fix

// before
<Sequence name={title}>...</Sequence>
// after
<Sequence name="Title">...</Sequence>
Defensive patterns

Strategy: validation

Validate before calling

const nameAttr = element.openingElement.attributes.find((a) => a.type === 'JSXAttribute' && a.name.name === 'name');
if (nameAttr && !(nameAttr.value?.type === 'StringLiteral' || (nameAttr.value?.type === 'JSXExpressionContainer' && nameAttr.value.expression.type === 'StringLiteral'))) {
  throw new Error('name prop must be a static string before duplicating');
}

Type guard

const isStringNameAttr = (attr: JSXAttribute | null | undefined): attr is JSXAttribute & {value: StringLiteral} =>
  !!attr && (attr.value?.type === 'StringLiteral' || (attr.value?.type === 'JSXExpressionContainer' && attr.value.expression.type === 'StringLiteral'));

Try / catch

try { await duplicateJsxNodes({input, nodePaths}); } catch (e) { if ((e as Error).message.includes('name attribute to be a string')) { /* fall back: duplicate without rename or ask user to make name static */ } else throw e; }

Prevention

When it happens

Trigger: Calling duplicateJsxNodes / getDuplicatedJsxSource on source where an element has a `name` attribute whose value is a JSXExpressionContainer wrapping something other than a string literal (e.g. `name={getTitle()}`), or a malformed/container-valued name attribute that slipped past the filter.

Common situations: Duplicating a Sequence whose name prop is computed dynamically (`name={someVar}`) instead of a static string; programmatically generated JSX with container-wrapped name values; older hand-written compositions where name was dynamic.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of remotion-dev/remotion@46a3a6bf13 (2026-09-18). Data as JSON: /api/errors/808275803fa27827. Report an issue: GitHub.

Appendix: source

Thrown at packages/studio-codemods/src/duplicate-jsx-node.ts:204

	const end = recastLocToOffset(input, element.loc.end);
	let source = input.slice(start, end);
	const nameAttribute = element.openingElement.attributes.find(
		(attribute) =>
			attribute.type === 'JSXAttribute' &&
			attribute.name.type === 'JSXIdentifier' &&
			attribute.name.name === 'name' &&
			(attribute.value?.type === 'StringLiteral' ||
				(attribute.value?.type === 'JSXExpressionContainer' &&
					attribute.value.expression.type === 'StringLiteral')),
	);

	if (nameAttribute?.type === 'JSXAttribute') {
		const value =
			nameAttribute.value?.type === 'JSXExpressionContainer'
				? nameAttribute.value.expression
				: nameAttribute.value;
		if (value?.type !== 'StringLiteral') {
			throw new Error('Expected the JSX name attribute to be a string');
		}

		const edit = getJsxStringAttributeValueSourceEdit({
			attribute: nameAttribute,
			input,
			newValue: `${value.value}-copy`,
		});
		source =
			source.slice(0, edit.start - start) +
			edit.replacement +
			source.slice(edit.end - start);
	}

	const originalIndent = getLineIndent({input, offset: start});
	return source
		.split(/\r?\n/)
		.map((line, index) => {
			if (index === 0) {

View on GitHub (pinned to 46a3a6bf13)