diff --git a/CHANGELOG.md b/CHANGELOG.md index 65bdce7..89670ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## Unreleased +- Fix: `.vue` Single File Components now extract their ` close tag + pos = m.end() + if lang is None: + lang_m = _VUE_SCRIPT_LANG_RE.search(m.group(1)) + if lang_m: + lang = lang_m.group(1).lower() + out.append(_blank(src[pos:])) + return "".join(out), lang + + +def extract_vue(path: Path) -> dict: + """Extract imports, symbols, and type refs from a ``.vue`` SFC. + + Masks the non-``\n" + ) + masked, lang = _vue_mask_non_script(src) + assert lang == "ts" + # Same number of lines (newlines preserved) so line numbers are stable. + assert masked.count("\n") == src.count("\n") + # Template content is gone; the script body survives verbatim. + assert "div" not in masked + assert "const msg = 'hi'" in masked + # The script body sits on the same line it does in the source (line 6). + assert masked.splitlines()[5].strip() == "const msg = 'hi'" + + +def test_script_setup_ts_static_imports_resolve(tmp_path): + _write(tmp_path / "Child.vue", "\n") + _write(tmp_path / "utils/helper.ts", "export function helper(){}\n") + comp = _write( + tmp_path / "Comp.vue", + """ + + +""", + ) + result = extract_vue(comp) + targets = _targets(result, relation="imports_from") + assert _make_id(str(tmp_path / "Child.vue")) in targets + assert _make_id(str(tmp_path / "utils/helper.ts")) in targets + + +def test_script_setup_extracts_symbols_with_correct_lines(tmp_path): + comp = _write( + tmp_path / "Widget.vue", + """ + + +""", + ) + result = extract_vue(comp) + by_label = {n["label"]: n for n in result["nodes"]} + assert "count" in by_label + assert "onClick()" in by_label + # count is declared on line 8, onClick on line 10 of the SFC. + assert by_label["count"]["source_location"] == "L8" + assert by_label["onClick()"]["source_location"] == "L10" + + +def test_typed_props_reference_imported_type(tmp_path): + _write(tmp_path / "types.ts", "export interface Thing { id: number }\n") + comp = _write( + tmp_path / "Typed.vue", + """ + + +""", + ) + result = extract_vue(comp) + # The imported type is referenced from the script. + assert _make_id(str(tmp_path / "types.ts")) in _targets(result, relation="imports_from") + + +def test_two_script_blocks_both_parsed(tmp_path): + """Vue allows a classic `` + + + + +""", + ) + result = extract_vue(comp) + targets = _targets(result, relation="imports_from") + assert _make_id(str(tmp_path / "a.ts")) in targets + assert _make_id(str(tmp_path / "b.ts")) in targets + + +def test_dynamic_import_recovered(tmp_path): + _write(tmp_path / "Lazy.vue", "\n") + comp = _write( + tmp_path / "Host.vue", + """ + + +""", + ) + result = extract_vue(comp) + assert _make_id(str(tmp_path / "Lazy.vue")) in _targets(result, relation="dynamic_import") + + +def test_plain_js_script_block(tmp_path): + _write(tmp_path / "dep.js", "export const x = 1\n") + comp = _write( + tmp_path / "Legacy.vue", + """ + + +""", + ) + result = extract_vue(comp) + assert _make_id(str(tmp_path / "dep.js")) in _targets(result, relation="imports_from") + + +def test_template_only_file_does_not_crash(tmp_path): + comp = _write(tmp_path / "Static.vue", "\n") + result = extract_vue(comp) + assert isinstance(result, dict) + # Only the file node; no script means no imports/symbols. + assert _targets(result, relation="imports_from") == set() + + +def test_whole_file_to_js_grammar_would_extract_nothing(tmp_path): + """The SFC must not be parsed as one JS blob. + + With the bug, a real SFC yields just the file node; after the fix it yields + its imports. + """ + _write(tmp_path / "dep.ts", "export const v = 1\n") + comp = _write( + tmp_path / "Guard.vue", + """ + + +""", + ) + result = extract_vue(comp) + assert _make_id(str(tmp_path / "dep.ts")) in _targets(result, relation="imports_from") + + +def test_vue_joins_cross_file_symbol_resolution(tmp_path): + """A ``.vue`` calling an imported function wires to the real symbol across files. + + The SFC's calls should resolve like any ``.ts`` file's would. + """ + helper = _write(tmp_path / "helper.ts", "export function helper() {}\n") + comp = _write( + tmp_path / "Caller.vue", + """ + + +""", + ) + result = extract([comp, helper], cache_root=tmp_path) + by_label = {n["label"]: n["id"] for n in result["nodes"]} + edges = {(e["source"], e["target"], e["relation"]) for e in result["edges"]} + assert (by_label["go()"], by_label["helper()"], "calls") in edges + + + +def test_generic_component_open_tag_with_angle_brackets(tmp_path): + """A Vue 3.3+ generic= attribute containing '>' (e.g. Record) + must not prematurely end the +""", + ) + result = extract_vue(comp) + # the import inside the script body is recovered (body wasn't masked away) + assert _make_id(str(tmp_path / "utils/helper.ts")) in _targets(result, relation="imports_from") + # and no stray '">' leaked from the open tag into a parse error + masked, lang = _vue_mask_non_script(comp.read_text(encoding="utf-8")) + assert lang == "ts" + assert 'generic="T extends Record' not in masked # open tag fully blanked + assert "import { helper }" in masked # body preserved