Skip to content

fix(desktop): resolve the packaged pnpm entry from app.asar.unpacked - #830

Open
eatmoreduck wants to merge 1 commit into
dataelement:mainfrom
eatmoreduck:fix/829-pnpm-asar-unpacked
Open

eatmoreduck wants to merge 1 commit into
dataelement:mainfrom
eatmoreduck:fix/829-pnpm-asar-unpacked

Conversation

@eatmoreduck

Copy link
Copy Markdown
Contributor

Fixes #829.

What

Every plugin-market install fails in the packaged app with {"error":"spawn ENOTDIR"}. Both pnpm entry resolutions hand out an app.asar member path:

  • src/main/index.ts bundledPnpmEntryPath() builds <resources>/app.asar/node_modules/pnpm/bin/pnpm.cjs and injects it as DSH_DESKTOP_PNPM_ENTRY;
  • packages/dsh-desktop-market-installer/index.js resolvePnpmEntry() returns the same spelling for the .desktop-bin shims.

Electron's asar-aware fs validates those paths, but pnpm is executed by node-mode children (ELECTRON_RUN_AS_NODE=1 — the Harness hand-off via buildPackagedPnpmInvocation, and the shims) that cannot read archive members. The OS sees app.asar as a regular file, so traversing app.asar/node_modules/... fails with ENOTDIR — exactly the reported error, for every install, on every platform.

The comment above bundledRuntimeRoot() already states the design — "native files the OS executes are unpacked and reached through their own resolution (node-pty, ripgrep, and the Office engine hook)" — pnpm was missing from that list.

Change

  • package.json: asarUnpack gains node_modules/pnpm/**/*. pnpm has zero external dependencies (bin/pnpm.cjs → the self-contained 8.6 MB dist/pnpm.cjs bundle), so the unpacked copy is self-contained; its native reflink.*.node files were already covered by **/*.{node}.
  • src/main/bundled-pnpm.ts (new): unpackedBundledPath() rewrites the app.asar path segment to its app.asar.unpacked sibling (archive root or member, / or \); bundledPnpmEntryFrom() picks the entry from the unpacked bin. bundledPnpmEntryPath() in src/main/index.ts now delegates to it.
  • resolvePnpmEntry() in the market installer rewrites dirname(require.resolve('pnpm')) the same way before probing bin/pnpm.{cjs,mjs}.

Development installs carry no archive segment and are returned unchanged. A packaged build without the unpacked copy now surfaces a missing-entry miss at resolution time (downstream: buildPackagedPnpmInvocation falls back to PATH pnpm with its existing warning) instead of a cryptic spawn failure.

Test plan

  • New test/bundled-pnpm.test.ts: archive-member and archive-root rewrites (posix + Windows separators), non-archive passthrough, packaged-entry selection preferring the physical copy, and the fallback spelling when nothing physical exists.
  • New case in test/market-installer.test.js: a fixture tree with both app.asar/node_modules/pnpm and app.asar.unpacked/node_modules/pnpm — resolvePnpmEntry returns the unpacked pnpm.cjs. Verified red on origin/main (returns the archive path) and green with the fix.
  • test/market-installer.test.js + test/bundled-pnpm.test.ts + test/harness-node-entry.test.ts: 52 passed.
  • npm run typecheck: clean.
  • Full suite (npm test): 2,853 passed / 3 failed — the three failures (office-runtime real-subprocess cases ×2, security-policy media grant, signed-test-build git-history tag walk) reproduce identically on a clean origin/main checkout in this environment; none touch pnpm/asar resolution.
  • Packaged-app verification: needs a package:mac build with a plugin-market install — no signing/notarization environment here. The unit contracts above pin both resolutions; the asarUnpack glob is the same mechanism the Office engine already ships through.

Notes

  • Windows uses the same archive layout, so the rewrite is written for both separators; the shim path (pnpm.cmd / pnpm) and the runner staging were already physical files and are unaffected.
  • The staged-runner fallback in stagePnpmRunner can still return an archive spelling if the copy fails, but that path was already best-effort and is not what 插件市场安装插件返回spawn ENOTDIR #829 exercises.

Every plugin-market install fails in the packaged app with
spawn ENOTDIR: both pnpm entry resolutions (the desktop main
process that injects DSH_DESKTOP_PNPM_ENTRY, and the market
installer's resolvePnpmEntry that writes the .desktop-bin
shims) return the app.asar member spelling. Electron's
asar-aware fs validates it, but pnpm is executed by node-mode
children that cannot read archive members, and the OS answers
ENOTDIR because app.asar is a regular file.

The build now unpacks node_modules/pnpm (it has no external
dependencies, so the copy is self-contained) and both
resolvers rewrite the archive segment to its app.asar.unpacked
sibling — the same rule node-pty, ripgrep, and the Office
engine already follow. Development installs carry no archive
segment and are unchanged; a build without the unpacked copy
surfaces a missing-entry error at resolution instead of a
cryptic spawn failure.

Fixes dataelement#829
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.

插件市场安装插件返回spawn ENOTDIR

1 participant