refactor(boot): keep runtime-only profile resolution
This commit is contained in:
1 parent
bd2247c510
commit
9fd0a5ad52
27 files changed
+297
-1567
No files matched your search
@@ -19,8 +19,6 @@ import {
|
||||
boot,
|
||||
readProfilePatches,
|
||||
createProfileResolutionGeneration,
|
||||
healProfilesModuleFallback,
|
||||
healIsolatedProfileModuleFallback,
|
||||
initProfile,
|
||||
installFailLoud,
|
||||
loadOverlayPatches,
|
||||
@@ -32,7 +30,6 @@ import {
|
||||
type ProfileContext,
|
||||
type Profile,
|
||||
type ProfileResolutionGeneration,
|
||||
type ProfileResolutionMode,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
|
||||
@@ -191,7 +188,6 @@ interface ComposedProfile {
|
||||
* then the telemetry switch.
|
||||
* @param name - the profile name.
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
* @param resolutionMode - runtime lookup, disk links, or dual verification of both.
|
||||
* @param fromDefaultProfile - shipped template for a missing named profile.
|
||||
* @param resolvedProfile - application-owned profile and installation.
|
||||
* @returns the profile and its patch layers.
|
||||
@@ -199,17 +195,13 @@ interface ComposedProfile {
|
||||
async function composeProfile(
|
||||
name: string,
|
||||
patchFiles: readonly string[],
|
||||
resolutionMode: ProfileResolutionMode,
|
||||
fromDefaultProfile?: string,
|
||||
resolvedProfile?: ResolvedProfileRuntime,
|
||||
): Promise<ComposedProfile> {
|
||||
const profile = resolvedProfile?.profile ?? prepareProfile(name, true, fromDefaultProfile)
|
||||
if (resolvedProfile !== undefined) writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
|
||||
const resolutionOptions = { installAnchor: resolvedProfile?.installAnchor ?? INSTALL_ANCHOR, profile }
|
||||
if (resolvedProfile !== undefined && resolutionMode !== 'runtime') healIsolatedProfileModuleFallback(resolvedProfile)
|
||||
const resolution = resolutionMode === 'runtime' || resolvedProfile !== undefined
|
||||
? await createProfileResolutionGeneration(resolutionOptions)
|
||||
: await healProfilesModuleFallback(resolutionOptions)
|
||||
const resolution = await createProfileResolutionGeneration(resolutionOptions)
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
return { profile, resolution, overlays }
|
||||
}
|
||||
@@ -238,8 +230,6 @@ export interface RunProfileOptions {
|
||||
args: readonly string[]
|
||||
/** Application-owned package runtime, scoped to plugin package operations. */
|
||||
packageManager?: ProfileContext['packageManager']
|
||||
/** Module fallback backend; defaults to runtime. Plain Node callers may override it; pkg executables always use runtime. */
|
||||
resolutionMode?: ProfileResolutionMode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,8 +249,6 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
(message) => { process.stderr.write(`${NAME}: ${message}\n`) },
|
||||
)
|
||||
|
||||
const packaged = (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
|
||||
const resolutionMode = packaged ? 'runtime' : options.resolutionMode ?? 'runtime'
|
||||
const app: { current?: Context } = {}
|
||||
let disposal: Promise<void> | undefined
|
||||
const dispose = (): Promise<void> => disposal ??= (async () => {
|
||||
@@ -273,7 +261,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
})()
|
||||
try {
|
||||
const composed = await composeProfile(
|
||||
options.profile, options.patchFiles, resolutionMode, options.fromDefaultProfile, options.resolvedProfile,
|
||||
options.profile, options.patchFiles, options.fromDefaultProfile, options.resolvedProfile,
|
||||
)
|
||||
const appReady = createAppReady()
|
||||
const shutdown = createProcessShutdown(dispose)
|
||||
@@ -309,9 +297,8 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// Before any config-tree entry mounts, so plugins resolve all launch-time
|
||||
// environment values from the same immutable launch snapshot.
|
||||
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
|
||||
await hostCtx.plugin(PluginPackages, resolutionMode === 'link' ? {} : {
|
||||
await hostCtx.plugin(PluginPackages, {
|
||||
generation: composed.resolution,
|
||||
behavior: resolutionMode === 'dual' ? 'verify' : 'enforce',
|
||||
})
|
||||
// The command line and bounded exit request are launcher facts available
|
||||
// to every app plugin that injects the argument snapshot.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** Application-owned profiles share the named profile launch lifecycle. */
|
||||
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import {
|
||||
boot, composeEntries, createProfileResolutionGeneration, healIsolatedProfileModuleFallback,
|
||||
boot, composeEntries, createProfileResolutionGeneration,
|
||||
PluginPackages, type Profile,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
|
||||
@@ -19,7 +19,6 @@ vi.mock('@deepseek-ai/dsh-app-boot', async (importOriginal) => {
|
||||
...actual,
|
||||
boot: vi.fn(),
|
||||
createProfileResolutionGeneration: vi.fn(actual.createProfileResolutionGeneration),
|
||||
healIsolatedProfileModuleFallback: vi.fn(actual.healIsolatedProfileModuleFallback),
|
||||
installFailLoud: vi.fn(),
|
||||
}
|
||||
})
|
||||
@@ -35,10 +34,8 @@ afterEach(() => {
|
||||
|
||||
describe('runProfile with an application-owned profile', () => {
|
||||
it.each(
|
||||
(['link', 'runtime'] as const).flatMap(resolutionMode =>
|
||||
(['composition', 'boot', 'watch', 'cleanup', 'tree-cleanup', 'both-cleanups'] as const)
|
||||
.map(stage => ({ resolutionMode, stage }))),
|
||||
)('releases startup resources after a $stage failure in $resolutionMode mode', async ({ resolutionMode, stage }) => {
|
||||
['composition', 'boot', 'watch', 'cleanup', 'tree-cleanup', 'both-cleanups'] as const,
|
||||
)('releases startup resources after a %s failure', async (stage) => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-profile-startup-failure-'))
|
||||
homes.push(home)
|
||||
mkdirSync(join(home, 'runtime'))
|
||||
@@ -70,7 +67,6 @@ describe('runProfile with an application-owned profile', () => {
|
||||
try {
|
||||
const application = runProfile({
|
||||
environment: createLaunchEnvironmentSnapshot([]), profile: 'desktop', patchFiles: [], args: ['--no-open'],
|
||||
resolutionMode,
|
||||
resolvedProfile: { profile, installAnchor: join(home, 'runtime/package.json') },
|
||||
})
|
||||
if (stage === 'both-cleanups') {
|
||||
@@ -90,12 +86,7 @@ describe('runProfile with an application-owned profile', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ selection: 'default', options: {}, mode: 'runtime' },
|
||||
{ selection: 'link', options: { resolutionMode: 'link' }, mode: 'link' },
|
||||
{ selection: 'dual', options: { resolutionMode: 'dual' }, mode: 'dual' },
|
||||
{ selection: 'runtime', options: { resolutionMode: 'runtime' }, mode: 'runtime' },
|
||||
] as const)('uses shared layers, $selection resolution, and shutdown', async ({ options, mode }) => {
|
||||
it('uses shared layers, runtime resolution, and shutdown', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-resolved-profile-'))
|
||||
homes.push(home)
|
||||
mkdirSync(join(home, 'runtime'))
|
||||
@@ -145,25 +136,14 @@ describe('runProfile with an application-owned profile', () => {
|
||||
const runtime = { profile, installAnchor: join(home, 'runtime/package.json') }
|
||||
try {
|
||||
const { shutdown } = await runProfile({
|
||||
environment, profile: 'desktop', resolvedProfile: runtime, ...options,
|
||||
environment, profile: 'desktop', resolvedProfile: runtime,
|
||||
patchFiles: [overlay], args: ['--port', '0', '--no-open'],
|
||||
})
|
||||
expect(installProxyFromEnvironment).toHaveBeenCalledWith(environment, expect.any(Function))
|
||||
if (mode !== 'runtime') {
|
||||
expect(healIsolatedProfileModuleFallback).toHaveBeenCalledWith({ profile, installAnchor: runtime.installAnchor })
|
||||
} else {
|
||||
expect(healIsolatedProfileModuleFallback).not.toHaveBeenCalled()
|
||||
}
|
||||
const generation = vi.mocked(createProfileResolutionGeneration).mock.settledResults
|
||||
.find(result => result.type === 'fulfilled')?.value
|
||||
expect(generation?.profileDir).toBe(home)
|
||||
expect(plugin).toHaveBeenCalledWith(PluginPackages, mode === 'link' ? {} : {
|
||||
generation,
|
||||
behavior: mode === 'dual' ? 'verify' : 'enforce',
|
||||
})
|
||||
expect(existsSync(join(home, 'profiles/node_modules'))).toBe(false)
|
||||
expect(existsSync(join(home, '.dsh-module-fallback'))).toBe(mode !== 'runtime')
|
||||
expect(existsSync(join(home, 'node_modules/test-runtime'))).toBe(mode !== 'runtime')
|
||||
expect(plugin).toHaveBeenCalledWith(PluginPackages, { generation })
|
||||
expect(lstatSync(localPackageDir).isDirectory()).toBe(true)
|
||||
expect(readFileSync(join(localPackageDir, 'package.json'), 'utf8')).toBe(localManifest)
|
||||
const requireFromProfile = createRequire(join(home, 'package.json'))
|
||||
|
||||
@@ -18,18 +18,17 @@ async function main(): Promise<void> {
|
||||
const application = runProfile({
|
||||
environment: loadLayeredEnv('dsh'),
|
||||
profile: 'desktop',
|
||||
resolutionMode: process.argv[5] === 'runtime' ? 'runtime' : 'link',
|
||||
resolvedProfile: { profile, installAnchor },
|
||||
patchFiles: [],
|
||||
args: ['--no-open', '--port', '19387'],
|
||||
...(process.argv[6] === undefined ? {} : {
|
||||
...(process.argv[5] === undefined ? {} : {
|
||||
packageManager: {
|
||||
command: process.execPath,
|
||||
args: ['--expose-internals', process.argv[6]],
|
||||
args: ['--expose-internals', process.argv[5]],
|
||||
env: {
|
||||
ELECTRON_RUN_AS_NODE: '1',
|
||||
DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
||||
PATH: `${process.argv[7] ?? ''}${delimiter}${process.env.PATH ?? ''}`,
|
||||
PATH: `${process.argv[6] ?? ''}${delimiter}${process.env.PATH ?? ''}`,
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function smokeDesktopRuntime(
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-desktop-smoke-'))
|
||||
const profile = join(home, 'profiles', 'desktop')
|
||||
const host = new DesktopHostProcess(node, root, profile, undefined, { ...environment, DSH_HOME: home },
|
||||
undefined, join(resourcesRuntime, 'primary-runtime'), 'runtime',
|
||||
undefined, join(resourcesRuntime, 'primary-runtime'),
|
||||
{ pnpm: join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.cjs'), nodeBin: join(resourcesRuntime, 'bin') })
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
try {
|
||||
|
||||
@@ -91,7 +91,6 @@ export class DesktopHostProcess {
|
||||
* @param primaryRuntime - Optional bundled dependency payload; when supplied, missing sibling
|
||||
* `office-skills` resources fail Host startup.
|
||||
* @param packageManager - Bundled pnpm entry and Node launcher directory, scoped to package operations.
|
||||
* @param profileResolution - Package resolution mode for the application-owned profile.
|
||||
*/
|
||||
constructor(
|
||||
private readonly node: string,
|
||||
@@ -101,7 +100,6 @@ export class DesktopHostProcess {
|
||||
private readonly environment: NodeJS.ProcessEnv = process.env,
|
||||
private readonly onFailure?: (error: Error) => void,
|
||||
private readonly primaryRuntime?: string,
|
||||
private readonly profileResolution: 'link' | 'runtime' = 'link',
|
||||
private readonly packageManager?: { readonly pnpm: string; readonly nodeBin: string },
|
||||
) {}
|
||||
|
||||
@@ -119,7 +117,6 @@ export class DesktopHostProcess {
|
||||
this.runtimeDir,
|
||||
this.projectDir,
|
||||
this.primaryRuntime ?? join(this.runtimeDir, '..', 'runtime', 'primary-runtime'),
|
||||
this.profileResolution,
|
||||
...this.packageManager === undefined ? [] : [this.packageManager.pnpm, this.packageManager.nodeBin],
|
||||
], {
|
||||
cwd: this.projectDir,
|
||||
|
||||
@@ -251,7 +251,7 @@ async function main(): Promise<void> {
|
||||
hostInspectPort, process.env, onFailure,
|
||||
development ? join(app.getAppPath(), '.desktop-build', 'targets', `${process.platform === 'darwin' ? 'mac' : 'win'}-${process.arch}`, 'runtime', 'primary-runtime')
|
||||
: join(process.resourcesPath, 'runtime', 'primary-runtime'),
|
||||
development ? 'link' : 'runtime', resources)
|
||||
resources)
|
||||
return {
|
||||
start: async () => {
|
||||
const ready = await host.start()
|
||||
|
||||
@@ -65,8 +65,7 @@ export function cleanProfileCorePackages(profile: string, packageNames: readonly
|
||||
if (existsSync(recordPath)) {
|
||||
for (const entry of readDesktopCorePackageSet(profile).packages) names.add(entry.name)
|
||||
}
|
||||
const roots = [join(profile, 'node_modules'), join(profile, '.dsh-module-fallback', 'node_modules')]
|
||||
requireDirectory(join(profile, '.dsh-module-fallback'))
|
||||
const roots = [join(profile, 'node_modules')]
|
||||
for (const root of roots) {
|
||||
requireDirectory(root)
|
||||
for (const name of names) requireDirectory(dirname(join(root, name)))
|
||||
|
||||
@@ -17,9 +17,9 @@ export class DesktopHostProcess extends Host {
|
||||
fixture.taskQueries.push({ at: Date.now(), action, active: result })
|
||||
return result
|
||||
}
|
||||
constructor(node, runtime, project, _inspectPort, environment, onFailure) {
|
||||
constructor(node, runtime, project, _inspectPort, environment, onFailure, primaryRuntime, packageManager) {
|
||||
// Port zero lets the OS allocate the development inspector; no fixed listener is acquired.
|
||||
super(node, runtime, project, 0, environment, onFailure)
|
||||
super(node, runtime, project, 0, environment, onFailure, primaryRuntime, packageManager)
|
||||
fixture.host = this
|
||||
}
|
||||
async start() {
|
||||
|
||||
@@ -110,15 +110,15 @@ describe('desktop host process', () => {
|
||||
expect(failure).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes external dependencies and runtime profile resolution to the Host', async () => {
|
||||
it('passes external dependencies and package-manager paths to the Host', async () => {
|
||||
const runtime = projectWithHost(HTTP_HOST.replace('runtime: process.argv[2]',
|
||||
'pnpm: process.argv[6], nodeBin: process.argv[7], primaryRuntime: process.argv[4], profileResolution: process.argv[5], runtime: process.argv[2]'))
|
||||
'pnpm: process.argv[5], nodeBin: process.argv[6], primaryRuntime: process.argv[4], runtime: process.argv[2]'))
|
||||
const primaryRuntime = join(runtime, 'external-primary-runtime')
|
||||
const host = new DesktopHostProcess(process.execPath, runtime, runtime, undefined, process.env,
|
||||
undefined, primaryRuntime, 'runtime', { pnpm: join(runtime, 'pnpm.mjs'), nodeBin: join(runtime, 'bin') })
|
||||
undefined, primaryRuntime, { pnpm: join(runtime, 'pnpm.mjs'), nodeBin: join(runtime, 'bin') })
|
||||
hosts.push(host)
|
||||
const { url } = await host.start()
|
||||
expect(await (await fetch(url)).json()).toMatchObject({ primaryRuntime, profileResolution: 'runtime', pnpm: join(runtime, 'pnpm.mjs'), nodeBin: join(runtime, 'bin') })
|
||||
expect(await (await fetch(url)).json()).toMatchObject({ primaryRuntime, pnpm: join(runtime, 'pnpm.mjs'), nodeBin: join(runtime, 'bin') })
|
||||
})
|
||||
|
||||
it('reports a fatal event after readiness once', async () => {
|
||||
|
||||
@@ -108,7 +108,7 @@ const harness = await vi.hoisted(async () => {
|
||||
constructor(
|
||||
readonly node: string, readonly runtime: string, readonly profile: string,
|
||||
readonly inspectPort?: number, readonly environment?: NodeJS.ProcessEnv, readonly onFailure?: (error: Error) => void,
|
||||
readonly primaryRuntime?: string, readonly profileResolution?: string,
|
||||
readonly primaryRuntime?: string,
|
||||
readonly packageManager?: { pnpm: string; nodeBin: string },
|
||||
) { hosts.push(this) }
|
||||
}
|
||||
@@ -1322,7 +1322,6 @@ describe('desktop main startup', () => {
|
||||
node: process.execPath,
|
||||
runtime: join(harness.app.getAppPath(), 'dsh'),
|
||||
primaryRuntime: join('desktop-test-resources', 'runtime', 'primary-runtime'),
|
||||
profileResolution: 'runtime',
|
||||
profile: 'desktop-test-profile',
|
||||
})
|
||||
expect(harness.hosts[0]!.environment).toBe(process.env)
|
||||
|
||||
@@ -56,19 +56,15 @@ it('does not clean development profiles', () => {
|
||||
expect(existsSync(join(root, 'pnpm-lock.yaml'))).toBe(true)
|
||||
})
|
||||
|
||||
it('unlinks development fallbacks without deleting their target, including dangling links', () => {
|
||||
it('unlinks core packages without deleting their target, including dangling links', () => {
|
||||
const root = fixture()
|
||||
const target = join(root, 'development-package')
|
||||
mkdirSync(target)
|
||||
writeFileSync(join(target, 'sentinel'), 'keep')
|
||||
const owned = join(root, '.dsh-module-fallback', 'node_modules', core)
|
||||
mkdirSync(join(owned, '..'), { recursive: true })
|
||||
symlinkSync(target, owned, 'junction')
|
||||
rmSync(join(root, 'node_modules', core), { recursive: true })
|
||||
symlinkSync(owned, join(root, 'node_modules', core), 'junction')
|
||||
symlinkSync(target, join(root, 'node_modules', core), 'junction')
|
||||
cleanProfileCorePackages(root, [core], true)
|
||||
expect(readFileSync(join(target, 'sentinel'), 'utf8')).toBe('keep')
|
||||
expect(existsSync(owned)).toBe(false)
|
||||
symlinkSync(target, join(root, 'node_modules', core), 'junction')
|
||||
rmSync(target, { recursive: true })
|
||||
cleanProfileCorePackages(root, [core], true)
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
writesCurrentSessionFixtures,
|
||||
type NormalizeContext,
|
||||
} from '@deepseek-ai/dsh-session-snapshot'
|
||||
import type { Profile, ProfileContext, ProfileResolutionMode } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { Profile, ProfileContext } from '@deepseek-ai/dsh-app-boot'
|
||||
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
||||
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
||||
import type {
|
||||
@@ -304,8 +304,6 @@ export interface WebScaffold {
|
||||
export interface LaunchOptions {
|
||||
/** The scaffold enables developer tools unless false preserves the shipped default. */
|
||||
developerTools?: boolean
|
||||
/** Profile resolver backend used by this test Host; defaults to runtime coverage. */
|
||||
profileResolutionMode?: Extract<ProfileResolutionMode, 'dual' | 'runtime'>
|
||||
/** Enable the real Open In rows with deterministic launch-environment facts. */
|
||||
openInAppEnvironment?: LaunchEnvironmentSnapshot
|
||||
/** Compare the replayed root session with `replayFixture`; defaults on for a manifest-owned canonical recording. */
|
||||
@@ -467,7 +465,7 @@ async function cleanupScaffoldWorld(ctx: Context, workspaceCwd: string, persiste
|
||||
export async function launchWebScaffold(options: LaunchOptions = {}): Promise<WebScaffold> {
|
||||
requireDist()
|
||||
const {
|
||||
auditStartupEntries, composeEntries, createProfileResolutionGeneration, healProfilesModuleFallback, initProfile,
|
||||
auditStartupEntries, composeEntries, createProfileResolutionGeneration, initProfile,
|
||||
mountRootInclude, readProfileManifest, readProfilePatches, loadProfileDirectory, loadOverlayPatches, PluginPackages,
|
||||
} = appBoot()
|
||||
const mode = webSnapshotMode()
|
||||
@@ -750,11 +748,8 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
patchPath: join(profileDir, 'cordis.patch.yml'),
|
||||
patches: [],
|
||||
}
|
||||
const profileResolutionMode = options.profileResolutionMode ?? 'runtime'
|
||||
const resolutionOptions = { installAnchor: INSTALL_ANCHOR, home: harnessHome, profile }
|
||||
const resolution = profileResolutionMode === 'runtime'
|
||||
? await createProfileResolutionGeneration(resolutionOptions)
|
||||
: await healProfilesModuleFallback(resolutionOptions)
|
||||
const resolution = await createProfileResolutionGeneration(resolutionOptions)
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
await writeFile(rootConfig, '[]\n')
|
||||
@@ -802,7 +797,6 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
})
|
||||
await ctx.plugin(PluginPackages, {
|
||||
generation: resolution,
|
||||
behavior: profileResolutionMode === 'dual' ? 'verify' : 'enforce',
|
||||
})
|
||||
await ctx.plugin(Loader)
|
||||
if (profileContext === undefined) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// and asserts its catalog, defaults, Loader lifecycle, and one complete Auto
|
||||
// producer-to-tool path. Browser scenarios in this lane own visual behavior.
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -513,7 +513,6 @@ afterEach(async () => {
|
||||
|
||||
it('assembles the shipped Web transport, catalog, guidance, and defaults', async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
expect(existsSync(join(scaffold.harnessHome, 'profiles', 'node_modules'))).toBe(false)
|
||||
const ctx = scaffold.ctx
|
||||
expect(ctx.llm.listProviders().some(provider => provider.id === 'deepseek-messages')).toBe(false)
|
||||
expect(ctx.agentDefaultModel.currentSelection()).toEqual({ provider: 'deepseek-official', model: 'deepseek-flash' })
|
||||
@@ -641,9 +640,8 @@ it('assembles the shipped Web transport, catalog, guidance, and defaults', async
|
||||
}
|
||||
}, 120_000)
|
||||
|
||||
it('ships PTC with run_code but without the general workflow SDK binding under dual resolution', async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, profileResolutionMode: 'dual' })
|
||||
expect(existsSync(join(scaffold.harnessHome, 'profiles', 'node_modules'))).toBe(true)
|
||||
it('ships PTC with run_code but without the general workflow SDK binding', async () => {
|
||||
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true })
|
||||
const ctx = scaffold.ctx
|
||||
const handle = await ctx.agents.create({
|
||||
sessionId: SessionId('shipped-ptc-composition'),
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@deepseek-ai/dsh-atomic-write": "workspace:^",
|
||||
"@deepseek-ai/dsh-package-manifest": "workspace:^",
|
||||
"js-yaml": "^4.2.0",
|
||||
"node-addon-require-builtin": "^0.1.6",
|
||||
|
||||
@@ -38,9 +38,6 @@ export {
|
||||
createProfileResolutionGeneration,
|
||||
DEFAULT_PROFILE_BUNDLES,
|
||||
OPTIONAL_BUNDLES,
|
||||
healProfilesModuleFallback,
|
||||
healIsolatedProfileModuleFallback,
|
||||
unlinkProfileModuleFallback,
|
||||
initProfile,
|
||||
loadProfile,
|
||||
loadProfileDirectory,
|
||||
@@ -54,10 +51,9 @@ export {
|
||||
type Profile,
|
||||
type ProfileLayer,
|
||||
type ProfileManifest,
|
||||
type ProfileModuleFallbackOptions,
|
||||
type ProfileResolutionOptions,
|
||||
type ProfileResolutionEntry,
|
||||
type ProfileResolutionGeneration,
|
||||
type ProfileResolutionMode,
|
||||
type ProfileTemplate,
|
||||
} from './profile.ts'
|
||||
export {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
/** Legacy profile-link inspection shared by the disk materializer and runtime resolver. */
|
||||
/** Package directory canonicalization for Node and packaged executables. */
|
||||
|
||||
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'
|
||||
@@ -84,9 +83,6 @@ interface CompiledGeneration {
|
||||
readonly cjsRoutes: ResolutionRoutes
|
||||
}
|
||||
|
||||
/** Whether runtime resolution redirects requests or verifies the materialized backend. */
|
||||
export type ProfileResolutionBehavior = 'enforce' | 'verify'
|
||||
|
||||
/** Active resolver registration in one Node isolate. */
|
||||
export interface ProfileResolutionRegistration {
|
||||
/**
|
||||
@@ -171,14 +167,14 @@ function nativePackageDir(parent: string, name: string): string | undefined {
|
||||
|
||||
function localPackageCandidate(
|
||||
searchPath: string, name: string, flavor: 'esm' | 'cjs',
|
||||
): { packageDir: string; canBeManagedLink: boolean } | undefined {
|
||||
): { packageDir: 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 ? { packageDir: candidate } : undefined
|
||||
}
|
||||
|
||||
function selfReferenceName(parent: string): string | false | null {
|
||||
@@ -280,9 +276,7 @@ function isUnselectedPackageMiss(error: unknown): boolean {
|
||||
|
||||
function sameResolution(left: string, right: string): boolean {
|
||||
if (left === right) return true
|
||||
const leftPath = left.startsWith('file:') ? fileURLToPath(left) : left
|
||||
const rightPath = right.startsWith('file:') ? fileURLToPath(right) : right
|
||||
return canonicalPath(leftPath) === canonicalPath(rightPath)
|
||||
return canonicalPath(left) === canonicalPath(right)
|
||||
}
|
||||
|
||||
/** One mutable pointer to immutable generation data. */
|
||||
@@ -345,19 +339,13 @@ class ResolutionRouter {
|
||||
}
|
||||
|
||||
const target = generation.entries.get(name)
|
||||
const candidates: Array<{ packageDir: string; canBeManagedLink: boolean }> = []
|
||||
const candidates: Array<{ packageDir: string }> = []
|
||||
const localSearchPaths: string[] = []
|
||||
for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
|
||||
if (generation.shared.has(resolve(searchPath))) 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) {
|
||||
@@ -391,7 +379,7 @@ class ResolutionRouter {
|
||||
|
||||
const eligible = target?.scope === 'installation'
|
||||
|| (target?.scope === 'profile' && parentRoutes.activeProfile)
|
||||
const after = join(dirname(profilesDir), 'package.json')
|
||||
const after = join(profilesDir, 'package.json')
|
||||
const route: ResolutionRoute = eligible
|
||||
? { kind: 'fallback', entry: target, after }
|
||||
: { kind: 'after-fallback', parent: after }
|
||||
@@ -597,32 +585,13 @@ function throwWithoutCjsAnchor(error: unknown, anchor: string): never {
|
||||
throw error
|
||||
}
|
||||
|
||||
function assertEquivalent(actual: string, expected: string, request: string, parent: string): void {
|
||||
if (sameResolution(actual, expected)) return
|
||||
throw new Error(
|
||||
`profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk resolved ${actual}, generation resolved ${expected}`,
|
||||
)
|
||||
}
|
||||
|
||||
function assertOptionalEquivalent(
|
||||
actual: string | undefined, expected: string | undefined, request: string, parent: string,
|
||||
): void {
|
||||
if (actual === undefined && expected === undefined) return
|
||||
if (actual !== undefined && expected !== undefined && sameResolution(actual, expected)) return
|
||||
throw new Error(
|
||||
`profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk selected ${actual ?? 'nothing'}, generation selected ${expected ?? 'nothing'}`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Install one profile generation on Node's default ESM and CommonJS resolvers.
|
||||
* @param generation - complete package table and profile scope.
|
||||
* @param behavior - enforce the generation, or verify a materialized generation.
|
||||
* @returns a registration that replaces the generation or restores the native methods.
|
||||
*/
|
||||
export function installProfileResolution(
|
||||
generation: ProfileResolutionGeneration,
|
||||
behavior: ProfileResolutionBehavior = 'enforce',
|
||||
): ProfileResolutionRegistration {
|
||||
const router = new ResolutionRouter(generation)
|
||||
const { esm, esmDefaultResolve, esmConditions, cjs, cjsConditions, modern } = internalModules()
|
||||
@@ -669,18 +638,7 @@ export function installProfileResolution(
|
||||
} catch (error) {
|
||||
return restoreImporter(error)
|
||||
}
|
||||
if (behavior === 'enforce') return expected
|
||||
const actual = native(request, parent, attributes)
|
||||
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
||||
if (expected instanceof Promise || actual instanceof Promise) {
|
||||
return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
||||
assertEquivalent(resolved.url, wanted.url, request, parent)
|
||||
return resolved
|
||||
})
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
assertEquivalent(actual.url, expected.url, request, parent)
|
||||
return actual
|
||||
return expected
|
||||
}
|
||||
const cacheable = attributes === EMPTY_ATTRIBUTES || Object.keys(attributes).length === 0
|
||||
if (cacheable && state.esm !== undefined) return state.esm
|
||||
@@ -691,51 +649,20 @@ export function installProfileResolution(
|
||||
return result
|
||||
}
|
||||
const routedParent = pathToFileURL(route.kind === 'fallback' ? route.entry.declarer : route.parent).href
|
||||
if (behavior === 'enforce') {
|
||||
const previous = delegatedEsm
|
||||
delegatedEsm = { parent: routedParent, request }
|
||||
const restoreImporter = (error: unknown): never => throwWithImporter(error, routedParent, parent)
|
||||
try {
|
||||
let result: ResolveResult | Promise<ResolveResult>
|
||||
try {
|
||||
result = native(request, routedParent, attributes)
|
||||
} catch (error) {
|
||||
return restoreImporter(error)
|
||||
}
|
||||
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
||||
if (result instanceof Promise) return result.catch(restoreImporter)
|
||||
if (cacheable) state.esm = result
|
||||
return result
|
||||
} finally {
|
||||
delegatedEsm = previous
|
||||
}
|
||||
}
|
||||
const actual = native(request, parent, attributes)
|
||||
const previous = delegatedEsm
|
||||
delegatedEsm = { parent: routedParent, request }
|
||||
const restoreImporter = (error: unknown): never => throwWithImporter(error, routedParent, parent)
|
||||
try {
|
||||
let expected: ResolveResult | Promise<ResolveResult>
|
||||
let result: ResolveResult | Promise<ResolveResult>
|
||||
try {
|
||||
const result = native(request, routedParent, attributes)
|
||||
expected = result
|
||||
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
||||
if (result instanceof Promise) expected = result.catch(restoreImporter)
|
||||
result = native(request, routedParent, attributes)
|
||||
} catch (error) {
|
||||
return restoreImporter(error)
|
||||
}
|
||||
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
||||
if (expected instanceof Promise || actual instanceof Promise) {
|
||||
return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
||||
assertEquivalent(resolved.url, wanted.url, request, parent)
|
||||
if (cacheable) state.esm = resolved
|
||||
return resolved
|
||||
})
|
||||
}
|
||||
/* v8 ignore stop */
|
||||
assertEquivalent(actual.url, expected.url, request, parent)
|
||||
if (cacheable) state.esm = actual
|
||||
return actual
|
||||
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
||||
if (result instanceof Promise) return result.catch(restoreImporter)
|
||||
if (cacheable) state.esm = result
|
||||
return result
|
||||
} finally {
|
||||
delegatedEsm = previous
|
||||
}
|
||||
@@ -867,11 +794,7 @@ export function installProfileResolution(
|
||||
? packageImportsTarget(parentFilename, request, conditions)
|
||||
: undefined
|
||||
if (target === undefined) return originalFilename.call(cjs, request, parent, main, options)
|
||||
const expected = resolvePackageImportCjs(target, conditions)
|
||||
if (behavior === 'enforce') return expected
|
||||
const actual = originalFilename.call(cjs, request, parent, main, options)
|
||||
assertEquivalent(actual, expected, request, parentFilename)
|
||||
return actual
|
||||
return resolvePackageImportCjs(target, conditions)
|
||||
}
|
||||
if (cacheable && state.cjs !== undefined) return state.cjs
|
||||
const route = state.route
|
||||
@@ -920,14 +843,8 @@ export function installProfileResolution(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (behavior === 'enforce') {
|
||||
if (cacheable) state.cjs = expected
|
||||
return expected
|
||||
}
|
||||
const actual = originalFilename.call(cjs, request, parent, main, options)
|
||||
assertEquivalent(actual, expected, request, parentFilename)
|
||||
if (cacheable) state.cjs = actual
|
||||
return actual
|
||||
if (cacheable) state.cjs = expected
|
||||
return expected
|
||||
} finally {
|
||||
delegatedCjs--
|
||||
}
|
||||
@@ -936,19 +853,7 @@ export function installProfileResolution(
|
||||
|
||||
return {
|
||||
packageDir(specifier, parentURL) {
|
||||
const expected = router.packageDir(specifier, parentURL)
|
||||
if (behavior !== 'verify' || !startsWithin(parentURL, profileUrls)) return expected
|
||||
const name = barePackageName(specifier)
|
||||
if (name === undefined) return expected
|
||||
let parent: string
|
||||
try {
|
||||
parent = fileURLToPath(parentURL)
|
||||
} catch {
|
||||
return expected
|
||||
}
|
||||
const actual = nativePackageDir(parent, name)
|
||||
assertOptionalEquivalent(actual, expected, specifier, parentURL)
|
||||
return expected
|
||||
return router.packageDir(specifier, parentURL)
|
||||
},
|
||||
replace(next) { router.replace(next) },
|
||||
dispose() {
|
||||
@@ -962,14 +867,12 @@ export function installProfileResolution(
|
||||
/**
|
||||
* Publish one generation for Harness-owned Workers.
|
||||
* @param generation - complete package table and profile scope.
|
||||
* @param behavior - enforce or verify the generation in newly created Workers.
|
||||
* @returns a disposer restoring the previous thread environment data.
|
||||
*/
|
||||
export function registerWorkerResolution(
|
||||
generation: ProfileResolutionGeneration,
|
||||
behavior: ProfileResolutionBehavior = 'enforce',
|
||||
): () => void {
|
||||
const previous = getEnvironmentData(WORKER_RESOLUTION_KEY)
|
||||
setEnvironmentData(WORKER_RESOLUTION_KEY, { generation, behavior })
|
||||
setEnvironmentData(WORKER_RESOLUTION_KEY, { generation })
|
||||
return () => { setEnvironmentData(WORKER_RESOLUTION_KEY, previous) }
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
barePackageName,
|
||||
installProfileResolution,
|
||||
registerWorkerResolution,
|
||||
type ProfileResolutionBehavior,
|
||||
type ProfileResolutionRegistration,
|
||||
} from './resolver.ts'
|
||||
import type { ProfileResolutionGeneration } from '../profile.ts'
|
||||
@@ -38,8 +37,6 @@ export interface PluginPackage {
|
||||
export interface PluginPackagesConfig {
|
||||
/** Complete package table; omit it to expose native package lookup only. */
|
||||
generation?: ProfileResolutionGeneration
|
||||
/** Enforce the table or compare it with a materialized fallback. */
|
||||
behavior?: ProfileResolutionBehavior
|
||||
}
|
||||
|
||||
function readPackage(dir: string, fallbackName: string): PluginPackage | undefined {
|
||||
@@ -61,15 +58,13 @@ function readPackage(dir: string, fallbackName: string): PluginPackage | undefin
|
||||
export class PluginPackages extends Service {
|
||||
private packages = new Map<string, PluginPackage | undefined>()
|
||||
private readonly resolver: ProfileResolutionRegistration | undefined
|
||||
private readonly behavior: ProfileResolutionBehavior
|
||||
private disposeWorkerResolution: (() => void) | undefined
|
||||
|
||||
constructor(ctx: Context, config: PluginPackagesConfig = {}) {
|
||||
super(ctx, 'pluginPackages')
|
||||
this.behavior = config.behavior ?? 'enforce'
|
||||
if (config.generation === undefined) return
|
||||
const resolver = installProfileResolution(config.generation, this.behavior)
|
||||
this.disposeWorkerResolution = registerWorkerResolution(config.generation, this.behavior)
|
||||
const resolver = installProfileResolution(config.generation)
|
||||
this.disposeWorkerResolution = registerWorkerResolution(config.generation)
|
||||
this.resolver = resolver
|
||||
ctx.effect(() => () => {
|
||||
this.disposeWorkerResolution?.()
|
||||
@@ -86,7 +81,7 @@ export class PluginPackages extends Service {
|
||||
this.resolver.replace(generation)
|
||||
this.packages = new Map()
|
||||
this.disposeWorkerResolution?.()
|
||||
this.disposeWorkerResolution = registerWorkerResolution(generation, this.behavior)
|
||||
this.disposeWorkerResolution = registerWorkerResolution(generation)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/** Install an inherited profile resolution generation in one Harness-owned Worker. */
|
||||
|
||||
import { getEnvironmentData } from 'node:worker_threads'
|
||||
import { installProfileResolution, type ProfileResolutionBehavior } from './resolver.ts'
|
||||
import { installProfileResolution } from './resolver.ts'
|
||||
import type { ProfileResolutionGeneration } from '../profile.ts'
|
||||
|
||||
const registration = getEnvironmentData(
|
||||
'@deepseek-ai/dsh-app-boot/profile-resolution',
|
||||
) as {
|
||||
generation: ProfileResolutionGeneration
|
||||
behavior: ProfileResolutionBehavior
|
||||
} | undefined
|
||||
if (registration !== undefined) installProfileResolution(registration.generation, registration.behavior)
|
||||
if (registration !== undefined) installProfileResolution(registration.generation)
|
||||
@@ -15,36 +15,20 @@
|
||||
* Module resolution is two-anchor by construction: a bundle name resolves
|
||||
* first from the dsh installation (the launcher's own package), then from the
|
||||
* profile directory. Pnpm-managed entries in the profile's `node_modules`
|
||||
* resolve first. Dsh-owned links add packages carried only by selected
|
||||
* bundles, while `$DSH_HOME/profiles/node_modules` supplies the installation
|
||||
* dependency closure through Node's ordinary parent-walk. Plain Node uses
|
||||
* symlinks for that shared fallback; packaged executables use ESM proxies so
|
||||
* external plugins retain the installation's module instances.
|
||||
* resolve first. A runtime generation supplies packages carried by the
|
||||
* installation and selected bundles to Node's ESM and CommonJS resolvers.
|
||||
* @module @deepseek-ai/dsh-app-boot/profile
|
||||
*/
|
||||
|
||||
import { createRequire } from 'node:module'
|
||||
import {
|
||||
existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync,
|
||||
symlinkSync, unlinkSync, writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { basename, dirname, join, relative, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
|
||||
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 { resolve as resolvePackage, type Package as ResolvePackageManifest } from 'resolve.exports'
|
||||
import { loadOverlayPatches } from './index.ts'
|
||||
import {
|
||||
canonicalLinkPath,
|
||||
isPackagedExecutable,
|
||||
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'
|
||||
@@ -113,9 +97,6 @@ export interface ProfileResolutionGeneration {
|
||||
readonly entries: readonly ProfileResolutionEntry[]
|
||||
}
|
||||
|
||||
/** Startup backend selection for one computed profile resolution generation. */
|
||||
export type ProfileResolutionMode = 'link' | 'dual' | 'runtime'
|
||||
|
||||
/**
|
||||
* Resolve a profile's directory under the Harness home.
|
||||
* @param name - the profile name (`dsh --profile <name>`).
|
||||
@@ -124,7 +105,7 @@ export type ProfileResolutionMode = 'link' | 'dual' | 'runtime'
|
||||
*/
|
||||
export function resolveProfileDir(name: string, home: string = resolveDshHome()): string {
|
||||
if (name === '' || name.includes('/') || name.includes('\\') || name === '.' || name === '..'
|
||||
// The launcher-maintained flat module fallback lives at this sibling path.
|
||||
// Node reserves this name for dependency lookup.
|
||||
|| name === 'node_modules') {
|
||||
throw new Error(`dsh: invalid profile name ${JSON.stringify(name)}`)
|
||||
}
|
||||
@@ -175,8 +156,7 @@ const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied
|
||||
`
|
||||
|
||||
// The hoisted linker gives out-of-tree plugins a flat node_modules whose
|
||||
// missing peers (cordis and friends) fall through to the healed
|
||||
// profiles/node_modules installation fallback, so every plugin shares the
|
||||
// missing peers (cordis and friends) use the runtime generation, so every plugin shares the
|
||||
// installation's single cordis instance instead of a duplicate. pnpm ≥10
|
||||
// reads its settings from pnpm-workspace.yaml, not .npmrc.
|
||||
const PROFILE_PNPM_WORKSPACE = `packages:
|
||||
@@ -214,247 +194,6 @@ export function initProfile(
|
||||
if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE)
|
||||
}
|
||||
|
||||
function readModuleProxyRecord(link: string): ModuleProxyRecord | undefined {
|
||||
try {
|
||||
return JSON.parse(readFileSync(join(link, 'package.json'), 'utf8')) as ModuleProxyRecord
|
||||
} catch {
|
||||
// Missing or invalid metadata is not managed state; callers reject it.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure `link` is a symlink to `target`, replacing a wrong link or a dsh-managed packaged proxy. */
|
||||
function ensureSymlink(link: string, target: string): void {
|
||||
let stat
|
||||
try {
|
||||
stat = lstatSync(link)
|
||||
} catch {
|
||||
// Missing link (first run) — created below. Any other lstat failure on a
|
||||
// path we just created the parent of would resurface on symlinkSync.
|
||||
stat = undefined
|
||||
}
|
||||
if (stat !== undefined) {
|
||||
if (!stat.isSymbolicLink()) {
|
||||
const existing = stat.isDirectory() ? readModuleProxyRecord(link) : undefined
|
||||
if (existing?.dsh?.moduleFallback?.targets === undefined) {
|
||||
throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
|
||||
}
|
||||
rmSync(link, { recursive: true })
|
||||
stat = undefined
|
||||
}
|
||||
if (stat !== undefined) {
|
||||
if (symlinkPointsTo(link, target)) return
|
||||
// unlink deletes the reparse point itself on Windows too; rmSync treats a
|
||||
// junction as a directory and throws EISDIR unless recursive.
|
||||
unlinkSync(link)
|
||||
}
|
||||
}
|
||||
try {
|
||||
symlinkSync(target, link, 'junction')
|
||||
} catch (error) {
|
||||
// Concurrent launches heal the same fallback; losing the race to a
|
||||
// process writing the identical link is success, anything else is not.
|
||||
// The window between the lstat miss above and this write cannot be
|
||||
// staged deterministically from the public API.
|
||||
/* v8 ignore next 4 */
|
||||
if ((error as NodeJS.ErrnoException).code !== 'EEXIST'
|
||||
|| !lstatSync(link).isSymbolicLink() || !symlinkPointsTo(link, target)) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Add one profile-owned fallback link without replacing a pnpm-managed entry. */
|
||||
function ensureProfileSymlink(link: string, target: string): void {
|
||||
try {
|
||||
lstatSync(link)
|
||||
return
|
||||
} catch (error) {
|
||||
/* v8 ignore next -- a non-ENOENT lstat failure requires a host filesystem fault */
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
}
|
||||
ensureSymlink(link, target)
|
||||
}
|
||||
|
||||
/** Package names represented by owned symlinks below one fallback node_modules. */
|
||||
function ownedPackageNames(modulesDir: string): string[] {
|
||||
return readdirSync(modulesDir, { withFileTypes: true }).flatMap((entry) => {
|
||||
if (entry.name.startsWith('@') && entry.isDirectory()) {
|
||||
return readdirSync(join(modulesDir, entry.name), { withFileTypes: true })
|
||||
.filter(child => child.isSymbolicLink())
|
||||
.map(child => `${entry.name}/${child.name}`)
|
||||
}
|
||||
return entry.isSymbolicLink() ? [entry.name] : []
|
||||
})
|
||||
}
|
||||
|
||||
/** Remove an obsolete owned target and its profile projection when still connected. */
|
||||
function removeProfileSymlink(profileModulesDir: string, ownedModulesDir: string, packageName: string): void {
|
||||
const ownedLink = join(ownedModulesDir, packageName)
|
||||
const profileLink = join(profileModulesDir, packageName)
|
||||
try {
|
||||
if (lstatSync(profileLink).isSymbolicLink() && symlinkPointsTo(profileLink, ownedLink)) unlinkSync(profileLink)
|
||||
} catch (error) {
|
||||
/* v8 ignore next -- a non-ENOENT lstat failure requires a host filesystem fault */
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
}
|
||||
try {
|
||||
unlinkSync(ownedLink)
|
||||
} catch (error) {
|
||||
/* v8 ignore next -- concurrent identical cleanup may remove the link first */
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
|
||||
}
|
||||
}
|
||||
|
||||
interface ModuleProxyManifest {
|
||||
name: string
|
||||
version: string
|
||||
private: true
|
||||
type: 'module'
|
||||
exports: Record<string, string>
|
||||
dsh: { moduleFallback: { targets: Record<string, string> } }
|
||||
}
|
||||
|
||||
interface ModuleProxyRecord {
|
||||
version?: unknown
|
||||
dsh?: { moduleFallback?: { targets?: unknown } }
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
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 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) {
|
||||
throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`)
|
||||
}
|
||||
const declared = manifest.exports
|
||||
if (declared === undefined) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
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.endsWith('/') && key !== './package.json'
|
||||
))
|
||||
: ['.']
|
||||
const targets: Record<string, string> = {}
|
||||
for (const subpath of subpaths) {
|
||||
const target = packageEntryFromPackage(
|
||||
packageName,
|
||||
packageDir,
|
||||
declared as ResolvePackageManifest['exports'],
|
||||
subpath,
|
||||
)
|
||||
if (target !== undefined) targets[subpath] = target
|
||||
}
|
||||
return { version: manifest.version, targets }
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize a real package proxy whose exports retain pkg's virtual module
|
||||
* URL. Files outside the executable cannot traverse a symlink into
|
||||
* `/snapshot`, while an ESM re-export can import that URL and preserves the
|
||||
* executable's single module instance for out-of-tree plugin peers.
|
||||
*/
|
||||
function ensureModuleProxy(
|
||||
link: string,
|
||||
packageName: string,
|
||||
version: string,
|
||||
targets: Record<string, string>,
|
||||
): void {
|
||||
const proxyExports = Object.fromEntries(
|
||||
Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`]),
|
||||
)
|
||||
const manifest: ModuleProxyManifest = {
|
||||
name: packageName,
|
||||
version,
|
||||
private: true,
|
||||
type: 'module',
|
||||
exports: proxyExports,
|
||||
dsh: { moduleFallback: { targets } },
|
||||
}
|
||||
let stat
|
||||
try {
|
||||
stat = lstatSync(link)
|
||||
} catch {
|
||||
stat = undefined
|
||||
}
|
||||
if (stat?.isSymbolicLink()) {
|
||||
unlinkSync(link)
|
||||
stat = undefined
|
||||
}
|
||||
if (stat !== undefined) {
|
||||
const existing = readModuleProxyRecord(link)
|
||||
if (existing?.dsh?.moduleFallback?.targets === undefined) {
|
||||
throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
|
||||
}
|
||||
if (existing.version === version
|
||||
&& JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)
|
||||
&& Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return
|
||||
rmSync(link, { recursive: true })
|
||||
}
|
||||
mkdirSync(link, { recursive: true })
|
||||
writeFileSync(join(link, 'package.json'), JSON.stringify(manifest, undefined, 2) + '\n')
|
||||
for (const [index, target] of Object.values(targets).entries()) {
|
||||
const specifier = JSON.stringify(target)
|
||||
writeFileSync(
|
||||
join(link, `entry-${index}.js`),
|
||||
`export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type ModuleFallbackEntry =
|
||||
| { kind: 'symlink'; packageName: string; packageDir: string }
|
||||
| { kind: 'proxy'; packageName: string; version: string; targets: Record<string, string> }
|
||||
|
||||
/** Read one package manifest used while traversing a module-fallback dependency graph. */
|
||||
function readModuleFallbackManifest(anchor: string): ProfileManifest {
|
||||
return JSON.parse(readFileSync(anchor, 'utf8')) as ProfileManifest
|
||||
@@ -467,9 +206,8 @@ function profileDependencyNames(manifest: ProfileManifest): string[] {
|
||||
|
||||
/** Resolve the installation generation that every profile must find through the fallback directory. */
|
||||
function resolveModuleFallbackEntries(
|
||||
installAnchor: string, materialize = true, skippedBundles: ReadonlySet<string> = new Set(),
|
||||
installAnchor: string, skippedBundles: ReadonlySet<string>,
|
||||
): {
|
||||
entries: ModuleFallbackEntry[]
|
||||
packageNames: ReadonlySet<string>
|
||||
packageDirs: ReadonlyMap<string, string>
|
||||
declarers: ReadonlyMap<string, string>
|
||||
@@ -513,90 +251,40 @@ function resolveModuleFallbackEntries(
|
||||
queue.push({ anchor: manifestPath, manifest })
|
||||
}
|
||||
}
|
||||
const entries = !materialize
|
||||
? []
|
||||
: !isPackagedExecutable()
|
||||
? [...links].map(([packageName, packageDir]) => ({ kind: 'symlink' as const, packageName, packageDir }))
|
||||
: [...links].flatMap(([packageName, packageDir]) => {
|
||||
const source = packageProxySource(packageName, packageDir)
|
||||
return Object.keys(source.targets).length === 0
|
||||
? []
|
||||
: [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }]
|
||||
})
|
||||
return { entries, packageNames: new Set(links.keys()), packageDirs: links, declarers, versions }
|
||||
return { packageNames: new Set(links.keys()), packageDirs: links, declarers, versions }
|
||||
}
|
||||
|
||||
/** Return whether one existing fallback entry already matches its resolved installation generation. */
|
||||
function moduleFallbackEntryCurrent(modulesDir: string, entry: ModuleFallbackEntry): boolean {
|
||||
const link = join(modulesDir, entry.packageName)
|
||||
try {
|
||||
const stat = lstatSync(link)
|
||||
if (entry.kind === 'symlink') {
|
||||
return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir
|
||||
}
|
||||
if (!stat.isDirectory()) return false
|
||||
const existing = readModuleProxyRecord(link)
|
||||
return existing?.version === entry.version
|
||||
&& JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets)
|
||||
&& Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Return whether every required fallback entry is already ready for this installation. */
|
||||
function moduleFallbackCurrent(modulesDir: string, entries: readonly ModuleFallbackEntry[]): boolean {
|
||||
return entries.every(entry => moduleFallbackEntryCurrent(modulesDir, entry))
|
||||
}
|
||||
|
||||
/** Inputs for {@link healProfilesModuleFallback}. */
|
||||
export interface ProfileModuleFallbackOptions {
|
||||
/** Inputs for {@link createProfileResolutionGeneration}. */
|
||||
export interface ProfileResolutionOptions {
|
||||
/** Absolute package.json path of the running dsh installation. */
|
||||
installAnchor: string
|
||||
/** Loaded profile whose selected bundles may carry profile-local plugins. */
|
||||
profile?: Profile
|
||||
/** Harness home; defaults to {@link resolveDshHome}. */
|
||||
home?: string
|
||||
/** Whether to materialize the computed generation; defaults to true. */
|
||||
materialize?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintain module fallbacks for one profile launch. The shared
|
||||
* `$DSH_HOME/profiles/node_modules` mirrors the dsh installation dependency
|
||||
* closure. Plain Node writes symlinks; a packaged executable writes ESM
|
||||
* proxies under a cross-process lock because operating-system links cannot
|
||||
* enter pkg's virtual filesystem. Missing packages carried only by selected
|
||||
* bundles are linked through a profile-owned directory into that profile's
|
||||
* `node_modules`; pnpm-managed entries remain authoritative, and another
|
||||
* profile's links cannot change its resolution.
|
||||
* Compute a profile resolution generation without writing module-resolution files.
|
||||
* @param options - installation anchor, optional loaded profile, and Harness home.
|
||||
* @returns the computed fallback generation after optional materialization.
|
||||
* @returns the complete immutable generation.
|
||||
*/
|
||||
export async function healProfilesModuleFallback(
|
||||
options: ProfileModuleFallbackOptions,
|
||||
export async function createProfileResolutionGeneration(
|
||||
options: ProfileResolutionOptions,
|
||||
): Promise<ProfileResolutionGeneration> {
|
||||
const { installAnchor, profile, home = resolveDshHome(), materialize = true } = options
|
||||
const { installAnchor, profile, home = resolveDshHome() } = options
|
||||
const profilesDir = join(home, PROFILES_DIR)
|
||||
const modulesDir = join(profilesDir, 'node_modules')
|
||||
if (materialize) mkdirSync(modulesDir, { recursive: true })
|
||||
const manifest = readOptionalProfileManifest(profile)
|
||||
const { entries, packageNames, packageDirs, declarers, versions } = resolveModuleFallbackEntries(
|
||||
installAnchor, materialize, skippedProfileBundles(profile, manifest),
|
||||
const { packageNames, packageDirs, declarers, versions } = resolveModuleFallbackEntries(
|
||||
installAnchor, skippedProfileBundles(profile, manifest),
|
||||
)
|
||||
if (materialize && !moduleFallbackCurrent(modulesDir, entries)) {
|
||||
await withFileLock(modulesDir, () => {
|
||||
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir)
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
const profileDeclarers = new Map<string, string>()
|
||||
const profileVersions = new Map<string, string | undefined>()
|
||||
const localPackageNames = profile === undefined ? [] : installedProfilePackageNames(profile, manifest)
|
||||
const profilePackages: ReadonlyMap<string, string> = profile === undefined
|
||||
? new Map<string, string>()
|
||||
: healProfileModuleFallback(profile, packageNames, materialize, profileDeclarers, profileVersions)
|
||||
return Object.freeze({
|
||||
: resolveProfileModuleFallback(profile, packageNames, profileDeclarers, profileVersions)
|
||||
return await Promise.resolve(Object.freeze({
|
||||
profilesDir,
|
||||
profileDir: profile?.dir,
|
||||
localPackageNames: Object.freeze(localPackageNames),
|
||||
@@ -610,7 +298,7 @@ export async function healProfilesModuleFallback(
|
||||
declarer: profileDeclarers.get(name) as string, scope: 'profile' as const,
|
||||
})),
|
||||
]),
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/** Synthetic profiles used by direct callers may have no on-disk manifest. */
|
||||
@@ -636,65 +324,13 @@ function installedProfilePackageNames(profile: Profile, manifest: ProfileManifes
|
||||
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 existsSync(join(candidate, 'package.json'))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a profile resolution generation without materializing links or proxies.
|
||||
* @param options - installation anchor, profile, and optional Harness home.
|
||||
* @returns the complete immutable generation.
|
||||
*/
|
||||
export function createProfileResolutionGeneration(
|
||||
options: Omit<ProfileModuleFallbackOptions, 'materialize'>,
|
||||
): Promise<ProfileResolutionGeneration> {
|
||||
return healProfilesModuleFallback({ ...options, materialize: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* Supply an application-owned profile with filesystem packages from its installation and selected bundles.
|
||||
* All fallback links belong to the profile; no shared Harness-home directory is written.
|
||||
* Existing pnpm-managed packages remain authoritative. The caller serializes profile mutations.
|
||||
* @param options - owning installation package.json and the loaded application profile.
|
||||
*/
|
||||
export function healIsolatedProfileModuleFallback(options: { installAnchor: string; profile: Profile }): void {
|
||||
const installationLinks = resolveModuleFallbackEntries(
|
||||
options.installAnchor, false, skippedProfileBundles(options.profile, readOptionalProfileManifest(options.profile)),
|
||||
).packageDirs
|
||||
healProfileModuleFallback(options.profile, new Set(installationLinks.keys()), true, undefined, undefined, installationLinks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach this profile's fallback links before a package-manager mutation.
|
||||
* Installed packages and links replaced by pnpm remain untouched; the next profile launch restores fallbacks.
|
||||
* @param profileDir - profile directory whose package mutation is serialized by the caller.
|
||||
*/
|
||||
export function unlinkProfileModuleFallback(profileDir: string): void {
|
||||
const ownedModulesDir = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, 'node_modules')
|
||||
if (!existsSync(ownedModulesDir)) return
|
||||
for (const name of ownedPackageNames(ownedModulesDir)) {
|
||||
removeProfileSymlink(join(profileDir, 'node_modules'), ownedModulesDir, name)
|
||||
}
|
||||
}
|
||||
|
||||
/** Heal one module-fallback generation while the cross-process writer lock is held. */
|
||||
function healProfilesModuleFallbackLocked(entries: readonly ModuleFallbackEntry[], modulesDir: string): void {
|
||||
for (const entry of entries) {
|
||||
const link = join(modulesDir, entry.packageName)
|
||||
mkdirSync(dirname(link), { recursive: true })
|
||||
if (entry.kind === 'proxy') {
|
||||
ensureModuleProxy(link, entry.packageName, entry.version, entry.targets)
|
||||
} else {
|
||||
ensureSymlink(link, entry.packageDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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> {
|
||||
@@ -717,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)
|
||||
@@ -733,51 +369,18 @@ function dependencyClosure(
|
||||
return links
|
||||
}
|
||||
|
||||
/** Reconcile packages carried only by selected bundles into one profile. */
|
||||
function healProfileModuleFallback(
|
||||
profile: Profile, installationPackageNames: ReadonlySet<string>, materialize = true,
|
||||
/** Resolve packages carried only by selected bundles for one profile. */
|
||||
function resolveProfileModuleFallback(
|
||||
profile: Profile, installationPackageNames: ReadonlySet<string>,
|
||||
declarers?: Map<string, string>,
|
||||
versions?: Map<string, string | undefined>,
|
||||
installationLinks: ReadonlyMap<string, string> = new Map(),
|
||||
): Map<string, string> {
|
||||
const profileModulesDir = join(profile.dir, 'node_modules')
|
||||
const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, 'node_modules')
|
||||
if (materialize) {
|
||||
mkdirSync(profileModulesDir, { recursive: true })
|
||||
mkdirSync(ownedModulesDir, { recursive: true })
|
||||
}
|
||||
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)
|
||||
const links = new Map([...installationLinks, ...bundleLinks])
|
||||
if (!materialize) return links
|
||||
for (const packageName of ownedPackageNames(ownedModulesDir)) {
|
||||
if (!links.has(packageName)) removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName)
|
||||
}
|
||||
for (const [packageName, target] of links) {
|
||||
const ownedLink = join(ownedModulesDir, packageName)
|
||||
mkdirSync(dirname(ownedLink), { recursive: true })
|
||||
ensureSymlink(ownedLink, target)
|
||||
const profileLink = join(profileModulesDir, packageName)
|
||||
mkdirSync(dirname(profileLink), { recursive: true })
|
||||
ensureProfileSymlink(profileLink, ownedLink)
|
||||
}
|
||||
return links
|
||||
return bundleLinks
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -851,13 +454,12 @@ function normalizeShippedProfile(name: string, dir: string, manifest: ProfileMan
|
||||
*/
|
||||
function packageDirFromAnchor(
|
||||
anchor: string, packageName: string,
|
||||
exclude: (candidate: string, packageName: string) => boolean = () => false,
|
||||
): 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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Package metadata queries share the active profile resolution generation. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
@@ -133,23 +133,6 @@ describe('profile package metadata service', () => {
|
||||
)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not revive a stale disk fallback after the runtime generation misses', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-stale-package-service-'))
|
||||
roots.push(root)
|
||||
const profilesDir = join(root, 'profiles')
|
||||
const profileDir = join(profilesDir, 'test')
|
||||
pkg(join(profilesDir, 'node_modules', 'stale-metadata'), '0.9.0', 'stale-metadata')
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(PluginPackages, {
|
||||
generation: { profilesDir, profileDir, localPackageNames: [], entries: [] },
|
||||
})
|
||||
|
||||
expect(ctx.pluginPackages.packageOf(
|
||||
'stale-metadata', pathToFileURL(join(profileDir, 'entry.mjs')).href,
|
||||
)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('publishes additive generations to the process and future Workers', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-package-service-generation-'))
|
||||
roots.push(root)
|
||||
@@ -158,30 +141,18 @@ describe('profile package metadata service', () => {
|
||||
const first = join(root, 'first')
|
||||
const firstAnchor = pkg(first, '1.0.0')
|
||||
const initial = generation(profilesDir, profileDir, first, firstAnchor, '1.0.0')
|
||||
mkdirSync(join(profilesDir, 'node_modules'), { recursive: true })
|
||||
symlinkSync(
|
||||
first,
|
||||
join(profilesDir, 'node_modules', 'metadata-lib'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir',
|
||||
)
|
||||
const key = '@deepseek-ai/dsh-app-boot/profile-resolution'
|
||||
const previous = getEnvironmentData(key)
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(PluginPackages, { generation: initial, behavior: 'verify' })
|
||||
await ctx.plugin(PluginPackages, { generation: initial })
|
||||
const initialWorkerData = getEnvironmentData(key) as {
|
||||
generation: ProfileResolutionGeneration
|
||||
behavior: string
|
||||
}
|
||||
expect(initialWorkerData).toEqual({ generation: initial, behavior: 'verify' })
|
||||
expect(initialWorkerData).toEqual({ generation: initial })
|
||||
|
||||
const added = join(root, 'added')
|
||||
const addedAnchor = pkg(added, '2.0.0', 'added-metadata')
|
||||
symlinkSync(
|
||||
added,
|
||||
join(profilesDir, 'node_modules', 'added-metadata'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir',
|
||||
)
|
||||
const next = {
|
||||
...initial,
|
||||
entries: [...initial.entries, {
|
||||
@@ -192,7 +163,6 @@ describe('profile package metadata service', () => {
|
||||
ctx.pluginPackages.replace(next)
|
||||
expect(getEnvironmentData(key)).toEqual({
|
||||
generation: next,
|
||||
behavior: 'verify',
|
||||
})
|
||||
expect(ctx.pluginPackages.packageOf(
|
||||
'added-metadata', pathToFileURL(join(profileDir, 'entry.mjs')).href,
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { ProfileResolutionGeneration } from '../src/profile.ts'
|
||||
const harness = vi.hoisted(() => ({
|
||||
data: undefined as {
|
||||
generation: ProfileResolutionGeneration
|
||||
behavior: 'enforce' | 'verify'
|
||||
} | undefined,
|
||||
install: vi.fn(),
|
||||
}))
|
||||
@@ -30,14 +29,14 @@ it('does nothing without inherited profile resolution data', async () => {
|
||||
expect(harness.install).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('installs the inherited generation and behavior', async () => {
|
||||
it('installs the inherited generation', async () => {
|
||||
const generation: ProfileResolutionGeneration = {
|
||||
profilesDir: '/profiles',
|
||||
profileDir: '/profiles/test',
|
||||
localPackageNames: [],
|
||||
entries: [],
|
||||
}
|
||||
harness.data = { generation, behavior: 'verify' }
|
||||
harness.data = { generation }
|
||||
await import('../src/profile-resolution/worker-bootstrap.ts')
|
||||
expect(harness.install).toHaveBeenCalledWith(generation, 'verify')
|
||||
expect(harness.install).toHaveBeenCalledWith(generation)
|
||||
})
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from '../src/profile-resolution/resolver.ts'
|
||||
import {
|
||||
createProfileResolutionGeneration,
|
||||
healProfilesModuleFallback,
|
||||
type Profile,
|
||||
type ProfileResolutionGeneration,
|
||||
} from '../src/profile.ts'
|
||||
@@ -165,16 +164,15 @@ function fixture(name = 'resolution-lib'): {
|
||||
}
|
||||
|
||||
async function generationOf(f: ReturnType<typeof fixture>): Promise<ProfileResolutionGeneration> {
|
||||
return await healProfilesModuleFallback({
|
||||
return await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
materialize: false,
|
||||
})
|
||||
}
|
||||
|
||||
describe('profile resolution generation', { concurrent: false }, () => {
|
||||
it('computes the old fallback graph without materializing it', async () => {
|
||||
it('computes an immutable runtime generation without writing profile packages', async () => {
|
||||
const f = fixture()
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.entries.find(entry => entry.name === 'resolution-lib')).toMatchObject({
|
||||
@@ -184,7 +182,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
scope: 'installation',
|
||||
})
|
||||
expect(existsSync(join(generation.profilesDir, 'node_modules'))).toBe(false)
|
||||
expect(existsSync(join(f.profile.dir, '.dsh-module-fallback'))).toBe(false)
|
||||
expect(existsSync(join(f.profile.dir, 'node_modules'))).toBe(false)
|
||||
expect(Object.isFrozen(generation)).toBe(true)
|
||||
expect(Object.isFrozen(generation.entries)).toBe(true)
|
||||
expect(generation.entries.every(Object.isFrozen)).toBe(true)
|
||||
@@ -208,23 +206,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(existsSync(join(f.root, 'profiles', 'node_modules'))).toBe(false)
|
||||
})
|
||||
|
||||
it('materializes exactly the package targets in the computed generation', async () => {
|
||||
const f = fixture()
|
||||
const computed = await generationOf(f)
|
||||
const materialized = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
expect(materialized).toEqual(computed)
|
||||
for (const entry of computed.entries) {
|
||||
const projected = entry.scope === 'installation'
|
||||
? join(computed.profilesDir, 'node_modules', entry.name)
|
||||
: join(f.profile.dir, '.dsh-module-fallback', 'node_modules', entry.name)
|
||||
expect(realpathSync(projected)).toBe(realpathSync(entry.packageDir))
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps each earlier root complete before considering a later root', async () => {
|
||||
const f = fixture('installation-bridge')
|
||||
const installedBridge = f.installed
|
||||
@@ -308,20 +289,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs'))('aliased-lib')).toEqual({ marker: 5 })
|
||||
})
|
||||
|
||||
it('verifies a materialized scoped CommonJS package against the generation', async () => {
|
||||
const f = fixture('@scope/resolution-lib')
|
||||
const generation = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const registration = installProfileResolution(generation, 'verify')
|
||||
registrations.push(registration)
|
||||
|
||||
expect(createRequire(join(f.profile.dir, 'dual-entry.cjs')).resolve('@scope/resolution-lib'))
|
||||
.toBe(realpathSync(join(f.installed, 'index.cjs')))
|
||||
})
|
||||
|
||||
it('routes an application-owned profile outside the shared profiles directory', async () => {
|
||||
const f = fixture()
|
||||
const profileDir = join(f.root, 'application-profile')
|
||||
@@ -331,11 +298,10 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
dir: profileDir,
|
||||
patchPath: join(profileDir, 'cordis.patch.yml'),
|
||||
}
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile,
|
||||
home: f.root,
|
||||
materialize: false,
|
||||
})
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -371,7 +337,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
|
||||
it('keeps earlier explicit CommonJS paths ahead of a managed local failure', async () => {
|
||||
const f = fixture()
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -386,7 +352,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [alternative, f.profile.dir]
|
||||
expect(require.resolve('resolution-lib', { paths })).toBe(selected)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -400,17 +365,17 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
file(join(selected, 'package.json'), JSON.stringify({ name: 'resolution-lib', main: './missing.cjs' }))
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [invalid, f.profile.dir]
|
||||
const linkError = thrownError(() => require.resolve('resolution-lib', { paths }))
|
||||
const nativeError = thrownError(() => require.resolve('resolution-lib', { paths }))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve('resolution-lib', { paths }))
|
||||
|
||||
expect(runtimeError).toMatchObject({
|
||||
code: linkError.code,
|
||||
path: linkError.path,
|
||||
requestPath: linkError.requestPath,
|
||||
code: nativeError.code,
|
||||
path: nativeError.path,
|
||||
requestPath: nativeError.requestPath,
|
||||
})
|
||||
expect(runtimeError.message).toBe(linkError.message)
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a missing fallback legacy main before later explicit CommonJS paths', async () => {
|
||||
@@ -422,17 +387,17 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
pkg(join(alternative, 'node_modules', name), name, 2)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [f.profile.dir, alternative]
|
||||
const linkError = thrownError(() => require.resolve(name, { paths }))
|
||||
const nativeError = thrownError(() => require.resolve(name, { paths }))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve(name, { paths }))
|
||||
|
||||
expect(runtimeError).toMatchObject({
|
||||
code: linkError.code,
|
||||
path: linkError.path,
|
||||
requestPath: linkError.requestPath,
|
||||
code: nativeError.code,
|
||||
path: nativeError.path,
|
||||
requestPath: nativeError.requestPath,
|
||||
})
|
||||
expect(runtimeError.message).toBe(linkError.message)
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a missing fallback legacy main after a generation miss', async () => {
|
||||
@@ -445,17 +410,17 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
rmSync(f.installed, { recursive: true })
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [f.profile.dir, alternative]
|
||||
const linkError = thrownError(() => require.resolve('resolution-lib', { paths }))
|
||||
const nativeError = thrownError(() => require.resolve('resolution-lib', { paths }))
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve('resolution-lib', { paths }))
|
||||
|
||||
expect(runtimeError).toMatchObject({
|
||||
code: linkError.code,
|
||||
path: linkError.path,
|
||||
requestPath: linkError.requestPath,
|
||||
code: nativeError.code,
|
||||
path: nativeError.path,
|
||||
requestPath: nativeError.requestPath,
|
||||
})
|
||||
expect(runtimeError.message).toBe(linkError.message)
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a profile-local package ahead of the generation', async () => {
|
||||
@@ -506,7 +471,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
const alias = join(f.profile.dir, 'node_modules', 'alias')
|
||||
pkg(alias, 'real-name', 6)
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -515,7 +480,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const require = createRequire(join(alias, 'inside.cjs'))
|
||||
expect(require.resolve('real-name')).toBe(join(alias, 'index.cjs'))
|
||||
expect(require.resolve('real-name', { paths: [f.profile.dir] })).toBe(join(alias, 'index.cjs'))
|
||||
expect(resolveFrom('real-name', pathToFileURL(join(alias, 'inside-link.mjs')).href)).toBe(
|
||||
expect(resolveFrom('real-name', pathToFileURL(join(alias, 'inside-native.mjs')).href)).toBe(
|
||||
pathToFileURL(join(alias, 'index.js')).href,
|
||||
)
|
||||
const invalidScope = join(f.profile.dir, 'node_modules', 'invalid-scope')
|
||||
@@ -524,8 +489,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
.toThrow(/Invalid package config/u)
|
||||
expect(() => resolveFrom('resolution-lib', pathToFileURL(join(invalidScope, 'inside.mjs')).href))
|
||||
.toThrow(/Invalid package config/u)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'real-name'))
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -547,17 +510,16 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
private: true,
|
||||
imports: { '#resolution-lib': 'resolution-lib' },
|
||||
}))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const nested = join(f.profile.dir, 'nested')
|
||||
const require = createRequire(join(nested, 'entry.cjs'))
|
||||
const linkParent = pathToFileURL(join(nested, 'entry-link.mjs')).href
|
||||
expect(require.resolve('#resolution-lib')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(resolveFrom('#resolution-lib', linkParent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
const nativeParent = pathToFileURL(join(nested, 'entry-native.mjs')).href
|
||||
expect(() => require.resolve('#resolution-lib')).toThrow(/Cannot find module/u)
|
||||
expect(() => resolveFrom('#resolution-lib', nativeParent)).toThrow(/Cannot find package/u)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -566,61 +528,21 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(resolveFrom('#resolution-lib', runtimeParent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
|
||||
})
|
||||
|
||||
it('verifies package imports aliases against the generation', async () => {
|
||||
it('imports package aliases through the generation and reports the original importer', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'test-profile',
|
||||
private: true,
|
||||
imports: { '#resolution-lib': 'resolution-lib' },
|
||||
imports: { '#library': 'resolution-lib', '#missing': 'resolution-lib/missing' },
|
||||
}))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const registration = installProfileResolution(generation, 'verify')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs')).resolve('#resolution-lib'))
|
||||
.toBe(realpathSync(join(f.installed, 'index.cjs')))
|
||||
expect(resolveFrom(
|
||||
'#resolution-lib', pathToFileURL(join(f.profile.dir, 'entry-dual.mjs')).href,
|
||||
)).toBe(pathToFileURL(realpathSync(join(f.installed, 'index.js'))).href)
|
||||
})
|
||||
|
||||
it('ignores a stale package imports projection and detects it in dual mode', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'test-profile',
|
||||
private: true,
|
||||
imports: { '#resolution-lib': 'resolution-lib' },
|
||||
}))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
await expect(importFrom('#library', parent)).resolves.toMatchObject({ marker: 1 })
|
||||
await expect(importFrom('#missing', parent)).rejects.toMatchObject({
|
||||
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED',
|
||||
message: expect.stringContaining(fileURLToPath(parent)) as unknown as string,
|
||||
})
|
||||
const projection = join(generation.profilesDir, 'node_modules', 'resolution-lib')
|
||||
unlinkSync(projection)
|
||||
const stale = join(f.root, 'stale', 'resolution-lib')
|
||||
pkg(stale, 'resolution-lib', 9)
|
||||
symlinkSync(stale, projection, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
|
||||
const runtime = installProfileResolution(generation)
|
||||
expect(require.resolve('#resolution-lib')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(resolveFrom(
|
||||
'#resolution-lib', pathToFileURL(join(f.profile.dir, 'runtime.mjs')).href,
|
||||
)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
|
||||
runtime.dispose()
|
||||
|
||||
const dual = installProfileResolution(generation, 'verify')
|
||||
registrations.push(dual)
|
||||
expect(() => require.resolve('#resolution-lib')).toThrow(/profile resolution mismatch/u)
|
||||
expect(() => resolveFrom(
|
||||
'#resolution-lib', pathToFileURL(join(f.profile.dir, 'dual-stale.mjs')).href,
|
||||
)).toThrow(/profile resolution mismatch/u)
|
||||
})
|
||||
|
||||
it('leaves relative package imports targets and their diagnostics to Node', async () => {
|
||||
@@ -630,7 +552,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
private: true,
|
||||
imports: { '#missing-relative': './missing.cjs' },
|
||||
}))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -639,7 +561,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
const cjsMessage = thrownMessage(() => require.resolve('#missing-relative'))
|
||||
const esmMessage = thrownMessage(() => resolveFrom('#missing-relative', parent))
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -655,7 +576,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
imports: { '#local': 'local-import', '#ancestor': 'ancestor-import' },
|
||||
}))
|
||||
const local = join(f.profile.dir, 'node_modules', 'local-import')
|
||||
const ancestor = join(f.root, 'node_modules', 'ancestor-import')
|
||||
const ancestor = join(f.root, 'profiles', 'node_modules', 'ancestor-import')
|
||||
pkg(local, 'local-import', 4)
|
||||
pkg(ancestor, 'ancestor-import', 5)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
@@ -675,11 +596,11 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
name: 'outside', private: true, imports: { '#missing': 'missing-target' },
|
||||
}))
|
||||
const require = createRequire(join(outside, 'entry.cjs'))
|
||||
const linkMessage = thrownMessage(() => require.resolve('#missing'))
|
||||
const nativeMessage = thrownMessage(() => require.resolve('#missing'))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(thrownMessage(() => require.resolve('#missing'))).toBe(linkMessage)
|
||||
expect(thrownMessage(() => require.resolve('#missing'))).toBe(nativeMessage)
|
||||
})
|
||||
|
||||
it('does not inherit package imports across node_modules', async () => {
|
||||
@@ -690,11 +611,11 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const nested = join(f.profile.dir, 'node_modules', 'manifestless', 'entry.cjs')
|
||||
file(nested, '')
|
||||
const require = createRequire(nested)
|
||||
const linkMessage = thrownMessage(() => require.resolve('#resolution-lib'))
|
||||
const nativeMessage = thrownMessage(() => require.resolve('#resolution-lib'))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(thrownMessage(() => require.resolve('#resolution-lib'))).toBe(linkMessage)
|
||||
expect(thrownMessage(() => require.resolve('#resolution-lib'))).toBe(nativeMessage)
|
||||
})
|
||||
|
||||
it('falls through a missing local CommonJS subpath to the generation', async () => {
|
||||
@@ -742,14 +663,13 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
file(join(local, 'package.json'), JSON.stringify({
|
||||
name: 'resolution-lib', version: '2.0.0', type: 'module', exports: null,
|
||||
}))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const require = createRequire(join(local, 'entry.cjs'))
|
||||
expect(require.resolve('resolution-lib/only-install.cjs')).toBe(installedSubpath)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
expect(() => require.resolve('resolution-lib/only-install.cjs')).toThrow(/Cannot find module/u)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -763,7 +683,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
const installedSubpath = join(f.installed, 'only-install.cjs')
|
||||
file(installedSubpath, 'module.exports = { marker: 8 }\n')
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -777,8 +697,8 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
file(join(f.root, 'node_modules', 'resolution-lib', 'index.cjs'), 'module.exports = {}\n')
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('resolution-lib/only-install.cjs')).toBe(installedSubpath)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
expect(thrownError(() => require.resolve('resolution-lib/only-install.cjs')))
|
||||
.toMatchObject({ code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' })
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -888,17 +808,17 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const local = join(f.profile.dir, 'node_modules', 'resolution-lib')
|
||||
file(join(local, 'package.json'), JSON.stringify({ name: 'resolution-lib', main: './missing.cjs' }))
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const linkError = thrownError(() => require.resolve('resolution-lib'))
|
||||
const nativeError = thrownError(() => require.resolve('resolution-lib'))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve('resolution-lib'))
|
||||
|
||||
expect(runtimeError).toMatchObject({
|
||||
code: linkError.code,
|
||||
path: linkError.path,
|
||||
requestPath: linkError.requestPath,
|
||||
code: nativeError.code,
|
||||
path: nativeError.path,
|
||||
requestPath: nativeError.requestPath,
|
||||
})
|
||||
expect(runtimeError.message).toBe(linkError.message)
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a profile-local CommonJS package file ahead of the generation', async () => {
|
||||
@@ -987,15 +907,14 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
const ancestorSubpath = join(home, 'node_modules', 'resolution-lib', 'sub.cjs')
|
||||
file(ancestorSubpath, 'module.exports = { marker: 3 }\n')
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile,
|
||||
home,
|
||||
})
|
||||
const require = createRequire(join(profileDir, 'entry.cjs'))
|
||||
expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(() => require.resolve('resolution-lib')).toThrow(/Cannot find module/u)
|
||||
expect(require.resolve('resolution-lib/sub.cjs')).toBe(ancestorSubpath)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
@@ -1019,30 +938,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
})).toBe(alternativeSubpath)
|
||||
})
|
||||
|
||||
it('skips stale shared and profile-owned fallback entries when the generation misses', async () => {
|
||||
const f = fixture()
|
||||
pkg(join(f.root, 'profiles', 'node_modules', 'stale-shared'), 'stale-shared', 9)
|
||||
pkg(join(f.root, 'node_modules', 'stale-shared'), 'stale-shared', 3)
|
||||
const target = join(f.root, 'stale-private-target')
|
||||
const owned = join(f.profile.dir, '.dsh-module-fallback', 'node_modules', 'stale-private')
|
||||
const projected = join(f.profile.dir, 'node_modules', 'stale-private')
|
||||
pkg(target, 'stale-private', 9)
|
||||
mkdirSync(dirname(owned), { recursive: true })
|
||||
symlinkSync(target, owned, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
mkdirSync(dirname(projected), { recursive: true })
|
||||
symlinkSync(owned, projected, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
pkg(join(f.root, 'node_modules', 'stale-private'), 'stale-private', 3)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
for (const name of ['stale-shared', 'stale-private']) {
|
||||
expect(require(name)).toEqual({ marker: 3 })
|
||||
expect(require.resolve(name, { paths: [f.profile.dir] })).toBe(join(f.root, 'node_modules', name, 'index.cjs'))
|
||||
expect(await importFrom(name, pathToFileURL(join(f.profile.dir, `${name}.mjs`)).href))
|
||||
.toMatchObject({ marker: 3 })
|
||||
}
|
||||
})
|
||||
|
||||
it('continues after a canonicalized profiles directory from the matching parent tree', async () => {
|
||||
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-profile-generation-symlink-')))
|
||||
roots.push(root)
|
||||
@@ -1053,9 +948,10 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
mkdirSync(realProfileDir, { recursive: true })
|
||||
mkdirSync(dirname(profilesDir), { recursive: true })
|
||||
symlinkSync(realProfilesDir, profilesDir, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
pkg(join(realProfilesDir, 'node_modules', 'stale-only'), 'stale-only', 9)
|
||||
pkg(join(carrier, 'node_modules', 'stale-only'), 'stale-only', 3)
|
||||
pkg(join(root, 'home', 'node_modules', 'stale-only'), 'stale-only', 4)
|
||||
const parentPackage = join(realProfilesDir, 'node_modules', 'parent-lib')
|
||||
pkg(parentPackage, 'parent-lib', 9)
|
||||
pkg(join(carrier, 'node_modules', 'ancestor-lib'), 'ancestor-lib', 3)
|
||||
pkg(join(root, 'home', 'node_modules', 'ancestor-lib'), 'ancestor-lib', 4)
|
||||
const registration = installProfileResolution({
|
||||
profilesDir,
|
||||
profileDir: join(profilesDir, 'test'),
|
||||
@@ -1063,7 +959,12 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
entries: [],
|
||||
})
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(realProfileDir, 'entry.cjs'))('stale-only')).toEqual({ marker: 3 })
|
||||
const require = createRequire(join(realProfileDir, 'entry.cjs'))
|
||||
expect(require('parent-lib')).toEqual({ marker: 9 })
|
||||
expect(require('ancestor-lib')).toEqual({ marker: 3 })
|
||||
const parent = pathToFileURL(join(realProfileDir, 'entry.mjs')).href
|
||||
expect(await importFrom('parent-lib', parent)).toMatchObject({ marker: 9 })
|
||||
expect(registration.packageDir('parent-lib', parent)).toBe(parentPackage)
|
||||
})
|
||||
|
||||
it('leaves conditional exports to Node', async () => {
|
||||
@@ -1159,14 +1060,13 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: 'resolution-lib', version: '1.0.0', type: 'module', main: './index.cjs',
|
||||
}))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const linkRequire = createRequire(join(f.profile.dir, 'entry-link.cjs'))
|
||||
const linkError = thrownError(() => linkRequire.resolve('resolution-lib/missing.cjs'))
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
const nativeRequire = createRequire(join(f.profile.dir, 'entry-native.cjs'))
|
||||
const nativeError = thrownError(() => nativeRequire.resolve('resolution-lib/missing.cjs'))
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
const runtimeRequire = createRequire(join(f.profile.dir, 'entry-runtime.cjs'))
|
||||
@@ -1176,38 +1076,37 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
join(f.profile.dir, 'entry-runtime.cjs'),
|
||||
])
|
||||
expect(runtimeError.message).not.toContain(f.installAnchor)
|
||||
expect(linkError.requireStack).toEqual([join(f.profile.dir, 'entry-link.cjs')])
|
||||
expect(nativeError.requireStack).toEqual([join(f.profile.dir, 'entry-native.cjs')])
|
||||
})
|
||||
|
||||
it('reports routed ESM failures from the original importer', async () => {
|
||||
const f = fixture()
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
const linkMessage = thrownMessage(() => resolveFrom('unavailable-lib', parent))
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const nativeMessage = thrownMessage(() => resolveFrom('unavailable-lib', parent))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
|
||||
expect(thrownMessage(() => resolveFrom('unavailable-lib', parent))).toBe(linkMessage)
|
||||
expect(thrownMessage(() => resolveFrom('unavailable-lib', parent))).toBe(nativeMessage)
|
||||
expect(thrownMessage(() => resolveFrom('resolution-lib/private', parent)))
|
||||
.toContain(` imported from ${fileURLToPath(parent)}`)
|
||||
})
|
||||
|
||||
it('leaves an invalid generation manifest error to Node', async () => {
|
||||
const f = fixture()
|
||||
const generation = await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
file(join(f.installed, 'package.json'), '{')
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
expect(() => resolveFrom('resolution-lib', parent)).toThrow(/Invalid package config/u)
|
||||
unlinkSync(join(generation.profilesDir, 'node_modules', 'resolution-lib'))
|
||||
expect(() => resolveFrom('resolution-lib', pathToFileURL(f.installAnchor).href))
|
||||
.toThrow(/Invalid package config/u)
|
||||
const registration = installProfileResolution(generation)
|
||||
registrations.push(registration)
|
||||
|
||||
@@ -1257,125 +1156,6 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
.toThrow(/valid "main" entry/u)
|
||||
})
|
||||
|
||||
it('detects a dual-mode mismatch instead of accepting another package', async () => {
|
||||
const f = fixture()
|
||||
const disk = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const second = join(f.root, 'second')
|
||||
pkg(second, 'resolution-lib', 2)
|
||||
const mismatched = {
|
||||
...disk,
|
||||
entries: disk.entries.map(entry => entry.name === 'resolution-lib'
|
||||
? { ...entry, packageDir: second, declarer: join(second, 'package.json') }
|
||||
: entry),
|
||||
}
|
||||
const registration = installProfileResolution(mismatched, 'verify')
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(() => { require.resolve('resolution-lib') }).toThrow(/profile resolution mismatch/u)
|
||||
await expect(importFrom(
|
||||
'resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href,
|
||||
)).rejects.toThrow(/profile resolution mismatch/u)
|
||||
})
|
||||
|
||||
it('detects a dual-mode package metadata mismatch before import', async () => {
|
||||
const f = fixture()
|
||||
const disk = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const second = join(f.root, 'second')
|
||||
pkg(second, 'resolution-lib', 2)
|
||||
const mismatched = {
|
||||
...disk,
|
||||
entries: disk.entries.map(entry => entry.name === 'resolution-lib'
|
||||
? { ...entry, packageDir: second, declarer: join(second, 'package.json') }
|
||||
: entry),
|
||||
}
|
||||
const registration = installProfileResolution(mismatched, 'verify')
|
||||
registrations.push(registration)
|
||||
|
||||
expect(() => registration.packageDir(
|
||||
'resolution-lib', pathToFileURL(join(f.profile.dir, 'metadata.mjs')).href,
|
||||
)).toThrow(/profile resolution mismatch/u)
|
||||
})
|
||||
|
||||
it('detects a dual-mode package metadata hit present on only one backend', async () => {
|
||||
const f = fixture()
|
||||
const generation = await generationOf(f)
|
||||
const registration = installProfileResolution(generation, 'verify')
|
||||
registrations.push(registration)
|
||||
|
||||
expect(() => registration.packageDir(
|
||||
'resolution-lib', pathToFileURL(join(f.profile.dir, 'metadata.mjs')).href,
|
||||
)).toThrow(/disk selected nothing, generation selected/u)
|
||||
})
|
||||
|
||||
it('detects package metadata present only in the dual-mode disk backend', async () => {
|
||||
const f = fixture()
|
||||
const disk = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const registration = installProfileResolution({
|
||||
...disk,
|
||||
entries: disk.entries.filter(entry => entry.name !== 'resolution-lib'),
|
||||
}, 'verify')
|
||||
registrations.push(registration)
|
||||
|
||||
expect(() => registration.packageDir(
|
||||
'resolution-lib', pathToFileURL(join(f.profile.dir, 'metadata.mjs')).href,
|
||||
)).toThrow(/disk selected .*generation selected nothing/u)
|
||||
})
|
||||
|
||||
it('reports a missing dual-mode generation target from the original importer', async () => {
|
||||
const f = fixture()
|
||||
const disk = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const missing = join(f.root, 'missing')
|
||||
const mismatched = {
|
||||
...disk,
|
||||
entries: disk.entries.map(entry => entry.name === 'resolution-lib'
|
||||
? { ...entry, packageDir: missing, declarer: join(missing, 'package.json') }
|
||||
: entry),
|
||||
}
|
||||
const registration = installProfileResolution(mismatched, 'verify')
|
||||
registrations.push(registration)
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
|
||||
expect(thrownMessage(() => resolveFrom('resolution-lib', parent))).toContain(fileURLToPath(parent))
|
||||
})
|
||||
|
||||
it('accepts matching disk and generation targets in dual mode', async () => {
|
||||
const f = fixture()
|
||||
const generation = await healProfilesModuleFallback({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const registration = installProfileResolution(generation, 'verify')
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(await importFrom('resolution-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href))
|
||||
.toMatchObject({ marker: 1 })
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'metadata.mjs')).href
|
||||
expect(registration.packageDir('resolution-lib', parent)).toBe(f.installed)
|
||||
expect(registration.packageDir('missing-metadata', parent)).toBeUndefined()
|
||||
expect(registration.packageDir('node:fs', parent)).toBeUndefined()
|
||||
expect(registration.packageDir(
|
||||
'missing-metadata', `${pathToFileURL(f.profile.dir).href}/%ZZ`,
|
||||
)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('publishes an additive generation and replaces its miss cache atomically', async () => {
|
||||
const f = fixture()
|
||||
const added = join(f.root, 'added')
|
||||
@@ -1497,9 +1277,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const generation = await generationOf(f)
|
||||
const key = '@deepseek-ai/dsh-app-boot/profile-resolution'
|
||||
const previous = getEnvironmentData(key)
|
||||
const dispose = registerWorkerResolution(generation, 'verify')
|
||||
const dispose = registerWorkerResolution(generation)
|
||||
try {
|
||||
expect(getEnvironmentData(key)).toEqual({ generation, behavior: 'verify' })
|
||||
expect(getEnvironmentData(key)).toEqual({ generation })
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
@@ -1509,9 +1289,11 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
it('restores CommonJS resolution when the registration is disposed', async () => {
|
||||
const f = fixture()
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('resolution-lib')).toBe(join(f.installed, 'index.cjs'))
|
||||
registration.dispose()
|
||||
registrations.pop()
|
||||
expect(() => { require.resolve('resolution-lib') }).toThrow(/Cannot find module/u)
|
||||
})
|
||||
})
|
||||
@@ -1,25 +1,21 @@
|
||||
/**
|
||||
* Profile machinery of `dsh-app-boot`: directory resolution and init,
|
||||
* manifest round-trips, two-anchor bundle resolution, patch-layer loading,
|
||||
* empty-root composition, and the installation module-fallback healing.
|
||||
* empty-root composition, and runtime package resolution.
|
||||
*/
|
||||
|
||||
import {
|
||||
existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync,
|
||||
unlinkSync, writeFileSync,
|
||||
existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { createRequire } from 'node:module'
|
||||
import { join } from 'node:path'
|
||||
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { afterAll, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import {
|
||||
composeEntries,
|
||||
createProfileResolutionGeneration,
|
||||
healProfilesModuleFallback,
|
||||
healIsolatedProfileModuleFallback,
|
||||
initProfile,
|
||||
unlinkProfileModuleFallback,
|
||||
loadProfile,
|
||||
loadProfileDirectory,
|
||||
PROFILE_PATCH_FILENAME,
|
||||
@@ -30,7 +26,9 @@ import {
|
||||
resolveProfileDir,
|
||||
writeProfileManifest,
|
||||
type Profile,
|
||||
type ProfileResolutionGeneration,
|
||||
} from '../src/index.ts'
|
||||
import { installProfileResolution } from '../src/profile-resolution/resolver.ts'
|
||||
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
@@ -93,13 +91,29 @@ function stageProfile(home: string, name: string, bundleAnchor: string): Profile
|
||||
}
|
||||
}
|
||||
|
||||
describe('healIsolatedProfileModuleFallback', () => {
|
||||
it.each([false, true])('resolves peers from each installation without sharing profile state (Web fallback: %s)', async (webFallback) => {
|
||||
async function importFromGeneration(
|
||||
generation: ProfileResolutionGeneration, specifier: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const addon = createRequire(import.meta.url)('node-addon-require-builtin') as {
|
||||
requireBuiltin(id: string): unknown
|
||||
}
|
||||
const loader = addon.requireBuiltin('internal/modules/esm/loader') as {
|
||||
getOrInitializeCascadedLoader(): {
|
||||
import(specifier: string, parent: string, attributes: ImportAttributes): Promise<Record<string, unknown>>
|
||||
}
|
||||
}
|
||||
const registration = installProfileResolution(generation)
|
||||
try {
|
||||
const parent = pathToFileURL(join(generation.profilesDir, 'entry.mjs')).href
|
||||
return await loader.getOrInitializeCascadedLoader().import(specifier, parent, {})
|
||||
} finally {
|
||||
registration.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
describe('isolated profile resolution', () => {
|
||||
it('resolves peers from each installation without sharing profile state', async () => {
|
||||
const home = tmp()
|
||||
const webAnchor = stageInstallation({ commander: {} })
|
||||
if (webFallback) await healProfilesModuleFallback({ installAnchor: webAnchor, home })
|
||||
const sharedCommander = join(home, 'profiles', 'node_modules', 'commander')
|
||||
const sharedTarget = webFallback ? readlinkSync(sharedCommander) : undefined
|
||||
const bundleAnchor = stageInstallation({ 'bundle-only': {} }, 'external-bundle')
|
||||
const anchorA = stageInstallation({ commander: {}, 'pnpm-owned': {} })
|
||||
const anchorB = stageInstallation({ commander: {}, 'pnpm-owned': {} })
|
||||
@@ -119,53 +133,36 @@ describe('healIsolatedProfileModuleFallback', () => {
|
||||
writeFileSync(join(installed, 'package.json'), JSON.stringify({ name: 'pnpm-owned', main: 'index.js' }))
|
||||
writeFileSync(join(installed, 'index.js'), 'module.exports = "profile-installed"\n')
|
||||
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchorA, profile: profileA })
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchorB, profile: profileB })
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchorA, profile: profileA })
|
||||
const generationA = await createProfileResolutionGeneration({ installAnchor: anchorA, profile: profileA, home })
|
||||
const generationB = await createProfileResolutionGeneration({ installAnchor: anchorB, profile: profileB, home })
|
||||
let registration = installProfileResolution(generationA)
|
||||
try {
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorA, '..', 'node_modules', 'commander', 'index.js')))
|
||||
registration.dispose()
|
||||
registration = installProfileResolution(generationB)
|
||||
expect(realpathSync.native(createRequire(consumerB).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorB, '..', 'node_modules', 'commander', 'index.js')))
|
||||
registration.dispose()
|
||||
registration = installProfileResolution(generationA)
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('pnpm-owned'))).toBe(realpathSync.native(join(installed, 'index.js')))
|
||||
expect(readFileSync(join(installed, 'index.js'), 'utf8')).toContain('profile-installed')
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('bundle-only')))
|
||||
.toBe(realpathSync.native(join(bundleAnchor, '..', 'node_modules', 'bundle-only', 'index.js')))
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
|
||||
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorA, '..', 'node_modules', 'commander', 'index.js')))
|
||||
expect(realpathSync.native(createRequire(consumerB).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorB, '..', 'node_modules', 'commander', 'index.js')))
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('pnpm-owned'))).toBe(realpathSync.native(join(installed, 'index.js')))
|
||||
expect(readFileSync(join(installed, 'index.js'), 'utf8')).toContain('profile-installed')
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('bundle-only')))
|
||||
.toBe(realpathSync.native(join(bundleAnchor, '..', 'node_modules', 'bundle-only', 'index.js')))
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(webFallback)
|
||||
if (webFallback) expect(readlinkSync(sharedCommander)).toBe(sharedTarget)
|
||||
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchorA, profile: { ...profileA, layers: [] } })
|
||||
expect(existsSync(join(profileA.dir, 'node_modules', 'bundle-only'))).toBe(false)
|
||||
expect(existsSync(join(profileB.dir, 'node_modules', 'bundle-only'))).toBe(true)
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorA, '..', 'node_modules', 'commander', 'index.js')))
|
||||
})
|
||||
})
|
||||
|
||||
describe('unlinkProfileModuleFallback', () => {
|
||||
it('detaches only this profile projections and restores missing packages from a relocated installation', () => {
|
||||
const home = tmp()
|
||||
const anchor = stageInstallation({ fallback: {}, '@scope/peer': {}, replaced: {} })
|
||||
const nextAnchor = stageInstallation({ fallback: {}, '@scope/peer': {}, replaced: {} })
|
||||
const bundleAnchor = stageInstallation({}, 'selected-bundle')
|
||||
const profile = stageProfile(home, 'desktop', bundleAnchor)
|
||||
const other = stageProfile(home, 'other', bundleAnchor)
|
||||
unlinkProfileModuleFallback(profile.dir)
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchor, profile })
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchor, profile: other })
|
||||
const modules = join(profile.dir, 'node_modules')
|
||||
unlinkSync(join(modules, 'replaced'))
|
||||
mkdirSync(join(modules, 'replaced'))
|
||||
writeFileSync(join(modules, 'replaced', 'sentinel'), 'pnpm')
|
||||
unlinkProfileModuleFallback(profile.dir)
|
||||
unlinkProfileModuleFallback(profile.dir)
|
||||
expect(existsSync(join(modules, 'fallback'))).toBe(false)
|
||||
expect(existsSync(join(modules, '@scope/peer'))).toBe(false)
|
||||
expect(readFileSync(join(modules, 'replaced', 'sentinel'), 'utf8')).toBe('pnpm')
|
||||
expect(existsSync(join(other.dir, 'node_modules', 'fallback'))).toBe(true)
|
||||
healIsolatedProfileModuleFallback({ installAnchor: nextAnchor, profile })
|
||||
expect(realpathSync(join(modules, 'fallback'))).toBe(realpathSync(join(nextAnchor, '..', 'node_modules', 'fallback')))
|
||||
expect(readFileSync(join(modules, 'replaced', 'sentinel'), 'utf8')).toBe('pnpm')
|
||||
registration.dispose()
|
||||
registration = installProfileResolution(await createProfileResolutionGeneration({
|
||||
installAnchor: anchorA, profile: { ...profileA, layers: [] }, home,
|
||||
}))
|
||||
expect(() => createRequire(consumerA).resolve('bundle-only')).toThrow(/Cannot find module/u)
|
||||
expect(generationB.entries.find(entry => entry.name === 'bundle-only')?.packageDir)
|
||||
.toBe(realpathSync.native(join(bundleAnchor, '..', 'node_modules', 'bundle-only')))
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorA, '..', 'node_modules', 'commander', 'index.js')))
|
||||
} finally {
|
||||
registration.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -387,8 +384,6 @@ describe('loadProfile', () => {
|
||||
const unavailable = failure === 'missing package' || failure === 'invalid manifest'
|
||||
expect(generation.entries.map(entry => entry.name))
|
||||
.toEqual(['dsh-app', 'before', ...unavailable ? [] : ['broken'], 'after'])
|
||||
healIsolatedProfileModuleFallback({ installAnchor: anchor, profile })
|
||||
expect(existsSync(join(dir, 'node_modules', 'broken'))).toBe(!unavailable)
|
||||
mkdirSync(bundleDir, { recursive: true })
|
||||
writeFileSync(manifestPath, original)
|
||||
writeFileSync(patchPath, '[]\n')
|
||||
@@ -423,8 +418,8 @@ describe('composeEntries', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('healProfilesModuleFallback', () => {
|
||||
it('links the app and bundle dependency surface flat under profiles/node_modules', async () => {
|
||||
describe('createProfileResolutionGeneration', () => {
|
||||
it('collects the app and bundle dependencies without writing profile packages', async () => {
|
||||
const anchor = stageInstallation({
|
||||
'bundle-a': { patch: '[]\n', deps: { 'dep-of-a': '0.0.0', 'ghost-dep': '0.0.0' } },
|
||||
'plain-lib': {},
|
||||
@@ -438,29 +433,11 @@ describe('healProfilesModuleFallback', () => {
|
||||
mkdirSync(join(modules, 'dep-of-a'), { recursive: true })
|
||||
writeFileSync(join(modules, 'dep-of-a', 'package.json'), JSON.stringify({ name: 'dep-of-a', version: '0.0.0' }))
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
// App deps, the bundle's own deps, and the bundle itself are linked; the
|
||||
// plain library is linked as an app dep (harmless), the app itself too.
|
||||
for (const name of ['bundle-a', 'plain-lib', 'dep-of-a', 'dsh-app']) {
|
||||
expect(lstatSync(join(fallback, name)).isSymbolicLink(), name).toBe(true)
|
||||
}
|
||||
// Idempotent, and a moved target is re-pointed.
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const before = readlinkSync(join(fallback, 'dep-of-a'))
|
||||
expect(before).toContain('dep-of-a')
|
||||
})
|
||||
|
||||
it('throws when a fallback entry is a foreign file or directory', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
for (const kind of ['file', 'directory']) {
|
||||
const home = tmp()
|
||||
const entry = join(home, 'profiles', 'node_modules', 'dsh-app')
|
||||
mkdirSync(join(entry, '..'), { recursive: true })
|
||||
if (kind === 'directory') mkdirSync(entry)
|
||||
else writeFileSync(entry, '')
|
||||
await expect(healProfilesModuleFallback({ installAnchor: anchor, home })).rejects.toThrow('is not a symlink')
|
||||
}
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
expect(generation.entries.map(entry => entry.name)).toEqual(['dsh-app', 'bundle-a', 'plain-lib', 'dep-of-a'])
|
||||
expect(generation.entries.find(entry => entry.name === 'dep-of-a')?.packageDir).toBe(join(modules, 'dep-of-a'))
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
|
||||
await expect(createProfileResolutionGeneration({ installAnchor: anchor, home })).resolves.toEqual(generation)
|
||||
})
|
||||
|
||||
it('keeps selected bundle closures profile-local without overriding installation packages', async () => {
|
||||
@@ -470,51 +447,37 @@ describe('healProfilesModuleFallback', () => {
|
||||
const home = tmp()
|
||||
const profileA = stageProfile(home, 'a', bundleA)
|
||||
const profileB = stageProfile(home, 'b', bundleB)
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile: profileA, home })
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile: profileA, home })
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile: profileB, home })
|
||||
const sharedFallback = join(home, 'profiles', 'node_modules')
|
||||
const ownedA = join(profileA.dir, '.dsh-module-fallback', 'node_modules', '@scope', 'bundle-only')
|
||||
const ownedB = join(profileB.dir, '.dsh-module-fallback', 'node_modules', '@scope', 'bundle-only')
|
||||
const generationA = await createProfileResolutionGeneration({ installAnchor: installationAnchor, profile: profileA, home })
|
||||
await expect(createProfileResolutionGeneration({ installAnchor: installationAnchor, profile: profileA, home }))
|
||||
.resolves.toEqual(generationA)
|
||||
const generationB = await createProfileResolutionGeneration({ installAnchor: installationAnchor, profile: profileB, home })
|
||||
|
||||
expect(realpathSync.native(readlinkSync(join(sharedFallback, 'shared'))))
|
||||
.toBe(realpathSync.native(join(installationAnchor, '..', 'node_modules', 'shared')))
|
||||
expect(existsSync(join(sharedFallback, '@scope', 'bundle-only'))).toBe(false)
|
||||
for (const generation of [generationA, generationB]) {
|
||||
expect(generation.entries.find(entry => entry.name === 'shared')).toMatchObject({
|
||||
packageDir: join(installationAnchor, '..', 'node_modules', 'shared'), scope: 'installation',
|
||||
})
|
||||
}
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
|
||||
expect(existsSync(join(profileA.dir, 'node_modules', 'shared'))).toBe(false)
|
||||
expect(existsSync(join(profileB.dir, 'node_modules', 'shared'))).toBe(false)
|
||||
expect(readlinkSync(join(profileA.dir, 'node_modules', '@scope', 'bundle-only'))).toBe(ownedA)
|
||||
expect(readlinkSync(ownedA))
|
||||
.toBe(realpathSync.native(join(bundleA, '..', 'node_modules', '@scope', 'bundle-only')))
|
||||
expect(readlinkSync(join(profileB.dir, 'node_modules', '@scope', 'bundle-only'))).toBe(ownedB)
|
||||
expect(readlinkSync(ownedB))
|
||||
.toBe(realpathSync.native(join(bundleB, '..', 'node_modules', '@scope', 'bundle-only')))
|
||||
expect(generationA.entries.find(entry => entry.name === '@scope/bundle-only')).toMatchObject({
|
||||
packageDir: realpathSync.native(join(bundleA, '..', 'node_modules', '@scope', 'bundle-only')), scope: 'profile',
|
||||
})
|
||||
expect(generationB.entries.find(entry => entry.name === '@scope/bundle-only')).toMatchObject({
|
||||
packageDir: realpathSync.native(join(bundleB, '..', 'node_modules', '@scope', 'bundle-only')), scope: 'profile',
|
||||
})
|
||||
|
||||
await healProfilesModuleFallback({
|
||||
const withoutBundles = await createProfileResolutionGeneration({
|
||||
installAnchor: installationAnchor,
|
||||
profile: { ...profileA, layers: [] },
|
||||
home,
|
||||
})
|
||||
expect(existsSync(join(profileA.dir, 'node_modules', '@scope', 'bundle-only'))).toBe(false)
|
||||
expect(existsSync(ownedA)).toBe(false)
|
||||
expect(existsSync(join(profileB.dir, 'node_modules', '@scope', 'bundle-only'))).toBe(true)
|
||||
expect(withoutBundles.entries.some(entry => entry.name === '@scope/bundle-only')).toBe(false)
|
||||
expect(generationB.entries.find(entry => entry.name === '@scope/bundle-only')?.packageDir)
|
||||
.toBe(realpathSync.native(join(bundleB, '..', 'node_modules', '@scope', 'bundle-only')))
|
||||
})
|
||||
|
||||
it('combines packaged installation proxies with profile-local bundle links', async () => {
|
||||
const installationAnchor = stageInstallation({ shared: {} })
|
||||
const bundleAnchor = stageInstallation({ shared: {}, 'bundle-only': {} }, 'selected-bundle')
|
||||
const home = tmp()
|
||||
const profile = stageProfile(home, 'packaged', bundleAnchor)
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
expect(lstatSync(join(home, 'profiles', 'node_modules', 'shared')).isDirectory()).toBe(true)
|
||||
expect(lstatSync(join(profile.dir, 'node_modules', 'bundle-only')).isSymbolicLink()).toBe(true)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('discovers dependencies beside a symlinked bundle real path', async () => {
|
||||
it.each([false, true])('discovers dependencies beside a symlinked bundle real path (packaged: %s)', async (packaged) => {
|
||||
const installationAnchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const dir = resolveProfileDir('symlinked', home)
|
||||
@@ -545,10 +508,16 @@ describe('healProfilesModuleFallback', () => {
|
||||
patches: [],
|
||||
}
|
||||
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
|
||||
expect(readlinkSync(join(dir, '.dsh-module-fallback', 'node_modules', 'bundle-only')))
|
||||
.toBe(realpathSync.native(realDependency))
|
||||
const previous = Object.getOwnPropertyDescriptor(process, 'pkg')
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: packaged ? {} : undefined })
|
||||
try {
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: installationAnchor, profile, home })
|
||||
expect(generation.entries.find(entry => entry.name === 'bundle-only')?.packageDir)
|
||||
.toBe(realpathSync.native(realDependency))
|
||||
} finally {
|
||||
if (previous === undefined) Reflect.deleteProperty(process, 'pkg')
|
||||
else Object.defineProperty(process, 'pkg', previous)
|
||||
}
|
||||
})
|
||||
|
||||
it('traverses every explicit bundle root even when a nested package has the same name', async () => {
|
||||
@@ -589,226 +558,10 @@ describe('healProfilesModuleFallback', () => {
|
||||
patches: [],
|
||||
}
|
||||
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: installationAnchor, profile, home })
|
||||
|
||||
const ownedModules = join(dir, '.dsh-module-fallback', 'node_modules')
|
||||
expect(readlinkSync(join(ownedModules, 'nested-only'))).toBe(realpathSync.native(nestedOnly))
|
||||
expect(readlinkSync(join(ownedModules, 'explicit-only'))).toBe(realpathSync.native(explicitOnly))
|
||||
})
|
||||
|
||||
it('ignores owned projections while recomputing an ordered bundle closure', async () => {
|
||||
const installationAnchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const dir = resolveProfileDir('ordered', home)
|
||||
const profileModules = join(dir, 'node_modules')
|
||||
const bundleA = join(profileModules, 'bundle-a')
|
||||
const bundleB = join(profileModules, 'bundle-b')
|
||||
const nested = join(bundleB, 'node_modules', 'bundle-only')
|
||||
mkdirSync(bundleA, { recursive: true })
|
||||
mkdirSync(nested, { recursive: true })
|
||||
writeFileSync(join(bundleA, 'package.json'), JSON.stringify({
|
||||
name: 'bundle-a',
|
||||
peerDependencies: { 'bundle-only': '0.0.0' },
|
||||
}))
|
||||
writeFileSync(join(bundleB, 'package.json'), JSON.stringify({
|
||||
name: 'bundle-b',
|
||||
dependencies: { 'bundle-only': '0.0.0' },
|
||||
}))
|
||||
writeFileSync(join(nested, 'package.json'), JSON.stringify({ name: 'bundle-only' }))
|
||||
const profile: Profile = {
|
||||
name: 'ordered',
|
||||
dir,
|
||||
layers: ([['bundle-a', bundleA], ['bundle-b', bundleB]] as const).map(([packageName, packageDir]) => ({
|
||||
packageDir,
|
||||
packageName,
|
||||
patchPath: join(packageDir, 'cordis.patch.yml'),
|
||||
patches: [],
|
||||
})),
|
||||
patchPath: join(dir, PROFILE_PATCH_FILENAME),
|
||||
patches: [],
|
||||
}
|
||||
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
|
||||
const owned = join(dir, '.dsh-module-fallback', 'node_modules', 'bundle-only')
|
||||
expect(readlinkSync(owned)).toBe(realpathSync.native(nested))
|
||||
expect(JSON.parse(readFileSync(join(profileModules, 'bundle-only', 'package.json'), 'utf8')))
|
||||
.toMatchObject({ name: 'bundle-only' })
|
||||
})
|
||||
|
||||
it('cleans owned projections without removing profile-managed entries', async () => {
|
||||
const installationAnchor = stageInstallation({})
|
||||
const bundleAnchor = stageInstallation({ fallback: {}, 'managed-dir': {}, 'managed-link': {} }, 'selected-bundle')
|
||||
const home = tmp()
|
||||
const profile = stageProfile(home, 'managed', bundleAnchor)
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
const ownedModules = join(profile.dir, '.dsh-module-fallback', 'node_modules')
|
||||
const profileModules = join(profile.dir, 'node_modules')
|
||||
const foreignTarget = tmp()
|
||||
unlinkSync(join(profileModules, 'managed-dir'))
|
||||
mkdirSync(join(profileModules, 'managed-dir'))
|
||||
unlinkSync(join(profileModules, 'managed-link'))
|
||||
symlinkSync(foreignTarget, join(profileModules, 'managed-link'), 'junction')
|
||||
mkdirSync(join(ownedModules, 'foreign-directory'))
|
||||
mkdirSync(join(ownedModules, '@foreign', 'directory'), { recursive: true })
|
||||
|
||||
await healProfilesModuleFallback({
|
||||
installAnchor: installationAnchor,
|
||||
profile: { ...profile, layers: [] },
|
||||
home,
|
||||
})
|
||||
|
||||
expect(existsSync(join(profileModules, 'fallback'))).toBe(false)
|
||||
expect(lstatSync(join(profileModules, 'managed-dir')).isDirectory()).toBe(true)
|
||||
expect(readlinkSync(join(profileModules, 'managed-link'))).toBe(foreignTarget)
|
||||
expect(existsSync(join(ownedModules, 'fallback'))).toBe(false)
|
||||
expect(existsSync(join(ownedModules, 'managed-dir'))).toBe(false)
|
||||
expect(existsSync(join(ownedModules, 'managed-link'))).toBe(false)
|
||||
})
|
||||
|
||||
it('cleans owned projections whose junction target uses a canonical parent path', async () => {
|
||||
const installationAnchor = stageInstallation({})
|
||||
const realHome = tmp()
|
||||
const aliasRoot = tmp()
|
||||
const home = join(aliasRoot, 'home')
|
||||
symlinkSync(realHome, home, 'junction')
|
||||
const bundleAnchor = stageInstallation({ fallback: {} }, 'selected-bundle')
|
||||
const profile = stageProfile(home, 'canonical', bundleAnchor)
|
||||
await healProfilesModuleFallback({ installAnchor: installationAnchor, profile, home })
|
||||
const profileLink = join(profile.dir, 'node_modules', 'fallback')
|
||||
const ownedModules = join(profile.dir, '.dsh-module-fallback', 'node_modules')
|
||||
unlinkSync(profileLink)
|
||||
symlinkSync(join(realpathSync(ownedModules), 'fallback'), profileLink, 'junction')
|
||||
|
||||
await healProfilesModuleFallback({
|
||||
installAnchor: installationAnchor,
|
||||
profile: { ...profile, layers: [] },
|
||||
home,
|
||||
})
|
||||
|
||||
expect(existsSync(profileLink)).toBe(false)
|
||||
expect(existsSync(join(ownedModules, 'fallback'))).toBe(false)
|
||||
})
|
||||
|
||||
it('replaces a wrong symlink', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
mkdirSync(fallback, { recursive: true })
|
||||
symlinkSync(tmp(), join(fallback, 'dsh-app'), 'junction')
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app')
|
||||
})
|
||||
|
||||
it('retains current links while repairing a missing sibling', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const home = tmp()
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const appTarget = readlinkSync(join(fallback, 'dsh-app'))
|
||||
unlinkSync(join(fallback, 'bundle-a'))
|
||||
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
|
||||
expect(readlinkSync(join(fallback, 'dsh-app'))).toBe(appTarget)
|
||||
expect(lstatSync(join(fallback, 'bundle-a')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('serializes concurrent healers and retains the identical link', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
await Promise.all([
|
||||
healProfilesModuleFallback({ installAnchor: anchor, home }),
|
||||
healProfilesModuleFallback({ installAnchor: anchor, home }),
|
||||
])
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not acquire the writer lock for a complete generation', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const modules = join(home, 'profiles', 'node_modules')
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
let releaseLock: (() => void) | undefined
|
||||
let reportLock: (() => void) | undefined
|
||||
const lockHeld = new Promise<void>((resolve) => { reportLock = resolve })
|
||||
const release = new Promise<void>((resolve) => { releaseLock = resolve })
|
||||
const holder = withFileLock(modules, async () => {
|
||||
reportLock?.()
|
||||
await release
|
||||
})
|
||||
await lockHeld
|
||||
|
||||
const healer = healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const outcome = await Promise.race([
|
||||
healer.then(() => 'complete' as const),
|
||||
new Promise<'blocked'>(resolve => setTimeout(() => { resolve('blocked') }, 100)),
|
||||
])
|
||||
releaseLock?.()
|
||||
await Promise.all([holder, healer])
|
||||
expect(outcome).toBe('complete')
|
||||
})
|
||||
|
||||
it('waits for the module-fallback writer lock before publishing entries', async () => {
|
||||
const anchor = stageInstallation({})
|
||||
const home = tmp()
|
||||
const modules = join(home, 'profiles', 'node_modules')
|
||||
mkdirSync(modules, { recursive: true })
|
||||
let releaseLock: (() => void) | undefined
|
||||
let reportLock: (() => void) | undefined
|
||||
const lockHeld = new Promise<void>((resolve) => { reportLock = resolve })
|
||||
const release = new Promise<void>((resolve) => { releaseLock = resolve })
|
||||
const holder = withFileLock(modules, async () => {
|
||||
reportLock?.()
|
||||
await release
|
||||
})
|
||||
await lockHeld
|
||||
|
||||
const healer = healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
await new Promise(resolve => setTimeout(resolve, 20))
|
||||
expect(existsSync(join(modules, 'dsh-app'))).toBe(false)
|
||||
releaseLock?.()
|
||||
await Promise.all([holder, healer])
|
||||
expect(lstatSync(join(modules, 'dsh-app')).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('writes real ESM proxies for a packaged executable', async () => {
|
||||
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',
|
||||
'./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()
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
const proxy = join(fallback, 'bundle-a')
|
||||
expect(lstatSync(proxy).isDirectory()).toBe(true)
|
||||
const proxyManifest = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
|
||||
version: unknown
|
||||
exports: unknown
|
||||
dsh: { moduleFallback: { targets: Record<string, unknown> } }
|
||||
}
|
||||
expect(proxyManifest).toMatchObject({
|
||||
version: '0.0.0',
|
||||
exports: { '.': './entry-0.js', './feature': './entry-1.js' },
|
||||
})
|
||||
expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js'))
|
||||
await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' })
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
expect(generation.entries.find(entry => entry.name === 'nested-only')?.packageDir).toBe(realpathSync.native(nestedOnly))
|
||||
expect(generation.entries.find(entry => entry.name === 'explicit-only')?.packageDir).toBe(realpathSync.native(explicitOnly))
|
||||
})
|
||||
|
||||
it('resolves import-only exports from each package installation', async () => {
|
||||
@@ -827,20 +580,14 @@ describe('healProfilesModuleFallback', () => {
|
||||
type: 'module',
|
||||
exports: { import: './index.js' },
|
||||
}))
|
||||
writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "proxied"\n')
|
||||
writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "selected"\n')
|
||||
const home = tmp()
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const fallback = join(home, 'profiles', 'node_modules')
|
||||
await expect(import(join(fallback, 'bundle-a', 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(import(join(fallback, 'nested-esm', 'entry-0.js'))).resolves.toMatchObject({ nested: 'proxied' })
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(importFromGeneration(generation, 'nested-esm')).resolves.toMatchObject({ nested: 'selected' })
|
||||
})
|
||||
|
||||
it('resolves explicit condition targets without filesystem package lookup', async () => {
|
||||
it('resolves conditional subpath exports through Node', 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>
|
||||
@@ -854,45 +601,10 @@ describe('healProfilesModuleFallback', () => {
|
||||
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({ installAnchor: 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({ installAnchor: 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
|
||||
}
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(generation, 'bundle-a/mini')).resolves.toMatchObject({ mini: true })
|
||||
await expect(importFromGeneration(generation, 'bundle-a/web')).resolves.toMatchObject({ web: true })
|
||||
})
|
||||
|
||||
it('uses the legacy index fallback when a package has no exports or main', async () => {
|
||||
@@ -901,15 +613,9 @@ describe('healProfilesModuleFallback', () => {
|
||||
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))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback({ installAnchor: 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
|
||||
}
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
})
|
||||
|
||||
it('uses Node legacy resolution for an extensionless main entry', async () => {
|
||||
@@ -918,37 +624,9 @@ describe('healProfilesModuleFallback', () => {
|
||||
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({ installAnchor: 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({ installAnchor: 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
|
||||
}
|
||||
}
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
})
|
||||
|
||||
it('fails loud on a missing legacy main entry', async () => {
|
||||
@@ -958,15 +636,11 @@ describe('healProfilesModuleFallback', () => {
|
||||
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({ installAnchor: anchor, home: tmp() })).rejects.toThrow('main entry is missing')
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home: tmp() })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).rejects.toMatchObject({ code: 'ERR_MODULE_NOT_FOUND' })
|
||||
})
|
||||
|
||||
it('omits unavailable ESM exports and rejects malformed export targets', async () => {
|
||||
it('preserves native ESM export errors and null-map legacy resolution', 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')
|
||||
@@ -980,97 +654,18 @@ describe('healProfilesModuleFallback', () => {
|
||||
: { '.': mode === 'null' ? null : { import: target } }
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
if (mode === 'directory') mkdirSync(join(bundleDir, 'mini'))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
const home = tmp()
|
||||
if (mode === 'missing' || mode === 'directory' || mode === 'absent-map') {
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules', 'bundle-a'))).toBe(false)
|
||||
} else {
|
||||
await expect(healProfilesModuleFallback({ installAnchor: 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
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
if (mode === 'absent-map') {
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
} else {
|
||||
const specifier = mode === 'null-subpath' ? 'bundle-a/bad' : 'bundle-a'
|
||||
const code = mode === 'missing' ? 'ERR_MODULE_NOT_FOUND'
|
||||
: mode === 'directory' ? 'ERR_UNSUPPORTED_DIR_IMPORT'
|
||||
: mode === 'null' || mode === 'null-subpath' ? 'ERR_PACKAGE_PATH_NOT_EXPORTED'
|
||||
: 'ERR_INVALID_PACKAGE_TARGET'
|
||||
await expect(importFromGeneration(generation, specifier)).rejects.toMatchObject({ code })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('requires a package version before writing a packaged proxy', 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.version = ''
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await expect(healProfilesModuleFallback({ installAnchor: anchor, home: tmp() })).rejects.toThrow(
|
||||
'installed package bundle-a must declare a non-empty version',
|
||||
)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('replaces plain-node links and stale managed proxies in packaged mode', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const home = tmp()
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
expect(lstatSync(proxy).isSymbolicLink()).toBe(true)
|
||||
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
expect(lstatSync(proxy).isDirectory()).toBe(true)
|
||||
const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
|
||||
version: string
|
||||
}
|
||||
stale.version = 'stale'
|
||||
writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale))
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({
|
||||
version: '0.0.0',
|
||||
})
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
|
||||
it('replaces a managed packaged proxy with a plain-node symlink', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
const home = tmp()
|
||||
const fallback = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
expect(lstatSync(fallback).isDirectory()).toBe(true)
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
|
||||
await healProfilesModuleFallback({ installAnchor: anchor, home })
|
||||
expect(lstatSync(fallback).isSymbolicLink()).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects foreign packaged fallback directories with valid or invalid metadata', async () => {
|
||||
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
|
||||
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||
try {
|
||||
for (const metadata of ['{}', '{']) {
|
||||
const home = tmp()
|
||||
const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
|
||||
mkdirSync(proxy, { recursive: true })
|
||||
writeFileSync(join(proxy, 'package.json'), metadata)
|
||||
await expect(healProfilesModuleFallback({ installAnchor: anchor, home })).rejects.toThrow(
|
||||
'exists and is not a dsh-managed module proxy',
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
delete (process as NodeJS.Process & { pkg?: unknown }).pkg
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -23,9 +23,6 @@
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../util/atomic-write"
|
||||
},
|
||||
{
|
||||
"path": "../../util/launch-environment"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import {
|
||||
boot,
|
||||
healProfilesModuleFallback,
|
||||
createProfileResolutionGeneration,
|
||||
loadOverlayPatches,
|
||||
loadProfile,
|
||||
PluginPackages,
|
||||
@@ -86,7 +86,7 @@ export async function bootProductionProfile(options: ProductionProfileOptions):
|
||||
const moduleLayers = options.overlayPaths.flatMap((path, index) => (
|
||||
overlayModuleLayers(path, overlays[index] ?? [])
|
||||
))
|
||||
await healProfilesModuleFallback({
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
installAnchor,
|
||||
profile: { ...profile, layers: [...profile.layers, ...moduleLayers] },
|
||||
})
|
||||
@@ -98,7 +98,7 @@ export async function bootProductionProfile(options: ProductionProfileOptions):
|
||||
...overlays.flat(),
|
||||
],
|
||||
async (ctx) => {
|
||||
await ctx.plugin(PluginPackages, {})
|
||||
await ctx.plugin(PluginPackages, { generation })
|
||||
await options.prepare?.(ctx)
|
||||
},
|
||||
)
|
||||
|
||||
Generated
-3
@@ -1411,9 +1411,6 @@ importers:
|
||||
|
||||
packages/boot/app-boot:
|
||||
dependencies:
|
||||
'@deepseek-ai/dsh-atomic-write':
|
||||
specifier: workspace:^
|
||||
version: link:../../util/atomic-write
|
||||
'@deepseek-ai/dsh-package-manifest':
|
||||
specifier: workspace:^
|
||||
version: link:../../util/package-manifest
|
||||
|
||||
Reference in new issue
Block a user