Skip to content

Vite resolver can't reach host/client_app/node_modules from wheel-installed module pages, even when deps are installed #123

Description

@antosubash

Summary

Sister issue to #116. Even after sm host sync-js-deps puts a module's required npm packages into host/client_app/node_modules, Vite still can't resolve those bare imports from .tsx files that live outside the host directory. The Node module-resolution algorithm walks up from each importer's directory looking for node_modules, and from .venv/lib/python3.12/site-packages/<module>/pages/Foo.tsx (or modules/<pkg>/<pkg>/pages/Foo.tsx) the walk never reaches host/client_app/node_modules — there is no symlink in any intermediate directory.

So #116 fixes the install; this issue is what's left after the install: the resolve still fails.

The host's vite.config.ts template already hand-aliases react, react-dom, @inertiajs/react, but every other bare specifier a module page can plausibly import (sonner, lucide-react, clsx, class-variance-authority, tailwind-merge, cmdk, maplibre-gl, pmtiles, recharts, vaul, @simple-module-py/ui/..., @simple-module-py/i18n/..., …) is left to Node resolution. The comment in the scaffolded vite.config.ts claims:

Subpath imports (e.g. @simple-module-py/ui/components/ui/badge) resolve via the symlinked node_modules in module dirs and the package's own exports map.

But no such symlinks are created by sm gen-pages, sm sync-js-deps, or anything else the framework ships.

Reproduction

  1. Fresh sm new host, install the framework's modules + a wheel-installed module that imports e.g. sonner.
  2. cd client_app && npm install sonner so the dep is present in host/client_app/node_modules/sonner.
  3. Open any page that triggers loading the module's .tsx.
[plugin:vite:import-analysis] Failed to resolve import "sonner" from
"../../.venv/lib/python3.12/site-packages/<mod>/pages/UploadCard.tsx".

The package is installed; Vite just can't see it from that importer.

Expected

sm gen-pages (or a sibling command, or a Vite plugin shipped under simple_module_hosting) should make module pages able to resolve any package that lives in host/client_app/node_modules. Two viable shapes:

A. Ship a simple_module_hosting/vite_plugin.ts (npm-published or template-copied into the scaffold) that intercepts bare-specifier resolution and retries against host/client_app/node_modules whenever the importer is outside the host directory:

{
  name: 'sm:module-host-resolve',
  enforce: 'pre',
  async resolveId(id, importer) {
    if (!importer || importer.startsWith(__dirname)) return null;
    if (id.startsWith('.') || id.startsWith('/') || id.startsWith('\0') || id.includes('?')) return null;
    return this.resolve(id, path.join(hostNodeModules, '__entry__'), { skipSelf: true });
  },
}

B. Have sm gen-pages materialize a node_modules symlink (or junction) inside each module's package root that points at host/client_app/node_modules. This is what the vite.config.ts comment already claims happens.

(A) is portable and doesn't litter the wheel install tree; (B) lets Node resolution work without a custom plugin and is closer to how npm/pnpm workspaces do it.

Acceptance

  • A fresh sm new host with one wheel-installed module that imports sonner boots make dev cleanly, with no hand-edits to client_app/vite.config.ts.
  • The scaffolded vite.config.ts either contains the resolver plugin from (A) or the comment about "symlinked node_modules in module dirs" matches reality.

Workaround

In host/client_app/vite.config.ts, add a pre-resolver before react():

const moduleHostResolve = {
  name: 'host:module-host-resolve',
  enforce: 'pre' as const,
  async resolveId(id: string, importer?: string) {
    if (!importer) return null;
    if (id.startsWith('.') || id.startsWith('/') || id.startsWith('\0')) return null;
    if (id.startsWith('virtual:') || id.startsWith('@vite/') || id.includes('?')) return null;
    if (importer.startsWith(__dirname) || importer.startsWith(hostNodeModules)) return null;
    try {
      const resolved = await this.resolve(id, path.join(hostNodeModules, '__entry__'), { skipSelf: true });
      if (resolved) return resolved;
    } catch { /* fall through */ }
    return null;
  },
};

Related

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions