mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
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.
This commit is contained in:
+2
-2
@@ -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
|
||||
|
||||
@@ -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
@@ -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、快照,以及可信真实提供方轮次。
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](..
|
||||
|
||||
## Profiles
|
||||
|
||||
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves exports with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged.
|
||||
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged.
|
||||
|
||||
User-level machine-local preferences also live in the Harness home:
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面
|
||||
|
||||
## Profiles
|
||||
|
||||
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则按 Node ESM import 条件解析 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。
|
||||
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。
|
||||
|
||||
用户级的机器本地偏好同样位于 harness home 中:
|
||||
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Generated
+9
-3
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user