You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Profile plugins fail to import when a dependency requires a subpath of an npm package named like a Node builtin (buffer/index, string_decoder/) — resolve.paths() returns null#8986
Version: dsh 0.2.0-rc.2 (@deepseek-ai/dsh-app-boot 0.2.0-rc.2), Node v24.20.0, Linux (WSL). The code is unchanged on master (5badb15, 0.2.1-alpha.1).
Symptom
A profile plugin silently disappears. The startup log only says:
dsh: warning: N entries did not activate
<plugin>: failed to import
The real error (visible only after temporarily adding console.error in cordis-plugin-loader_init()) is:
TypeError: createRequire.resolve.paths is not a function or its return value is not iterable
A plain import() of the same plugin file outside dsh works fine.
Trigger
Any CommonJS (or ESM) file inside a profile that requests a subpath of an npm package whose name coincides with a Node builtin. This is a long-standing idiom for "use the userland polyfill, not the builtin":
barePackageName() (L146–148) checks isBuiltin(request) on the full request. isBuiltin('buffer/index') === false, so it returns the name buffer.
routeScoped() (L470) then iterates createRequire(parent).resolve.paths(name) as string[]. For name = 'buffer' Node returns null (documented: require.resolve.paths() returns null for builtin names), and for...of null throws.
The as string[] cast hides this from the type checker. The same unguarded pattern exists in nativePackageDir() (L247), routeLinked() (L523) and service.tspackageDirFromParent() (L138). profile.ts L685–687 already guards it with ?? [], with a comment that this only happens for builtins.
Why ?? [] is not enough
Adding ?? [] in routeScoped() removes the crash, but the local search over the profile's own node_modules is then skipped. The request falls through to native-after-interception with parent <profiles>/package.json, which searches from <profiles>/node_modules and misses <profiles>/<profile>/node_modules/buffer. Result: Cannot find module 'buffer/index'.
Suggested fix
The local search in routeScoped() only needs the importer's node_modules chain, which does not depend on the name being a builtin. Use the nodeModulePaths that ResolutionRouter already receives:
- for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {+ for (const searchPath of this.nodeModulePaths(dirname(parent))) {
if (!searchPath.startsWith(layer.localPrefix)) break
Do the same (or use Module._nodeModulePaths(dirname(parent))) in the other three call sites.
Verification
I instantiated ResolutionRouter from the published 0.2.0-rc.2 lib/index.js with a real profile layout and routed require('buffer/index') from <profiles>/web/node_modules/msedge-tts/dist/MsEdgeTTS.js:
When a profile plugin fails at import time, the cause is only reported to the logger, which does not write to a file yet during startup. Showing the underlying error next to failed to import would have made this a five-minute diagnosis.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Version: dsh 0.2.0-rc.2 (
@deepseek-ai/dsh-app-boot0.2.0-rc.2), Node v24.20.0, Linux (WSL). The code is unchanged onmaster(5badb15, 0.2.1-alpha.1).Symptom
A profile plugin silently disappears. The startup log only says:
The real error (visible only after temporarily adding
console.errorincordis-plugin-loader_init()) is:A plain
import()of the same plugin file outside dsh works fine.Trigger
Any CommonJS (or ESM) file inside a profile that requests a subpath of an npm package whose name coincides with a Node builtin. This is a long-standing idiom for "use the userland polyfill, not the builtin":
msedge-tts→require("buffer/index")readable-stream@2→require('string_decoder/')(very widely used)Root cause
packages/boot/app-boot/src/profile-resolution/resolver.ts:barePackageName()(L146–148) checksisBuiltin(request)on the full request.isBuiltin('buffer/index') === false, so it returns the namebuffer.routeScoped()(L470) then iteratescreateRequire(parent).resolve.paths(name) as string[]. Forname = 'buffer'Node returnsnull(documented:require.resolve.paths()returnsnullfor builtin names), andfor...of nullthrows.The
as string[]cast hides this from the type checker. The same unguarded pattern exists innativePackageDir()(L247),routeLinked()(L523) andservice.tspackageDirFromParent()(L138).profile.tsL685–687 already guards it with?? [], with a comment that this only happens for builtins.Why
?? []is not enoughAdding
?? []inrouteScoped()removes the crash, but the local search over the profile's ownnode_modulesis then skipped. The request falls through tonative-after-interceptionwith parent<profiles>/package.json, which searches from<profiles>/node_modulesand misses<profiles>/<profile>/node_modules/buffer. Result:Cannot find module 'buffer/index'.Suggested fix
The local search in
routeScoped()only needs the importer'snode_moduleschain, which does not depend on the name being a builtin. Use thenodeModulePathsthatResolutionRouteralready receives:Do the same (or use
Module._nodeModulePaths(dirname(parent))) in the other three call sites.Verification
I instantiated
ResolutionRouterfrom the published 0.2.0-rc.2lib/index.jswith a real profile layout and routedrequire('buffer/index')from<profiles>/web/node_modules/msedge-tts/dist/MsEdgeTTS.js:TypeError: … not iterable?? []on every callnative-after-interception, parent<profiles>/package.jsonCannot find module 'buffer/index'this.nodeModulePaths(dirname(parent))inrouteScopednative,packageDir: <profiles>/web/node_modules/buffer<profiles>/web/node_modules/buffer/index.js✅Side note
When a profile plugin fails at import time, the cause is only reported to the logger, which does not write to a file yet during startup. Showing the underlying error next to
failed to importwould have made this a five-minute diagnosis.All reactions