fix(boot): occupy the interception layer instead of hiding it
The generation stands in for $DSH_HOME/profiles/node_modules/<name> only for names it carries; other names see that directory as an ordinary ancestor. The interception layer is the first ancestor node_modules above the importer's profile directory, so application-owned profiles follow the same rule. Stale .dsh-module-fallback projections are ordinary symlinks: their shape recognition leaves the resolver, the local package names, and the bundle dependency traversal. The resolver spec gains a table-driven matrix over importer, package kind, ancestor layers, and directory or symlink form, with readable fixture names.
This commit is contained in:
1 parent
db831733da
commit
6f57030958
4 files changed
+496
-361
No files matched your search
@@ -1,10 +1,6 @@
|
||||
/** Package directory canonicalization and stale profile-projection recognition. */
|
||||
/** Package directory canonicalization through the active runtime carrier's filesystem. */
|
||||
|
||||
import { lstatSync, readlinkSync, realpathSync } from 'node:fs'
|
||||
import { basename, dirname, join, resolve } from 'node:path'
|
||||
|
||||
/** Profile-private package links projected into its pnpm-managed node_modules. */
|
||||
export const PROFILE_MODULE_FALLBACK_DIR = '.dsh-module-fallback'
|
||||
import { realpathSync } from 'node:fs'
|
||||
|
||||
/**
|
||||
* Return whether the process reads application modules from pkg's virtual filesystem.
|
||||
@@ -22,52 +18,3 @@ export function isPackagedExecutable(): boolean {
|
||||
export function realModuleDirectory(path: string): string {
|
||||
return isPackagedExecutable() ? realpathSync(path) : realpathSync.native(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a link target without following the final path component.
|
||||
* @param path - candidate path whose parent is canonicalized.
|
||||
* @returns the canonical candidate, or undefined when its parent is absent.
|
||||
*/
|
||||
export function canonicalLinkPath(path: string): string | undefined {
|
||||
try {
|
||||
return join(realModuleDirectory(dirname(path)), basename(path))
|
||||
} catch (error) {
|
||||
// A missing parent means the candidate cannot identify an existing owned link.
|
||||
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
|
||||
/* v8 ignore next -- see the host-filesystem exception above */
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a symlink or junction points at the same path as `target`.
|
||||
* @param link - symlink or junction to inspect.
|
||||
* @param target - expected target path.
|
||||
* @returns whether both paths identify the same entry.
|
||||
*/
|
||||
export function symlinkPointsTo(link: string, target: string): boolean {
|
||||
const actual = resolve(dirname(link), readlinkSync(link))
|
||||
const canonicalActual = canonicalLinkPath(actual)
|
||||
const canonicalTarget = canonicalLinkPath(resolve(target))
|
||||
return canonicalActual !== undefined && canonicalActual === canonicalTarget
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether an observed profile package must not claim local precedence.
|
||||
* @param profileDir - profile directory containing the package projection.
|
||||
* @param packageName - bare package name to inspect.
|
||||
* @returns whether the entry is a managed fallback link or disappeared during inspection.
|
||||
*/
|
||||
export function isProfileModuleFallbackLink(profileDir: string, packageName: string): boolean {
|
||||
const link = join(profileDir, 'node_modules', packageName)
|
||||
const target = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, 'node_modules', packageName)
|
||||
try {
|
||||
return lstatSync(link).isSymbolicLink() && symlinkPointsTo(link, target)
|
||||
} catch (error) {
|
||||
/* v8 ignore next 2 -- a vanished candidate cannot claim local precedence */
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true
|
||||
/* v8 ignore next -- non-ENOENT lstat/readlink failures require a host filesystem fault */
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { getEnvironmentData, setEnvironmentData } from 'node:worker_threads'
|
||||
import type { ModuleLoaderV1, ModuleLoaderV2, ResolveResult } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { imports as resolvePackageImports, type Package as ResolvePackageManifest } from 'resolve.exports'
|
||||
import { isProfileModuleFallbackLink } from './legacy-links.ts'
|
||||
import type { ProfileResolutionEntry, ProfileResolutionGeneration } from '../profile.ts'
|
||||
|
||||
const WORKER_RESOLUTION_KEY = '@deepseek-ai/dsh-app-boot/profile-resolution'
|
||||
@@ -48,6 +47,12 @@ type EsmResolve = (
|
||||
request: string, parent: string | undefined, attributes: ImportAttributes,
|
||||
) => ResolveResult | Promise<ResolveResult>
|
||||
|
||||
/**
|
||||
* Where a scoped bare request resolves. A `fallback` entry occupies the package
|
||||
* directory at the interception layer; `after` anchors the ancestors above that
|
||||
* layer for a CommonJS subpath the entry lacks. An `after-fallback` route anchors
|
||||
* the interception layer itself for a name the generation does not carry.
|
||||
*/
|
||||
type ResolutionRoute =
|
||||
| { readonly kind: 'fallback'; readonly entry: ProfileResolutionEntry; readonly after: string }
|
||||
| { readonly kind: 'after-fallback'; readonly parent: string }
|
||||
@@ -62,7 +67,8 @@ interface ResolutionRouteState {
|
||||
|
||||
interface ParentRoutes {
|
||||
readonly parent: string
|
||||
readonly profilesDir: string
|
||||
/** Profile directory owning the importer; its node_modules are local, the first ancestor above it is the interception layer. */
|
||||
readonly localRoot: string
|
||||
readonly activeProfile: boolean
|
||||
readonly requests: Map<string, ResolutionRouteState>
|
||||
selfReferenceName?: string | false | null
|
||||
@@ -79,7 +85,6 @@ interface CompiledGeneration {
|
||||
readonly profile: readonly string[]
|
||||
readonly activeProfileUrls: readonly string[]
|
||||
readonly localPackageNames: ReadonlySet<string>
|
||||
readonly shared: ReadonlySet<string>
|
||||
readonly esmRoutes: ResolutionRoutes
|
||||
readonly cjsRoutes: ResolutionRoutes
|
||||
}
|
||||
@@ -145,7 +150,6 @@ function compileGeneration(generation: ProfileResolutionGeneration): CompiledGen
|
||||
profile,
|
||||
activeProfileUrls: profile.map(path => pathToFileURL(path).href),
|
||||
localPackageNames: new Set(generation.localPackageNames),
|
||||
shared: new Set(profilePaths.map(prefix => join(prefix, 'node_modules'))),
|
||||
esmRoutes: new Map(),
|
||||
cjsRoutes: new Map(),
|
||||
}
|
||||
@@ -158,6 +162,18 @@ function startsWithin(path: string, roots: readonly string[]): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* The profile directory owning a module inside the profiles tree: the first path segment below the tree root.
|
||||
* @param path - module path below `treePrefix`.
|
||||
* @param treePrefix - profiles directory with a trailing separator.
|
||||
* @returns the profile directory without a trailing separator.
|
||||
*/
|
||||
function profileChild(path: string, treePrefix: string): string {
|
||||
const rest = path.slice(treePrefix.length)
|
||||
const end = rest.indexOf(sep)
|
||||
return treePrefix + (end < 0 ? rest : rest.slice(0, end))
|
||||
}
|
||||
|
||||
function nativePackageDir(parent: string, name: string): string | undefined {
|
||||
for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
|
||||
const candidate = join(searchPath, name)
|
||||
@@ -166,16 +182,14 @@ function nativePackageDir(parent: string, name: string): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
function localPackageCandidate(
|
||||
searchPath: string, name: string, flavor: 'esm' | 'cjs',
|
||||
): { packageDir: string; canBeManagedLink: boolean } | undefined {
|
||||
function localPackageCandidate(searchPath: string, name: string, flavor: 'esm' | 'cjs'): string | undefined {
|
||||
const candidate = join(searchPath, name)
|
||||
const stat = statSync(candidate, { throwIfNoEntry: false })
|
||||
const found = flavor === 'esm'
|
||||
? stat?.isDirectory() === true
|
||||
: stat !== undefined
|
||||
|| ['.js', '.json', '.node'].some(extension => existsSync(candidate + extension))
|
||||
return found ? { packageDir: candidate, canBeManagedLink: stat !== undefined } : undefined
|
||||
return found ? candidate : undefined
|
||||
}
|
||||
|
||||
function selfReferenceName(parent: string): string | false | null {
|
||||
@@ -326,7 +340,7 @@ class ResolutionRouter {
|
||||
nativeResolve?: (searchPaths: readonly string[]) => string,
|
||||
cacheable = false,
|
||||
): ResolutionRouteState | undefined {
|
||||
const { parent, profilesDir, requests } = parentRoutes
|
||||
const { parent, localRoot, requests } = parentRoutes
|
||||
const name = barePackageName(request)
|
||||
if (name === undefined) return undefined
|
||||
|
||||
@@ -340,31 +354,26 @@ class ResolutionRouter {
|
||||
}
|
||||
|
||||
const target = generation.entries.get(name)
|
||||
const candidates: Array<{ packageDir: string; canBeManagedLink: boolean }> = []
|
||||
const candidates: string[] = []
|
||||
const localSearchPaths: string[] = []
|
||||
const localPrefix = localRoot + sep
|
||||
for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
|
||||
if (generation.shared.has(resolve(searchPath))) break
|
||||
if (!searchPath.startsWith(localPrefix)) break
|
||||
localSearchPaths.push(searchPath)
|
||||
const candidate = localPackageCandidate(searchPath, name, flavor)
|
||||
if (candidate !== undefined) {
|
||||
const legacy = candidate.canBeManagedLink && generation.profile.some(prefix => (
|
||||
candidate.packageDir === join(prefix, 'node_modules', name)
|
||||
&& isProfileModuleFallbackLink(prefix.slice(0, -1), name)
|
||||
))
|
||||
if (!legacy) candidates.push(candidate)
|
||||
}
|
||||
if (candidate !== undefined) candidates.push(candidate)
|
||||
}
|
||||
if (candidates.length > 0) {
|
||||
if (flavor === 'cjs' && nativeResolve !== undefined) {
|
||||
try {
|
||||
const resolved = nativeResolve(localSearchPaths)
|
||||
const selected = candidates.find(candidate => (
|
||||
localCandidateOwnsResolution(candidate.packageDir, resolved, request, name)
|
||||
localCandidateOwnsResolution(candidate, resolved, request, name)
|
||||
))
|
||||
if (selected !== undefined) {
|
||||
const state = {
|
||||
route: { kind: 'native' as const, packageDir: selected.packageDir },
|
||||
packageDir: selected.packageDir,
|
||||
route: { kind: 'native' as const, packageDir: selected },
|
||||
packageDir: selected,
|
||||
...(cacheable ? { cjs: resolved } : {}),
|
||||
}
|
||||
requests.set(request, state)
|
||||
@@ -374,22 +383,23 @@ class ResolutionRouter {
|
||||
if (!isUnselectedPackageMiss(error)) throw error
|
||||
}
|
||||
} else {
|
||||
const selected = candidates[0] as { packageDir: string }
|
||||
const selected = candidates[0] as string
|
||||
const state = {
|
||||
route: { kind: 'native' as const, packageDir: selected.packageDir },
|
||||
packageDir: selected.packageDir,
|
||||
route: { kind: 'native' as const, packageDir: selected },
|
||||
packageDir: selected,
|
||||
}
|
||||
requests.set(request, state)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// The interception layer is the node_modules directory of the profile's parent.
|
||||
const layer = dirname(localRoot)
|
||||
const eligible = target?.scope === 'installation'
|
||||
|| (target?.scope === 'profile' && parentRoutes.activeProfile)
|
||||
const after = join(dirname(profilesDir), 'package.json')
|
||||
const route: ResolutionRoute = eligible
|
||||
? { kind: 'fallback', entry: target, after }
|
||||
: { kind: 'after-fallback', parent: after }
|
||||
? { kind: 'fallback', entry: target, after: join(dirname(layer), 'package.json') }
|
||||
: { kind: 'after-fallback', parent: join(layer, 'package.json') }
|
||||
const state: ResolutionRouteState = { route }
|
||||
if (route.kind === 'fallback') requests.set(request, state)
|
||||
return state
|
||||
@@ -425,17 +435,13 @@ class ResolutionRouter {
|
||||
generation.esmRoutes.set(parentURL, false)
|
||||
return undefined
|
||||
}
|
||||
const profileRoot = profileIndex >= 0
|
||||
? generation.profilePaths[profileIndex]
|
||||
: generation.profile[activeProfileIndex]
|
||||
const treeRoot = generation.profilePaths[profileIndex]
|
||||
const localRoot = treeRoot !== undefined
|
||||
? profileChild(parent, treeRoot)
|
||||
: generation.profile[activeProfileIndex]?.slice(0, -1)
|
||||
/* v8 ignore next -- one matching index was established above */
|
||||
if (profileRoot === undefined) return undefined
|
||||
parentRoutes = {
|
||||
parent,
|
||||
profilesDir: profileRoot.slice(0, -1),
|
||||
activeProfile: activeProfileIndex >= 0,
|
||||
requests: new Map(),
|
||||
}
|
||||
if (localRoot === undefined) return undefined
|
||||
parentRoutes = { parent, localRoot, activeProfile: activeProfileIndex >= 0, requests: new Map() }
|
||||
generation.esmRoutes.set(parentURL, parentRoutes)
|
||||
}
|
||||
const local = this.routeLocalPackage(request, parentRoutes, generation)
|
||||
@@ -452,18 +458,11 @@ class ResolutionRouter {
|
||||
const cached = parentRoutes?.requests.get(request)
|
||||
if (cached !== undefined) return cached
|
||||
if (parentRoutes === undefined) {
|
||||
const profilesDir = generation.profilePaths.find(prefix => parent.startsWith(prefix))
|
||||
const treeRoot = generation.profilePaths.find(prefix => parent.startsWith(prefix))
|
||||
const activeProfile = generation.profile.find(prefix => parent.startsWith(prefix))
|
||||
if (profilesDir !== undefined || activeProfile !== undefined) {
|
||||
const profileRoot = profilesDir ?? activeProfile
|
||||
/* v8 ignore next -- one matching root was established above */
|
||||
if (profileRoot === undefined) return undefined
|
||||
parentRoutes = {
|
||||
parent,
|
||||
profilesDir: profileRoot.slice(0, -1),
|
||||
activeProfile: activeProfile !== undefined,
|
||||
requests: new Map(),
|
||||
}
|
||||
const localRoot = treeRoot !== undefined ? profileChild(parent, treeRoot) : activeProfile?.slice(0, -1)
|
||||
if (localRoot !== undefined) {
|
||||
parentRoutes = { parent, localRoot, activeProfile: activeProfile !== undefined, requests: new Map() }
|
||||
generation.cjsRoutes.set(parent, parentRoutes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,14 @@
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
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 type { DshPackageManifest } from '@deepseek-ai/dsh-package-manifest'
|
||||
import { loadOverlayPatches } from './index.ts'
|
||||
import {
|
||||
canonicalLinkPath, isProfileModuleFallbackLink, PROFILE_MODULE_FALLBACK_DIR, realModuleDirectory, symlinkPointsTo,
|
||||
} from './profile-resolution/legacy-links.ts'
|
||||
import { realModuleDirectory } from './profile-resolution/legacy-links.ts'
|
||||
|
||||
/** Directory under the Harness home holding every profile. */
|
||||
export const PROFILES_DIR = 'profiles'
|
||||
@@ -326,17 +324,13 @@ function skippedProfileBundles(profile: Profile | undefined, manifest: ProfileMa
|
||||
/** Return installed direct dependencies that Node resolves before profile fallback. */
|
||||
function installedProfilePackageNames(profile: Profile, manifest: ProfileManifest | undefined): string[] {
|
||||
if (manifest === undefined) return []
|
||||
return profileDependencyNames(manifest).filter((name) => {
|
||||
const candidate = join(profile.dir, 'node_modules', name)
|
||||
if (!existsSync(join(candidate, 'package.json'))) return false
|
||||
return !isProfileModuleFallbackLink(profile.dir, name)
|
||||
})
|
||||
return profileDependencyNames(manifest)
|
||||
.filter(name => existsSync(join(profile.dir, 'node_modules', name, 'package.json')))
|
||||
}
|
||||
|
||||
/** Collect the first resolvable package directory for each dependency name. */
|
||||
function dependencyClosure(
|
||||
anchors: readonly string[], reserved: ReadonlySet<string>,
|
||||
exclude: (candidate: string, packageName: string) => boolean,
|
||||
declarers?: Map<string, string>,
|
||||
versions?: Map<string, string | undefined>,
|
||||
): Map<string, string> {
|
||||
@@ -359,7 +353,7 @@ function dependencyClosure(
|
||||
/* v8 ignore next -- an installable package manifest always declares dependencies or peers */
|
||||
for (const dep of profileDependencyNames(next.manifest)) {
|
||||
if (visited.has(dep)) continue
|
||||
const dir = packageDirFromAnchor(next.anchor, dep, exclude)
|
||||
const dir = packageDirFromAnchor(next.anchor, dep)
|
||||
// A declared-but-uninstalled dependency cannot be loader-visible.
|
||||
if (dir === undefined) continue
|
||||
visited.add(dep)
|
||||
@@ -381,25 +375,10 @@ function resolveProfileModuleFallback(
|
||||
declarers?: Map<string, string>,
|
||||
versions?: Map<string, string | undefined>,
|
||||
): Map<string, string> {
|
||||
const profileModulesDir = join(profile.dir, 'node_modules')
|
||||
const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, 'node_modules')
|
||||
const bundleAnchors = profile.layers
|
||||
.filter(layer => !installationPackageNames.has(layer.packageName))
|
||||
.map(layer => join(layer.packageDir, 'package.json'))
|
||||
const bundleLinks = dependencyClosure(bundleAnchors, installationPackageNames, (candidate, packageName) => {
|
||||
const profileLink = join(profileModulesDir, packageName)
|
||||
if (canonicalLinkPath(candidate) !== canonicalLinkPath(profileLink)) return false
|
||||
try {
|
||||
return lstatSync(profileLink).isSymbolicLink()
|
||||
&& symlinkPointsTo(profileLink, join(ownedModulesDir, packageName))
|
||||
} catch (error) {
|
||||
// A concurrent cleanup may remove the projection after package discovery.
|
||||
/* v8 ignore next 2 -- a non-ENOENT lstat failure requires a host filesystem fault */
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return true
|
||||
/* v8 ignore next -- see the host-filesystem exception above */
|
||||
throw error
|
||||
}
|
||||
}, declarers, versions)
|
||||
const bundleLinks = dependencyClosure(bundleAnchors, installationPackageNames, declarers, versions)
|
||||
for (const layer of profile.layers) bundleLinks.delete(layer.packageName)
|
||||
return bundleLinks
|
||||
}
|
||||
@@ -473,15 +452,12 @@ function normalizeShippedProfile(name: string, dir: string, manifest: ProfileMan
|
||||
* matches what the Loader would import from the same anchor, and
|
||||
* `existsSync` follows the symlinks pnpm's isolated layout uses.
|
||||
*/
|
||||
function packageDirFromAnchor(
|
||||
anchor: string, packageName: string,
|
||||
exclude: (candidate: string, packageName: string) => boolean = () => false,
|
||||
): string | undefined {
|
||||
function packageDirFromAnchor(anchor: string, packageName: string): string | undefined {
|
||||
// resolve.paths returns null only for builtins, which no bundle name is.
|
||||
/* v8 ignore next */
|
||||
for (const searchPath of createRequire(anchor).resolve.paths(packageName) ?? []) {
|
||||
const candidate = join(searchPath, packageName)
|
||||
if (existsSync(join(candidate, 'package.json')) && !exclude(candidate, packageName)) return candidate
|
||||
if (existsSync(join(candidate, 'package.json'))) return candidate
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user