Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions desloppify/languages/_framework/frameworks/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,26 @@

FRAMEWORK_SPECS: dict[str, FrameworkSpec] = {}

# Per-ecosystem cache for ``framework_source_extensions``. Invalidated by any
# registry mutation (``register_framework_spec``). Keyed by the normalized
# ecosystem string ("" for the unfiltered query) so each call shape gets its
# own slot. In production the registry is immutable after startup, so this
# caches a single tuple per ecosystem for the process lifetime; in tests the
# fixture-driven mutations invalidate it cleanly.
_EXTENSIONS_CACHE: dict[str, tuple[str, ...]] = {}


def _invalidate_extensions_cache() -> None:
_EXTENSIONS_CACHE.clear()


def register_framework_spec(spec: FrameworkSpec) -> None:
"""Register a framework spec by id."""
key = str(spec.id or "").strip()
if not key:
raise ValueError("FrameworkSpec.id must be non-empty")
FRAMEWORK_SPECS[key] = spec
_invalidate_extensions_cache()


def get_framework_spec(framework_id: str) -> FrameworkSpec | None:
Expand All @@ -39,19 +52,56 @@ def _register_builtin_specs() -> None:
"""Register built-in framework specs shipped with the repo."""
if FRAMEWORK_SPECS:
return
from .specs.astro import ASTRO_SPEC
from .specs.nextjs import NEXTJS_SPEC
from .specs.svelte import SVELTE_SPEC
from .specs.vue import VUE_SPEC

register_framework_spec(NEXTJS_SPEC)
register_framework_spec(ASTRO_SPEC)
register_framework_spec(SVELTE_SPEC)
register_framework_spec(VUE_SPEC)


def ensure_builtin_specs_loaded() -> None:
"""Idempotently load built-in framework specs."""
_register_builtin_specs()


def framework_source_extensions(ecosystem: str | None = None) -> tuple[str, ...]:
"""Return the sorted, deduplicated source extensions across registered specs.

Used by dep-graph builders to learn which non-host-language file types
(e.g. ``.astro``, ``.svelte``, ``.vue``) should be scanned as importers
of the host language's modules. Lookup is driven by the registry, so
adding a new framework with ``source_extensions`` automatically extends
every plugin that calls this — no infrastructure edits required.

Cached per ecosystem key; ``register_framework_spec`` invalidates the
cache so tests that mutate the registry see fresh results.
"""
ensure_builtin_specs_loaded()
cache_key = (ecosystem or "").strip().lower()
cached = _EXTENSIONS_CACHE.get(cache_key)
if cached is not None:
return cached
result = tuple(
sorted(
{
ext
for spec in list_framework_specs(ecosystem=ecosystem).values()
for ext in spec.source_extensions
}
)
)
_EXTENSIONS_CACHE[cache_key] = result
return result


__all__ = [
"FRAMEWORK_SPECS",
"ensure_builtin_specs_loaded",
"framework_source_extensions",
"get_framework_spec",
"list_framework_specs",
"register_framework_spec",
Expand Down
25 changes: 25 additions & 0 deletions desloppify/languages/_framework/frameworks/specs/astro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Astro framework spec (Node ecosystem)."""

from __future__ import annotations

from ..types import DetectionConfig, FrameworkSpec

ASTRO_SPEC = FrameworkSpec(
id="astro",
label="Astro",
ecosystem="node",
detection=DetectionConfig(
dependencies=("astro",),
config_files=(
"astro.config.mjs",
"astro.config.js",
"astro.config.ts",
"astro.config.cjs",
),
script_pattern=r"(?:^|\s)astro(?:\s|$)",
),
source_extensions=(".astro",),
)


__all__ = ["ASTRO_SPEC"]
29 changes: 29 additions & 0 deletions desloppify/languages/_framework/frameworks/specs/svelte.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Svelte framework spec (Node ecosystem).

Covers both Svelte 4 and SvelteKit. The detection config matches the
package name (``svelte``) and SvelteKit's metadata (``@sveltejs/kit``)
so projects using either show as present.
"""

from __future__ import annotations

from ..types import DetectionConfig, FrameworkSpec

SVELTE_SPEC = FrameworkSpec(
id="svelte",
label="Svelte",
ecosystem="node",
detection=DetectionConfig(
dependencies=("svelte", "@sveltejs/kit"),
config_files=(
"svelte.config.js",
"svelte.config.mjs",
"svelte.config.ts",
),
script_pattern=r"(?:^|\s)svelte-kit(?:\s|$)",
),
source_extensions=(".svelte",),
)


__all__ = ["SVELTE_SPEC"]
31 changes: 31 additions & 0 deletions desloppify/languages/_framework/frameworks/specs/vue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Vue framework spec (Node ecosystem).

Covers Vue 3 (and Vue 2 leftovers) as well as Nuxt, which builds on
Vue's single-file component model and shares the ``.vue`` extension.
"""

from __future__ import annotations

from ..types import DetectionConfig, FrameworkSpec

VUE_SPEC = FrameworkSpec(
id="vue",
label="Vue",
ecosystem="node",
detection=DetectionConfig(
dependencies=("vue", "nuxt"),
config_files=(
"vue.config.js",
"vue.config.mjs",
"vue.config.ts",
"nuxt.config.js",
"nuxt.config.mjs",
"nuxt.config.ts",
),
script_pattern=r"(?:^|\s)(?:vue-cli-service|nuxt)(?:\s|$)",
),
source_extensions=(".vue",),
)


__all__ = ["VUE_SPEC"]
7 changes: 7 additions & 0 deletions desloppify/languages/_framework/frameworks/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ class FrameworkSpec:
excludes: tuple[str, ...] = ()
scanners: tuple[ScannerRule, ...] = ()
tools: tuple[ToolIntegration, ...] = ()
# Non-host-language source files that should still be scanned as importers
# of the host language's modules (e.g. `.astro` files importing `.js`).
# The dep-graph builder reads these files via a regex pass and records
# importer edges into ``file_set``; framework files themselves are not
# added as graph nodes. Default empty for specs whose source files are
# already covered by the host language's extensions (e.g. Next.js).
source_extensions: tuple[str, ...] = ()


@dataclass(frozen=True)
Expand Down
1 change: 1 addition & 0 deletions desloppify/languages/_framework/generic_support/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def generic_lang(
file_finder, extract_fn, dep_graph_fn, has_treesitter, ts_spec = _resolve_generic_extractors(
path_extensions=extensions,
opts=opts,
frameworks=frameworks,
)
phases = _build_generic_phases(
tool_specs=tool_specs,
Expand Down
26 changes: 25 additions & 1 deletion desloppify/languages/_framework/generic_support/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def _resolve_generic_extractors(
*,
path_extensions: list[str],
opts: GenericLangOptions,
frameworks: bool = False,
) -> tuple[Any, Any, Any, bool, Any]:
file_finder = make_file_finder(path_extensions, opts.exclude)
extract_fn = noop_extract_functions
Expand All @@ -90,13 +91,36 @@ def _resolve_generic_extractors(
if not is_available():
return file_finder, extract_fn, dep_graph_fn, has_treesitter, ts_spec

from desloppify.languages._framework.frameworks.registry import (
framework_source_extensions,
)
from desloppify.languages._framework.treesitter.analysis.extractors import make_ts_extractor
from desloppify.languages._framework.treesitter.imports.graph import make_ts_dep_builder

has_treesitter = True
extract_fn = make_ts_extractor(ts_spec, file_finder)
if ts_spec.import_query and ts_spec.resolve_import:
dep_graph_fn = make_ts_dep_builder(ts_spec, file_finder)
# Pull the framework source-extension list from the FrameworkSpec
# registry — adding a new framework (e.g. Qwik, Solid, Marko) is one
# spec file with ``source_extensions=(...)`` and zero edits here.
framework_extensions = (
framework_source_extensions(ecosystem="node") if frameworks else None
)
# Build a framework-scoped file finder that respects the same
# ``opts.exclude`` as the host language finder, so framework files in
# user-excluded directories (e.g. ``examples/``, ``e2e/``) don't
# contribute spurious importer edges.
framework_file_finder = (
make_file_finder(list(framework_extensions), opts.exclude)
if framework_extensions
else None
)
dep_graph_fn = make_ts_dep_builder(
ts_spec,
file_finder,
framework_extensions=framework_extensions,
framework_file_finder=framework_file_finder,
)
return file_finder, extract_fn, dep_graph_fn, has_treesitter, ts_spec


Expand Down
Loading
Loading