Skip to content

fix: resolve profile-installed plugins from cordis-plugin-loader - #83

Open
yaojin3616 wants to merge 3 commits into
mainfrom
fix/profile-node-modules-resolution
Open

fix: resolve profile-installed plugins from cordis-plugin-loader#83
yaojin3616 wants to merge 3 commits into
mainfrom
fix/profile-node-modules-resolution

Conversation

@yaojin3616

@yaojin3616 yaojin3616 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #73

Root cause

Plugins installed via the plugin market go into the profile's node_modules (e.g. DSH_HOME/profiles/web/node_modules), but the cordis-plugin-loader resolves packages from the app bundle's own node_modules directory. After Electron packaging, these two paths are different, so both hot-mount and post-restart plugin loading fail with Cannot find package errors.

The key error from user logs:

hot-mount: @liustack/modlens: fell back to restart —
  Cannot find package '@liustack/modlens' imported from
  ...app.asar.unpacked/node_modules/@deepseek-ai/cordis-plugin-loader/lib/index.js

Fix

Injects the profile's node_modules directories into the harness Node.js process via two mechanisms:

  1. CommonJS: Sets NODE_PATH and appends to Module.globalPaths
  2. ESM: Registers a custom resolve hook (via --import) that falls back to profile node_modules when default resolution fails

The ESM hook uses a synthetic parent URL inside each profile's node_modules directory so that Node's standard resolver — including package.json exports, conditions, and subpaths — handles the actual resolution correctly.

Files changed

  • build/profile-module-paths.mjs — Setup script: collects profile node_modules dirs from DSH_HOME, configures CJS paths, registers ESM hook
  • build/profile-esm-resolver.mjs — ESM resolve hook with synthetic parent URL fallback
  • src/main/runtime/harness-runtime.ts — Passes --import flag and DSH_DESKTOP_PROFILE_MODULE_PATHS env to harness process
  • src/main/index.ts — Wires profile module paths resource into HarnessRuntime
  • package.json — Adds new build files to extraResources
  • test/runtime.test.ts — Updated tests for new arguments
  • test/profile-module-paths.test.ts — Tests for ESM resolver behavior

Resolution priority

Profile (project-level) node_modules take priority over the app bundle, matching Node's local-first resolution semantics:

  • ESM: the resolve hook tries each profile's node_modules first (via synthetic parent URL), then falls back to default resolution
  • CJS: Module._resolveFilename is wrapped to try profile paths first via options.paths, then falls back to default resolution

This means a plugin can pin its own dependency version in the profile without being shadowed by a different version bundled with the app.

yaojin and others added 3 commits August 18, 2026 19:28
Plugins installed via the plugin market (dsh-market) go into the
profile's node_modules (e.g. DSH_HOME/profiles/web/node_modules),
but the cordis-plugin-loader resolves packages from the app bundle's
own node_modules directory. After packaging, these two paths are
different, so both hot-mount and post-restart plugin loading fail
with 'Cannot find package' errors.

This change injects the profile's node_modules directories into the
harness Node.js process via two mechanisms:

1. NODE_PATH + Module.globalPaths for CommonJS resolution
2. A custom ESM resolve hook (registered via --import) that falls
   back to profile node_modules when the default resolution fails

The ESM hook uses a synthetic parent URL inside each profile's
node_modules directory so that Node's standard resolver (including
package.json exports, conditions, subpaths) handles the actual
resolution correctly.

Closes #73
Profile (project-level) packages now take priority during both ESM and
CJS resolution, with the app bundle acting as fallback — matching Node's
local-first resolution semantics.

- ESM resolver tries synthetic profile parents before default resolution
- CJS patches Module._resolveFilename to try profile paths first
- Add priority and fallback tests, including a CJS child-process test
Profile-first resolution broke more than it fixed. Node's nextResolve
merges the context it is handed into the shared context object, so once
the ESM hook had tried a synthetic profile parent, the fallback resolved
from that anchor instead of the real importer — every bare import the app
bundle owns failed with ERR_MODULE_NOT_FOUND as soon as any profile had a
node_modules directory. Profile-first also let a plugin's copy of a shared
package replace the app bundle's own nested dependency, and discarded the
paths callers pass to require.resolve.

Default resolution now runs first and profiles only fill in what the app
bundle cannot provide, which is all the original bug needs: the plugin
loader ships in the bundle and cannot see DSH_HOME/profiles/<name>/node_modules.
Plugins keep their pinned dependency versions either way, because the
market installs them with pnpm and their deps live beside their real path
in .pnpm.

- Pass the preload to --import as a file URL. A bare Windows path parses
  as a `c:` URL scheme, so Node exited before the harness entry ran and
  the app could not start at all on Windows.
- Skip the preload when the shim is missing instead of turning a missing
  best-effort resource into an unexplained startup crash.
- Look up profile directories on every failed resolution rather than
  snapshotting them at startup, so the first plugin installed into a fresh
  profile mounts without a restart.
- Inject only the profile the harness runs, named by DSH_DESKTOP_PROFILES,
  instead of every directory under DSH_HOME/profiles in readdir order.
- Report the importer's original error when no profile provides the
  package, and surface exports/package-config errors from a profile copy
  instead of hiding them behind the next candidate.
- Drop the NODE_PATH mutation: it has no effect on the running process and
  leaked into every child the harness spawns.

Rewrites the ESM tests as real child-process integration tests. The
previous ones mocked nextResolve, which is why they passed against a
resolver that could not resolve the app bundle at all; 7 of the new tests
fail against the previous implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

插件市场装的插件在 v2.0.0 中无法加载:loader 从 app.asar.unpacked 解析,插件在 profile 的 node_modules

1 participant