mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
Merge pull request #9 from pineapplestrikesback/feat/svelte-import-parsing
feat: add Svelte and Vue import parsing to dependency graph Thanks pineapplestrikesback, nice idea and nicely implemented with zero-dependencies.
This commit is contained in:
@@ -305,6 +305,9 @@ export function getAstGrepLang(ext: string): Lang | string | null {
|
||||
".dart": "dart",
|
||||
".lua": "lua",
|
||||
".sh": "bash", ".bash": "bash", ".zsh": "bash",
|
||||
// Composite languages (parsed via HTML + script re-parse)
|
||||
".svelte": "svelte",
|
||||
".vue": "vue",
|
||||
// Built-in languages (Lang enum)
|
||||
".js": Lang.JavaScript, ".jsx": Lang.JavaScript, ".mjs": Lang.JavaScript, ".cjs": Lang.JavaScript,
|
||||
".ts": Lang.TypeScript,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
|
||||
import { type Lang, parse } from "@ast-grep/napi";
|
||||
import { Lang, parse } from "@ast-grep/napi";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
// ── Import extraction per language ───────────────────────────────────────
|
||||
@@ -10,6 +10,46 @@ export interface ImportInfo {
|
||||
isDynamic: boolean;
|
||||
}
|
||||
|
||||
/** Extract JS/TS imports from an ast-grep root node. Shared by JS/TS and Svelte/Vue handlers. */
|
||||
function extractJsTsImportsFromNode(sgNode: ReturnType<ReturnType<typeof parse>["root"]>): ImportInfo[] {
|
||||
const imports: ImportInfo[] = [];
|
||||
|
||||
// import ... from "..."
|
||||
for (const node of sgNode.findAll({ rule: { kind: "import_statement" } })) {
|
||||
const sourceNode = node.find({ rule: { kind: "string" } });
|
||||
if (sourceNode) {
|
||||
const spec = sourceNode.text().replace(/['"]/g, "");
|
||||
imports.push({ moduleSpecifier: spec, isDynamic: false });
|
||||
}
|
||||
}
|
||||
// require("...")
|
||||
for (const node of sgNode.findAll({ rule: { kind: "call_expression" } })) {
|
||||
const text = node.text();
|
||||
const match = text.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
||||
if (match) {
|
||||
imports.push({ moduleSpecifier: match[1], isDynamic: false });
|
||||
}
|
||||
}
|
||||
// dynamic import("...")
|
||||
for (const node of sgNode.findAll({ rule: { kind: "call_expression" } })) {
|
||||
const text = node.text();
|
||||
const match = text.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
||||
if (match) {
|
||||
imports.push({ moduleSpecifier: match[1], isDynamic: true });
|
||||
}
|
||||
}
|
||||
// export ... from "..."
|
||||
for (const node of sgNode.findAll({ rule: { kind: "export_statement" } })) {
|
||||
const sourceNode = node.find({ rule: { kind: "string" } });
|
||||
if (sourceNode) {
|
||||
const spec = sourceNode.text().replace(/['"]/g, "");
|
||||
imports.push({ moduleSpecifier: spec, isDynamic: false });
|
||||
}
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract import statements from source code using ast-grep.
|
||||
* Returns raw module specifiers for each language's import syntax.
|
||||
@@ -43,6 +83,29 @@ export function extractImports(source: string, lang: Lang | string, _ext: string
|
||||
return imports;
|
||||
}
|
||||
|
||||
// ── Svelte/Vue: parse as HTML, extract <script> blocks, re-parse as TS ──
|
||||
if (langKey === "svelte" || langKey === "vue") {
|
||||
try {
|
||||
const htmlRoot = parse(Lang.Html, source).root();
|
||||
const scriptElements = htmlRoot.findAll({ rule: { kind: "script_element" } });
|
||||
|
||||
for (const scriptEl of scriptElements) {
|
||||
const rawText = scriptEl.find({ rule: { kind: "raw_text" } });
|
||||
if (!rawText) continue;
|
||||
|
||||
const scriptContent = rawText.text();
|
||||
if (!scriptContent.trim()) continue;
|
||||
|
||||
// Default to TypeScript (superset of JS, safe for both)
|
||||
const scriptRoot = parse(Lang.TypeScript, scriptContent).root();
|
||||
imports.push(...extractJsTsImportsFromNode(scriptRoot));
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn("Failed to parse Svelte/Vue file for imports", { error: String(err) });
|
||||
}
|
||||
return imports;
|
||||
}
|
||||
|
||||
// ── AST-based extraction for languages with grammar support ───────────
|
||||
try {
|
||||
const sgNode = parse(lang, source).root();
|
||||
@@ -74,38 +137,7 @@ export function extractImports(source: string, lang: Lang | string, _ext: string
|
||||
case "JavaScript":
|
||||
case "TypeScript":
|
||||
case "Tsx": {
|
||||
// import ... from "..."
|
||||
for (const node of sgNode.findAll({ rule: { kind: "import_statement" } })) {
|
||||
const sourceNode = node.find({ rule: { kind: "string" } });
|
||||
if (sourceNode) {
|
||||
const spec = sourceNode.text().replace(/['"]/g, "");
|
||||
imports.push({ moduleSpecifier: spec, isDynamic: false });
|
||||
}
|
||||
}
|
||||
// require("...")
|
||||
for (const node of sgNode.findAll({ rule: { kind: "call_expression" } })) {
|
||||
const text = node.text();
|
||||
const match = text.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
||||
if (match) {
|
||||
imports.push({ moduleSpecifier: match[1], isDynamic: false });
|
||||
}
|
||||
}
|
||||
// dynamic import("...")
|
||||
for (const node of sgNode.findAll({ rule: { kind: "call_expression" } })) {
|
||||
const text = node.text();
|
||||
const match = text.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
||||
if (match) {
|
||||
imports.push({ moduleSpecifier: match[1], isDynamic: true });
|
||||
}
|
||||
}
|
||||
// export ... from "..."
|
||||
for (const node of sgNode.findAll({ rule: { kind: "export_statement" } })) {
|
||||
const sourceNode = node.find({ rule: { kind: "string" } });
|
||||
if (sourceNode) {
|
||||
const spec = sourceNode.text().replace(/['"]/g, "");
|
||||
imports.push({ moduleSpecifier: spec, isDynamic: false });
|
||||
}
|
||||
}
|
||||
imports.push(...extractJsTsImportsFromNode(sgNode));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,11 +22,13 @@ export function resolveImport(
|
||||
|
||||
switch (language) {
|
||||
case "javascript":
|
||||
case "typescript": {
|
||||
case "typescript":
|
||||
case "svelte":
|
||||
case "vue": {
|
||||
// Relative imports: ./foo, ../bar
|
||||
if (moduleSpecifier.startsWith(".")) {
|
||||
return resolveRelativePath(moduleSpecifier, sourceDir, projectPath, fileSet, [
|
||||
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
||||
".svelte", ".vue", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
||||
]);
|
||||
}
|
||||
return null; // npm packages
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { Lang } from "@ast-grep/napi";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { ensureDynamicLanguages } from "../../src/services/code-graph.js";
|
||||
import { extractImports, } from "../../src/services/graph-imports.js";
|
||||
import { extractImports } from "../../src/services/graph-imports.js";
|
||||
|
||||
// Register dynamic language grammars once before all tests
|
||||
beforeAll(() => {
|
||||
@@ -37,7 +37,9 @@ const mod = await import("./dynamic-module.js");
|
||||
const dynamicImports = imports.filter((i) => i.isDynamic);
|
||||
|
||||
expect(dynamicImports.length).toBeGreaterThanOrEqual(1);
|
||||
expect(dynamicImports.some((i) => i.moduleSpecifier === "./dynamic-module.js")).toBe(true);
|
||||
expect(
|
||||
dynamicImports.some((i) => i.moduleSpecifier === "./dynamic-module.js"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("extracts require() calls", () => {
|
||||
@@ -80,6 +82,112 @@ function hello() {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Svelte ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Svelte imports", () => {
|
||||
it("extracts imports from <script> blocks", () => {
|
||||
const source = `
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Button from "./Button.svelte";
|
||||
import { type Props } from "../types.js";
|
||||
</script>
|
||||
|
||||
<Button>Click me</Button>
|
||||
`;
|
||||
const imports = extractImports(source, "svelte", ".svelte");
|
||||
const specs = imports.map((i) => i.moduleSpecifier);
|
||||
|
||||
expect(specs).toContain("svelte");
|
||||
expect(specs).toContain("./Button.svelte");
|
||||
expect(specs).toContain("../types.js");
|
||||
});
|
||||
|
||||
it("extracts imports from <script module> blocks", () => {
|
||||
const source = `
|
||||
<script lang="ts" module>
|
||||
export type Variant = "primary" | "secondary";
|
||||
export { default as Button } from "./Button.svelte";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
</script>
|
||||
|
||||
<div>content</div>
|
||||
`;
|
||||
const imports = extractImports(source, "svelte", ".svelte");
|
||||
const specs = imports.map((i) => i.moduleSpecifier);
|
||||
|
||||
expect(specs).toContain("./Button.svelte");
|
||||
expect(specs).toContain("svelte");
|
||||
});
|
||||
|
||||
it("extracts dynamic imports from Svelte files", () => {
|
||||
const source = `
|
||||
<script lang="ts">
|
||||
const Component = await import("./DynamicComponent.svelte");
|
||||
</script>
|
||||
`;
|
||||
const imports = extractImports(source, "svelte", ".svelte");
|
||||
const dynamicImports = imports.filter((i) => i.isDynamic);
|
||||
|
||||
expect(dynamicImports.length).toBeGreaterThanOrEqual(1);
|
||||
expect(
|
||||
dynamicImports.some(
|
||||
(i) => i.moduleSpecifier === "./DynamicComponent.svelte",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("handles Svelte files with no script block", () => {
|
||||
const source = `
|
||||
<div>Just markup, no script</div>
|
||||
<style>
|
||||
div { color: red; }
|
||||
</style>
|
||||
`;
|
||||
const imports = extractImports(source, "svelte", ".svelte");
|
||||
expect(imports).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("handles Svelte files with JavaScript (no lang=ts)", () => {
|
||||
const source = `
|
||||
<script>
|
||||
import { writable } from "svelte/store";
|
||||
import Item from "./Item.svelte";
|
||||
</script>
|
||||
`;
|
||||
const imports = extractImports(source, "svelte", ".svelte");
|
||||
const specs = imports.map((i) => i.moduleSpecifier);
|
||||
|
||||
expect(specs).toContain("svelte/store");
|
||||
expect(specs).toContain("./Item.svelte");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Vue ────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Vue imports", () => {
|
||||
it("extracts imports from <script> blocks", () => {
|
||||
const source = `
|
||||
<script lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import MyComponent from "./MyComponent.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MyComponent />
|
||||
</template>
|
||||
`;
|
||||
const imports = extractImports(source, "vue", ".vue");
|
||||
const specs = imports.map((i) => i.moduleSpecifier);
|
||||
|
||||
expect(specs).toContain("vue");
|
||||
expect(specs).toContain("./MyComponent.vue");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Python ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe("Python imports", () => {
|
||||
@@ -342,8 +450,12 @@ import kotlinx.coroutines.launch
|
||||
const specs = imports.map((i) => i.moduleSpecifier);
|
||||
|
||||
expect(specs.length).toBeGreaterThanOrEqual(3);
|
||||
expect(specs.some((s) => s.includes("com.example.models.User"))).toBe(true);
|
||||
expect(specs.some((s) => s.includes("com.example.utils.StringHelper"))).toBe(true);
|
||||
expect(specs.some((s) => s.includes("com.example.models.User"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
specs.some((s) => s.includes("com.example.utils.StringHelper")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("handles wildcard imports", () => {
|
||||
@@ -353,7 +465,9 @@ import com.example.models.*
|
||||
const imports = extractImports(source, "kotlin", ".kt");
|
||||
|
||||
expect(imports.length).toBeGreaterThanOrEqual(1);
|
||||
expect(imports.some((i) => i.moduleSpecifier.includes("com.example.models"))).toBe(true);
|
||||
expect(
|
||||
imports.some((i) => i.moduleSpecifier.includes("com.example.models")),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -372,7 +486,11 @@ import com.example.services._
|
||||
const specs = imports.map((i) => i.moduleSpecifier);
|
||||
|
||||
expect(specs.length).toBeGreaterThanOrEqual(2);
|
||||
expect(specs.some((s) => s.includes("scala.collection") || s.includes("ListBuffer"))).toBe(true);
|
||||
expect(
|
||||
specs.some(
|
||||
(s) => s.includes("scala.collection") || s.includes("ListBuffer"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -429,7 +547,9 @@ using static System.Math;
|
||||
const imports = extractImports(source, "csharp", ".cs");
|
||||
|
||||
expect(imports.length).toBeGreaterThanOrEqual(1);
|
||||
expect(imports.some((i) => i.moduleSpecifier.includes("System.Math"))).toBe(true);
|
||||
expect(
|
||||
imports.some((i) => i.moduleSpecifier.includes("System.Math")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("skips using alias directives", () => {
|
||||
|
||||
Reference in New Issue
Block a user