diff --git a/graphify/detect.py b/graphify/detect.py
index 630c638f..a7a31b5d 100644
--- a/graphify/detect.py
+++ b/graphify/detect.py
@@ -24,7 +24,7 @@ class FileType(str, Enum):
_MANIFEST_PATH = "graphify-out/manifest.json"
-CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk'}
+CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk'}
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.txt', '.rst', '.html', '.yaml', '.yml'}
PAPER_EXTENSIONS = {'.pdf'}
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
diff --git a/graphify/extract.py b/graphify/extract.py
index 80da56ad..fe2916ff 100644
--- a/graphify/extract.py
+++ b/graphify/extract.py
@@ -2123,6 +2123,142 @@ def extract_svelte(path: Path) -> dict:
return result
+def extract_astro(path: Path) -> dict:
+ """Extract imports from .astro files: frontmatter (TS) + template regex fallback.
+
+ Astro files start with a ``---\\n...\\n---`` frontmatter block of TypeScript
+ setup code (where almost all imports live), followed by an HTML-with-expressions
+ template body, and optionally ``", _re.IGNORECASE
+ )
+ static_import_re = _re.compile(
+ r"""import\s+(?:[^'"`;]+?\s+from\s+)?['"]([^'"]+)['"]"""
+ )
+ regions: list[str] = []
+ fm = frontmatter_re.search(src)
+ if fm:
+ regions.append(fm.group(1))
+ for script_match in script_re.finditer(src):
+ regions.append(script_match.group(1))
+ for region in regions:
+ for m in static_import_re.finditer(region):
+ raw = m.group(1)
+ if not raw:
+ continue
+ if raw.startswith("."):
+ resolved = Path(os.path.normpath(path.parent / raw))
+ if resolved.suffix == ".js":
+ resolved = resolved.with_suffix(".ts")
+ elif resolved.suffix == ".jsx":
+ resolved = resolved.with_suffix(".tsx")
+ node_id = _make_id(str(resolved))
+ stub_source_file = str(resolved)
+ else:
+ resolved_alias = None
+ for alias_prefix, alias_base in aliases.items():
+ if raw == alias_prefix or raw.startswith(alias_prefix + "/"):
+ rest = raw[len(alias_prefix):].lstrip("/")
+ resolved_alias = Path(os.path.normpath(Path(alias_base) / rest))
+ break
+ if resolved_alias is not None:
+ node_id = _make_id(str(resolved_alias))
+ stub_source_file = str(resolved_alias)
+ else:
+ module_name = raw.split("/")[-1]
+ if not module_name:
+ continue
+ node_id = _make_id(module_name)
+ stub_source_file = raw
+ if node_id in existing_ids:
+ result.setdefault("edges", []).append({
+ "source": file_node_id, "target": node_id,
+ "relation": "imports_from", "confidence": "EXTRACTED",
+ "source_file": str(path),
+ })
+ continue
+ result.setdefault("nodes", []).append({
+ "id": node_id, "label": raw,
+ "file_type": "code", "source_file": stub_source_file,
+ "confidence": "EXTRACTED",
+ })
+ result.setdefault("edges", []).append({
+ "source": file_node_id, "target": node_id,
+ "relation": "imports_from", "confidence": "EXTRACTED",
+ "source_file": str(path),
+ })
+ existing_ids.add(node_id)
+ except Exception:
+ pass
+ return result
+
+
def extract_java(path: Path) -> dict:
"""Extract classes, interfaces, methods, constructors, and imports from a .java file."""
return _extract_generic(path, _JAVA_CONFIG)
@@ -5490,6 +5626,7 @@ _DISPATCH: dict[str, Any] = {
".F08": extract_fortran,
".vue": extract_js,
".svelte": extract_svelte,
+ ".astro": extract_astro,
".dart": extract_dart,
".v": extract_verilog,
".sv": extract_verilog,
diff --git a/tests/test_astro_extraction.py b/tests/test_astro_extraction.py
new file mode 100644
index 00000000..c21e66c2
--- /dev/null
+++ b/tests/test_astro_extraction.py
@@ -0,0 +1,143 @@
+"""Tests for `.astro` extraction (#850).
+
+Astro files have a TypeScript frontmatter block (`---...---`) at the top where
+nearly all imports live, followed by an HTML-with-expressions template and
+optionally `
+""",
+ )
+ layout = _write(tmp_path / "src/layouts/Layout.astro", "---\n---\n