Two related defects that make browser entry points invisible in the graph. Found while using graphify to orient in an Astro site whose Cloudflare Turnstile callbacks are published on window — the graph showed the callbacks as absent, which read as dead code.
Version: 0.9.53 (latest on PyPI at time of writing).
1. A function assigned to a global receiver produces no node
// entry.js
function helper() { return 1; }
const arrow = () => 2;
window.onTurnstileOk = (token) => { helper(); };
globalThis.__DEBUG_HOOK__ = function () { return 3; };
$ python -c "from graphify.extract import extract; from pathlib import Path; \
print([n['label'] for n in extract([Path('entry.js')], cache_root=Path('.'))['nodes']])"
['entry.js', 'helper()', 'arrow()']
_js_member_assignment_target (graphify/extractors/engine.py) classifies the receiver and materializes a node only for exports.X and Foo.prototype.X. An arbitrary identifier receiver returns ("object", name, member), and both call sites drop it unless the receiver is a local object-literal binding — deliberately, per the #1077 phantom-god-node guard.
The guard is right in general and wrong for the global object specifically. Assigning a function to window / globalThis / self is how browser code publishes an entry point: an inline onload=, a data-callback= attribute, a third-party script that invokes it by name (Turnstile, reCAPTCHA, Google Maps), a debug hook. The call site is outside the module graph by construction, so the definition already has no incoming edge — dropping the node too means live code reads as absent.
The #1077 collision concern is answered by the id, not by the drop: qualifying with the file stem and the receiver keeps two files publishing the same global name as two nodes, exactly as the exports branch already does.
2. .astro is handed to the JS grammar unmasked
extract_astro calls _extract_generic(path, _JS_CONFIG) on the whole file, so tree-sitter errors out on the template and the <script> block's symbols survive only as far as error recovery carries them — which varies with what precedes them. On a real 750-line component: 9 nodes, and warning: first error at line 1.
.vue already solves this by blanking every non-<script> region before parsing (_vue_mask_non_script). .astro has the same shape — TS frontmatter plus <script> blocks — so it can take the same treatment.
Worth flagging for whoever implements it: scanning for <script> across the whole file finds the one in this frontmatter comment
---
// keep this in sync, do not duplicate the labels inside the <script>.
---
which then "closes" at the real </script> 580 lines below and swallows the entire client-side region. Scanning only after the frontmatter avoids it. (This bit me while writing the fix; there is a regression test for it in the PR.)
Measured on a real repository
Three files from a production Astro site, before → after both fixes:
| file |
nodes |
edges |
globals found |
NeonTerminal.astro |
20 → 31 |
38 → 55 |
window.askTurnstileOk, window.askTurnstileErr |
Servizi.astro |
9 → 17 |
20 → 27 |
window.svcTurnstileOk, window.svcTurnstileErr |
worker/sentry.js |
2 → 2 |
3 → 3 |
globalThis.__SEGNALA_SENTRY__ |
The warning: ... syntax errors on both .astro files is gone.
PR follows, with 5 tests: the two receiver cases (module level and inside a setup function), file-scoped ids, .astro script symbols, and the frontmatter-comment regression. Full suite unchanged: same 24 pre-existing failures (test_terraform.py, test_skillgen.py) before and after, 4913 → 4918 passed.
Two related defects that make browser entry points invisible in the graph. Found while using graphify to orient in an Astro site whose Cloudflare Turnstile callbacks are published on
window— the graph showed the callbacks as absent, which read as dead code.Version: 0.9.53 (latest on PyPI at time of writing).
1. A function assigned to a global receiver produces no node
_js_member_assignment_target(graphify/extractors/engine.py) classifies the receiver and materializes a node only forexports.XandFoo.prototype.X. An arbitrary identifier receiver returns("object", name, member), and both call sites drop it unless the receiver is a local object-literal binding — deliberately, per the #1077 phantom-god-node guard.The guard is right in general and wrong for the global object specifically. Assigning a function to
window/globalThis/selfis how browser code publishes an entry point: an inlineonload=, adata-callback=attribute, a third-party script that invokes it by name (Turnstile, reCAPTCHA, Google Maps), a debug hook. The call site is outside the module graph by construction, so the definition already has no incoming edge — dropping the node too means live code reads as absent.The #1077 collision concern is answered by the id, not by the drop: qualifying with the file stem and the receiver keeps two files publishing the same global name as two nodes, exactly as the
exportsbranch already does.2.
.astrois handed to the JS grammar unmaskedextract_astrocalls_extract_generic(path, _JS_CONFIG)on the whole file, so tree-sitter errors out on the template and the<script>block's symbols survive only as far as error recovery carries them — which varies with what precedes them. On a real 750-line component: 9 nodes, andwarning: first error at line 1..vuealready solves this by blanking every non-<script>region before parsing (_vue_mask_non_script)..astrohas the same shape — TS frontmatter plus<script>blocks — so it can take the same treatment.Worth flagging for whoever implements it: scanning for
<script>across the whole file finds the one in this frontmatter commentwhich then "closes" at the real
</script>580 lines below and swallows the entire client-side region. Scanning only after the frontmatter avoids it. (This bit me while writing the fix; there is a regression test for it in the PR.)Measured on a real repository
Three files from a production Astro site, before → after both fixes:
NeonTerminal.astrowindow.askTurnstileOk,window.askTurnstileErrServizi.astrowindow.svcTurnstileOk,window.svcTurnstileErrworker/sentry.jsglobalThis.__SEGNALA_SENTRY__The
warning: ... syntax errorson both.astrofiles is gone.PR follows, with 5 tests: the two receiver cases (module level and inside a setup function), file-scoped ids,
.astroscript symbols, and the frontmatter-comment regression. Full suite unchanged: same 24 pre-existing failures (test_terraform.py,test_skillgen.py) before and after, 4913 → 4918 passed.