withastro/astro · error · Error

Expected a matching import for component

Error message

Expected a matching import for component `${tagName}`. Did you forget to import it?

What it means

The hast-astro-metadata pass (in the mdx integration's satteri module) collects island metadata for MDX builds: every component element carrying a `client:` directive or `server:defer` must match an import specifier so its module can be located for hydration/defer metadata. When `findMatchingImport` returns nothing, it throws with the tag name and a hint about the missing import. It is the metadata-collection sibling of the NoMatchingImport error.

Solutions

  1. Import the component in the same .mdx file with a name matching the tag.
  2. Verify the import specifier is not named-vs-default mismatched (import the default export under the tag's name).
  3. Drop the directive if the tag is plain HTML or should not hydrate/defer.
  4. Note `server:defer` components are also subject to this check — import them too.

Example fix

import Hero from '../../components/Hero';

<Hero server:defer />
Defensive patterns

Strategy: validation

Validate before calling

// CI check: components used with client:* OR server:defer in MDX must be imported
import { globSync } from 'glob';
import fs from 'node:fs';
const importRe = /import\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
const tagRe = /<([A-Z][\w.]*)[^>]*?\s(?:client:[a-z]+|server:defer)/g;
for (const f of globSync('src/**/*.mdx')) {
  const src = fs.readFileSync(f, 'utf8');
  const imported = new Set([...src.matchAll(importRe)].map((m) => m[1]));
  for (const m of src.matchAll(tagRe)) {
    if (!imported.has(m[1])) throw new Error(`${f}: <${m[1]}> used with client:*/server:defer without an import`);
  }
}

Prevention

When it happens

Trigger: An .mdx file using `<Widget client:visible />` or `<Widget server:defer />` where no import named Widget exists, or the imported name differs from the tag.

Common situations: Omitting imports when copying component examples into MDX; using `server:defer` on a component that was never imported; renaming an import without updating its usages.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/53cd5d5faefcbdc4. Report an issue: GitHub.

Appendix: source

Thrown at packages/integrations/mdx/src/satteri/hast-astro-metadata.ts:104

	}
}

function processJsxNode(
	node: MdxJsxHastNode,
	ctx: HastVisitorContext,
	imports: Map<string, Set<ImportSpecifier>>,
	filePath: string,
) {
	const tagName = node.name;
	if (!tagName || !isComponent(tagName)) return;

	const hasClient = hasDirective(node, 'client:');
	const hasServerDefer = !hasClient && hasDirective(node, 'server:defer');
	if (!hasClient && !hasServerDefer) return;

	const matchedImport = findMatchingImport(tagName, imports);
	if (!matchedImport) {
		throw new Error(
			`Expected a matching import for component \`${tagName}\`. Did you forget to import it?`,
		);
	}

	if (matchedImport.path.endsWith('.astro') && hasClient) {
		let clientAttr = 'client:*';
		for (const a of node.attributes) {
			if (a.type === 'mdxJsxAttribute' && a.name.startsWith('client:')) {
				clientAttr = a.name;
				break;
			}
		}
		console.warn(
			`You are attempting to render <${tagName} ${clientAttr} />, but ${tagName} is an Astro component. Astro components do not render in the client and should not have a hydration directive. Please use a framework component for client rendering.`,
		);
	}

	const resolvedPath = resolvePath(matchedImport.path, filePath);

View on GitHub (pinned to 52e6c34790)