mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-21 13:05:41 +00:00
fix: treat .cjs (explicit CommonJS) as a code extension
.cjs was half-registered: the language maps in build.py and extract.py already routed it to the JS grammar, but it was missing from CODE_EXTENSIONS, the extractor _DISPATCH, _LANG_FAMILY, and the JS resolution/cache suffix sets — so .cjs sources (Electron main/preload scripts, CommonJS escape hatches in "type": "module" packages) were silently skipped during a build: classified as non-code and never handed to the JS extractor. Add .cjs alongside .mjs in the six lists that gate/route JS files (detect.py, extract.py _DISPATCH, analyze.py _LANG_FAMILY, extractors/models.py cache-bypass, extractors/resolution.py resolve exts, cli.py _HOOK_SOURCE_EXTS), with regression locks mirroring the .mts/.cts fix (#1607). Real-world impact: an Electron app whose ~2000-line main.cjs backend was invisible went from 201 to 294 nodes and 256 to 441 edges on rebuild. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
0d783915a6
commit
2851f31a07
+1
-1
@@ -23,7 +23,7 @@ _BUILTIN_NOISE_LABELS = frozenset({
|
||||
# Language families — extensions sharing a runtime can legitimately call each other
|
||||
_LANG_FAMILY: dict[str, str] = {
|
||||
**{e: "python" for e in (".py", ".pyw")},
|
||||
**{e: "js" for e in (".js", ".jsx", ".mjs", ".ejs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte")},
|
||||
**{e: "js" for e in (".js", ".jsx", ".mjs", ".cjs", ".ejs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte")},
|
||||
**{e: "go" for e in (".go",)},
|
||||
**{e: "rust" for e in (".rs",)},
|
||||
**{e: "jvm" for e in (".java", ".kt", ".kts", ".scala")},
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ _READ_NUDGE = json.dumps({
|
||||
}
|
||||
}, ensure_ascii=False, separators=(",", ":")) + "\n"
|
||||
_HOOK_SOURCE_EXTS = (
|
||||
'.py', '.js', '.ts', '.tsx', '.jsx', '.astro', '.vue', '.svelte', '.go',
|
||||
'.py', '.js', '.cjs', '.ts', '.tsx', '.jsx', '.astro', '.vue', '.svelte', '.go',
|
||||
'.rs', '.java', '.rb', '.c', '.h', '.cpp', '.hpp', '.cc', '.cs', '.kt',
|
||||
'.swift', '.php', '.scala', '.lua', '.sh', '.md', '.rst', '.txt', '.mdx',
|
||||
)
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ class FileType(str, Enum):
|
||||
|
||||
_MANIFEST_PATH = str(out_path("manifest.json"))
|
||||
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'}
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.ejs', '.ets', '.go', '.rs', '.java', '.groovy', '.gradle', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.cu', '.cuh', '.metal', '.rb', '.rake', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.luau', '.toc', '.zig', '.ps1', '.psm1', '.psd1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.astro', '.dart', '.v', '.sv', '.svh', '.sql', '.r', '.f', '.F', '.f90', '.F90', '.f95', '.F95', '.f03', '.F03', '.f08', '.F08', '.pas', '.pp', '.dpr', '.dpk', '.lpr', '.inc', '.dfm', '.lfm', '.lpk', '.sh', '.bash', '.json', '.tf', '.tfvars', '.hcl', '.dm', '.dme', '.dmi', '.dmm', '.dmf', '.sln', '.slnx', '.csproj', '.fsproj', '.vbproj', '.xaml', '.razor', '.cshtml', '.cls', '.trigger'}
|
||||
DOC_EXTENSIONS = {'.md', '.mdx', '.qmd', '.skill', '.txt', '.rst', '.html', '.yaml', '.yml'}
|
||||
PAPER_EXTENSIONS = {'.pdf'}
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
||||
|
||||
@@ -3838,6 +3838,7 @@ _DISPATCH: dict[str, Any] = {
|
||||
".js": extract_js,
|
||||
".jsx": extract_js,
|
||||
".mjs": extract_js,
|
||||
".cjs": extract_js,
|
||||
".ts": extract_js,
|
||||
".tsx": extract_js,
|
||||
".mts": extract_js,
|
||||
|
||||
@@ -8,7 +8,7 @@ from dataclasses import dataclass, field
|
||||
|
||||
_WORKSPACE_PACKAGE_CACHE: dict[str, dict[str, Path]] = {}
|
||||
|
||||
_JS_CACHE_BYPASS_SUFFIXES = {".js", ".jsx", ".mjs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte"}
|
||||
_JS_CACHE_BYPASS_SUFFIXES = {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".vue", ".svelte"}
|
||||
|
||||
@dataclass
|
||||
class LanguageConfig:
|
||||
|
||||
@@ -21,7 +21,7 @@ _TSCONFIG_ALIAS_CACHE: dict[str, dict[str, list[str]]] = {}
|
||||
|
||||
_WORKSPACE_MANIFEST_NAMES = ("pnpm-workspace.yaml", "package.json")
|
||||
|
||||
_JS_RESOLVE_EXTS = (".ts", ".tsx", ".mts", ".cts", ".svelte", ".js", ".jsx", ".mjs")
|
||||
_JS_RESOLVE_EXTS = (".ts", ".tsx", ".mts", ".cts", ".svelte", ".js", ".jsx", ".mjs", ".cjs")
|
||||
|
||||
_JS_INDEX_FILES = ("index.ts", "index.tsx", "index.svelte", "index.js", "index.jsx", "index.mjs")
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""CommonJS module extension (`.cjs`) is treated as code.
|
||||
|
||||
`.cjs` is the explicit-CommonJS counterpart of `.mjs` (used pervasively in
|
||||
Electron main/preload scripts and in `"type": "module"` packages that need a
|
||||
CommonJS escape hatch). The language maps in `build.py` and `extract.py`
|
||||
already routed `.cjs` to the JS grammar, but the extension was missing from
|
||||
`CODE_EXTENSIONS`, the extractor `_DISPATCH`, `_LANG_FAMILY`, and the JS
|
||||
resolution/cache sets — so `.cjs` sources were silently skipped during a build
|
||||
(detected as non-code and never handed to the JS extractor). These are
|
||||
regression locks for the extension sets plus an end-to-end extraction proving
|
||||
`.cjs` parses identically to `.js`.
|
||||
|
||||
Same shape of gap (and fix) as `.mts`/`.cts` in
|
||||
tests/test_typescript_module_extensions.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _labels(r):
|
||||
return [n["label"] for n in r["nodes"]]
|
||||
|
||||
|
||||
def test_cjs_registered_as_code():
|
||||
from graphify.detect import CODE_EXTENSIONS
|
||||
assert ".cjs" in CODE_EXTENSIONS
|
||||
|
||||
|
||||
def test_cjs_in_extractor_dispatch():
|
||||
from graphify.extract import _DISPATCH, extract_js
|
||||
assert _DISPATCH.get(".cjs") is extract_js
|
||||
|
||||
|
||||
def test_cjs_in_js_language_family():
|
||||
from graphify.analyze import _LANG_FAMILY
|
||||
assert _LANG_FAMILY.get(".cjs") == "js"
|
||||
|
||||
|
||||
def test_cjs_in_js_resolution_sets():
|
||||
from graphify.extract import _JS_CACHE_BYPASS_SUFFIXES, _JS_RESOLVE_EXTS
|
||||
assert ".cjs" in _JS_RESOLVE_EXTS
|
||||
assert ".cjs" in _JS_CACHE_BYPASS_SUFFIXES
|
||||
|
||||
|
||||
def test_cjs_in_hook_source_exts():
|
||||
from graphify.cli import _HOOK_SOURCE_EXTS
|
||||
assert ".cjs" in _HOOK_SOURCE_EXTS
|
||||
|
||||
|
||||
# A representative CommonJS source: require() imports, a class, a function, and
|
||||
# module.exports — the shape of an Electron main-process script.
|
||||
_CJS_SOURCE = (
|
||||
"const path = require('path');\n"
|
||||
"const { app, BrowserWindow } = require('electron');\n"
|
||||
"class WindowManager {\n"
|
||||
" open() { return new BrowserWindow(); }\n"
|
||||
"}\n"
|
||||
"function createWindow() {\n"
|
||||
" const manager = new WindowManager();\n"
|
||||
" return manager.open();\n"
|
||||
"}\n"
|
||||
"module.exports = { createWindow };\n"
|
||||
)
|
||||
|
||||
|
||||
def _extract(tmp_path: Path, ext: str):
|
||||
from graphify.extract import extract_js
|
||||
f = tmp_path / f"main{ext}"
|
||||
f.write_text(_CJS_SOURCE, encoding="utf-8")
|
||||
return extract_js(f)
|
||||
|
||||
|
||||
def test_cjs_extracts_like_js(tmp_path):
|
||||
# `.cjs` must parse identically to the same source saved as `.js` — same
|
||||
# node set (require() imports, class, function) modulo the file-node label.
|
||||
cjs = _extract(tmp_path, ".cjs")
|
||||
js = _extract(tmp_path, ".js")
|
||||
assert "error" not in cjs
|
||||
cjs_labels = set(_labels(cjs))
|
||||
js_labels = set(_labels(js))
|
||||
assert any("WindowManager" in label for label in cjs_labels), (
|
||||
".cjs class declaration missing — file was not parsed as JS"
|
||||
)
|
||||
assert any("createWindow" in label for label in cjs_labels), (
|
||||
".cjs function declaration missing — file was not parsed as JS"
|
||||
)
|
||||
assert {label for label in cjs_labels if not label.endswith(".cjs")} == {
|
||||
label for label in js_labels if not label.endswith(".js")
|
||||
}
|
||||
Reference in New Issue
Block a user