diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index ededd48a87..42c21df7a4 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -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 { 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 | undefined const dispose = (): Promise => 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. diff --git a/apps/cli/tests/resolved-profile-boot.spec.ts b/apps/cli/tests/resolved-profile-boot.spec.ts index e968f91504..b269abe061 100644 --- a/apps/cli/tests/resolved-profile-boot.spec.ts +++ b/apps/cli/tests/resolved-profile-boot.spec.ts @@ -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')) diff --git a/apps/desktop-host/src/index.ts b/apps/desktop-host/src/index.ts index 573ef11541..3964a53291 100644 --- a/apps/desktop-host/src/index.ts +++ b/apps/desktop-host/src/index.ts @@ -18,18 +18,17 @@ async function main(): Promise { 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 ?? ''}`, }, }, }), diff --git a/apps/desktop/scripts/smoke-runtime.ts b/apps/desktop/scripts/smoke-runtime.ts index f3697cf521..06768be372 100644 --- a/apps/desktop/scripts/smoke-runtime.ts +++ b/apps/desktop/scripts/smoke-runtime.ts @@ -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 | undefined try { diff --git a/apps/desktop/src/host-process.ts b/apps/desktop/src/host-process.ts index e4be343c99..6b611abb79 100644 --- a/apps/desktop/src/host-process.ts +++ b/apps/desktop/src/host-process.ts @@ -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, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index ecf81b2e29..10ac24a9d9 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -251,7 +251,7 @@ async function main(): Promise { 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() diff --git a/apps/desktop/src/profile-core-cleanup.ts b/apps/desktop/src/profile-core-cleanup.ts index 9bdf5c9e54..c75d7c8136 100644 --- a/apps/desktop/src/profile-core-cleanup.ts +++ b/apps/desktop/src/profile-core-cleanup.ts @@ -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))) diff --git a/apps/desktop/tests/fixtures/workspace-update-adapters.mjs b/apps/desktop/tests/fixtures/workspace-update-adapters.mjs index 8b9a735282..995eb6e753 100644 --- a/apps/desktop/tests/fixtures/workspace-update-adapters.mjs +++ b/apps/desktop/tests/fixtures/workspace-update-adapters.mjs @@ -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() { diff --git a/apps/desktop/tests/host-process.spec.ts b/apps/desktop/tests/host-process.spec.ts index 45f5d4d9c5..dc678e2fa5 100644 --- a/apps/desktop/tests/host-process.spec.ts +++ b/apps/desktop/tests/host-process.spec.ts @@ -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 () => { diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 99e4b50067..ecc5280a8b 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -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) diff --git a/apps/desktop/tests/profile-core-cleanup.spec.ts b/apps/desktop/tests/profile-core-cleanup.spec.ts index 40f4825571..08d1e727c4 100644 --- a/apps/desktop/tests/profile-core-cleanup.spec.ts +++ b/apps/desktop/tests/profile-core-cleanup.spec.ts @@ -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) diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index 764381c70e..85d561bda5 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -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 /** 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 { 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 { 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'), diff --git a/packages/boot/app-boot/package.json b/packages/boot/app-boot/package.json index 1b66d184d1..3b464d917a 100644 --- a/packages/boot/app-boot/package.json +++ b/packages/boot/app-boot/package.json @@ -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", diff --git a/packages/boot/app-boot/src/index.ts b/packages/boot/app-boot/src/index.ts index 6164f6f080..0b50be9687 100644 --- a/packages/boot/app-boot/src/index.ts +++ b/packages/boot/app-boot/src/index.ts @@ -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 { diff --git a/packages/boot/app-boot/src/profile-resolution/legacy-links.ts b/packages/boot/app-boot/src/profile-resolution/legacy-links.ts index d9eb618d47..82d63c4a4f 100644 --- a/packages/boot/app-boot/src/profile-resolution/legacy-links.ts +++ b/packages/boot/app-boot/src/profile-resolution/legacy-links.ts @@ -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 - } -} diff --git a/packages/boot/app-boot/src/profile-resolution/resolver.ts b/packages/boot/app-boot/src/profile-resolution/resolver.ts index 959482c02a..788bf44764 100644 --- a/packages/boot/app-boot/src/profile-resolution/resolver.ts +++ b/packages/boot/app-boot/src/profile-resolution/resolver.ts @@ -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 - 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 + let result: ResolveResult | Promise 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) } } diff --git a/packages/boot/app-boot/src/profile-resolution/service.ts b/packages/boot/app-boot/src/profile-resolution/service.ts index 3281f42ce5..3b8c5f7714 100644 --- a/packages/boot/app-boot/src/profile-resolution/service.ts +++ b/packages/boot/app-boot/src/profile-resolution/service.ts @@ -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() 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) } /** diff --git a/packages/boot/app-boot/src/profile-resolution/worker-bootstrap.ts b/packages/boot/app-boot/src/profile-resolution/worker-bootstrap.ts index f7258d42f3..503402d8be 100644 --- a/packages/boot/app-boot/src/profile-resolution/worker-bootstrap.ts +++ b/packages/boot/app-boot/src/profile-resolution/worker-bootstrap.ts @@ -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) diff --git a/packages/boot/app-boot/src/profile.ts b/packages/boot/app-boot/src/profile.ts index d4f70fd398..ce504353f4 100644 --- a/packages/boot/app-boot/src/profile.ts +++ b/packages/boot/app-boot/src/profile.ts @@ -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 `). @@ -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 - dsh: { moduleFallback: { targets: Record } } -} - -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 } { - 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 = {} - 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, -): 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 } - /** 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 = new Set(), + installAnchor: string, skippedBundles: ReadonlySet, ): { - entries: ModuleFallbackEntry[] packageNames: ReadonlySet packageDirs: ReadonlyMap declarers: ReadonlyMap @@ -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 { - 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() const profileVersions = new Map() const localPackageNames = profile === undefined ? [] : installedProfilePackageNames(profile, manifest) const profilePackages: ReadonlyMap = profile === undefined ? new Map() - : 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, -): Promise { - 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, - exclude: (candidate: string, packageName: string) => boolean, declarers?: Map, versions?: Map, ): Map { @@ -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, materialize = true, +/** Resolve packages carried only by selected bundles for one profile. */ +function resolveProfileModuleFallback( + profile: Profile, installationPackageNames: ReadonlySet, declarers?: Map, versions?: Map, - installationLinks: ReadonlyMap = new Map(), ): Map { - 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 } diff --git a/packages/boot/app-boot/tests/profile-resolution-service.spec.ts b/packages/boot/app-boot/tests/profile-resolution-service.spec.ts index 62ce14795a..0df44cc661 100644 --- a/packages/boot/app-boot/tests/profile-resolution-service.spec.ts +++ b/packages/boot/app-boot/tests/profile-resolution-service.spec.ts @@ -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, diff --git a/packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts b/packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts index a0ab5a7616..334ec0917e 100644 --- a/packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts +++ b/packages/boot/app-boot/tests/profile-resolution-worker-bootstrap.spec.ts @@ -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) }) diff --git a/packages/boot/app-boot/tests/profile-resolution.spec.ts b/packages/boot/app-boot/tests/profile-resolution.spec.ts index c47fd2eeaf..29630006e8 100644 --- a/packages/boot/app-boot/tests/profile-resolution.spec.ts +++ b/packages/boot/app-boot/tests/profile-resolution.spec.ts @@ -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): Promise { - 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) }) }) diff --git a/packages/boot/app-boot/tests/profile.spec.ts b/packages/boot/app-boot/tests/profile.spec.ts index 324cebdb5f..a694334769 100644 --- a/packages/boot/app-boot/tests/profile.spec.ts +++ b/packages/boot/app-boot/tests/profile.spec.ts @@ -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> { + 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> + } + } + 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((resolve) => { reportLock = resolve }) - const release = new Promise((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((resolve) => { reportLock = resolve }) - const release = new Promise((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 - 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 } } - } - 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 @@ -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 } - 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 } } } - 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 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 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 - 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 - 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 - } - }) }) diff --git a/packages/boot/app-boot/tsconfig.json b/packages/boot/app-boot/tsconfig.json index a7a959aaaf..c1e709f660 100644 --- a/packages/boot/app-boot/tsconfig.json +++ b/packages/boot/app-boot/tsconfig.json @@ -23,9 +23,6 @@ { "path": "../../core/system-prompt" }, - { - "path": "../../util/atomic-write" - }, { "path": "../../util/launch-environment" }, diff --git a/packages/test-support/loader-smoke/tests/fixtures/production-profile.ts b/packages/test-support/loader-smoke/tests/fixtures/production-profile.ts index dabe5bd735..98fb43dd98 100644 --- a/packages/test-support/loader-smoke/tests/fixtures/production-profile.ts +++ b/packages/test-support/loader-smoke/tests/fixtures/production-profile.ts @@ -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) }, ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 711ce7f562..1e0ab41b40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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