Skip to content

JS: jsconfig.json baseUrl not honoured — non-relative imports (static AND dynamic) produce no edge, orphaning every module in a Rails/webpacker app #2153

Closed

Description

@infusini

Summary

JS imports written against jsconfig.json's compilerOptions.baseUrl produce no edge. Relative specifiers in the same file resolve fine. This affects static and dynamic imports equally — it is not a dynamic-import() issue.

Graphify already parses jsconfig.json (it emits compilerOptions / baseUrl nodes), it just doesn't use it for module resolution.

Related but distinct: #147 asks for TypeScript tsconfig paths. This is plain JS + jsconfig baseUrl, which is the default layout for Rails/webpacker and Vue SPAs.

Reproduction

gfjs/
  jsconfig.json                       {"compilerOptions": {"baseUrl": "app/javascript"}}
  app/javascript/mods/Local.js        export default class Local {}
  app/javascript/mods/Widget.js       export default class Widget {}
  app/javascript/mods/Registrar.js
// Registrar.js
import Local  from './Local.js';        // relative
import Widget from 'mods/Widget.js';    // baseUrl-relative

export default class Registrar {
  static L = Local;
  static W = Widget;
  static async lazy() { return import('mods/Widget.js'); }
}

graphify extract . --code-only --force on 0.9.25:

Registrar.js     --imports_from --> Local.js
Registrar.js     --imports      --> Local
Registrar.js     --contains     --> Registrar
Registrar        --method       --> .lazy()
Widget.js        --contains     --> Widget      <-- no inbound edge at all
jsconfig.json    --contains     --> compilerOptions
compilerOptions  --contains     --> baseUrl     <-- config IS parsed
specifier edge created?
'./Local.js' relative ✅
'mods/Widget.js' baseUrl static ❌
import('mods/Widget.js') baseUrl dynamic ❌

A separate matrix (no config) confirms relative works with and without the extension, so the variable is non-relative, not the extension.

Why it matters

In a real Rails + webpacker app (~17k nodes, jsconfig.json with baseUrl: app/javascript), every dashboard module and instruction is an orphan node. The registrar does:

// DashboardModuleRegistrar.js:58
import('workspaces/modules/registry/modules/EvosepSystemSuitabilityModule'),

and EvosepSystemSuitabilityModule.js has 0 inbound edges. So:

$ graphify affected "EvosepSystemSuitabilityModule.js" --depth 2
No affected nodes found.

…even though the registrar is a real dependency. On a "what will break if I change this?" question, a confident empty result is the worst possible failure mode — plain grep finds it in one call. This silently affects every module/instruction in the app.

Suggested fix

Honour jsconfig.json / tsconfig.json compilerOptions.baseUrl (and paths) when resolving JS specifiers, same as #147 proposes for TS. Falling back to "try <baseUrl>/<specifier>" would cover the common case.

Environment

graphifyy 0.9.25 · macOS 15 (darwin 25.5.0) · Python 3.9 · --code-only

Activity

  1. Rishet11 commented on Jul 25, 2026

    @Rishet11
    Contributor

    Confirmed, with one correction to the diagnosis that changes the fix.

    baseUrl is not simply unparsed — it is read, at resolution.py:146-147:

    base_url = compiler_options.get("baseUrl") or "."
    paths_base = base_dir / base_url

    but only as the base that paths targets resolve against. _read_tsconfig_aliases returns only paths entries, so a config with baseUrl and no paths — exactly the webpacker layout — yields an empty alias map and every non-relative specifier dies. That is the primary defect.

    The second is the one you spotted: _load_tsconfig_aliases only probes tsconfig.json (resolution.py:177), never jsconfig.json, even though json_config.py already indexes jsconfig.json — which is why you see compilerOptions/baseUrl nodes in the graph while resolution ignores the file.

    Verified on 0.9.26 with your layout (jsconfig.json with "baseUrl": "app/javascript", packs/dashboard.js importing 'mods/Widget.js'). Before, dashboard.js has zero outbound edges. After, it has imports and re_exports edges into mods/Widget.js.

    PR shortly. baseUrl becomes a resolution root of last resort, tried only after every declared alias misses, so existing paths precedence (#1269) is untouched — notably it is not modelled as a synthesized * alias, because that would outrank declared directory-prefix aliases and shadow them. Static and both dynamic-import paths are covered.

  2. safishamsi commented on Jul 26, 2026

    @safishamsi
    Member

    Fixed in v0.9.27 (#2176, thanks @Rishet11). Non-relative JS/TS imports resolve through jsconfig.json/tsconfig.json baseUrl and paths. https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.27

  3. added a commit that references this issue on Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions