refactor(boot): rename the profile resolution generation to the runtime resolution
RuntimeResolution, RuntimeResolutionEntry, RuntimeResolutionOptions, and createRuntimeResolution replace the generation-named table types; installRuntimeInterception returns a RuntimeInterception; PluginPackages takes { resolution }; Worker environment data carries { resolution }; routes are native, interception, and native-after-interception. replace() keeps its successor-generation wording.
This commit is contained in:
1 parent
5f48b2fff6
commit
15e07d2721
15 files changed
+364
-364
No files matched your search
@@ -18,7 +18,7 @@ import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import {
|
||||
boot,
|
||||
readProfilePatches,
|
||||
createProfileResolutionGeneration,
|
||||
createRuntimeResolution,
|
||||
initProfile,
|
||||
installFailLoud,
|
||||
loadOverlayPatches,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
resolveProfileDir,
|
||||
type ProfileContext,
|
||||
type Profile,
|
||||
type ProfileResolutionGeneration,
|
||||
type RuntimeResolution,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
|
||||
@@ -174,7 +174,7 @@ export function prepareProfile(name: string, userLayer = true, fromDefaultProfil
|
||||
interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Immutable package fallback selected before any plugin imports. */
|
||||
resolution: ProfileResolutionGeneration
|
||||
resolution: RuntimeResolution
|
||||
/** Command-line overlay contents, frozen for this invocation. */
|
||||
overlays: PatchOptions[]
|
||||
}
|
||||
@@ -201,7 +201,7 @@ async function composeProfile(
|
||||
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 }
|
||||
const resolution = await createProfileResolutionGeneration(resolutionOptions)
|
||||
const resolution = await createRuntimeResolution(resolutionOptions)
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
return { profile, resolution, overlays }
|
||||
}
|
||||
@@ -298,7 +298,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// environment values from the same immutable launch snapshot.
|
||||
hostCtx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, options.environment)
|
||||
await hostCtx.plugin(PluginPackages, {
|
||||
generation: composed.resolution,
|
||||
resolution: composed.resolution,
|
||||
})
|
||||
// The command line and bounded exit request are launcher facts available
|
||||
// to every app plugin that injects the argument snapshot.
|
||||
|
||||
@@ -153,7 +153,7 @@ export function testProfileResolution(mode: ExampleMode): void {
|
||||
' ctx.effect(() => ready.onReady(() => {',
|
||||
` const cjs = require('${leafName}').leaf`,
|
||||
` const externalCjs = require('${externalName}').external`,
|
||||
" const entries = getEnvironmentData('@deepseek-ai/dsh-app-boot/profile-resolution').generation.entries",
|
||||
" const entries = getEnvironmentData('@deepseek-ai/dsh-app-boot/profile-resolution').resolution.entries",
|
||||
' const evidence = {',
|
||||
' execArgv: process.execArgv, esm: leaf, cjs,',
|
||||
' sameEsmLeaf: leaf === bridgeLeaf,',
|
||||
|
||||
@@ -6,7 +6,7 @@ import { join } from 'node:path'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { createLaunchEnvironmentSnapshot } from '@deepseek-ai/dsh-launch-environment'
|
||||
import {
|
||||
boot, composeEntries, createProfileResolutionGeneration,
|
||||
boot, composeEntries, createRuntimeResolution,
|
||||
PluginPackages, type Profile,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
|
||||
@@ -18,7 +18,7 @@ vi.mock('@deepseek-ai/dsh-app-boot', async (importOriginal) => {
|
||||
return {
|
||||
...actual,
|
||||
boot: vi.fn(),
|
||||
createProfileResolutionGeneration: vi.fn(actual.createProfileResolutionGeneration),
|
||||
createRuntimeResolution: vi.fn(actual.createRuntimeResolution),
|
||||
installFailLoud: vi.fn(),
|
||||
}
|
||||
})
|
||||
@@ -59,7 +59,7 @@ describe('runProfile with an application-owned profile', () => {
|
||||
await setup?.(ctx)
|
||||
throw failure
|
||||
})
|
||||
if (stage === 'composition') vi.mocked(createProfileResolutionGeneration).mockRejectedValueOnce(failure)
|
||||
if (stage === 'composition') vi.mocked(createRuntimeResolution).mockRejectedValueOnce(failure)
|
||||
const profile: Profile = {
|
||||
name: 'desktop', dir: home, patchPath: join(home, 'cordis.patch.yml'),
|
||||
patches: [], layers: [],
|
||||
@@ -140,10 +140,10 @@ describe('runProfile with an application-owned profile', () => {
|
||||
patchFiles: [overlay], args: ['--port', '0', '--no-open'],
|
||||
})
|
||||
expect(installProxyFromEnvironment).toHaveBeenCalledWith(environment, expect.any(Function))
|
||||
const generation = vi.mocked(createProfileResolutionGeneration).mock.settledResults
|
||||
const resolution = vi.mocked(createRuntimeResolution).mock.settledResults
|
||||
.find(result => result.type === 'fulfilled')?.value
|
||||
expect(generation?.profileDir).toBe(home)
|
||||
expect(plugin).toHaveBeenCalledWith(PluginPackages, { generation })
|
||||
expect(resolution?.profileDir).toBe(home)
|
||||
expect(plugin).toHaveBeenCalledWith(PluginPackages, { resolution })
|
||||
expect(lstatSync(localPackageDir).isDirectory()).toBe(true)
|
||||
expect(readFileSync(join(localPackageDir, 'package.json'), 'utf8')).toBe(localManifest)
|
||||
const requireFromProfile = createRequire(join(home, 'package.json'))
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Context } from '@deepseek-ai/cordis'
|
||||
import {
|
||||
boot,
|
||||
initProfile,
|
||||
createProfileResolutionGeneration,
|
||||
createRuntimeResolution,
|
||||
loadOverlayPatches,
|
||||
loadProfile,
|
||||
PluginPackages,
|
||||
@@ -156,7 +156,7 @@ async function bootWeb(
|
||||
profile = loadProfile('dsh-test', 'spec', INSTALL_ANCHOR, home, { userLayer: false })
|
||||
bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
}
|
||||
const resolution = await createProfileResolutionGeneration({ installAnchor: INSTALL_ANCHOR, home, profile })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: INSTALL_ANCHOR, home, profile })
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
await writeFile(rootConfig, '[]\n')
|
||||
return await boot('dsh-test', rootConfig, [...bundlePatches, ...overrides], async (bootCtx) => {
|
||||
@@ -164,7 +164,7 @@ async function bootWeb(
|
||||
installAnchor: INSTALL_ANCHOR, home, cwd: home,
|
||||
startedBundles: profileBundles ?? ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'],
|
||||
overlays: overrides, telemetryDisabledEnv: '1' })
|
||||
await bootCtx.plugin(PluginPackages, { generation: resolution })
|
||||
await bootCtx.plugin(PluginPackages, { resolution })
|
||||
bootCtx.provide('connection', {
|
||||
fetch: { register: () => () => {} },
|
||||
rpc: { intercept: () => () => {} },
|
||||
|
||||
@@ -465,7 +465,7 @@ async function cleanupScaffoldWorld(ctx: Context, workspaceCwd: string, persiste
|
||||
export async function launchWebScaffold(options: LaunchOptions = {}): Promise<WebScaffold> {
|
||||
requireDist()
|
||||
const {
|
||||
auditStartupEntries, composeEntries, createProfileResolutionGeneration, initProfile,
|
||||
auditStartupEntries, composeEntries, createRuntimeResolution, initProfile,
|
||||
mountRootInclude, readProfileManifest, readProfilePatches, loadProfileDirectory, loadOverlayPatches, PluginPackages,
|
||||
} = appBoot()
|
||||
const mode = webSnapshotMode()
|
||||
@@ -749,7 +749,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
patches: [],
|
||||
}
|
||||
const resolutionOptions = { installAnchor: INSTALL_ANCHOR, home: harnessHome, profile }
|
||||
const resolution = await createProfileResolutionGeneration(resolutionOptions)
|
||||
const resolution = await createRuntimeResolution(resolutionOptions)
|
||||
await mkdir(profileDir, { recursive: true })
|
||||
const rootConfig = join(profileDir, 'cordis.yml')
|
||||
await writeFile(rootConfig, '[]\n')
|
||||
@@ -796,7 +796,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
},
|
||||
})
|
||||
await ctx.plugin(PluginPackages, {
|
||||
generation: resolution,
|
||||
resolution,
|
||||
})
|
||||
await ctx.plugin(Loader)
|
||||
if (profileContext === undefined) {
|
||||
@@ -813,7 +813,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
||||
} else {
|
||||
// The launcher's own mount, so the manager's reloads find the root Include
|
||||
// and compose the same layers the profile files name; bare names still
|
||||
// resolve through the resolution generation above, as in the direct mount.
|
||||
// resolve through the runtime resolution above, as in the direct mount.
|
||||
await mountRootInclude(ctx, rootConfig, readProfilePatches('dsh', profileContext))
|
||||
}
|
||||
await ctx.loader.await()
|
||||
|
||||
@@ -35,7 +35,7 @@ declare module '@deepseek-ai/cordis' {
|
||||
|
||||
export {
|
||||
composeEntries,
|
||||
createProfileResolutionGeneration,
|
||||
createRuntimeResolution,
|
||||
DEFAULT_PROFILE_BUNDLES,
|
||||
OPTIONAL_BUNDLES,
|
||||
initProfile,
|
||||
@@ -52,9 +52,9 @@ export {
|
||||
type Profile,
|
||||
type ProfileLayer,
|
||||
type ProfileManifest,
|
||||
type ProfileResolutionOptions,
|
||||
type ProfileResolutionEntry,
|
||||
type ProfileResolutionGeneration,
|
||||
type RuntimeResolutionOptions,
|
||||
type RuntimeResolutionEntry,
|
||||
type RuntimeResolution,
|
||||
type ProfileTemplate,
|
||||
} from './profile.ts'
|
||||
export {
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 type { ProfileResolutionEntry, ProfileResolutionGeneration } from '../profile.ts'
|
||||
import type { RuntimeResolutionEntry, RuntimeResolution } from '../profile.ts'
|
||||
|
||||
const WORKER_RESOLUTION_KEY = '@deepseek-ai/dsh-app-boot/profile-resolution'
|
||||
const EMPTY_ATTRIBUTES: ImportAttributes = Object.freeze({})
|
||||
@@ -49,14 +49,14 @@ type EsmResolve = (
|
||||
|
||||
/**
|
||||
* Where Node continues a scoped bare request. `native` keeps the importer's own lookup: the chain is
|
||||
* answered below the interception layer. `generation` answers at the interception layer from the
|
||||
* answered below the interception layer. `interception` answers at the interception layer from the
|
||||
* entry's declaring manifest; `after` anchors Node's chain above the layer for a CommonJS subpath the
|
||||
* entry lacks. `native-after-generation` has no entry: Node's chain resumes at the interception layer,
|
||||
* entry lacks. `native-after-interception` has no entry: Node's chain resumes at the interception layer,
|
||||
* anchored by `parent`.
|
||||
*/
|
||||
type ResolutionRoute =
|
||||
| { readonly kind: 'generation'; readonly entry: ProfileResolutionEntry; readonly after: string }
|
||||
| { readonly kind: 'native-after-generation'; readonly parent: string }
|
||||
| { readonly kind: 'interception'; readonly entry: RuntimeResolutionEntry; readonly after: string }
|
||||
| { readonly kind: 'native-after-interception'; readonly parent: string }
|
||||
| { readonly kind: 'native'; readonly packageDir?: string }
|
||||
|
||||
interface ResolutionRouteState {
|
||||
@@ -77,8 +77,8 @@ interface ParentRoutes {
|
||||
|
||||
type ResolutionRoutes = Map<string, ParentRoutes | false>
|
||||
|
||||
interface CompiledGeneration {
|
||||
readonly entries: ReadonlyMap<string, ProfileResolutionEntry>
|
||||
interface CompiledResolution {
|
||||
readonly entries: ReadonlyMap<string, RuntimeResolutionEntry>
|
||||
readonly profilesDir: string
|
||||
readonly profileDir: string | undefined
|
||||
readonly profilePaths: readonly string[]
|
||||
@@ -90,8 +90,8 @@ interface CompiledGeneration {
|
||||
readonly cjsRoutes: ResolutionRoutes
|
||||
}
|
||||
|
||||
/** Active resolver registration in one Node isolate. */
|
||||
export interface ProfileResolutionRegistration {
|
||||
/** Active interception in one Node isolate. */
|
||||
export interface RuntimeInterception {
|
||||
/**
|
||||
* Locate a bare package without requiring one of its exports.
|
||||
* @param specifier - bare package or package-subpath specifier.
|
||||
@@ -100,11 +100,11 @@ export interface ProfileResolutionRegistration {
|
||||
*/
|
||||
packageDir(specifier: string, parentURL: string): string | undefined
|
||||
/**
|
||||
* Atomically publish an additive package table and fresh generation-owned caches.
|
||||
* @param generation - fully constructed successor generation.
|
||||
* Atomically publish an additive package table and fresh caches.
|
||||
* @param successor - fully constructed additive successor generation.
|
||||
* @throws when the profile scope or an existing package mapping changes.
|
||||
*/
|
||||
replace(generation: ProfileResolutionGeneration): void
|
||||
replace(successor: RuntimeResolution): void
|
||||
/** Restore the native resolver methods. Registrations dispose in reverse order. */
|
||||
dispose(): void
|
||||
}
|
||||
@@ -128,7 +128,7 @@ function canonicalPath(path: string): string {
|
||||
try {
|
||||
return realpathSync(path)
|
||||
} catch {
|
||||
// A generation may name a profile scope before that directory is materialized.
|
||||
// A runtime resolution may name a profile scope before that directory is materialized.
|
||||
return resolve(path)
|
||||
}
|
||||
}
|
||||
@@ -139,18 +139,18 @@ function prefixes(path: string): readonly string[] {
|
||||
return canonical === configured ? [configured] : [configured, canonical]
|
||||
}
|
||||
|
||||
function compileGeneration(generation: ProfileResolutionGeneration): CompiledGeneration {
|
||||
const profilePaths = prefixes(generation.profilesDir)
|
||||
const profile = generation.profileDir === undefined ? [] : prefixes(generation.profileDir)
|
||||
function compileResolution(resolution: RuntimeResolution): CompiledResolution {
|
||||
const profilePaths = prefixes(resolution.profilesDir)
|
||||
const profile = resolution.profileDir === undefined ? [] : prefixes(resolution.profileDir)
|
||||
return {
|
||||
entries: new Map(generation.entries.map(entry => [entry.name, entry])),
|
||||
profilesDir: generation.profilesDir,
|
||||
profileDir: generation.profileDir,
|
||||
entries: new Map(resolution.entries.map(entry => [entry.name, entry])),
|
||||
profilesDir: resolution.profilesDir,
|
||||
profileDir: resolution.profileDir,
|
||||
profilePaths,
|
||||
profileUrls: profilePaths.map(path => pathToFileURL(path).href),
|
||||
profile,
|
||||
activeProfileUrls: profile.map(path => pathToFileURL(path).href),
|
||||
localPackageNames: new Set(generation.localPackageNames),
|
||||
localPackageNames: new Set(resolution.localPackageNames),
|
||||
esmRoutes: new Map(),
|
||||
cjsRoutes: new Map(),
|
||||
}
|
||||
@@ -255,10 +255,10 @@ function packageImportsTarget(
|
||||
}
|
||||
|
||||
function packageSearchPaths(
|
||||
entry: ProfileResolutionEntry, request: string, cjs: CommonJsModule,
|
||||
entry: RuntimeResolutionEntry, request: string, cjs: CommonJsModule,
|
||||
): string[] {
|
||||
const name = barePackageName(request)
|
||||
/* v8 ignore next -- generation routes are created only for bare package requests */
|
||||
/* v8 ignore next -- interception routes are created only for bare package requests */
|
||||
if (name === undefined) return cjs._nodeModulePaths(dirname(entry.declarer))
|
||||
const suffix = sep + name.split('/').join(sep)
|
||||
return entry.packageDir.endsWith(suffix)
|
||||
@@ -295,19 +295,19 @@ function sameResolution(left: string, right: string): boolean {
|
||||
return canonicalPath(left) === canonicalPath(right)
|
||||
}
|
||||
|
||||
/** One mutable pointer to immutable generation data. */
|
||||
/** One mutable pointer to the immutable runtime resolution. */
|
||||
class ResolutionRouter {
|
||||
private current: CompiledGeneration
|
||||
private current: CompiledResolution
|
||||
|
||||
constructor(generation: ProfileResolutionGeneration) {
|
||||
this.current = compileGeneration(generation)
|
||||
constructor(resolution: RuntimeResolution) {
|
||||
this.current = compileResolution(resolution)
|
||||
}
|
||||
|
||||
replace(generation: ProfileResolutionGeneration): void {
|
||||
const entries = new Map(generation.entries.map(entry => [entry.name, entry]))
|
||||
if (generation.profilesDir !== this.current.profilesDir
|
||||
|| generation.profileDir !== this.current.profileDir) {
|
||||
throw new Error('profile resolution: a generation cannot change its profile scope')
|
||||
replace(successor: RuntimeResolution): void {
|
||||
const entries = new Map(successor.entries.map(entry => [entry.name, entry]))
|
||||
if (successor.profilesDir !== this.current.profilesDir
|
||||
|| successor.profileDir !== this.current.profileDir) {
|
||||
throw new Error('profile resolution: a successor cannot change its profile scope')
|
||||
}
|
||||
for (const [name, current] of this.current.entries) {
|
||||
const next = entries.get(name)
|
||||
@@ -319,7 +319,7 @@ class ResolutionRouter {
|
||||
throw new Error(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`)
|
||||
}
|
||||
}
|
||||
const localPackageNames = new Set(generation.localPackageNames)
|
||||
const localPackageNames = new Set(successor.localPackageNames)
|
||||
for (const name of this.current.localPackageNames) {
|
||||
if (!localPackageNames.has(name)) {
|
||||
throw new Error(`profile resolution: removing local package ${JSON.stringify(name)} requires a process restart`)
|
||||
@@ -330,13 +330,13 @@ class ResolutionRouter {
|
||||
throw new Error(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`)
|
||||
}
|
||||
}
|
||||
this.current = compileGeneration(generation)
|
||||
this.current = compileResolution(successor)
|
||||
}
|
||||
|
||||
private routeScoped(
|
||||
request: string,
|
||||
parentRoutes: ParentRoutes,
|
||||
generation: CompiledGeneration,
|
||||
resolution: CompiledResolution,
|
||||
flavor: 'esm' | 'cjs',
|
||||
nativeResolve?: (searchPaths: readonly string[]) => string,
|
||||
cacheable = false,
|
||||
@@ -354,7 +354,7 @@ class ResolutionRouter {
|
||||
return state
|
||||
}
|
||||
|
||||
const target = generation.entries.get(name)
|
||||
const target = resolution.entries.get(name)
|
||||
const candidates: string[] = []
|
||||
const localSearchPaths: string[] = []
|
||||
const localPrefix = localRoot + sep
|
||||
@@ -394,85 +394,85 @@ class ResolutionRouter {
|
||||
}
|
||||
}
|
||||
|
||||
// The interception layer is `<profileParent>/node_modules`: a generation entry occupies its name
|
||||
// The interception layer is `<profileParent>/node_modules`: a runtime resolution entry occupies its name
|
||||
// there, so its subpath misses continue above it; a name without an entry continues at it.
|
||||
const profileParent = dirname(localRoot)
|
||||
const eligible = target?.scope === 'installation'
|
||||
|| (target?.scope === 'profile' && parentRoutes.activeProfile)
|
||||
const route: ResolutionRoute = eligible
|
||||
? { kind: 'generation', entry: target, after: join(dirname(profileParent), 'package.json') }
|
||||
: { kind: 'native-after-generation', parent: join(profileParent, 'package.json') }
|
||||
? { kind: 'interception', entry: target, after: join(dirname(profileParent), 'package.json') }
|
||||
: { kind: 'native-after-interception', parent: join(profileParent, 'package.json') }
|
||||
const state: ResolutionRouteState = { route }
|
||||
if (route.kind === 'generation') requests.set(request, state)
|
||||
if (route.kind === 'interception') requests.set(request, state)
|
||||
return state
|
||||
}
|
||||
|
||||
private routeLocalPackage(
|
||||
request: string, parentRoutes: ParentRoutes, generation: CompiledGeneration,
|
||||
request: string, parentRoutes: ParentRoutes, resolution: CompiledResolution,
|
||||
): ResolutionRouteState | undefined {
|
||||
const name = barePackageName(request)
|
||||
if (name === undefined || !parentRoutes.activeProfile || !generation.localPackageNames.has(name)) return undefined
|
||||
if (name === undefined || !parentRoutes.activeProfile || !resolution.localPackageNames.has(name)) return undefined
|
||||
const state = { route: { kind: 'native' as const } }
|
||||
parentRoutes.requests.set(request, state)
|
||||
return state
|
||||
}
|
||||
|
||||
routeUrl(request: string, parentURL: string): ResolutionRouteState | undefined {
|
||||
const generation = this.current
|
||||
let parentRoutes = generation.esmRoutes.get(parentURL)
|
||||
const resolution = this.current
|
||||
let parentRoutes = resolution.esmRoutes.get(parentURL)
|
||||
if (parentRoutes === false) return undefined
|
||||
const cached = parentRoutes?.requests.get(request)
|
||||
if (cached !== undefined) return cached
|
||||
if (parentRoutes === undefined) {
|
||||
const profileIndex = generation.profileUrls.findIndex(prefix => parentURL.startsWith(prefix))
|
||||
const activeProfileIndex = generation.activeProfileUrls.findIndex(prefix => parentURL.startsWith(prefix))
|
||||
const profileIndex = resolution.profileUrls.findIndex(prefix => parentURL.startsWith(prefix))
|
||||
const activeProfileIndex = resolution.activeProfileUrls.findIndex(prefix => parentURL.startsWith(prefix))
|
||||
if (profileIndex < 0 && activeProfileIndex < 0) {
|
||||
generation.esmRoutes.set(parentURL, false)
|
||||
resolution.esmRoutes.set(parentURL, false)
|
||||
return undefined
|
||||
}
|
||||
let parent: string
|
||||
try {
|
||||
parent = fileURLToPath(parentURL)
|
||||
} catch {
|
||||
generation.esmRoutes.set(parentURL, false)
|
||||
resolution.esmRoutes.set(parentURL, false)
|
||||
return undefined
|
||||
}
|
||||
const treeRoot = generation.profilePaths[profileIndex]
|
||||
const treeRoot = resolution.profilePaths[profileIndex]
|
||||
const localRoot = treeRoot !== undefined
|
||||
? profileChild(parent, treeRoot)
|
||||
: generation.profile[activeProfileIndex]?.slice(0, -1)
|
||||
: resolution.profile[activeProfileIndex]?.slice(0, -1)
|
||||
/* v8 ignore next -- one matching index was established above */
|
||||
if (localRoot === undefined) return undefined
|
||||
parentRoutes = { parent, localRoot, activeProfile: activeProfileIndex >= 0, requests: new Map() }
|
||||
generation.esmRoutes.set(parentURL, parentRoutes)
|
||||
resolution.esmRoutes.set(parentURL, parentRoutes)
|
||||
}
|
||||
const local = this.routeLocalPackage(request, parentRoutes, generation)
|
||||
const local = this.routeLocalPackage(request, parentRoutes, resolution)
|
||||
if (local !== undefined) return local
|
||||
return this.routeScoped(request, parentRoutes, generation, 'esm')
|
||||
return this.routeScoped(request, parentRoutes, resolution, 'esm')
|
||||
}
|
||||
|
||||
routePath(
|
||||
request: string, parent: string, nativeResolve?: (searchPaths: readonly string[]) => string, cacheable = false,
|
||||
): ResolutionRouteState | undefined {
|
||||
const generation = this.current
|
||||
let parentRoutes = generation.cjsRoutes.get(parent)
|
||||
const resolution = this.current
|
||||
let parentRoutes = resolution.cjsRoutes.get(parent)
|
||||
if (parentRoutes === false) return undefined
|
||||
const cached = parentRoutes?.requests.get(request)
|
||||
if (cached !== undefined) return cached
|
||||
if (parentRoutes === undefined) {
|
||||
const treeRoot = generation.profilePaths.find(prefix => parent.startsWith(prefix))
|
||||
const activeProfile = generation.profile.find(prefix => parent.startsWith(prefix))
|
||||
const treeRoot = resolution.profilePaths.find(prefix => parent.startsWith(prefix))
|
||||
const activeProfile = resolution.profile.find(prefix => parent.startsWith(prefix))
|
||||
const localRoot = treeRoot !== undefined ? profileChild(parent, treeRoot) : activeProfile?.slice(0, -1)
|
||||
if (localRoot !== undefined) {
|
||||
parentRoutes = { parent, localRoot, activeProfile: activeProfile !== undefined, requests: new Map() }
|
||||
generation.cjsRoutes.set(parent, parentRoutes)
|
||||
resolution.cjsRoutes.set(parent, parentRoutes)
|
||||
}
|
||||
}
|
||||
if (parentRoutes === undefined) {
|
||||
generation.cjsRoutes.set(parent, false)
|
||||
resolution.cjsRoutes.set(parent, false)
|
||||
return undefined
|
||||
}
|
||||
return this.routeScoped(request, parentRoutes, generation, 'cjs', nativeResolve, cacheable)
|
||||
return this.routeScoped(request, parentRoutes, resolution, 'cjs', nativeResolve, cacheable)
|
||||
}
|
||||
|
||||
explicitRoute(
|
||||
@@ -499,11 +499,11 @@ class ResolutionRouter {
|
||||
const name = barePackageName(specifier)
|
||||
if (name === undefined) return undefined
|
||||
const state = this.routeUrl(specifier, parentURL)
|
||||
if (state?.route.kind === 'generation') return state.route.entry.packageDir
|
||||
if (state?.route.kind === 'interception') return state.route.entry.packageDir
|
||||
if (state?.packageDir !== undefined) return state.packageDir
|
||||
let parent: string
|
||||
try {
|
||||
parent = state?.route.kind === 'native-after-generation' ? state.route.parent : fileURLToPath(parentURL)
|
||||
parent = state?.route.kind === 'native-after-interception' ? state.route.parent : fileURLToPath(parentURL)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
@@ -594,19 +594,19 @@ function throwWithoutCjsAnchor(error: unknown, anchor: string): never {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install one profile generation on Node's default ESM and CommonJS resolvers.
|
||||
* @param generation - complete package table and profile scope.
|
||||
* @returns a registration that replaces the generation or restores the native methods.
|
||||
* Install one runtime resolution as the interception on Node's default ESM and CommonJS resolvers.
|
||||
* @param resolution - complete package table and profile scope.
|
||||
* @returns an interception that publishes a successor or restores the native methods.
|
||||
*/
|
||||
export function installProfileResolution(
|
||||
generation: ProfileResolutionGeneration,
|
||||
): ProfileResolutionRegistration {
|
||||
const router = new ResolutionRouter(generation)
|
||||
export function installRuntimeInterception(
|
||||
resolution: RuntimeResolution,
|
||||
): RuntimeInterception {
|
||||
const router = new ResolutionRouter(resolution)
|
||||
const { esm, esmDefaultResolve, esmConditions, cjs, cjsConditions, modern } = internalModules()
|
||||
const esmScope = new Map<string, boolean>()
|
||||
const profilePaths = [
|
||||
...prefixes(generation.profilesDir),
|
||||
...(generation.profileDir === undefined ? [] : prefixes(generation.profileDir)),
|
||||
...prefixes(resolution.profilesDir),
|
||||
...(resolution.profileDir === undefined ? [] : prefixes(resolution.profileDir)),
|
||||
]
|
||||
const profileUrls = profilePaths.map(path => pathToFileURL(path).href)
|
||||
let recentEsmParent: string | undefined
|
||||
@@ -656,7 +656,7 @@ export function installProfileResolution(
|
||||
if (cacheable && !(result instanceof Promise)) state.esm = result
|
||||
return result
|
||||
}
|
||||
const routedParent = pathToFileURL(route.kind === 'generation' ? route.entry.declarer : route.parent).href
|
||||
const routedParent = pathToFileURL(route.kind === 'interception' ? route.entry.declarer : route.parent).href
|
||||
const previous = delegatedEsm
|
||||
delegatedEsm = { parent: routedParent, request }
|
||||
const restoreImporter = (error: unknown): never => throwWithImporter(error, routedParent, parent)
|
||||
@@ -724,12 +724,12 @@ export function installProfileResolution(
|
||||
request: string, routed: Exclude<ResolutionRoute, { kind: 'native' }>,
|
||||
parent: CommonJsParent, main: boolean, options?: CommonJsOptions,
|
||||
): string => {
|
||||
const anchor = routed.kind === 'generation' ? routed.entry.declarer : routed.parent
|
||||
const anchor = routed.kind === 'interception' ? routed.entry.declarer : routed.parent
|
||||
const synthetic = new cjs(anchor)
|
||||
// Late parent assignment preserves Node's require stack without publishing this routing anchor in parent.children.
|
||||
synthetic.parent = parent
|
||||
synthetic.filename = anchor
|
||||
synthetic.paths = routed.kind === 'generation'
|
||||
synthetic.paths = routed.kind === 'interception'
|
||||
? packageSearchPaths(routed.entry, request, cjs)
|
||||
: cjs._nodeModulePaths(dirname(anchor))
|
||||
try {
|
||||
@@ -760,7 +760,7 @@ export function installProfileResolution(
|
||||
if (state === undefined) return resolveFrom(target.parentURL)
|
||||
if (state.route.kind === 'native') return resolveFrom(target.parentURL)
|
||||
const route = state.route
|
||||
if (route.kind === 'native-after-generation') return resolveFrom(pathToFileURL(route.parent).href)
|
||||
if (route.kind === 'native-after-interception') return resolveFrom(pathToFileURL(route.parent).href)
|
||||
return resolveFrom(pathToFileURL(route.entry.declarer).href)
|
||||
}
|
||||
const wrappedFilename: CommonJsModule['_resolveFilename'] = (request, parent, main, options) => {
|
||||
@@ -811,7 +811,7 @@ export function installProfileResolution(
|
||||
if (cacheable) state.cjs = result
|
||||
return result
|
||||
}
|
||||
if (route.kind === 'native-after-generation' && explicit !== undefined && explicitPaths !== undefined) {
|
||||
if (route.kind === 'native-after-interception' && explicit !== undefined && explicitPaths !== undefined) {
|
||||
try {
|
||||
return originalFilename.call(cjs, request, parent, main, {
|
||||
...options,
|
||||
@@ -831,10 +831,10 @@ export function installProfileResolution(
|
||||
try {
|
||||
expected = resolveRoutedCjs(request, route, parent, main, routedOptions)
|
||||
} catch (error) {
|
||||
if (route.kind !== 'generation' || !isUnselectedPackageMiss(error)) throw error
|
||||
if (route.kind !== 'interception' || !isUnselectedPackageMiss(error)) throw error
|
||||
try {
|
||||
expected = resolveRoutedCjs(
|
||||
request, { kind: 'native-after-generation', parent: route.after }, parent, main, routedOptions,
|
||||
request, { kind: 'native-after-interception', parent: route.after }, parent, main, routedOptions,
|
||||
)
|
||||
} catch (afterError) {
|
||||
const remaining = explicit === undefined || explicitPaths === undefined
|
||||
@@ -873,14 +873,14 @@ export function installProfileResolution(
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish one generation for Harness-owned Workers.
|
||||
* @param generation - complete package table and profile scope.
|
||||
* Publish one runtime resolution for Harness-owned Workers.
|
||||
* @param resolution - complete package table and profile scope.
|
||||
* @returns a disposer restoring the previous thread environment data.
|
||||
*/
|
||||
export function registerWorkerResolution(
|
||||
generation: ProfileResolutionGeneration,
|
||||
resolution: RuntimeResolution,
|
||||
): () => void {
|
||||
const previous = getEnvironmentData(WORKER_RESOLUTION_KEY)
|
||||
setEnvironmentData(WORKER_RESOLUTION_KEY, { generation })
|
||||
setEnvironmentData(WORKER_RESOLUTION_KEY, { resolution })
|
||||
return () => { setEnvironmentData(WORKER_RESOLUTION_KEY, previous) }
|
||||
}
|
||||
@@ -6,11 +6,11 @@ import { join } from 'node:path'
|
||||
import { Service, type Context } from '@deepseek-ai/cordis'
|
||||
import {
|
||||
barePackageName,
|
||||
installProfileResolution,
|
||||
installRuntimeInterception,
|
||||
registerWorkerResolution,
|
||||
type ProfileResolutionRegistration,
|
||||
type RuntimeInterception,
|
||||
} from './resolver.ts'
|
||||
import type { ProfileResolutionGeneration } from '../profile.ts'
|
||||
import type { RuntimeResolution } from '../profile.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
@@ -33,10 +33,10 @@ export interface PluginPackage {
|
||||
manifest: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** Optional runtime resolver installed and owned by {@link PluginPackages}. */
|
||||
/** Optional runtime interception installed and owned by {@link PluginPackages}. */
|
||||
export interface PluginPackagesConfig {
|
||||
/** Complete package table; omit it to expose native package lookup only. */
|
||||
generation?: ProfileResolutionGeneration
|
||||
resolution?: RuntimeResolution
|
||||
}
|
||||
|
||||
function readPackage(dir: string, fallbackName: string): PluginPackage | undefined {
|
||||
@@ -57,14 +57,14 @@ function readPackage(dir: string, fallbackName: string): PluginPackage | undefin
|
||||
/** Package lookup shared by metadata consumers in one profile process. */
|
||||
export class PluginPackages extends Service {
|
||||
private packages = new Map<string, PluginPackage | undefined>()
|
||||
private readonly resolver: ProfileResolutionRegistration | undefined
|
||||
private readonly resolver: RuntimeInterception | undefined
|
||||
private disposeWorkerResolution: (() => void) | undefined
|
||||
|
||||
constructor(ctx: Context, config: PluginPackagesConfig = {}) {
|
||||
super(ctx, 'pluginPackages')
|
||||
if (config.generation === undefined) return
|
||||
const resolver = installProfileResolution(config.generation)
|
||||
this.disposeWorkerResolution = registerWorkerResolution(config.generation)
|
||||
if (config.resolution === undefined) return
|
||||
const resolver = installRuntimeInterception(config.resolution)
|
||||
this.disposeWorkerResolution = registerWorkerResolution(config.resolution)
|
||||
this.resolver = resolver
|
||||
ctx.effect(() => () => {
|
||||
this.disposeWorkerResolution?.()
|
||||
@@ -73,15 +73,15 @@ export class PluginPackages extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an additive generation for this process and subsequently created Workers.
|
||||
* @param generation - fully constructed successor generation.
|
||||
* Publish an additive successor generation for this process and subsequently created Workers.
|
||||
* @param successor - fully constructed successor generation.
|
||||
*/
|
||||
replace(generation: ProfileResolutionGeneration): void {
|
||||
replace(successor: RuntimeResolution): void {
|
||||
if (this.resolver === undefined) throw new Error('plugin-packages: runtime resolution is not installed')
|
||||
this.resolver.replace(generation)
|
||||
this.resolver.replace(successor)
|
||||
this.packages = new Map()
|
||||
this.disposeWorkerResolution?.()
|
||||
this.disposeWorkerResolution = registerWorkerResolution(generation)
|
||||
this.disposeWorkerResolution = registerWorkerResolution(successor)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/** Install an inherited profile resolution generation in one Harness-owned Worker. */
|
||||
/** Install the inherited runtime resolution in one Harness-owned Worker. */
|
||||
|
||||
import { getEnvironmentData } from 'node:worker_threads'
|
||||
import { installProfileResolution } from './resolver.ts'
|
||||
import type { ProfileResolutionGeneration } from '../profile.ts'
|
||||
import { installRuntimeInterception } from './resolver.ts'
|
||||
import type { RuntimeResolution } from '../profile.ts'
|
||||
|
||||
const registration = getEnvironmentData(
|
||||
'@deepseek-ai/dsh-app-boot/profile-resolution',
|
||||
) as {
|
||||
generation: ProfileResolutionGeneration
|
||||
resolution: RuntimeResolution
|
||||
} | undefined
|
||||
if (registration !== undefined) installProfileResolution(registration.generation)
|
||||
if (registration !== undefined) installRuntimeInterception(registration.resolution)
|
||||
@@ -15,7 +15,7 @@
|
||||
* 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. A runtime generation supplies packages carried by the
|
||||
* resolve first. The runtime resolution supplies packages carried by the
|
||||
* installation and selected bundles to Node's ESM and CommonJS resolvers.
|
||||
* @module @deepseek-ai/dsh-app-boot/profile
|
||||
*/
|
||||
@@ -71,8 +71,8 @@ export interface Profile {
|
||||
patches: PatchOptions[]
|
||||
}
|
||||
|
||||
/** One package the generation supplies at the interception layer. */
|
||||
export interface ProfileResolutionEntry {
|
||||
/** One package the runtime resolution supplies at the interception layer. */
|
||||
export interface RuntimeResolutionEntry {
|
||||
/** Bare package name. */
|
||||
readonly name: string
|
||||
/** Package directory selected by the existing dependency traversal. */
|
||||
@@ -86,7 +86,7 @@ export interface ProfileResolutionEntry {
|
||||
}
|
||||
|
||||
/** Complete immutable package table for one profile launch. */
|
||||
export interface ProfileResolutionGeneration {
|
||||
export interface RuntimeResolution {
|
||||
/** Directory containing every profile; its node_modules is the interception layer. */
|
||||
readonly profilesDir: string
|
||||
/** Active profile directory, when profile-scope entries were included. */
|
||||
@@ -94,7 +94,7 @@ export interface ProfileResolutionGeneration {
|
||||
/** Profile-declared packages installed in the profile's own node_modules. */
|
||||
readonly localPackageNames: readonly string[]
|
||||
/** Installation-scope entries followed by profile-scope entries in precedence order. */
|
||||
readonly entries: readonly ProfileResolutionEntry[]
|
||||
readonly entries: readonly RuntimeResolutionEntry[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +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) use the runtime generation, so every plugin shares the
|
||||
// missing peers (cordis and friends) use the runtime resolution, 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:
|
||||
@@ -310,8 +310,8 @@ function collectInstallationScopePackages(
|
||||
return { packageNames: new Set(links.keys()), packageDirs: links, declarers, versions }
|
||||
}
|
||||
|
||||
/** Inputs for {@link createProfileResolutionGeneration}. */
|
||||
export interface ProfileResolutionOptions {
|
||||
/** Inputs for {@link createRuntimeResolution}. */
|
||||
export interface RuntimeResolutionOptions {
|
||||
/** Absolute package.json path of the running dsh installation. */
|
||||
installAnchor: string
|
||||
/** Loaded profile whose selected bundles may carry profile-local plugins. */
|
||||
@@ -321,13 +321,13 @@ export interface ProfileResolutionOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a profile resolution generation without writing module-resolution files.
|
||||
* Compute the runtime resolution without writing module-resolution files.
|
||||
* @param options - installation anchor, optional loaded profile, and Harness home.
|
||||
* @returns the complete immutable generation.
|
||||
* @returns the complete immutable runtime resolution.
|
||||
*/
|
||||
export async function createProfileResolutionGeneration(
|
||||
options: ProfileResolutionOptions,
|
||||
): Promise<ProfileResolutionGeneration> {
|
||||
export async function createRuntimeResolution(
|
||||
options: RuntimeResolutionOptions,
|
||||
): Promise<RuntimeResolution> {
|
||||
const { installAnchor, profile, home = resolveDshHome() } = options
|
||||
const profilesDir = join(home, PROFILES_DIR)
|
||||
const manifest = readOptionalProfileManifest(profile)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Package metadata queries share the active profile resolution generation. */
|
||||
/** Package metadata queries share the active profile resolution resolution. */
|
||||
|
||||
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
@@ -9,7 +9,7 @@ import { getEnvironmentData } from 'node:worker_threads'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { PluginPackages } from '../src/profile-resolution/service.ts'
|
||||
import type { ProfileResolutionGeneration } from '../src/profile.ts'
|
||||
import type { RuntimeResolution } from '../src/profile.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const contexts: Context[] = []
|
||||
@@ -36,9 +36,9 @@ function pkg(dir: string, version: string, name = 'metadata-lib'): string {
|
||||
return join(dir, 'package.json')
|
||||
}
|
||||
|
||||
function generation(
|
||||
function resolution(
|
||||
profilesDir: string, profileDir: string, packageDir: string, declarer: string, version: string,
|
||||
): ProfileResolutionGeneration {
|
||||
): RuntimeResolution {
|
||||
return {
|
||||
profilesDir,
|
||||
profileDir,
|
||||
@@ -48,7 +48,7 @@ function generation(
|
||||
}
|
||||
|
||||
describe('profile package metadata service', () => {
|
||||
it('resolves module URLs and package metadata through the current generation', async () => {
|
||||
it('resolves module URLs and package metadata through the current resolution', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-profile-package-service-'))
|
||||
roots.push(root)
|
||||
const profilesDir = join(root, 'profiles')
|
||||
@@ -62,7 +62,7 @@ describe('profile package metadata service', () => {
|
||||
contexts.push(ctx)
|
||||
ctx.baseUrl = pathToFileURL(profileDir).href + '/'
|
||||
await ctx.plugin(PluginPackages, {
|
||||
generation: generation(profilesDir, profileDir, first, firstAnchor, '1.0.0'),
|
||||
resolution: resolution(profilesDir, profileDir, first, firstAnchor, '1.0.0'),
|
||||
})
|
||||
|
||||
expect(ctx.pluginPackages.packageOf('metadata-lib/private', parentURL)).toMatchObject({
|
||||
@@ -126,7 +126,7 @@ describe('profile package metadata service', () => {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
await ctx.plugin(PluginPackages, {
|
||||
generation: generation(profilesDir, profileDir, missing, join(root, 'owner.json'), '1.0.0'),
|
||||
resolution: resolution(profilesDir, profileDir, missing, join(root, 'owner.json'), '1.0.0'),
|
||||
})
|
||||
expect(ctx.pluginPackages.packageOf(
|
||||
'metadata-lib', pathToFileURL(join(profileDir, 'entry.mjs')).href,
|
||||
@@ -134,22 +134,22 @@ describe('profile package metadata service', () => {
|
||||
})
|
||||
|
||||
it('publishes additive generations to the process and future Workers', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-package-service-generation-'))
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-package-service-resolution-'))
|
||||
roots.push(root)
|
||||
const profilesDir = join(root, 'profiles')
|
||||
const profileDir = join(profilesDir, 'test')
|
||||
const first = join(root, 'first')
|
||||
const firstAnchor = pkg(first, '1.0.0')
|
||||
const initial = generation(profilesDir, profileDir, first, firstAnchor, '1.0.0')
|
||||
const initial = resolution(profilesDir, profileDir, first, firstAnchor, '1.0.0')
|
||||
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 })
|
||||
await ctx.plugin(PluginPackages, { resolution: initial })
|
||||
const initialWorkerData = getEnvironmentData(key) as {
|
||||
generation: ProfileResolutionGeneration
|
||||
resolution: RuntimeResolution
|
||||
}
|
||||
expect(initialWorkerData).toEqual({ generation: initial })
|
||||
expect(initialWorkerData).toEqual({ resolution: initial })
|
||||
|
||||
const added = join(root, 'added')
|
||||
const addedAnchor = pkg(added, '2.0.0', 'added-metadata')
|
||||
@@ -162,7 +162,7 @@ describe('profile package metadata service', () => {
|
||||
}
|
||||
ctx.pluginPackages.replace(next)
|
||||
expect(getEnvironmentData(key)).toEqual({
|
||||
generation: next,
|
||||
resolution: next,
|
||||
})
|
||||
expect(ctx.pluginPackages.packageOf(
|
||||
'added-metadata', pathToFileURL(join(profileDir, 'entry.mjs')).href,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** Worker bootstrap installs only the generation inherited from its parent. */
|
||||
/** Worker bootstrap installs only the resolution inherited from its parent. */
|
||||
|
||||
import { beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { ProfileResolutionGeneration } from '../src/profile.ts'
|
||||
import type { RuntimeResolution } from '../src/profile.ts'
|
||||
|
||||
const harness = vi.hoisted(() => ({
|
||||
data: undefined as {
|
||||
generation: ProfileResolutionGeneration
|
||||
resolution: RuntimeResolution
|
||||
} | undefined,
|
||||
install: vi.fn(),
|
||||
}))
|
||||
@@ -15,7 +15,7 @@ vi.mock('node:worker_threads', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../src/profile-resolution/resolver.ts', () => ({
|
||||
installProfileResolution: harness.install,
|
||||
installRuntimeInterception: harness.install,
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -29,14 +29,14 @@ it('does nothing without inherited profile resolution data', async () => {
|
||||
expect(harness.install).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('installs the inherited generation', async () => {
|
||||
const generation: ProfileResolutionGeneration = {
|
||||
it('installs the inherited resolution', async () => {
|
||||
const resolution: RuntimeResolution = {
|
||||
profilesDir: '/profiles',
|
||||
profileDir: '/profiles/test',
|
||||
localPackageNames: [],
|
||||
entries: [],
|
||||
}
|
||||
harness.data = { generation }
|
||||
harness.data = { resolution }
|
||||
await import('../src/profile-resolution/worker-bootstrap.ts')
|
||||
expect(harness.install).toHaveBeenCalledWith(generation)
|
||||
expect(harness.install).toHaveBeenCalledWith(resolution)
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
/** Runtime profile resolution uses one eager generation for ESM and CommonJS. */
|
||||
/** Runtime profile resolution uses one eager resolution for ESM and CommonJS. */
|
||||
|
||||
import {
|
||||
existsSync,
|
||||
@@ -17,19 +17,19 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { getEnvironmentData } from 'node:worker_threads'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
installProfileResolution,
|
||||
installRuntimeInterception,
|
||||
registerWorkerResolution,
|
||||
type ProfileResolutionRegistration,
|
||||
type RuntimeInterception,
|
||||
} from '../src/profile-resolution/resolver.ts'
|
||||
import {
|
||||
createProfileResolutionGeneration,
|
||||
createRuntimeResolution,
|
||||
loadProfile,
|
||||
type Profile,
|
||||
type ProfileResolutionGeneration,
|
||||
type RuntimeResolution,
|
||||
} from '../src/profile.ts'
|
||||
|
||||
const roots: string[] = []
|
||||
const registrations: ProfileResolutionRegistration[] = []
|
||||
const registrations: RuntimeInterception[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const registration of registrations.splice(0).reverse()) registration.dispose()
|
||||
@@ -142,7 +142,7 @@ function fixture(name = '@deepseek-ai/dsh-core'): {
|
||||
// root plays $DSH_HOME; the running dsh lives in a global install outside the profiles tree.
|
||||
// Node reports resolved module paths through the native realpath: /private/var for a macOS tmpdir under /var,
|
||||
// and the long directory name for a Windows tmpdir spelled with an 8.3 short name.
|
||||
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-profile-generation-')))
|
||||
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-profile-resolution-')))
|
||||
roots.push(root)
|
||||
const installDir = join(root, 'global', 'node_modules', '@deepseek-ai', 'dsh')
|
||||
const installed = join(installDir, 'node_modules', name)
|
||||
@@ -166,8 +166,8 @@ function fixture(name = '@deepseek-ai/dsh-core'): {
|
||||
}
|
||||
}
|
||||
|
||||
async function generationOf(f: ReturnType<typeof fixture>): Promise<ProfileResolutionGeneration> {
|
||||
return await createProfileResolutionGeneration({
|
||||
async function resolutionOf(f: ReturnType<typeof fixture>): Promise<RuntimeResolution> {
|
||||
return await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -214,48 +214,48 @@ function lookupMatrix(): LookupCase[] {
|
||||
return cases
|
||||
}
|
||||
|
||||
/** The lookup order: profile-internal layers, then the generation at the interception layer, then the remaining ancestors. */
|
||||
function lookupWinner(matrixCase: LookupCase): LookupLayer | 'generation' | 'missing' {
|
||||
/** The lookup order: profile-internal layers, then the resolution at the interception layer, then the remaining ancestors. */
|
||||
function lookupWinner(matrixCase: LookupCase): LookupLayer | 'interception' | 'missing' {
|
||||
if (matrixCase.importer === 'plugin' && matrixCase.layers.includes('private')) return 'private'
|
||||
if (matrixCase.layers.includes('profile')) return 'profile'
|
||||
if (matrixCase.kind !== 'unlisted') return 'generation'
|
||||
if (matrixCase.kind !== 'unlisted') return 'interception'
|
||||
if (matrixCase.layers.includes('shared')) return 'shared'
|
||||
if (matrixCase.layers.includes('home')) return 'home'
|
||||
return 'missing'
|
||||
}
|
||||
|
||||
describe('profile resolution generation', { concurrent: false }, () => {
|
||||
it('computes an immutable runtime generation without writing profile packages', async () => {
|
||||
describe('runtime resolution', { concurrent: false }, () => {
|
||||
it('computes an immutable runtime resolution without writing profile packages', async () => {
|
||||
const f = fixture()
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.entries.find(entry => entry.name === '@deepseek-ai/dsh-core')).toMatchObject({
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.entries.find(entry => entry.name === '@deepseek-ai/dsh-core')).toMatchObject({
|
||||
packageDir: f.installed,
|
||||
version: '1.0.0',
|
||||
declarer: f.installAnchor,
|
||||
scope: 'installation',
|
||||
})
|
||||
expect(existsSync(join(generation.profilesDir, 'node_modules'))).toBe(false)
|
||||
expect(existsSync(join(resolution.profilesDir, 'node_modules'))).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)
|
||||
expect(Object.isFrozen(resolution)).toBe(true)
|
||||
expect(Object.isFrozen(resolution.entries)).toBe(true)
|
||||
expect(resolution.entries.every(Object.isFrozen)).toBe(true)
|
||||
|
||||
const installationOnly = await createProfileResolutionGeneration({
|
||||
const installationOnly = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
home: join(f.root, 'installation-only-home'),
|
||||
})
|
||||
expect(installationOnly.profileDir).toBeUndefined()
|
||||
expect(installationOnly.localPackageNames).toEqual([])
|
||||
const registration = installProfileResolution(installationOnly)
|
||||
const registration = installRuntimeInterception(installationOnly)
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(installationOnly.profilesDir, 'entry.cjs'))('@deepseek-ai/dsh-core'))
|
||||
.toEqual({ marker: 1 })
|
||||
})
|
||||
|
||||
it('fails generation construction before writing when the profile manifest is malformed', async () => {
|
||||
it('fails resolution construction before writing when the profile manifest is malformed', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), '{')
|
||||
await expect(generationOf(f)).rejects.toThrow(SyntaxError)
|
||||
await expect(resolutionOf(f)).rejects.toThrow(SyntaxError)
|
||||
expect(existsSync(join(f.root, 'profiles', 'node_modules'))).toBe(false)
|
||||
})
|
||||
|
||||
@@ -291,13 +291,13 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
})
|
||||
}
|
||||
expect(createRequire(join(bridge, 'node_modules', 'middle', 'package.json'))('leaf')).toEqual({ marker: 1 })
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor, profile: f.profile, home: f.root,
|
||||
})
|
||||
expect(generation.entries.find(entry => entry.name === '@deepseek-ai/dsh')?.declarer).toBe(f.installAnchor)
|
||||
expect(generation.entries.find(entry => entry.name === 'middle')?.declarer)
|
||||
expect(resolution.entries.find(entry => entry.name === '@deepseek-ai/dsh')?.declarer).toBe(f.installAnchor)
|
||||
expect(resolution.entries.find(entry => entry.name === 'middle')?.declarer)
|
||||
.toBe(join(bridge, 'package.json'))
|
||||
expect(generation.entries.find(entry => entry.name === 'leaf')).toMatchObject({
|
||||
expect(resolution.entries.find(entry => entry.name === 'leaf')).toMatchObject({
|
||||
packageDir: linked ? workspaceLeaf : logicalLeaf,
|
||||
declarer: join(middle, 'package.json'),
|
||||
version: linked ? '2.0.0' : '1.0.0',
|
||||
@@ -334,16 +334,16 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
patches: [],
|
||||
})
|
||||
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.entries.find(entry => entry.name === 'ordered-choice')).toMatchObject({
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.entries.find(entry => entry.name === 'ordered-choice')).toMatchObject({
|
||||
packageDir: installationChoice,
|
||||
scope: 'installation',
|
||||
})
|
||||
const bundleChoice = generation.entries.find(entry => entry.name === 'bundle-choice')
|
||||
if (bundleChoice === undefined) throw new Error('generation omitted bundle-choice')
|
||||
const bundleChoice = resolution.entries.find(entry => entry.name === 'bundle-choice')
|
||||
if (bundleChoice === undefined) throw new Error('resolution omitted bundle-choice')
|
||||
expect(bundleChoice).toMatchObject({ scope: 'profile' })
|
||||
expect(realpathSync.native(bundleChoice.packageDir)).toBe(realpathSync.native(firstBundleChoice))
|
||||
expect(generation.entries.find(entry => entry.name === 'peer-choice')).toMatchObject({
|
||||
expect(resolution.entries.find(entry => entry.name === 'peer-choice')).toMatchObject({
|
||||
packageDir: peerChoice,
|
||||
scope: 'installation',
|
||||
})
|
||||
@@ -351,7 +351,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
|
||||
it('routes ESM and CommonJS through the same installation entry', async () => {
|
||||
const f = fixture()
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require('@deepseek-ai/dsh-core')).toEqual({ marker: 1 })
|
||||
@@ -367,7 +367,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
|
||||
it('routes a scoped CommonJS package through its containing node_modules directory', async () => {
|
||||
const f = fixture('@scope/tools')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
|
||||
@@ -381,7 +381,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
pkg(target, 'real-lib', 5)
|
||||
rmSync(f.installed, { recursive: true })
|
||||
symlinkSync(target, f.installed, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs'))('aliased-lib')).toEqual({ marker: 5 })
|
||||
@@ -398,12 +398,12 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
dir: profileDir,
|
||||
patchPath: join(profileDir, 'cordis.patch.yml'),
|
||||
}
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile,
|
||||
home: f.root,
|
||||
})
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(profileDir, 'entry.cjs'))
|
||||
expect(require('@deepseek-ai/dsh-core')).toEqual({ marker: 1 })
|
||||
@@ -420,7 +420,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const alternative = join(f.root, 'alternative')
|
||||
const alternativePackage = join(alternative, 'node_modules', '@deepseek-ai/dsh-core')
|
||||
pkg(alternativePackage, '@deepseek-ai/dsh-core', 2)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(join(f.installed, 'index.cjs'))
|
||||
@@ -439,7 +439,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 createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -455,7 +455,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const paths = [alternative, f.profile.dir]
|
||||
expect(require.resolve('@deepseek-ai/dsh-core', { paths })).toBe(selected)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core', { paths })).toBe(selected)
|
||||
})
|
||||
@@ -468,7 +468,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [invalid, f.profile.dir]
|
||||
const nativeError = thrownError(() => require.resolve('@deepseek-ai/dsh-core', { paths }))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve('@deepseek-ai/dsh-core', { paths }))
|
||||
|
||||
@@ -490,7 +490,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [f.profile.dir, alternative]
|
||||
const nativeError = thrownError(() => require.resolve(name, { paths }))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve(name, { paths }))
|
||||
|
||||
@@ -502,18 +502,18 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a missing fallback legacy main after a generation miss', async () => {
|
||||
it('keeps a missing fallback legacy main after a resolution miss', async () => {
|
||||
const f = fixture()
|
||||
const selected = join(f.root, 'node_modules', '@deepseek-ai/dsh-core')
|
||||
file(join(selected, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-core', main: './missing.cjs' }))
|
||||
const alternative = join(f.root, 'alternative')
|
||||
pkg(join(alternative, 'node_modules', '@deepseek-ai/dsh-core'), '@deepseek-ai/dsh-core', 2)
|
||||
const generation = await generationOf(f)
|
||||
const resolution = await resolutionOf(f)
|
||||
rmSync(f.installed, { recursive: true })
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const paths = [f.profile.dir, alternative]
|
||||
const nativeError = thrownError(() => require.resolve('@deepseek-ai/dsh-core', { paths }))
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve('@deepseek-ai/dsh-core', { paths }))
|
||||
|
||||
@@ -525,7 +525,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a profile-local package ahead of the generation', async () => {
|
||||
it('keeps a profile-local package ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web',
|
||||
@@ -541,9 +541,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
join(f.profile.dir, 'node_modules', 'linked-local'),
|
||||
process.platform === 'win32' ? 'junction' : 'dir',
|
||||
)
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.localPackageNames).toEqual(['@deepseek-ai/dsh-core', 'linked-local'])
|
||||
const registration = installProfileResolution(generation)
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.localPackageNames).toEqual(['@deepseek-ai/dsh-core', 'linked-local'])
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require('@deepseek-ai/dsh-core')).toEqual({ marker: 2 })
|
||||
@@ -573,12 +573,12 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
const alias = join(f.profile.dir, 'node_modules', 'alias')
|
||||
pkg(alias, 'real-name', 6)
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
expect(generation.localPackageNames).toEqual(['alias'])
|
||||
expect(resolution.localPackageNames).toEqual(['alias'])
|
||||
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'))
|
||||
@@ -592,7 +592,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(() => resolveFrom('@deepseek-ai/dsh-core', pathToFileURL(join(invalidScope, 'inside.mjs')).href))
|
||||
.toThrow(/Invalid package config/u)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('real-name')).toBe(join(alias, 'index.cjs'))
|
||||
expect(require.resolve('real-name', { paths: [f.profile.dir] })).toBe(join(alias, 'index.cjs'))
|
||||
@@ -605,14 +605,14 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
.toThrow(/Invalid package config/u)
|
||||
})
|
||||
|
||||
it('keeps package imports aliases on the generation route', async () => {
|
||||
it('keeps package imports aliases on the resolution route', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web',
|
||||
private: true,
|
||||
imports: { '#@deepseek-ai/dsh-core': '@deepseek-ai/dsh-core' },
|
||||
}))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -623,21 +623,21 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(() => require.resolve('#@deepseek-ai/dsh-core')).toThrow(/Cannot find module/u)
|
||||
expect(() => resolveFrom('#@deepseek-ai/dsh-core', nativeParent)).toThrow(/Cannot find package/u)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const runtimeParent = pathToFileURL(join(nested, 'entry-runtime.mjs')).href
|
||||
expect(require.resolve('#@deepseek-ai/dsh-core')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(resolveFrom('#@deepseek-ai/dsh-core', runtimeParent)).toBe(pathToFileURL(join(f.installed, 'index.js')).href)
|
||||
})
|
||||
|
||||
it('imports package aliases through the generation and reports the original importer', async () => {
|
||||
it('imports package aliases through the resolution and reports the original importer', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web',
|
||||
private: true,
|
||||
imports: { '#library': '@deepseek-ai/dsh-core', '#missing': '@deepseek-ai/dsh-core/missing' },
|
||||
}))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
await expect(importFrom('#library', parent)).resolves.toMatchObject({ marker: 1 })
|
||||
@@ -654,7 +654,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
private: true,
|
||||
imports: { '#missing-relative': './missing.cjs' },
|
||||
}))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -664,13 +664,13 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const cjsMessage = thrownMessage(() => require.resolve('#missing-relative'))
|
||||
const esmMessage = thrownMessage(() => resolveFrom('#missing-relative', parent))
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(thrownMessage(() => require.resolve('#missing-relative'))).toBe(cjsMessage)
|
||||
expect(thrownMessage(() => resolveFrom('#missing-relative', parent))).toBe(esmMessage)
|
||||
})
|
||||
|
||||
it('keeps local and native-after-generation package imports targets in native order', async () => {
|
||||
it('keeps local and native-after-resolution package imports targets in native order', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web',
|
||||
@@ -685,7 +685,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(require.resolve('#local')).toBe(join(local, 'index.cjs'))
|
||||
expect(require.resolve('#ancestor')).toBe(join(ancestor, 'index.cjs'))
|
||||
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('#local')).toBe(join(local, 'index.cjs'))
|
||||
expect(require.resolve('#ancestor')).toBe(join(ancestor, 'index.cjs'))
|
||||
@@ -699,7 +699,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
const require = createRequire(join(outside, 'entry.cjs'))
|
||||
const nativeMessage = thrownMessage(() => require.resolve('#missing'))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(thrownMessage(() => require.resolve('#missing'))).toBe(nativeMessage)
|
||||
@@ -714,13 +714,13 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
file(nested, '')
|
||||
const require = createRequire(nested)
|
||||
const nativeMessage = thrownMessage(() => require.resolve('#@deepseek-ai/dsh-core'))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(thrownMessage(() => require.resolve('#@deepseek-ai/dsh-core'))).toBe(nativeMessage)
|
||||
})
|
||||
|
||||
it('falls through a missing local CommonJS subpath to the generation', async () => {
|
||||
it('falls through a missing local CommonJS subpath to the resolution', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'package.json'), JSON.stringify({
|
||||
name: 'dsh-profile-web',
|
||||
@@ -737,9 +737,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
name: '@deepseek-ai/dsh-core', version: '2.0.0', type: 'module', main: './index.cjs',
|
||||
}))
|
||||
file(join(local, 'index.cjs'), 'module.exports = { marker: 2 }\n')
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.localPackageNames).toEqual(['@deepseek-ai/dsh-core'])
|
||||
const registration = installProfileResolution(generation)
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.localPackageNames).toEqual(['@deepseek-ai/dsh-core'])
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs')).resolve('@deepseek-ai/dsh-core/only-install.cjs'))
|
||||
@@ -765,7 +765,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
file(join(local, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '2.0.0', type: 'module', exports: null,
|
||||
}))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -773,19 +773,19 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const require = createRequire(join(local, 'entry.cjs'))
|
||||
expect(() => require.resolve('@deepseek-ai/dsh-core/only-install.cjs')).toThrow(/Cannot find module/u)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core/only-install.cjs')).toBe(installedSubpath)
|
||||
})
|
||||
|
||||
it('stops a local CommonJS probe before the generation fallback position', async () => {
|
||||
it('stops a local CommonJS probe before the resolution fallback position', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '1.0.0', type: 'module', main: './index.cjs',
|
||||
}))
|
||||
const installedSubpath = join(f.installed, 'only-install.cjs')
|
||||
file(installedSubpath, 'module.exports = { marker: 8 }\n')
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -802,14 +802,14 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(thrownError(() => require.resolve('@deepseek-ai/dsh-core/only-install.cjs')))
|
||||
.toMatchObject({ code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' })
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core/only-install.cjs')).toBe(installedSubpath)
|
||||
})
|
||||
|
||||
it('observes a profile-local package installed after an earlier miss', async () => {
|
||||
const f = fixture()
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
@@ -828,7 +828,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const f = fixture()
|
||||
pkg(join(f.profile.dir, 'node_modules', 'undeclared-local'), 'undeclared-local', 6)
|
||||
file(join(f.profile.dir, 'relative.cjs'), 'module.exports = 7\n')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require('undeclared-local')).toEqual({ marker: 6 })
|
||||
@@ -859,39 +859,39 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(internal.Module._resolveFilename('node:path', undefined, false)).toBe('node:path')
|
||||
})
|
||||
|
||||
it('keeps a legacy CommonJS package without a manifest ahead of the generation', async () => {
|
||||
it('keeps a legacy CommonJS package without a manifest ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core', 'index.js'), 'module.exports = { marker: 2 }\n')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs'))('@deepseek-ai/dsh-core')).toEqual({ marker: 2 })
|
||||
})
|
||||
|
||||
it('keeps a manifestless local CommonJS subpath ahead of the generation', async () => {
|
||||
it('keeps a manifestless local CommonJS subpath ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
const localSubpath = join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core', 'sub.cjs')
|
||||
file(localSubpath, 'module.exports = { marker: 2 }\n')
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core/sub.cjs')).toBe(localSubpath)
|
||||
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core/sub.cjs')).toBe(localSubpath)
|
||||
})
|
||||
|
||||
it('keeps a local extensionless CommonJS package ahead of the generation', async () => {
|
||||
it('keeps a local extensionless CommonJS package ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
const local = join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core')
|
||||
file(local, 'module.exports = { marker: 2 }\n')
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(local)
|
||||
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(local)
|
||||
})
|
||||
|
||||
it('keeps a local legacy main outside its package directory ahead of the generation', async () => {
|
||||
it('keeps a local legacy main outside its package directory ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
const local = join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core')
|
||||
const outside = join(f.profile.dir, 'node_modules', '@deepseek-ai', 'outside.cjs')
|
||||
@@ -900,18 +900,18 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(outside)
|
||||
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(outside)
|
||||
})
|
||||
|
||||
it('keeps a missing local legacy main error ahead of the generation', async () => {
|
||||
it('keeps a missing local legacy main error ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
const local = join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core')
|
||||
file(join(local, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-core', main: './missing.cjs' }))
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const nativeError = thrownError(() => require.resolve('@deepseek-ai/dsh-core'))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const runtimeError = thrownError(() => require.resolve('@deepseek-ai/dsh-core'))
|
||||
|
||||
@@ -923,10 +923,10 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(runtimeError.message).toBe(nativeError.message)
|
||||
})
|
||||
|
||||
it('keeps a profile-local CommonJS package file ahead of the generation', async () => {
|
||||
it('keeps a profile-local CommonJS package file ahead of the resolution', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core.js'), 'module.exports = { marker: 2 }\n')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs'))('@deepseek-ai/dsh-core')).toEqual({ marker: 2 })
|
||||
expect(resolveFrom('@deepseek-ai/dsh-core', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href)).toBe(
|
||||
@@ -937,7 +937,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
it('preserves the ESM and CommonJS behavior of an empty local package directory', async () => {
|
||||
const f = fixture()
|
||||
mkdirSync(join(f.profile.dir, 'node_modules', '@deepseek-ai/dsh-core'), { recursive: true })
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs'))('@deepseek-ai/dsh-core')).toEqual({ marker: 1 })
|
||||
expect(() => resolveFrom(
|
||||
@@ -957,7 +957,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
patchPath: join(bundleDir, 'cordis.patch.yml'),
|
||||
patches: [],
|
||||
})
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs'))('bundle-only')).toEqual({ marker: 4 })
|
||||
const other = join(f.root, 'profiles', 'other', 'entry.cjs')
|
||||
@@ -970,8 +970,8 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
.toThrow(/Cannot find module/u)
|
||||
expect(() => createRequire(other).resolve('missing-explicit', { paths: [dirname(other)] }))
|
||||
.toThrow(/Cannot find module/u)
|
||||
file(join(f.root, 'node_modules', 'invalid-after-generation', 'package.json'), '{')
|
||||
expect(() => createRequire(other).resolve('invalid-after-generation', {
|
||||
file(join(f.root, 'node_modules', 'invalid-after-resolution', 'package.json'), '{')
|
||||
expect(() => createRequire(other).resolve('invalid-after-resolution', {
|
||||
paths: [dirname(other), f.profile.dir],
|
||||
})).toThrow(/Invalid package config/u)
|
||||
file(join(f.root, 'node_modules', 'invalid-explicit', 'package.json'), '{')
|
||||
@@ -980,10 +980,10 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
})).toThrow(/Invalid package config/u)
|
||||
})
|
||||
|
||||
it('keeps the generation ahead of packages above the shared fallback position', async () => {
|
||||
it('keeps the resolution ahead of packages above the shared fallback position', async () => {
|
||||
const f = fixture()
|
||||
pkg(join(f.root, 'node_modules', '@deepseek-ai/dsh-core'), '@deepseek-ai/dsh-core', 2)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require('@deepseek-ai/dsh-core')).toEqual({ marker: 1 })
|
||||
@@ -991,7 +991,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
.toMatchObject({ marker: 1 })
|
||||
})
|
||||
|
||||
it('continues the original ancestor lookup after a generation subpath miss', async () => {
|
||||
it('continues the original ancestor lookup after a resolution subpath miss', async () => {
|
||||
const f = fixture()
|
||||
const home = join(f.root, 'home')
|
||||
const profileDir = join(home, 'profiles', 'web')
|
||||
@@ -1009,7 +1009,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
}))
|
||||
const ancestorSubpath = join(home, 'node_modules', '@deepseek-ai/dsh-core', 'sub.cjs')
|
||||
file(ancestorSubpath, 'module.exports = { marker: 3 }\n')
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile,
|
||||
home,
|
||||
@@ -1018,13 +1018,13 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(() => require.resolve('@deepseek-ai/dsh-core')).toThrow(/Cannot find module/u)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core/sub.cjs')).toBe(ancestorSubpath)
|
||||
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(join(f.installed, 'index.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core/sub.cjs')).toBe(ancestorSubpath)
|
||||
})
|
||||
|
||||
it('continues explicit CommonJS paths after a generation subpath miss', async () => {
|
||||
it('continues explicit CommonJS paths after a resolution subpath miss', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '1.0.0', type: 'module', main: './index.cjs',
|
||||
@@ -1032,7 +1032,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const alternative = join(f.root, 'alternative')
|
||||
const alternativeSubpath = join(alternative, 'node_modules', '@deepseek-ai/dsh-core', 'sub.cjs')
|
||||
file(alternativeSubpath, 'module.exports = { marker: 4 }\n')
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(createRequire(join(f.profile.dir, 'entry.cjs')).resolve('@deepseek-ai/dsh-core/sub.cjs', {
|
||||
@@ -1040,12 +1040,12 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
})).toBe(alternativeSubpath)
|
||||
})
|
||||
|
||||
it('occupies the interception layer package directory for a generation hit and continues above it on a CommonJS subpath miss', async () => {
|
||||
it('occupies the interception layer package directory for a resolution hit and continues above it on a CommonJS subpath miss', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '1.0.0', main: './index.cjs',
|
||||
}))
|
||||
// The generation entry stands in for profiles/node_modules/@deepseek-ai/dsh-core, so this stale copy is never read.
|
||||
// The resolution entry stands in for profiles/node_modules/@deepseek-ai/dsh-core, so this stale copy is never read.
|
||||
const shared = join(f.root, 'profiles', 'node_modules', '@deepseek-ai/dsh-core')
|
||||
file(join(shared, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh-core', version: '9.0.0' }))
|
||||
file(join(shared, 'sub.cjs'), 'module.exports = { marker: 9 }\n')
|
||||
@@ -1056,7 +1056,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
// A name without an entry sees the physical directory at the same layer.
|
||||
pkg(join(f.root, 'profiles', 'node_modules', 'left-pad'), 'left-pad', 9)
|
||||
pkg(join(f.root, 'node_modules', 'left-pad'), 'left-pad', 3)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(join(f.installed, 'index.cjs'))
|
||||
@@ -1090,9 +1090,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const shared = join(f.root, 'profiles', 'node_modules', name)
|
||||
mkdirSync(dirname(shared), { recursive: true })
|
||||
symlinkSync(target, shared, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.localPackageNames).toEqual([])
|
||||
const registration = installProfileResolution(generation)
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.localPackageNames).toEqual([])
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(parentDir, 'entry.cjs'))
|
||||
const parent = pathToFileURL(join(parentDir, 'entry.mjs')).href
|
||||
@@ -1120,9 +1120,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
mkdirSync(dirname(projected), { recursive: true })
|
||||
symlinkSync(target, owned, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
symlinkSync(owned, projected, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.localPackageNames).toEqual(['@deepseek-ai/dsh-core'])
|
||||
const registration = installProfileResolution(generation)
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.localPackageNames).toEqual(['@deepseek-ai/dsh-core'])
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
@@ -1166,9 +1166,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
mkdirSync(dirname(shared), { recursive: true })
|
||||
symlinkSync(selected, shared, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
}
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.entries.some(entry => [dependency, middle, leaf].includes(entry.name))).toBe(false)
|
||||
const registration = installProfileResolution(generation)
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.entries.some(entry => [dependency, middle, leaf].includes(entry.name))).toBe(false)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
@@ -1217,11 +1217,11 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(profile.layers.map(layer => layer.packageName)).toEqual(['bundle-b'])
|
||||
expect(existsSync(join(profileDir, '.dsh-module-fallback'))).toBe(false)
|
||||
expect(existsSync(join(modules, plugin))).toBe(false)
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: f.installAnchor, profile, home: f.root })
|
||||
expect(generation.entries.find(entry => entry.name === plugin)).toMatchObject({
|
||||
const resolution = await createRuntimeResolution({ installAnchor: f.installAnchor, profile, home: f.root })
|
||||
expect(resolution.entries.find(entry => entry.name === plugin)).toMatchObject({
|
||||
packageDir: join(modules, 'bundle-b', 'node_modules', plugin), version: '2.0.0', scope: 'profile',
|
||||
})
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
expect(createRequire(join(profileDir, 'entry.cjs'))(plugin)).toEqual({ marker: 2 })
|
||||
expect(await importFrom(plugin, pathToFileURL(join(profileDir, 'entry.mjs')).href)).toMatchObject({ marker: 2 })
|
||||
@@ -1245,8 +1245,8 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
symlinkSync(owned, projected, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
const ancestor = join(f.root, 'node_modules', 'bridge')
|
||||
pkg(ancestor, 'bridge', 3)
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.entries.find(entry => entry.name === 'bridge'))
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.entries.find(entry => entry.name === 'bridge'))
|
||||
.toMatchObject({ packageDir: projected, version: '9.0.0', scope: 'profile' })
|
||||
expect(realpathSync(projected)).toBe(target)
|
||||
})
|
||||
@@ -1285,10 +1285,10 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
pkg(logical[layer], name, LOOKUP_MARKERS[layer])
|
||||
}
|
||||
}
|
||||
const generation = await generationOf(f)
|
||||
expect(generation.entries.find(entry => entry.name === name)?.scope)
|
||||
const resolution = await resolutionOf(f)
|
||||
expect(resolution.entries.find(entry => entry.name === name)?.scope)
|
||||
.toBe(kind === 'installation' ? 'installation' : kind === 'bundle' ? 'profile' : undefined)
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const importerDir = importer === 'plugin' ? pluginDir : f.profile.dir
|
||||
const require = createRequire(join(importerDir, 'entry.cjs'))
|
||||
@@ -1301,11 +1301,11 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(registration.packageDir(name, parent)).toBeUndefined()
|
||||
return
|
||||
}
|
||||
const packageDir = winner === 'generation'
|
||||
const packageDir = winner === 'interception'
|
||||
? (kind === 'installation' ? f.installed : bundleLib)
|
||||
: logical[winner]
|
||||
const realDir = winner === 'generation' ? packageDir : real[winner]
|
||||
const marker = winner === 'generation' ? (kind === 'installation' ? 1 : 2) : LOOKUP_MARKERS[winner]
|
||||
const realDir = winner === 'interception' ? packageDir : real[winner]
|
||||
const marker = winner === 'interception' ? (kind === 'installation' ? 1 : 2) : LOOKUP_MARKERS[winner]
|
||||
expect(require(name)).toEqual({ marker })
|
||||
expect(require.resolve(name)).toBe(join(realDir, 'index.cjs'))
|
||||
expect(require.resolve(name, { paths: [importerDir] })).toBe(join(realDir, 'index.cjs'))
|
||||
@@ -1315,7 +1315,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
})
|
||||
|
||||
it('uses shared packages in the canonicalized profiles tree before its ancestors', async () => {
|
||||
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-profile-generation-symlink-')))
|
||||
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'dsh-profile-resolution-symlink-')))
|
||||
roots.push(root)
|
||||
const carrier = join(root, 'carrier')
|
||||
const profilesDir = join(root, 'home', 'profiles')
|
||||
@@ -1328,7 +1328,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const ancestor = join(carrier, 'node_modules', 'left-pad')
|
||||
pkg(ancestor, 'left-pad', 3)
|
||||
pkg(join(root, 'home', 'node_modules', 'left-pad'), 'left-pad', 4)
|
||||
const registration = installProfileResolution({
|
||||
const registration = installRuntimeInterception({
|
||||
profilesDir,
|
||||
profileDir: join(profilesDir, 'web'),
|
||||
localPackageNames: [],
|
||||
@@ -1347,7 +1347,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
conditionalPkg(f.installed, 'conditional-lib', 11, 12)
|
||||
const local = join(f.profile.dir, 'node_modules', 'conditional-lib')
|
||||
conditionalPkg(local, 'conditional-lib', 21, 22)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
expect(await importFrom('conditional-lib', pathToFileURL(join(f.profile.dir, 'entry.mjs')).href))
|
||||
.toMatchObject({ marker: 21 })
|
||||
@@ -1376,10 +1376,10 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(require.resolve('conditional-lib')).toBe(join(local, 'require.cjs'))
|
||||
})
|
||||
|
||||
it('passes explicit CommonJS conditions to the generation target', async () => {
|
||||
it('passes explicit CommonJS conditions to the resolution target', async () => {
|
||||
const f = fixture('conditional-lib')
|
||||
conditionalPkg(f.installed, 'conditional-lib', 11, 12)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const addon = createRequire(import.meta.url)('node-addon-require-builtin') as { requireBuiltin(id: string): unknown }
|
||||
const internal = addon.requireBuiltin('internal/modules/cjs/loader') as {
|
||||
@@ -1406,7 +1406,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
|
||||
it('does not attach routed CommonJS anchors to the importing module', async () => {
|
||||
const f = fixture()
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const addon = createRequire(import.meta.url)('node-addon-require-builtin') as { requireBuiltin(id: string): unknown }
|
||||
const internal = addon.requireBuiltin('internal/modules/cjs/loader') as {
|
||||
@@ -1435,14 +1435,14 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '1.0.0', type: 'module', main: './index.cjs',
|
||||
}))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const nativeRequire = createRequire(join(f.profile.dir, 'entry-native.cjs'))
|
||||
const nativeError = thrownError(() => nativeRequire.resolve('@deepseek-ai/dsh-core/missing.cjs'))
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
const runtimeRequire = createRequire(join(f.profile.dir, 'entry-runtime.cjs'))
|
||||
const runtimeError = thrownError(() => runtimeRequire.resolve('@deepseek-ai/dsh-core/missing.cjs'))
|
||||
@@ -1458,12 +1458,12 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const f = fixture()
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
const nativeMessage = thrownMessage(() => resolveFrom('unavailable-lib', parent))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
})
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
|
||||
expect(thrownMessage(() => resolveFrom('unavailable-lib', parent))).toBe(nativeMessage)
|
||||
@@ -1471,9 +1471,9 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
.toContain(` imported from ${fileURLToPath(parent)}`)
|
||||
})
|
||||
|
||||
it('leaves an invalid generation manifest error to Node', async () => {
|
||||
it('leaves an invalid resolution manifest error to Node', async () => {
|
||||
const f = fixture()
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
installAnchor: f.installAnchor,
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -1482,7 +1482,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
expect(() => resolveFrom('@deepseek-ai/dsh-core', pathToFileURL(f.installAnchor).href))
|
||||
.toThrow(/Invalid package config/u)
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
registrations.push(registration)
|
||||
|
||||
expect(() => resolveFrom('@deepseek-ai/dsh-core', parent)).toThrow(/Invalid package config/u)
|
||||
@@ -1494,7 +1494,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
name: 'broken-lib',
|
||||
exports: './missing.js',
|
||||
}))
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(() => { require('broken-lib') }).toThrow(/Cannot find module|could not find/u)
|
||||
@@ -1503,20 +1503,20 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
)).rejects.toThrow(/Cannot find module|Cannot find package/u)
|
||||
})
|
||||
|
||||
it('does not fall back after the generation selects a missing exports target', async () => {
|
||||
it('does not fall back after the resolution selects a missing exports target', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '1.0.0', type: 'module', exports: './missing.cjs',
|
||||
}))
|
||||
pkg(join(f.root, 'node_modules', '@deepseek-ai/dsh-core'), '@deepseek-ai/dsh-core', 2)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(() => createRequire(join(f.profile.dir, 'entry.cjs')).resolve('@deepseek-ai/dsh-core'))
|
||||
.toThrow(/Cannot find module/u)
|
||||
})
|
||||
|
||||
it('does not fall back after the generation selects a missing legacy main', async () => {
|
||||
it('does not fall back after the resolution selects a missing legacy main', async () => {
|
||||
const f = fixture()
|
||||
file(join(f.installed, 'package.json'), JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-core', version: '1.0.0', type: 'module', main: './missing.cjs',
|
||||
@@ -1524,19 +1524,19 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
unlinkSync(join(f.installed, 'index.cjs'))
|
||||
unlinkSync(join(f.installed, 'index.js'))
|
||||
pkg(join(f.root, 'node_modules', '@deepseek-ai/dsh-core'), '@deepseek-ai/dsh-core', 2)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
|
||||
expect(() => createRequire(join(f.profile.dir, 'entry.cjs')).resolve('@deepseek-ai/dsh-core'))
|
||||
.toThrow(/valid "main" entry/u)
|
||||
})
|
||||
|
||||
it('publishes an additive generation and replaces its miss cache atomically', async () => {
|
||||
it('publishes an additive resolution and replaces its miss cache atomically', async () => {
|
||||
const f = fixture()
|
||||
const added = join(f.root, 'added')
|
||||
pkg(added, 'added-lib', 2)
|
||||
const first = await generationOf(f)
|
||||
const registration = installProfileResolution(first)
|
||||
const first = await resolutionOf(f)
|
||||
const registration = installRuntimeInterception(first)
|
||||
registrations.push(registration)
|
||||
const parent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
expect(registration.packageDir('added-lib', parent)).toBeUndefined()
|
||||
@@ -1557,8 +1557,8 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
const f = fixture()
|
||||
const second = join(f.root, 'second')
|
||||
pkg(second, '@deepseek-ai/dsh-core', 2)
|
||||
const first = await generationOf(f)
|
||||
const registration = installProfileResolution(first)
|
||||
const first = await resolutionOf(f)
|
||||
const registration = installRuntimeInterception(first)
|
||||
registrations.push(registration)
|
||||
const alias = join(f.root, 'dsh-core-alias')
|
||||
symlinkSync(f.installed, alias, process.platform === 'win32' ? 'junction' : 'dir')
|
||||
@@ -1609,7 +1609,7 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
pkg(scopedPackage, '@scope/outside', 6)
|
||||
const ancestorPackage = join(f.root, 'node_modules', 'ancestor-lib')
|
||||
pkg(ancestorPackage, 'ancestor-lib', 7)
|
||||
const registration = installProfileResolution(await generationOf(f))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const profileParent = pathToFileURL(join(f.profile.dir, 'entry.mjs')).href
|
||||
const outsideParent = pathToFileURL(join(outside, 'entry.mjs')).href
|
||||
@@ -1632,12 +1632,12 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
expect(registration.packageDir('missing', `${pathToFileURL(f.profile.dir).href}/%ZZ`)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('leaves the published generation intact when successor construction fails', async () => {
|
||||
it('leaves the published resolution intact when successor construction fails', async () => {
|
||||
const f = fixture()
|
||||
const first = await generationOf(f)
|
||||
const registration = installProfileResolution(first)
|
||||
const first = await resolutionOf(f)
|
||||
const registration = installRuntimeInterception(first)
|
||||
registrations.push(registration)
|
||||
await expect(createProfileResolutionGeneration({
|
||||
await expect(createRuntimeResolution({
|
||||
installAnchor: join(f.root, 'missing', 'package.json'),
|
||||
profile: f.profile,
|
||||
home: f.root,
|
||||
@@ -1647,14 +1647,14 @@ describe('profile resolution generation', { concurrent: false }, () => {
|
||||
)).toBe(f.installed)
|
||||
})
|
||||
|
||||
it('publishes and restores the generation inherited by owned Workers', async () => {
|
||||
it('publishes and restores the resolution inherited by owned Workers', async () => {
|
||||
const f = fixture()
|
||||
const generation = await generationOf(f)
|
||||
const resolution = await resolutionOf(f)
|
||||
const key = '@deepseek-ai/dsh-app-boot/profile-resolution'
|
||||
const previous = getEnvironmentData(key)
|
||||
const dispose = registerWorkerResolution(generation)
|
||||
const dispose = registerWorkerResolution(resolution)
|
||||
try {
|
||||
expect(getEnvironmentData(key)).toEqual({ generation })
|
||||
expect(getEnvironmentData(key)).toEqual({ resolution })
|
||||
} finally {
|
||||
dispose()
|
||||
}
|
||||
@@ -1663,7 +1663,7 @@ 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))
|
||||
const registration = installRuntimeInterception(await resolutionOf(f))
|
||||
registrations.push(registration)
|
||||
const require = createRequire(join(f.profile.dir, 'entry.cjs'))
|
||||
expect(require.resolve('@deepseek-ai/dsh-core')).toBe(join(f.installed, 'index.cjs'))
|
||||
|
||||
@@ -14,7 +14,7 @@ import { pathToFileURL } from 'node:url'
|
||||
import { afterAll, describe, expect, it, onTestFinished, vi } from 'vitest'
|
||||
import {
|
||||
composeEntries,
|
||||
createProfileResolutionGeneration,
|
||||
createRuntimeResolution,
|
||||
initProfile,
|
||||
loadProfile,
|
||||
loadProfileDirectory,
|
||||
@@ -27,9 +27,9 @@ import {
|
||||
resolveProfileDir,
|
||||
writeProfileManifest,
|
||||
type Profile,
|
||||
type ProfileResolutionGeneration,
|
||||
type RuntimeResolution,
|
||||
} from '../src/index.ts'
|
||||
import { installProfileResolution } from '../src/profile-resolution/resolver.ts'
|
||||
import { installRuntimeInterception } from '../src/profile-resolution/resolver.ts'
|
||||
|
||||
const tempRoots: string[] = []
|
||||
afterAll(() => {
|
||||
@@ -93,7 +93,7 @@ function stageProfile(home: string, name: string, bundleAnchor: string): Profile
|
||||
}
|
||||
|
||||
async function importFromGeneration(
|
||||
generation: ProfileResolutionGeneration, specifier: string,
|
||||
resolution: RuntimeResolution, specifier: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const addon = createRequire(import.meta.url)('node-addon-require-builtin') as {
|
||||
requireBuiltin(id: string): unknown
|
||||
@@ -103,9 +103,9 @@ async function importFromGeneration(
|
||||
import(specifier: string, parent: string, attributes: ImportAttributes): Promise<Record<string, unknown>>
|
||||
}
|
||||
}
|
||||
const registration = installProfileResolution(generation)
|
||||
const registration = installRuntimeInterception(resolution)
|
||||
try {
|
||||
const parent = pathToFileURL(join(generation.profilesDir, 'entry.mjs')).href
|
||||
const parent = pathToFileURL(join(resolution.profilesDir, 'entry.mjs')).href
|
||||
return await loader.getOrInitializeCascadedLoader().import(specifier, parent, {})
|
||||
} finally {
|
||||
registration.dispose()
|
||||
@@ -134,18 +134,18 @@ describe('isolated profile resolution', () => {
|
||||
writeFileSync(join(installed, 'package.json'), JSON.stringify({ name: 'pnpm-owned', main: 'index.js' }))
|
||||
writeFileSync(join(installed, 'index.js'), 'module.exports = "profile-installed"\n')
|
||||
|
||||
const generationA = await createProfileResolutionGeneration({ installAnchor: anchorA, profile: profileA, home })
|
||||
const generationB = await createProfileResolutionGeneration({ installAnchor: anchorB, profile: profileB, home })
|
||||
let registration = installProfileResolution(generationA)
|
||||
const resolutionA = await createRuntimeResolution({ installAnchor: anchorA, profile: profileA, home })
|
||||
const resolutionB = await createRuntimeResolution({ installAnchor: anchorB, profile: profileB, home })
|
||||
let registration = installRuntimeInterception(resolutionA)
|
||||
try {
|
||||
expect(realpathSync.native(createRequire(consumerA).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorA, '..', 'node_modules', 'commander', 'index.js')))
|
||||
registration.dispose()
|
||||
registration = installProfileResolution(generationB)
|
||||
registration = installRuntimeInterception(resolutionB)
|
||||
expect(realpathSync.native(createRequire(consumerB).resolve('commander')))
|
||||
.toBe(realpathSync.native(join(anchorB, '..', 'node_modules', 'commander', 'index.js')))
|
||||
registration.dispose()
|
||||
registration = installProfileResolution(generationA)
|
||||
registration = installRuntimeInterception(resolutionA)
|
||||
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')))
|
||||
@@ -153,11 +153,11 @@ describe('isolated profile resolution', () => {
|
||||
expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false)
|
||||
|
||||
registration.dispose()
|
||||
registration = installProfileResolution(await createProfileResolutionGeneration({
|
||||
registration = installRuntimeInterception(await createRuntimeResolution({
|
||||
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)
|
||||
expect(resolutionB.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')))
|
||||
@@ -381,9 +381,9 @@ describe('loadProfile', () => {
|
||||
.toEqual([{ id: 'a', name: 'pkg-a', config: { value: 'after' } }])
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('skipping profile bundle "broken":'))
|
||||
expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(saved)
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, profile, home })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, profile, home })
|
||||
const unavailable = failure === 'missing package' || failure === 'invalid manifest'
|
||||
expect(generation.entries.map(entry => entry.name))
|
||||
expect(resolution.entries.map(entry => entry.name))
|
||||
.toEqual(['dsh-app', 'before', ...unavailable ? [] : ['broken'], 'after'])
|
||||
mkdirSync(bundleDir, { recursive: true })
|
||||
writeFileSync(manifestPath, original)
|
||||
@@ -419,7 +419,7 @@ describe('composeEntries', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('createProfileResolutionGeneration', () => {
|
||||
describe('createRuntimeResolution', () => {
|
||||
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' } },
|
||||
@@ -434,11 +434,11 @@ describe('createProfileResolutionGeneration', () => {
|
||||
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()
|
||||
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'))
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home })
|
||||
expect(resolution.entries.map(entry => entry.name)).toEqual(['dsh-app', 'bundle-a', 'plain-lib', 'dep-of-a'])
|
||||
expect(resolution.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)
|
||||
await expect(createRuntimeResolution({ installAnchor: anchor, home })).resolves.toEqual(resolution)
|
||||
})
|
||||
|
||||
it('keeps selected bundle closures profile-local without overriding installation packages', async () => {
|
||||
@@ -448,33 +448,33 @@ describe('createProfileResolutionGeneration', () => {
|
||||
const home = tmp()
|
||||
const profileA = stageProfile(home, 'a', bundleA)
|
||||
const profileB = stageProfile(home, 'b', bundleB)
|
||||
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 })
|
||||
const resolutionA = await createRuntimeResolution({ installAnchor: installationAnchor, profile: profileA, home })
|
||||
await expect(createRuntimeResolution({ installAnchor: installationAnchor, profile: profileA, home }))
|
||||
.resolves.toEqual(resolutionA)
|
||||
const resolutionB = await createRuntimeResolution({ installAnchor: installationAnchor, profile: profileB, home })
|
||||
|
||||
for (const generation of [generationA, generationB]) {
|
||||
expect(generation.entries.find(entry => entry.name === 'shared')).toMatchObject({
|
||||
for (const resolution of [resolutionA, resolutionB]) {
|
||||
expect(resolution.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(generationA.entries.find(entry => entry.name === '@scope/bundle-only')).toMatchObject({
|
||||
expect(resolutionA.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({
|
||||
expect(resolutionB.entries.find(entry => entry.name === '@scope/bundle-only')).toMatchObject({
|
||||
packageDir: realpathSync.native(join(bundleB, '..', 'node_modules', '@scope', 'bundle-only')), scope: 'profile',
|
||||
})
|
||||
|
||||
const withoutBundles = await createProfileResolutionGeneration({
|
||||
const withoutBundles = await createRuntimeResolution({
|
||||
installAnchor: installationAnchor,
|
||||
profile: { ...profileA, layers: [] },
|
||||
home,
|
||||
})
|
||||
expect(withoutBundles.entries.some(entry => entry.name === '@scope/bundle-only')).toBe(false)
|
||||
expect(generationB.entries.find(entry => entry.name === '@scope/bundle-only')?.packageDir)
|
||||
expect(resolutionB.entries.find(entry => entry.name === '@scope/bundle-only')?.packageDir)
|
||||
.toBe(realpathSync.native(join(bundleB, '..', 'node_modules', '@scope', 'bundle-only')))
|
||||
})
|
||||
|
||||
@@ -512,8 +512,8 @@ describe('createProfileResolutionGeneration', () => {
|
||||
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)
|
||||
const resolution = await createRuntimeResolution({ installAnchor: installationAnchor, profile, home })
|
||||
expect(resolution.entries.find(entry => entry.name === 'bundle-only')?.packageDir)
|
||||
.toBe(realpathSync.native(realDependency))
|
||||
} finally {
|
||||
if (previous === undefined) Reflect.deleteProperty(process, 'pkg')
|
||||
@@ -559,10 +559,10 @@ describe('createProfileResolutionGeneration', () => {
|
||||
patches: [],
|
||||
}
|
||||
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: installationAnchor, profile, home })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: installationAnchor, profile, home })
|
||||
|
||||
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))
|
||||
expect(resolution.entries.find(entry => entry.name === 'nested-only')?.packageDir).toBe(realpathSync.native(nestedOnly))
|
||||
expect(resolution.entries.find(entry => entry.name === 'explicit-only')?.packageDir).toBe(realpathSync.native(explicitOnly))
|
||||
})
|
||||
|
||||
it('resolves import-only exports from each package installation', async () => {
|
||||
@@ -583,9 +583,9 @@ describe('createProfileResolutionGeneration', () => {
|
||||
}))
|
||||
writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "selected"\n')
|
||||
const home = tmp()
|
||||
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' })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(resolution, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(importFromGeneration(resolution, 'nested-esm')).resolves.toMatchObject({ nested: 'selected' })
|
||||
})
|
||||
|
||||
it('resolves conditional subpath exports through Node', async () => {
|
||||
@@ -603,9 +603,9 @@ describe('createProfileResolutionGeneration', () => {
|
||||
mkdirSync(join(bundleDir, 'dist', 'web'), { recursive: true })
|
||||
writeFileSync(join(bundleDir, 'dist', 'web', 'index.mjs'), 'export const web = true\n')
|
||||
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 })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(resolution, 'bundle-a/mini')).resolves.toMatchObject({ mini: true })
|
||||
await expect(importFromGeneration(resolution, 'bundle-a/web')).resolves.toMatchObject({ web: true })
|
||||
})
|
||||
|
||||
it('uses the legacy index fallback when a package has no exports or main', async () => {
|
||||
@@ -615,8 +615,8 @@ describe('createProfileResolutionGeneration', () => {
|
||||
delete manifest.main
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(resolution, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
})
|
||||
|
||||
it('uses Node legacy resolution for an extensionless main entry', async () => {
|
||||
@@ -626,8 +626,8 @@ describe('createProfileResolutionGeneration', () => {
|
||||
manifest.main = './index'
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home })
|
||||
await expect(importFromGeneration(resolution, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
})
|
||||
|
||||
it('fails loud on a missing legacy main entry', async () => {
|
||||
@@ -637,8 +637,8 @@ describe('createProfileResolutionGeneration', () => {
|
||||
delete manifest.main
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
rmSync(join(bundleDir, 'index.js'))
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home: tmp() })
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).rejects.toMatchObject({ code: 'ERR_MODULE_NOT_FOUND' })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home: tmp() })
|
||||
await expect(importFromGeneration(resolution, 'bundle-a')).rejects.toMatchObject({ code: 'ERR_MODULE_NOT_FOUND' })
|
||||
})
|
||||
|
||||
it('preserves native ESM export errors and null-map legacy resolution', async () => {
|
||||
@@ -656,16 +656,16 @@ describe('createProfileResolutionGeneration', () => {
|
||||
writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
|
||||
if (mode === 'directory') mkdirSync(join(bundleDir, 'mini'))
|
||||
const home = tmp()
|
||||
const generation = await createProfileResolutionGeneration({ installAnchor: anchor, home })
|
||||
const resolution = await createRuntimeResolution({ installAnchor: anchor, home })
|
||||
if (mode === 'absent-map') {
|
||||
await expect(importFromGeneration(generation, 'bundle-a')).resolves.toMatchObject({ packageName: 'bundle-a' })
|
||||
await expect(importFromGeneration(resolution, '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 })
|
||||
await expect(importFromGeneration(resolution, specifier)).rejects.toMatchObject({ code })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import {
|
||||
boot,
|
||||
createProfileResolutionGeneration,
|
||||
createRuntimeResolution,
|
||||
loadOverlayPatches,
|
||||
loadProfile,
|
||||
PluginPackages,
|
||||
@@ -86,7 +86,7 @@ export async function bootProductionProfile(options: ProductionProfileOptions):
|
||||
const moduleLayers = options.overlayPaths.flatMap((path, index) => (
|
||||
overlayModuleLayers(path, overlays[index] ?? [])
|
||||
))
|
||||
const generation = await createProfileResolutionGeneration({
|
||||
const resolution = await createRuntimeResolution({
|
||||
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, { generation })
|
||||
await ctx.plugin(PluginPackages, { resolution })
|
||||
await options.prepare?.(ctx)
|
||||
},
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user