Kaynağa Gözat

fix(python-sdk): resolve packaged proxies from real module entries

The packaged dsh launcher must expose installation modules to profile-local plugins without writing symlinks into pkg's virtual filesystem. The first review fix selected ESM exports correctly in ordinary Node, but real carrier execution exposed package metadata and VFS behavior that a synthetic tree did not cover: executable and declaration packages have no import entry, legacy main fields rely on Node probing, and pkg's Windows VFS prevents filesystem package-scope resolution from seeing exports such as zod/mini and @google/genai/web.

Resolve explicit exports directly from each installed manifest with the maintained resolve.exports package under Node import conditions. Publish only package-local candidate files that exist, reject escaping or malformed targets, preserve the package installation URL without realpath, and keep Node's legacy resolver only for exports-less packages. This avoids pkg filesystem package lookup entirely while retaining fail-loud behavior for broken runtime entries.

Add regression coverage for import-only, nested, symlinked, zod-style, and genai-style condition maps; unavailable and types-only entries; invalid and escaping targets; executable/declaration packages; extensionless main; and legacy index fallback. profile.ts remains at 100% statements, branches, functions, and lines. Update the bilingual package and Agent Note contracts, replace the runtime dependency and generated notice, and regenerate the lockfile through pnpm.
Tianyi Cui 1 ay önce
ebeveyn
işleme
d801f262d8

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
-2026-08-23-python-sdk-dsh-profile-runtime.md: 14c58c85923aac1b54362c72f61f7f1e1c145124
-2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 9f584ca9bfcc7907caede70ef7e9781f2bc37aab
+2026-08-23-python-sdk-dsh-profile-runtime.md: 74484f46b59c3395d7807ae6819026ad8793d033
+2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 0bc9b65115a18411b83bec4e791c2ddff41e76e1

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md

@@ -30,7 +30,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe
 
 The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime-<platform>-<arch>`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged.
 
-Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit runtime exports under ESM import conditions and re-exports its virtual module URLs. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
+Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
 
 The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md

@@ -30,7 +30,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
 
 零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime-<platform>-<arch>`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。
 
-普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理按 ESM import 条件解析源包的显式运行时 exports,并重新导出其虚拟模块 URL。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
+普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
 
 已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。
 

+ 1 - 1
THIRD_PARTY_NOTICES.md

@@ -66,7 +66,6 @@ External packages that a workspace package resolves at runtime. The tier covers
 | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
 | [`fflate`](https://github.com/101arrowz/fflate) | MIT |
 | [`immer`](https://github.com/immerjs/immer) | MIT |
-| [`import-meta-resolve`](https://github.com/wooorm/import-meta-resolve) | MIT |
 | [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT |
 | [`katex`](https://github.com/KaTeX/KaTeX) | MIT |
 | [`koffi`](https://github.com/Koromix/koffi) | MIT |
@@ -89,6 +88,7 @@ External packages that a workspace package resolves at runtime. The tier covers
 | [`react`](https://github.com/facebook/react) | MIT |
 | [`react-dom`](https://github.com/facebook/react) | MIT |
 | [`readable-stream`](https://github.com/nodejs/readable-stream) | MIT |
+| [`resolve.exports`](https://github.com/lukeed/resolve.exports) | MIT |
 | [`sharp`](https://github.com/lovell/sharp) | Apache-2.0 |
 | [`shiki`](https://github.com/shikijs/shiki) | MIT |
 | [`supports-color`](https://github.com/chalk/supports-color) | MIT |

+ 2 - 2
packages/boot/app-boot/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
-README.md: f48abfa445e4ac604b5e1bc694f8bae395b566e7
-README.zh.md: e903c74fb12fee0fca1d201d217dd28be8b7e08d
+README.md: 1fa00eefae367e2a5a44966d2f2debff9f95c074
+README.zh.md: ef57478b8a3de723e4fa8ce88f87d563eb5a81e5

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
packages/boot/app-boot/README.md


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
packages/boot/app-boot/README.zh.md


+ 2 - 2
packages/boot/app-boot/package.json

@@ -33,8 +33,8 @@
   "license": "MIT",
   "dependencies": {
     "@deepseek-ai/dsh-atomic-write": "workspace:^",
-    "import-meta-resolve": "^4.2.0",
-    "js-yaml": "^4.2.0"
+    "js-yaml": "^4.2.0",
+    "resolve.exports": "^2.0.3"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis-plugin-group": "workspace:^",

+ 50 - 19
packages/boot/app-boot/src/profile.ts

@@ -24,15 +24,15 @@
 
 import { createRequire } from 'node:module'
 import {
-  existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync,
+  existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync,
 } from 'node:fs'
-import { basename, dirname, join } from 'node:path'
-import { fileURLToPath, pathToFileURL } from 'node:url'
+import { basename, dirname, join, relative, resolve } from 'node:path'
+import { pathToFileURL } from 'node:url'
 import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
 import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
 import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
-import { resolve as resolveImport } from 'import-meta-resolve'
+import { resolve as resolvePackage, type Package as ResolvePackageManifest } from 'resolve.exports'
 import { loadOverlayPatches } from './index.ts'
 
 /** Directory under the Harness home holding every profile. */
@@ -276,27 +276,44 @@ function isPackagedExecutable(): boolean {
   return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
 }
 
-/** Resolve one package export with the ESM conditions used by the generated proxy. */
-function packageEntryFromPackage(packageName: string, packageDir: string, specifier: string): string {
+/** Resolve one available explicit package export under Node ESM import conditions. */
+function packageEntryFromPackage(
+  packageName: string,
+  packageDir: string,
+  declared: ResolvePackageManifest['exports'],
+  subpath: string,
+): string | undefined {
+  let candidates: string[] | void
   try {
-    const resolved = resolveImport(specifier, pathToFileURL(join(packageDir, 'package.json')).href)
-    if (!resolved.startsWith('file:') || !existsSync(fileURLToPath(resolved))) {
-      throw new Error(`resolved to missing or non-file URL ${resolved}`)
-    }
-    return resolved
+    candidates = resolvePackage({ name: packageName, exports: declared }, subpath)
   } catch (error) {
+    if ((error as Error).message.startsWith('No known conditions for ')) return undefined
+    const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1)
     throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error })
   }
+  for (const candidate of candidates ?? []) {
+    const target = candidate
+    const entry = resolve(packageDir, target)
+    const relativeEntry = relative(packageDir, entry)
+    if (!target.startsWith('./') || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) {
+      throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`)
+    }
+    if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href
+  }
+  return undefined
 }
 
-/** Resolve every explicit runtime export that an out-of-tree plugin can import. */
+/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */
 function packageProxySource(
   packageName: string,
   packageDir: string,
 ): { version: string; targets: Record<string, string> } {
   const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
+    bin?: unknown
     exports?: unknown
     main?: unknown
+    types?: unknown
+    typings?: unknown
     version?: unknown
   }
   if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
@@ -304,20 +321,34 @@ function packageProxySource(
   }
   const declared = manifest.exports
   if (declared === undefined) {
-    const entry = join(packageDir, typeof manifest.main === 'string' ? manifest.main : 'index.js')
-    if (!existsSync(entry)) {
-      throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`)
+    const main = typeof manifest.main === 'string' && manifest.main.length > 0 ? manifest.main : undefined
+    const entry = join(packageDir, main ?? 'index')
+    try {
+      const resolved = createRequire(join(packageDir, 'package.json')).resolve(entry)
+      return { version: manifest.version, targets: { '.': pathToFileURL(resolved).href } }
+    } catch (error) {
+      if (main === undefined
+        && (manifest.bin !== undefined || manifest.types !== undefined || manifest.typings !== undefined)) {
+        return { version: manifest.version, targets: {} }
+      }
+      throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error })
     }
-    return { version: manifest.version, targets: { '.': pathToFileURL(entry).href } }
   }
   const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared)
     && Object.keys(declared).some(key => key.startsWith('.'))
-    ? Object.keys(declared).filter(key => key === '.' || (key.startsWith('./') && !key.includes('*') && key !== './package.json'))
+    ? Object.keys(declared).filter(key => key === '.' || (
+      key.startsWith('./') && !key.includes('*') && !key.endsWith('/') && key !== './package.json'
+    ))
     : ['.']
   const targets: Record<string, string> = {}
   for (const subpath of subpaths) {
-    const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1)
-    targets[subpath] = packageEntryFromPackage(packageName, packageDir, specifier)
+    const target = packageEntryFromPackage(
+      packageName,
+      packageDir,
+      declared as ResolvePackageManifest['exports'],
+      subpath,
+    )
+    if (target !== undefined) targets[subpath] = target
   }
   return { version: manifest.version, targets }
 }

+ 136 - 9
packages/boot/app-boot/tests/profile.spec.ts

@@ -359,7 +359,12 @@ describe('healProfilesModuleFallback', () => {
     const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
     const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
     const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
-    bundleManifest.exports = { '.': './index.js', './feature': './feature.js' }
+    bundleManifest.exports = {
+      '.': './index.js',
+      './feature': './feature.js',
+      './legacy/': './legacy/',
+      './types': { types: './feature.d.ts' },
+    }
     writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest))
     writeFileSync(join(bundleDir, 'feature.js'), 'export const feature = "proxied"\n')
     const home = tmp()
@@ -416,6 +421,61 @@ describe('healProfilesModuleFallback', () => {
     }
   })
 
+  it('resolves explicit condition targets without filesystem package lookup', async () => {
+    const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+    const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
+    const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
+    manifest.exports = {
+      '.': { import: './index.js', require: './index.cjs' },
+      './mini': { types: './mini/index.d.ts', import: './mini/index.js', require: './mini/index.cjs' },
+      './web': { types: './dist/web/web.d.ts', import: './dist/web/index.mjs', default: './dist/web/index.mjs' },
+    }
+    writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
+    mkdirSync(join(bundleDir, 'mini'))
+    writeFileSync(join(bundleDir, 'mini', 'index.js'), 'export const mini = true\n')
+    mkdirSync(join(bundleDir, 'dist', 'web'), { recursive: true })
+    writeFileSync(join(bundleDir, 'dist', 'web', 'index.mjs'), 'export const web = true\n')
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      const home = tmp()
+      await healProfilesModuleFallback(anchor, home)
+      const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
+      await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ mini: true })
+      await expect(import(join(proxy, 'entry-2.js'))).resolves.toMatchObject({ web: true })
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+  })
+
+  it('preserves the installation path while resolving packaged exports', async () => {
+    const anchor = stageInstallation({})
+    const appDir = join(anchor, '..')
+    const physical = tmp()
+    writeFileSync(join(physical, 'package.json'), JSON.stringify({
+      name: 'linked-esm',
+      version: '0.0.0',
+      type: 'module',
+      exports: { import: './index.js' },
+    }))
+    writeFileSync(join(physical, 'index.js'), 'export const linked = true\n')
+    symlinkSync(physical, join(appDir, 'node_modules', 'linked-esm'), 'junction')
+    const appManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies: Record<string, string> }
+    appManifest.dependencies['linked-esm'] = '0.0.0'
+    writeFileSync(anchor, JSON.stringify(appManifest))
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      const home = tmp()
+      await healProfilesModuleFallback(anchor, home)
+      const proxyManifest = JSON.parse(readFileSync(
+        join(home, 'profiles', 'node_modules', 'linked-esm', 'package.json'),
+        'utf8',
+      )) as { dsh: { moduleFallback: { targets: Record<string, string> } } }
+      expect(proxyManifest.dsh.moduleFallback.targets['.']).toContain('/app/node_modules/linked-esm/index.js')
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+  })
+
   it('uses the legacy index fallback when a package has no exports or main', async () => {
     const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
     const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
@@ -433,20 +493,87 @@ describe('healProfilesModuleFallback', () => {
     }
   })
 
-  it('fails loud on a missing legacy main entry or ESM export target', async () => {
-    for (const mode of ['legacy', 'exports']) {
+  it('uses Node legacy resolution for an extensionless main entry', async () => {
+    const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+    const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
+    const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
+    manifest.main = './index'
+    writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      const home = tmp()
+      await healProfilesModuleFallback(anchor, home)
+      await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js')))
+        .resolves.toMatchObject({ packageName: 'bundle-a' })
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+  })
+
+  it('skips executable-only and declaration-only packages without import entries', async () => {
+    for (const marker of ['bin', 'types', 'typings']) {
+      const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+      const manifest = JSON.parse(readFileSync(anchor, 'utf8')) as Record<string, unknown>
+      delete manifest.main
+      manifest[marker] = marker === 'bin' ? { dsh: './lib/bin.js' } : './index.d.ts'
+      if (marker === 'types') manifest.main = ''
+      writeFileSync(anchor, JSON.stringify(manifest))
+      rmSync(join(anchor, '..', 'index.js'))
+      Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+      try {
+        const home = tmp()
+        await healProfilesModuleFallback(anchor, home)
+        const fallback = join(home, 'profiles', 'node_modules')
+        expect(existsSync(join(fallback, 'dsh-app'))).toBe(false)
+        expect(existsSync(join(fallback, 'bundle-a', 'entry-0.js'))).toBe(true)
+      } finally {
+        delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+      }
+    }
+  })
+
+  it('fails loud on a missing legacy main entry', async () => {
+    const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+    const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
+    const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
+    delete manifest.main
+    writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
+    rmSync(join(bundleDir, 'index.js'))
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow('main entry is missing')
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+  })
+
+  it('omits unavailable ESM exports and rejects malformed export targets', async () => {
+    for (const mode of ['missing', 'directory', 'absent-map', 'invalid', 'escape', 'null', 'null-subpath']) {
       const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
       const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
       const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
-      if (mode === 'legacy') delete manifest.main
-      else manifest.exports = { '.': { import: './missing.js' } }
+      const target = mode === 'missing' ? './missing.js'
+        : mode === 'directory' ? './mini'
+          : mode === 'escape' ? './../outside.js'
+            : '../outside.js'
+      manifest.exports = mode === 'absent-map' ? null
+        : mode === 'null-subpath' ? { './bad': null }
+          : { '.': mode === 'null' ? null : { import: target } }
       writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
-      rmSync(join(bundleDir, 'index.js'))
+      if (mode === 'directory') mkdirSync(join(bundleDir, 'mini'))
       Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
       try {
-        await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow(
-          mode === 'legacy' ? 'main entry is missing' : 'cannot resolve ESM export bundle-a',
-        )
+        const home = tmp()
+        if (mode === 'missing' || mode === 'directory' || mode === 'absent-map') {
+          await healProfilesModuleFallback(anchor, home)
+          expect(existsSync(join(home, 'profiles', 'node_modules', 'bundle-a'))).toBe(false)
+        } else {
+          await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow(
+            mode === 'null' || mode === 'null-subpath'
+              ? 'cannot resolve ESM export bundle-a'
+              : 'resolves outside its package',
+          )
+        }
       } finally {
         delete (process as NodeJS.Process & { pkg?: unknown }).pkg
       }

+ 9 - 3
pnpm-lock.yaml

@@ -1148,12 +1148,12 @@ importers:
       '@deepseek-ai/dsh-atomic-write':
         specifier: workspace:^
         version: link:../../util/atomic-write
-      import-meta-resolve:
-        specifier: ^4.2.0
-        version: 4.2.0
       js-yaml:
         specifier: ^4.2.0
         version: 4.2.0
+      resolve.exports:
+        specifier: ^2.0.3
+        version: 2.0.3
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -14965,6 +14965,10 @@ packages:
   resolve-pkg-maps@1.0.0:
     resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
 
+  resolve.exports@2.0.3:
+    resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==}
+    engines: {node: '>=10'}
+
   retry@0.13.1:
     resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
     engines: {node: '>= 4'}
@@ -20607,6 +20611,8 @@ snapshots:
 
   resolve-pkg-maps@1.0.0: {}
 
+  resolve.exports@2.0.3: {}
+
   retry@0.13.1: {}
 
   rfdc@1.4.1: {}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor