@deepseek-ai/dsh-app-boot 0.2.2-rc.1 → 0.2.2-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +4 -4
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/lib/index.js +37 -17
- package/lib/types/bundle-platform.d.ts +4 -2
- package/lib/types/bundle-platform.d.ts.map +1 -0
- package/lib/types/bundle-platform.js.map +1 -0
- package/lib/types/compatibility-preflight.d.ts.map +1 -0
- package/lib/types/compatibility-preflight.js.map +1 -0
- package/lib/types/config-schema/collect.d.ts.map +1 -0
- package/lib/types/config-schema/collect.js.map +1 -0
- package/lib/types/config-schema/document.d.ts.map +1 -0
- package/lib/types/config-schema/document.js.map +1 -0
- package/lib/types/config-schema/index.d.ts.map +1 -0
- package/lib/types/config-schema/index.js.map +1 -0
- package/lib/types/config-schema/native.d.ts.map +1 -0
- package/lib/types/config-schema/native.js.map +1 -0
- package/lib/types/config-schema/pattern.d.ts.map +1 -0
- package/lib/types/config-schema/pattern.js.map +1 -0
- package/lib/types/config-schema/projector.d.ts.map +1 -0
- package/lib/types/config-schema/projector.js.map +1 -0
- package/lib/types/config-schema/types.d.ts.map +1 -0
- package/lib/types/config-schema/types.js.map +1 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js.map +1 -0
- package/lib/types/package-meta.d.ts.map +1 -0
- package/lib/types/package-meta.js.map +1 -0
- package/lib/types/plugin-compatibility.d.ts.map +1 -0
- package/lib/types/plugin-compatibility.js.map +1 -0
- package/lib/types/profile-compatibility.d.ts.map +1 -0
- package/lib/types/profile-compatibility.js.map +1 -0
- package/lib/types/profile-context.d.ts.map +1 -0
- package/lib/types/profile-context.js.map +1 -0
- package/lib/types/profile-plugins.d.ts.map +1 -0
- package/lib/types/profile-plugins.js.map +1 -0
- package/lib/types/profile-resolution/legacy-links.d.ts.map +1 -0
- package/lib/types/profile-resolution/legacy-links.js.map +1 -0
- package/lib/types/profile-resolution/resolver.d.ts.map +1 -0
- package/lib/types/profile-resolution/resolver.js.map +1 -0
- package/lib/types/profile-resolution/service.d.ts.map +1 -0
- package/lib/types/profile-resolution/service.js.map +1 -0
- package/lib/types/profile-resolution/worker-bootstrap.d.ts.map +1 -0
- package/lib/types/profile-resolution/worker-bootstrap.js.map +1 -0
- package/lib/types/profile-sanitize.d.ts.map +1 -0
- package/lib/types/profile-sanitize.js.map +1 -0
- package/lib/types/profile.d.ts.map +1 -0
- package/lib/types/profile.js.map +1 -0
- package/lib/types/startup-diagnostics.d.ts.map +1 -0
- package/lib/types/startup-diagnostics.js.map +1 -0
- package/package.json +15 -11
- package/src/bundle-platform.ts +35 -0
- package/src/compatibility-preflight.ts +187 -0
- package/src/config-schema/collect.ts +249 -0
- package/src/config-schema/document.ts +169 -0
- package/src/config-schema/index.ts +36 -0
- package/src/config-schema/native.ts +16 -0
- package/src/config-schema/pattern.ts +62 -0
- package/src/config-schema/projector.ts +455 -0
- package/src/config-schema/types.ts +95 -0
- package/src/index.ts +1286 -0
- package/src/package-meta.ts +200 -0
- package/src/plugin-compatibility.ts +117 -0
- package/src/profile-compatibility.ts +141 -0
- package/src/profile-context.ts +85 -0
- package/src/profile-plugins.ts +133 -0
- package/src/profile-resolution/legacy-links.ts +33 -0
- package/src/profile-resolution/resolver.ts +1072 -0
- package/src/profile-resolution/service.ts +148 -0
- package/src/profile-resolution/worker-bootstrap.ts +12 -0
- package/src/profile-sanitize.ts +34 -0
- package/src/profile.ts +762 -0
- package/src/startup-diagnostics.ts +104 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
/** In-memory profile package routing for Node's default ESM and CommonJS loaders. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
4
|
+
import { createRequire, isBuiltin } from 'node:module'
|
|
5
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { getEnvironmentData, setEnvironmentData } from 'node:worker_threads'
|
|
8
|
+
import type { ModuleLoaderV1, ModuleLoaderV2, ResolveResult } from '@deepseek-ai/cordis-plugin-loader'
|
|
9
|
+
import { imports as resolvePackageImports, type Package as ResolvePackageManifest } from 'resolve.exports'
|
|
10
|
+
import type { RuntimeResolutionEntry, RuntimeResolution } from '../profile.ts'
|
|
11
|
+
|
|
12
|
+
const WORKER_RESOLUTION_KEY = '@deepseek-ai/dsh-app-boot/profile-resolution'
|
|
13
|
+
const EMPTY_ATTRIBUTES: ImportAttributes = Object.freeze({})
|
|
14
|
+
|
|
15
|
+
interface CommonJsParent {
|
|
16
|
+
filename?: string | null
|
|
17
|
+
parent?: CommonJsParent | null
|
|
18
|
+
paths?: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CommonJsOptions {
|
|
22
|
+
paths?: string[]
|
|
23
|
+
conditions?: ReadonlySet<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CommonJsModule {
|
|
27
|
+
new(id?: string, parent?: CommonJsParent): CommonJsParent
|
|
28
|
+
_nodeModulePaths(path: string): string[]
|
|
29
|
+
_resolveFilename(
|
|
30
|
+
request: string, parent: CommonJsParent | null | undefined, isMain: boolean, options?: CommonJsOptions,
|
|
31
|
+
): string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface InternalModules {
|
|
35
|
+
esm: ModuleLoaderV1 | ModuleLoaderV2
|
|
36
|
+
esmDefaultResolve: (
|
|
37
|
+
specifier: string,
|
|
38
|
+
context: { parentURL?: string; conditions?: readonly string[] },
|
|
39
|
+
) => ResolveResult
|
|
40
|
+
esmConditions: readonly string[]
|
|
41
|
+
cjs: CommonJsModule
|
|
42
|
+
cjsConditions: ReadonlySet<string>
|
|
43
|
+
modern: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type EsmResolve = (
|
|
47
|
+
request: string, parent: string | undefined, attributes: ImportAttributes,
|
|
48
|
+
) => ResolveResult | Promise<ResolveResult>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Where Node continues a scoped bare request. `native` keeps the importer's own lookup: the chain is
|
|
52
|
+
* answered below the interception layer. `interception` answers at the interception layer from the
|
|
53
|
+
* entry's declaring manifest; `after` anchors Node's chain above the layer for a CommonJS subpath the
|
|
54
|
+
* entry lacks. `native-after-interception` resumes Node's chain from `parent`: the interception layer for a
|
|
55
|
+
* name without an entry, or the layer above it after an `interception` subpath miss.
|
|
56
|
+
*/
|
|
57
|
+
type ResolutionRoute =
|
|
58
|
+
| { readonly kind: 'interception'; readonly entry: RuntimeResolutionEntry; readonly after: string }
|
|
59
|
+
| { readonly kind: 'native-after-interception'; readonly parent: string }
|
|
60
|
+
| { readonly kind: 'native'; readonly packageDir?: string }
|
|
61
|
+
|
|
62
|
+
interface ResolutionRouteState {
|
|
63
|
+
readonly route: ResolutionRoute
|
|
64
|
+
packageDir?: string
|
|
65
|
+
esm?: ResolveResult
|
|
66
|
+
cjs?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface CommonJsLookup {
|
|
70
|
+
readonly cacheable: boolean
|
|
71
|
+
resolveNative(searchPaths: readonly string[]): string
|
|
72
|
+
resolveEntry(entry: RuntimeResolutionEntry, after: string): string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The lookup positions that one interception layer fixes on an importer's ancestor chain. */
|
|
76
|
+
interface LayerPositions {
|
|
77
|
+
/** Search paths with this prefix lie below the layer and are answered by their physical content. */
|
|
78
|
+
readonly localPrefix: string
|
|
79
|
+
/** Manifest anchoring Node's lookup at the layer's physical directory. */
|
|
80
|
+
readonly nativeAfter: string
|
|
81
|
+
/** Manifest anchoring Node's lookup above the layer after a CommonJS subpath miss. */
|
|
82
|
+
readonly after: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The interception layer of one importer. A profile's layer is its parent's node_modules and answers every entry
|
|
87
|
+
* for the active profile. Linked importers apply current peer declarations at each ancestor node_modules
|
|
88
|
+
* position without memoizing the selected route.
|
|
89
|
+
*/
|
|
90
|
+
type InterceptionLayer =
|
|
91
|
+
| LayerPositions & { readonly kind: 'profile'; readonly active: boolean }
|
|
92
|
+
| { readonly kind: 'linked' }
|
|
93
|
+
|
|
94
|
+
interface ParentRoutes {
|
|
95
|
+
readonly parent: string
|
|
96
|
+
readonly layer: InterceptionLayer
|
|
97
|
+
readonly requests: Map<string, ResolutionRouteState>
|
|
98
|
+
selfReferenceName?: string | false | null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
type ResolutionRoutes = Map<string, ParentRoutes | false>
|
|
102
|
+
|
|
103
|
+
/** A published entry with the real directory its modules load from. */
|
|
104
|
+
interface CompiledEntry extends RuntimeResolutionEntry {
|
|
105
|
+
readonly realDir: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface CompiledResolution {
|
|
109
|
+
readonly entries: ReadonlyMap<string, CompiledEntry>
|
|
110
|
+
readonly profilesDir: string
|
|
111
|
+
readonly profileDir: string | undefined
|
|
112
|
+
readonly profilePaths: readonly string[]
|
|
113
|
+
readonly profile: readonly string[]
|
|
114
|
+
readonly installationPaths: readonly string[]
|
|
115
|
+
readonly linkedPaths: readonly string[]
|
|
116
|
+
readonly localPackageNames: ReadonlySet<string>
|
|
117
|
+
readonly esmRoutes: ResolutionRoutes
|
|
118
|
+
readonly cjsRoutes: ResolutionRoutes
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A successor resolution the running process cannot adopt, such as a retained mapping in another directory; it waits for a restart. */
|
|
122
|
+
export class ResolutionRestartRequired extends Error {
|
|
123
|
+
/** @param message Diagnostic naming the rejected changes. */
|
|
124
|
+
constructor(message: string) {
|
|
125
|
+
super(message)
|
|
126
|
+
this.name = 'ResolutionRestartRequired'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Active interception in one Node isolate. */
|
|
131
|
+
export interface RuntimeInterception {
|
|
132
|
+
/**
|
|
133
|
+
* Locate a bare package without requiring one of its exports.
|
|
134
|
+
* A package subpath selects its package directory without resolving or validating the requested file.
|
|
135
|
+
* @param specifier - bare package or package-subpath specifier.
|
|
136
|
+
* @param parentURL - file URL whose lookup order applies.
|
|
137
|
+
* @returns selected package directory, or undefined when it is absent.
|
|
138
|
+
*/
|
|
139
|
+
packageDir(specifier: string, parentURL: string): string | undefined
|
|
140
|
+
/**
|
|
141
|
+
* Atomically publish a complete successor and fresh caches. Profile-scoped mappings, local names, and linked
|
|
142
|
+
* roots may be removed; removed roots stop intercepting uncovered directories. A retained profile mapping may
|
|
143
|
+
* change its version and declarer when its normalized directory and scope stay unchanged, as when a package is
|
|
144
|
+
* upgraded in place. Existing modules and Node caches remain intact: callers remove a package only after its
|
|
145
|
+
* plugins stop, and replace the loaded modules of a package upgraded in place separately.
|
|
146
|
+
* A previously published link name may point to another real directory; callers stop the plugins of its previous
|
|
147
|
+
* target first, as removing and installing the package again does.
|
|
148
|
+
* @param successor - fully constructed generation retaining the installation mappings.
|
|
149
|
+
* @throws when the profile scope changes; and `ResolutionRestartRequired` when a retained profile mapping's real
|
|
150
|
+
* directory or scope, or an installation version or declarer changes; an installation mapping is removed; or a new
|
|
151
|
+
* local name overrides an existing mapping.
|
|
152
|
+
*/
|
|
153
|
+
replace(successor: RuntimeResolution): void
|
|
154
|
+
/** Restore the native resolver methods. Interceptions dispose in reverse order. */
|
|
155
|
+
dispose(): void
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Split a bare request into its package name without allocating path segments.
|
|
160
|
+
* @param request - module specifier to classify.
|
|
161
|
+
* @returns the bare package name, or undefined for non-package requests.
|
|
162
|
+
*/
|
|
163
|
+
export function barePackageName(request: string): string | undefined {
|
|
164
|
+
if (!request || request[0] === '.' || request[0] === '/' || request[0] === '\\'
|
|
165
|
+
|| request[0] === '#' || request.includes(':') || isBuiltin(request)) return
|
|
166
|
+
const first = request.indexOf('/')
|
|
167
|
+
if (request[0] !== '@') return first < 0 ? request : request.slice(0, first)
|
|
168
|
+
if (first < 0) return
|
|
169
|
+
const second = request.indexOf('/', first + 1)
|
|
170
|
+
return second < 0 ? request : request.slice(0, second)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The JavaScript and native real paths of an existing directory; none for a directory that no longer exists. */
|
|
174
|
+
function realSpellings(path: string): string[] {
|
|
175
|
+
try {
|
|
176
|
+
// A packed worker's file system has no native realpath.
|
|
177
|
+
const native: unknown = Reflect.get(realpathSync, 'native')
|
|
178
|
+
return [realpathSync(path), ...typeof native === 'function' ? [String(Reflect.apply(native, realpathSync, [path]))] : []]
|
|
179
|
+
} catch (_error) {
|
|
180
|
+
// A removed previous directory keeps only its recorded spelling.
|
|
181
|
+
return []
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function canonicalPath(path: string): string {
|
|
186
|
+
try {
|
|
187
|
+
return realpathSync(path)
|
|
188
|
+
} catch {
|
|
189
|
+
// A runtime resolution may name a profile scope before that directory is materialized.
|
|
190
|
+
return resolve(path)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function prefixes(path: string): readonly string[] {
|
|
195
|
+
const configured = resolve(path) + sep
|
|
196
|
+
const canonical = canonicalPath(path) + sep
|
|
197
|
+
return canonical === configured ? [configured] : [configured, canonical]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function compileResolution(resolution: RuntimeResolution): CompiledResolution {
|
|
201
|
+
return {
|
|
202
|
+
entries: new Map(resolution.entries.map(entry => [entry.name, { ...entry, realDir: canonicalPath(entry.packageDir) }])),
|
|
203
|
+
profilesDir: resolution.profilesDir,
|
|
204
|
+
profileDir: resolution.profileDir,
|
|
205
|
+
profilePaths: prefixes(resolution.profilesDir),
|
|
206
|
+
profile: resolution.profileDir === undefined ? [] : prefixes(resolution.profileDir),
|
|
207
|
+
installationPaths: [...new Set(resolution.entries
|
|
208
|
+
.filter(entry => entry.scope === 'installation')
|
|
209
|
+
.flatMap(entry => prefixes(entry.packageDir)))],
|
|
210
|
+
linkedPaths: resolution.linkedRoots.flatMap(root => prefixes(root.realPath)),
|
|
211
|
+
localPackageNames: new Set(resolution.localPackageNames),
|
|
212
|
+
esmRoutes: new Map(),
|
|
213
|
+
cjsRoutes: new Map(),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The interception layer of a profile is `<profileParent>/node_modules`: an entry occupies its name there, so its
|
|
219
|
+
* subpath misses continue above it, and a name without an entry continues at it.
|
|
220
|
+
*/
|
|
221
|
+
function computeProfileLayer(dir: string, active: boolean): InterceptionLayer {
|
|
222
|
+
const profileParent = dirname(dir)
|
|
223
|
+
return {
|
|
224
|
+
kind: 'profile', active,
|
|
225
|
+
localPrefix: dir + sep,
|
|
226
|
+
nativeAfter: join(profileParent, 'package.json'),
|
|
227
|
+
after: join(dirname(profileParent), 'package.json'),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** The interception layer of a module path: from its profile directory inside a profiles tree, or its linked root. */
|
|
232
|
+
function findInterceptionLayer(path: string, resolution: CompiledResolution): InterceptionLayer | undefined {
|
|
233
|
+
const treeRoot = resolution.profilePaths.find(prefix => path.startsWith(prefix))
|
|
234
|
+
const activeProfile = resolution.profile.find(prefix => path.startsWith(prefix))
|
|
235
|
+
const dir = treeRoot !== undefined ? profileChild(path, treeRoot) : activeProfile?.slice(0, -1)
|
|
236
|
+
if (dir !== undefined) return computeProfileLayer(dir, activeProfile !== undefined)
|
|
237
|
+
if (!startsWithin(path, resolution.linkedPaths)) return undefined
|
|
238
|
+
if (startsWithin(path, resolution.installationPaths)) return undefined
|
|
239
|
+
return { kind: 'linked' }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Package names a directory's current manifest lists as peers; an unreadable manifest lists none. */
|
|
243
|
+
function readPeerNames(directory: string): ReadonlySet<string> {
|
|
244
|
+
try {
|
|
245
|
+
const manifest = JSON.parse(readFileSync(join(directory, 'package.json'), 'utf8')) as {
|
|
246
|
+
peerDependencies?: unknown
|
|
247
|
+
}
|
|
248
|
+
const peers = manifest.peerDependencies
|
|
249
|
+
return new Set(peers !== null && typeof peers === 'object' ? Object.keys(peers) : [])
|
|
250
|
+
} catch (_error) {
|
|
251
|
+
// Node reports the unreadable or invalid manifest itself when it resolves the package's own imports.
|
|
252
|
+
return new Set()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function startsWithin(path: string, roots: readonly string[]): boolean {
|
|
257
|
+
for (const root of roots) {
|
|
258
|
+
if (path.startsWith(root)) return true
|
|
259
|
+
}
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The profile directory owning a module inside the profiles tree: the first path segment below the tree root.
|
|
265
|
+
* @param path - module path below `treePrefix`.
|
|
266
|
+
* @param treePrefix - profiles directory with a trailing separator.
|
|
267
|
+
* @returns the profile directory without a trailing separator.
|
|
268
|
+
*/
|
|
269
|
+
function profileChild(path: string, treePrefix: string): string {
|
|
270
|
+
const rest = path.slice(treePrefix.length)
|
|
271
|
+
const end = rest.indexOf(sep)
|
|
272
|
+
return treePrefix + (end < 0 ? rest : rest.slice(0, end))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function nativePackageDir(parent: string, name: string): string | undefined {
|
|
276
|
+
for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
|
|
277
|
+
const candidate = join(searchPath, name)
|
|
278
|
+
if (existsSync(join(candidate, 'package.json'))) return candidate
|
|
279
|
+
}
|
|
280
|
+
return undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function localPackageCandidate(searchPath: string, name: string, flavor: 'esm' | 'cjs'): string | undefined {
|
|
284
|
+
const candidate = join(searchPath, name)
|
|
285
|
+
const stat = statSync(candidate, { throwIfNoEntry: false })
|
|
286
|
+
const found = flavor === 'esm'
|
|
287
|
+
? stat?.isDirectory() === true
|
|
288
|
+
: stat !== undefined
|
|
289
|
+
|| ['.js', '.json', '.node'].some(extension => existsSync(candidate + extension))
|
|
290
|
+
return found ? candidate : undefined
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function selfReferenceName(parent: string): string | false | null {
|
|
294
|
+
let current = dirname(parent)
|
|
295
|
+
while (true) {
|
|
296
|
+
const manifestPath = join(current, 'package.json')
|
|
297
|
+
if (existsSync(manifestPath)) {
|
|
298
|
+
let manifest: Record<string, unknown>
|
|
299
|
+
try {
|
|
300
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>
|
|
301
|
+
} catch (_error) {
|
|
302
|
+
// Only Node decides whether this request consumes an invalid package manifest.
|
|
303
|
+
return null
|
|
304
|
+
}
|
|
305
|
+
return typeof manifest.name === 'string' && manifest.exports != null
|
|
306
|
+
? manifest.name
|
|
307
|
+
: false
|
|
308
|
+
}
|
|
309
|
+
/* v8 ignore next -- scoped module requests normally find an owning manifest before node_modules. */
|
|
310
|
+
if (basename(current) === 'node_modules') return false
|
|
311
|
+
const next = dirname(current)
|
|
312
|
+
if (next === current) return false
|
|
313
|
+
current = next
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function packageImportsTarget(
|
|
318
|
+
parent: string, request: string, conditions: Iterable<string>,
|
|
319
|
+
): { specifier: string; parentURL: string } | undefined {
|
|
320
|
+
let current = dirname(parent)
|
|
321
|
+
while (true) {
|
|
322
|
+
const manifestPath = join(current, 'package.json')
|
|
323
|
+
if (existsSync(manifestPath)) {
|
|
324
|
+
let manifest: ResolvePackageManifest
|
|
325
|
+
try {
|
|
326
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as ResolvePackageManifest
|
|
327
|
+
} catch (_error) {
|
|
328
|
+
// The native resolver retains invalid package-target and manifest diagnostics.
|
|
329
|
+
/* v8 ignore next -- native resolution cannot report MODULE_NOT_FOUND after an invalid scope manifest */
|
|
330
|
+
return undefined
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const target = resolvePackageImports(manifest, request, {
|
|
334
|
+
conditions: [...conditions],
|
|
335
|
+
unsafe: true,
|
|
336
|
+
})?.[0]
|
|
337
|
+
return target !== undefined && barePackageName(target) !== undefined
|
|
338
|
+
? { specifier: target, parentURL: pathToFileURL(manifestPath).href }
|
|
339
|
+
: undefined
|
|
340
|
+
} catch (_error) {
|
|
341
|
+
// The native resolver retains missing mappings and unmatched-condition diagnostics.
|
|
342
|
+
/* v8 ignore next -- native resolution cannot report MODULE_NOT_FOUND before selecting a valid mapping */
|
|
343
|
+
return undefined
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (basename(current) === 'node_modules') return undefined
|
|
347
|
+
const next = dirname(current)
|
|
348
|
+
/* v8 ignore next -- a MODULE_NOT_FOUND package-import target always has an owning package scope */
|
|
349
|
+
if (next === current) return undefined
|
|
350
|
+
current = next
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function packageSearchPaths(
|
|
355
|
+
entry: RuntimeResolutionEntry, request: string, cjs: CommonJsModule,
|
|
356
|
+
): string[] {
|
|
357
|
+
const name = barePackageName(request)
|
|
358
|
+
/* v8 ignore next -- interception routes are created only for bare package requests */
|
|
359
|
+
if (name === undefined) return cjs._nodeModulePaths(dirname(entry.declarer))
|
|
360
|
+
const suffix = sep + name.split('/').join(sep)
|
|
361
|
+
return entry.packageDir.endsWith(suffix)
|
|
362
|
+
? [entry.packageDir.slice(0, -suffix.length)]
|
|
363
|
+
: cjs._nodeModulePaths(dirname(entry.declarer))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function localCandidateOwnsResolution(candidate: string, resolved: string, request: string, name: string): boolean {
|
|
367
|
+
if (startsWithin(resolved, prefixes(candidate))) return true
|
|
368
|
+
if (sameResolution(candidate, resolved)) return true
|
|
369
|
+
if (['.js', '.json', '.node'].some(extension => sameResolution(candidate + extension, resolved))) return true
|
|
370
|
+
/* v8 ignore next -- a bounded native lookup can escape a candidate only through its root legacy main */
|
|
371
|
+
if (request !== name) return false
|
|
372
|
+
try {
|
|
373
|
+
const manifest = JSON.parse(readFileSync(join(candidate, 'package.json'), 'utf8')) as Record<string, unknown>
|
|
374
|
+
/* v8 ignore next -- a bounded native lookup outside the package directory requires a legacy main */
|
|
375
|
+
if (typeof manifest.main !== 'string') return false
|
|
376
|
+
const main = createRequire(join(candidate, 'package.json')).resolve(resolve(candidate, manifest.main))
|
|
377
|
+
return sameResolution(main, resolved)
|
|
378
|
+
} catch (_error) {
|
|
379
|
+
// Native resolution already owns malformed manifests and missing legacy entries.
|
|
380
|
+
/* v8 ignore next -- this helper runs only after the same native resolution succeeded */
|
|
381
|
+
return false
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isUnselectedPackageMiss(error: unknown): boolean {
|
|
386
|
+
const failure = error as NodeJS.ErrnoException & { path?: unknown }
|
|
387
|
+
return failure.code === 'MODULE_NOT_FOUND' && failure.path === undefined
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function sameResolution(left: string, right: string): boolean {
|
|
391
|
+
if (left === right) return true
|
|
392
|
+
return canonicalPath(left) === canonicalPath(right)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** A directory as given and in its JavaScript and native real paths, each with a trailing separator. */
|
|
396
|
+
function spellings(dir: string): string[] {
|
|
397
|
+
const result = new Set([dir + sep])
|
|
398
|
+
for (const real of realSpellings(dir)) result.add(real + sep)
|
|
399
|
+
return [...result]
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The previous real directories of packages that moved, each mapped to the current one. Node keeps the real path of
|
|
404
|
+
* every link it resolved through and never refreshes it, so a lookup through a link that now points elsewhere still
|
|
405
|
+
* selects a file below the target Node saw first; {@link MovedDirectories.relocate} selects the same file below the
|
|
406
|
+
* current target instead.
|
|
407
|
+
*/
|
|
408
|
+
class MovedDirectories {
|
|
409
|
+
/** Previous directory to current directory, both with a trailing separator. */
|
|
410
|
+
private readonly targets = new Map<string, string>()
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Record that a package moved from one directory to another. Every spelling of the previous directory leads to the
|
|
414
|
+
* current one. A directory that an earlier move led to the previous one now leads to the current one as well, since
|
|
415
|
+
* Node still selects files below the earliest target.
|
|
416
|
+
* @param from Previous real directory.
|
|
417
|
+
* @param to Current real directory.
|
|
418
|
+
*/
|
|
419
|
+
record(from: string, to: string): void {
|
|
420
|
+
const previous = spellings(from)
|
|
421
|
+
const current = to + sep
|
|
422
|
+
for (const [key, value] of this.targets) {
|
|
423
|
+
if (previous.includes(value)) this.targets.set(key, current)
|
|
424
|
+
}
|
|
425
|
+
for (const key of previous) this.targets.set(key, current)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Forget the previous directories that are current again, in any spelling, and records that lead to themselves.
|
|
430
|
+
* @param current The real directories the published resolution maps.
|
|
431
|
+
*/
|
|
432
|
+
forget(current: readonly string[]): void {
|
|
433
|
+
const active = new Set<string>()
|
|
434
|
+
for (const dir of current) for (const spelling of spellings(dir)) active.add(spelling)
|
|
435
|
+
for (const [key, value] of [...this.targets]) {
|
|
436
|
+
if (active.has(key) || key === value) this.targets.delete(key)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The same file below the current directory when it exists, for a file below a previous directory that an importer
|
|
442
|
+
* outside that directory selected. An importer inside the previous directory keeps its own package's files. The
|
|
443
|
+
* innermost previous directory decides.
|
|
444
|
+
* @param filename Selected file.
|
|
445
|
+
* @param parent Importer path.
|
|
446
|
+
* @returns The file to load.
|
|
447
|
+
*/
|
|
448
|
+
relocate(filename: string, parent: string | undefined): string {
|
|
449
|
+
let from = ''
|
|
450
|
+
for (const dir of this.targets.keys()) {
|
|
451
|
+
if (filename.startsWith(dir) && dir.length > from.length) from = dir
|
|
452
|
+
}
|
|
453
|
+
if (from === '' || parent?.startsWith(from) === true) return filename
|
|
454
|
+
const relocated = (this.targets.get(from) as string) + filename.slice(from.length)
|
|
455
|
+
return existsSync(relocated) ? relocated : filename
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** One mutable pointer to the immutable runtime resolution. */
|
|
460
|
+
class ResolutionRouter {
|
|
461
|
+
private current: CompiledResolution
|
|
462
|
+
private linkedTargets: Map<string, string>
|
|
463
|
+
private readonly moved = new MovedDirectories()
|
|
464
|
+
|
|
465
|
+
constructor(resolution: RuntimeResolution, private readonly nodeModulePaths: (directory: string) => string[]) {
|
|
466
|
+
this.current = compileResolution(resolution)
|
|
467
|
+
this.linkedTargets = new Map(resolution.linkedRoots.map(root => [root.name, root.realPath]))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
replace(successor: RuntimeResolution): void {
|
|
471
|
+
const entries = new Map(successor.entries.map(entry => [entry.name, entry]))
|
|
472
|
+
if (successor.profilesDir !== this.current.profilesDir
|
|
473
|
+
|| successor.profileDir !== this.current.profileDir) {
|
|
474
|
+
throw new Error('profile resolution: a successor cannot change its profile scope')
|
|
475
|
+
}
|
|
476
|
+
const rejected: string[] = []
|
|
477
|
+
const moves: [string, string][] = []
|
|
478
|
+
for (const [name, current] of this.current.entries) {
|
|
479
|
+
const next = entries.get(name)
|
|
480
|
+
if (next === undefined && current.scope === 'profile') continue
|
|
481
|
+
if (next !== undefined && current.scope === 'profile' && next.scope === 'profile') {
|
|
482
|
+
// Real directories are compared: a link keeps its path when its target changes.
|
|
483
|
+
if (current.realDir === canonicalPath(next.packageDir)) continue
|
|
484
|
+
rejected.push(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`)
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
if (next === undefined
|
|
488
|
+
|| !sameResolution(current.packageDir, next.packageDir)
|
|
489
|
+
|| (current.scope === 'installation' && !sameResolution(current.declarer, next.declarer))
|
|
490
|
+
|| (current.scope === 'installation' && current.version !== next.version)
|
|
491
|
+
|| current.scope !== next.scope) {
|
|
492
|
+
rejected.push(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const localPackageNames = new Set(successor.localPackageNames)
|
|
496
|
+
for (const name of localPackageNames) {
|
|
497
|
+
if (!this.current.localPackageNames.has(name) && this.current.entries.has(name)) {
|
|
498
|
+
rejected.push(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// A link name pointing elsewhere, or installed again as a directory, moves its package. Names whose link and mapping
|
|
502
|
+
// are gone stay recorded, so the package that returns under the name is compared with its last target.
|
|
503
|
+
const targets = new Map(this.linkedTargets)
|
|
504
|
+
for (const [name, previous] of this.linkedTargets) {
|
|
505
|
+
const root = successor.linkedRoots.find(candidate => candidate.name === name)
|
|
506
|
+
const entry = entries.get(name)
|
|
507
|
+
const target = root?.realPath ?? (entry?.scope === 'profile' ? canonicalPath(entry.packageDir) : undefined)
|
|
508
|
+
if (target === undefined || sameResolution(previous, target)) continue
|
|
509
|
+
moves.push([previous, target])
|
|
510
|
+
targets.set(name, target)
|
|
511
|
+
}
|
|
512
|
+
if (rejected.length > 0) throw new ResolutionRestartRequired(rejected.join('; '))
|
|
513
|
+
const next = compileResolution(successor)
|
|
514
|
+
for (const { name, realPath } of successor.linkedRoots) targets.set(name, realPath)
|
|
515
|
+
this.linkedTargets = targets
|
|
516
|
+
for (const [from, to] of moves) this.moved.record(from, to)
|
|
517
|
+
const current: string[] = []
|
|
518
|
+
for (const entry of next.entries.values()) current.push(entry.realDir)
|
|
519
|
+
for (const root of successor.linkedRoots) current.push(root.realPath)
|
|
520
|
+
this.moved.forget(current)
|
|
521
|
+
this.current = next
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* The file to load instead of one Node selected through a link that has moved since; see {@link MovedDirectories}.
|
|
526
|
+
* @param filename Selected file.
|
|
527
|
+
* @param parent Importer path.
|
|
528
|
+
* @returns The file to load.
|
|
529
|
+
*/
|
|
530
|
+
relocate(filename: string, parent: string | undefined): string {
|
|
531
|
+
return this.moved.relocate(filename, parent)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** Whether a module path has an interception layer: it lies in the profiles tree, the active profile, or a linked root. */
|
|
535
|
+
hasInterceptionLayerForPath(path: string): boolean {
|
|
536
|
+
return findInterceptionLayer(path, this.current) !== undefined
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Whether an importer URL is scoped; the answer is memoized with the importer's routes. */
|
|
540
|
+
hasInterceptionLayerForUrl(parentURL: string): boolean {
|
|
541
|
+
return this.getOrCreateParentRoutesForUrl(parentURL) !== false
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private createParentRoutes(parent: string, resolution: CompiledResolution): ParentRoutes | undefined {
|
|
545
|
+
const layer = findInterceptionLayer(parent, resolution)
|
|
546
|
+
return layer === undefined ? undefined : { parent, layer, requests: new Map() }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private getOrCreateParentRoutesForUrl(parentURL: string): ParentRoutes | false {
|
|
550
|
+
const resolution = this.current
|
|
551
|
+
let parentRoutes = resolution.esmRoutes.get(parentURL)
|
|
552
|
+
if (parentRoutes !== undefined) return parentRoutes
|
|
553
|
+
let parent: string | undefined
|
|
554
|
+
try {
|
|
555
|
+
parent = fileURLToPath(parentURL)
|
|
556
|
+
} catch {
|
|
557
|
+
// Only file URLs can lie inside a profile tree or linked root.
|
|
558
|
+
parent = undefined
|
|
559
|
+
}
|
|
560
|
+
parentRoutes = parent === undefined ? false : this.createParentRoutes(parent, resolution) ?? false
|
|
561
|
+
resolution.esmRoutes.set(parentURL, parentRoutes)
|
|
562
|
+
return parentRoutes
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private routeScoped(
|
|
566
|
+
request: string,
|
|
567
|
+
parentRoutes: ParentRoutes,
|
|
568
|
+
resolution: CompiledResolution,
|
|
569
|
+
flavor: 'esm' | 'cjs',
|
|
570
|
+
cjs?: CommonJsLookup,
|
|
571
|
+
): ResolutionRouteState | undefined {
|
|
572
|
+
const { parent, layer, requests } = parentRoutes
|
|
573
|
+
const memo = layer.kind === 'profile' && (cjs === undefined || cjs.cacheable)
|
|
574
|
+
const name = barePackageName(request)
|
|
575
|
+
if (name === undefined) return undefined
|
|
576
|
+
|
|
577
|
+
if (parentRoutes.selfReferenceName === undefined || !memo) {
|
|
578
|
+
parentRoutes.selfReferenceName = selfReferenceName(parent)
|
|
579
|
+
}
|
|
580
|
+
if (parentRoutes.selfReferenceName === name || parentRoutes.selfReferenceName === null) {
|
|
581
|
+
const state = { route: { kind: 'native' as const } }
|
|
582
|
+
if (memo) requests.set(request, state)
|
|
583
|
+
return state
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (layer.kind === 'linked') return this.routeLinked(request, name, parent, resolution, flavor, cjs)
|
|
587
|
+
|
|
588
|
+
const target = resolution.entries.get(name)
|
|
589
|
+
const candidates: string[] = []
|
|
590
|
+
const localSearchPaths: string[] = []
|
|
591
|
+
for (const searchPath of createRequire(parent).resolve.paths(name) as string[]) {
|
|
592
|
+
if (!searchPath.startsWith(layer.localPrefix)) break
|
|
593
|
+
localSearchPaths.push(searchPath)
|
|
594
|
+
const candidate = localPackageCandidate(searchPath, name, flavor)
|
|
595
|
+
if (candidate !== undefined) candidates.push(candidate)
|
|
596
|
+
}
|
|
597
|
+
if (candidates.length > 0) {
|
|
598
|
+
if (cjs !== undefined) {
|
|
599
|
+
try {
|
|
600
|
+
const resolved = cjs.resolveNative(localSearchPaths)
|
|
601
|
+
const selected = candidates.find(candidate => (
|
|
602
|
+
localCandidateOwnsResolution(candidate, resolved, request, name)
|
|
603
|
+
))
|
|
604
|
+
if (selected !== undefined) {
|
|
605
|
+
const state = {
|
|
606
|
+
route: { kind: 'native' as const, packageDir: selected },
|
|
607
|
+
packageDir: selected,
|
|
608
|
+
cjs: resolved,
|
|
609
|
+
}
|
|
610
|
+
if (memo) requests.set(request, state)
|
|
611
|
+
return state
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (!isUnselectedPackageMiss(error)) throw error
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
const selected = candidates[0] as string
|
|
618
|
+
const state = {
|
|
619
|
+
route: { kind: 'native' as const, packageDir: selected },
|
|
620
|
+
packageDir: selected,
|
|
621
|
+
}
|
|
622
|
+
requests.set(request, state)
|
|
623
|
+
return state
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const route: ResolutionRoute = target !== undefined
|
|
628
|
+
&& (target.scope === 'installation' || layer.active)
|
|
629
|
+
? { kind: 'interception', entry: target, after: layer.after }
|
|
630
|
+
: { kind: 'native-after-interception', parent: layer.nativeAfter }
|
|
631
|
+
const state: ResolutionRouteState = { route }
|
|
632
|
+
if (route.kind === 'interception' && memo) requests.set(request, state)
|
|
633
|
+
return state
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private routeLinked(
|
|
637
|
+
request: string, name: string, parent: string, resolution: CompiledResolution,
|
|
638
|
+
flavor: 'esm' | 'cjs', cjs: CommonJsLookup | undefined,
|
|
639
|
+
): ResolutionRouteState {
|
|
640
|
+
const target = resolution.entries.get(name)
|
|
641
|
+
if (target === undefined) return { route: { kind: 'native' } }
|
|
642
|
+
const ancestors = this.nodeModulePaths(dirname(parent))
|
|
643
|
+
const ancestorSet = new Set(ancestors)
|
|
644
|
+
const searchPaths = flavor === 'esm' ? ancestors : createRequire(parent).resolve.paths(name) as string[]
|
|
645
|
+
for (const searchPath of searchPaths) {
|
|
646
|
+
const directory = dirname(searchPath)
|
|
647
|
+
if (ancestorSet.has(searchPath) && readPeerNames(directory).has(name)) {
|
|
648
|
+
const route: ResolutionRoute = { kind: 'interception', entry: target, after: join(dirname(directory), 'package.json') }
|
|
649
|
+
if (cjs === undefined) return { route }
|
|
650
|
+
try {
|
|
651
|
+
const resolved = cjs.resolveEntry(target, route.after)
|
|
652
|
+
if (localCandidateOwnsResolution(target.packageDir, resolved, request, name)) {
|
|
653
|
+
return { route, packageDir: target.packageDir, cjs: resolved }
|
|
654
|
+
}
|
|
655
|
+
} catch (error) {
|
|
656
|
+
if (!isUnselectedPackageMiss(error)) throw error
|
|
657
|
+
}
|
|
658
|
+
// This position is occupied even when a legacy CommonJS subpath is absent from the supplied package.
|
|
659
|
+
continue
|
|
660
|
+
}
|
|
661
|
+
const candidate = localPackageCandidate(searchPath, name, flavor)
|
|
662
|
+
if (candidate === undefined) continue
|
|
663
|
+
if (cjs === undefined) return { route: { kind: 'native', packageDir: candidate }, packageDir: candidate }
|
|
664
|
+
try {
|
|
665
|
+
const resolved = cjs.resolveNative([searchPath])
|
|
666
|
+
if (localCandidateOwnsResolution(candidate, resolved, request, name)) {
|
|
667
|
+
return { route: { kind: 'native', packageDir: candidate }, packageDir: candidate, cjs: resolved }
|
|
668
|
+
}
|
|
669
|
+
} catch (error) {
|
|
670
|
+
if (!isUnselectedPackageMiss(error)) throw error
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// An exhausted CommonJS lookup must not retry the physical copies at occupied peer positions.
|
|
674
|
+
return cjs === undefined ? { route: { kind: 'native' } } : { route: { kind: 'native' }, cjs: cjs.resolveNative([]) }
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private routeLocalPackage(
|
|
678
|
+
request: string, parentRoutes: ParentRoutes, resolution: CompiledResolution,
|
|
679
|
+
): ResolutionRouteState | undefined {
|
|
680
|
+
const name = barePackageName(request)
|
|
681
|
+
const { layer } = parentRoutes
|
|
682
|
+
if (name === undefined || layer.kind !== 'profile' || !layer.active || !resolution.localPackageNames.has(name)) return undefined
|
|
683
|
+
const state = { route: { kind: 'native' as const } }
|
|
684
|
+
parentRoutes.requests.set(request, state)
|
|
685
|
+
return state
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
routeUrl(request: string, parentURL: string): ResolutionRouteState | undefined {
|
|
689
|
+
const resolution = this.current
|
|
690
|
+
const parentRoutes = this.getOrCreateParentRoutesForUrl(parentURL)
|
|
691
|
+
if (parentRoutes === false) return undefined
|
|
692
|
+
const cached = parentRoutes.requests.get(request)
|
|
693
|
+
if (cached !== undefined) return cached
|
|
694
|
+
const local = this.routeLocalPackage(request, parentRoutes, resolution)
|
|
695
|
+
if (local !== undefined) return local
|
|
696
|
+
return this.routeScoped(request, parentRoutes, resolution, 'esm')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
routePath(
|
|
700
|
+
request: string, parent: string, cjs: CommonJsLookup,
|
|
701
|
+
): ResolutionRouteState | undefined {
|
|
702
|
+
const resolution = this.current
|
|
703
|
+
let parentRoutes = resolution.cjsRoutes.get(parent)
|
|
704
|
+
if (parentRoutes === false) return undefined
|
|
705
|
+
const cached = cjs.cacheable ? parentRoutes?.requests.get(request) : undefined
|
|
706
|
+
if (cached !== undefined) return cached
|
|
707
|
+
if (parentRoutes === undefined) {
|
|
708
|
+
parentRoutes = this.createParentRoutes(parent, resolution) ?? false
|
|
709
|
+
resolution.cjsRoutes.set(parent, parentRoutes)
|
|
710
|
+
if (parentRoutes === false) return undefined
|
|
711
|
+
}
|
|
712
|
+
return this.routeScoped(request, parentRoutes, resolution, 'cjs', cjs)
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
packageDir(specifier: string, parentURL: string): string | undefined {
|
|
716
|
+
const name = barePackageName(specifier)
|
|
717
|
+
if (name === undefined) return undefined
|
|
718
|
+
const state = this.routeUrl(name, parentURL)
|
|
719
|
+
if (state?.route.kind === 'interception') return state.route.entry.packageDir
|
|
720
|
+
if (state?.packageDir !== undefined) return state.packageDir
|
|
721
|
+
let parent: string
|
|
722
|
+
try {
|
|
723
|
+
parent = state?.route.kind === 'native-after-interception' ? state.route.parent : fileURLToPath(parentURL)
|
|
724
|
+
} catch {
|
|
725
|
+
return undefined
|
|
726
|
+
}
|
|
727
|
+
const found = nativePackageDir(parent, name)
|
|
728
|
+
if (state !== undefined && found !== undefined) state.packageDir = found
|
|
729
|
+
return found
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function internalModules(): InternalModules {
|
|
734
|
+
const require = createRequire(import.meta.url)
|
|
735
|
+
const addon = require('node-addon-require-builtin') as { requireBuiltin(moduleId: string): unknown }
|
|
736
|
+
const esmModule = addon.requireBuiltin('internal/modules/esm/loader') as {
|
|
737
|
+
getOrInitializeCascadedLoader(): ModuleLoaderV1 | ModuleLoaderV2
|
|
738
|
+
}
|
|
739
|
+
const cjsModule = addon.requireBuiltin('internal/modules/cjs/loader') as { Module: CommonJsModule }
|
|
740
|
+
const cjsHelpers = addon.requireBuiltin('internal/modules/helpers') as {
|
|
741
|
+
getCjsConditions(): ReadonlySet<string>
|
|
742
|
+
}
|
|
743
|
+
const esmUtils = addon.requireBuiltin('internal/modules/esm/utils') as {
|
|
744
|
+
getDefaultConditions(): readonly string[]
|
|
745
|
+
}
|
|
746
|
+
const esmResolve = addon.requireBuiltin('internal/modules/esm/resolve') as {
|
|
747
|
+
defaultResolve(
|
|
748
|
+
specifier: string,
|
|
749
|
+
context: { parentURL?: string; conditions?: readonly string[] },
|
|
750
|
+
): ResolveResult
|
|
751
|
+
}
|
|
752
|
+
const esm = esmModule.getOrInitializeCascadedLoader()
|
|
753
|
+
const modern = 'getOrCreateModuleJob' in esm
|
|
754
|
+
/* v8 ignore start -- the supported Node 24/26 matrix validates each available Internal interface */
|
|
755
|
+
if (typeof esm.resolveSync !== 'function'
|
|
756
|
+
|| typeof Reflect.get(esm, modern ? 'getOrCreateModuleJob' : 'getModuleJobForImport') !== 'function'
|
|
757
|
+
|| (!modern && typeof Reflect.get(esm, 'resolve') !== 'function')
|
|
758
|
+
|| typeof cjsModule.Module._resolveFilename !== 'function'
|
|
759
|
+
|| typeof cjsHelpers.getCjsConditions !== 'function'
|
|
760
|
+
|| typeof esmUtils.getDefaultConditions !== 'function'
|
|
761
|
+
|| typeof esmResolve.defaultResolve !== 'function') {
|
|
762
|
+
throw new Error('profile resolution: unsupported Node module loader')
|
|
763
|
+
}
|
|
764
|
+
/* v8 ignore stop */
|
|
765
|
+
return {
|
|
766
|
+
esm,
|
|
767
|
+
esmDefaultResolve: (specifier, context) => esmResolve.defaultResolve(specifier, context),
|
|
768
|
+
esmConditions: esmUtils.getDefaultConditions(),
|
|
769
|
+
cjs: cjsModule.Module,
|
|
770
|
+
cjsConditions: cjsHelpers.getCjsConditions(),
|
|
771
|
+
modern,
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Replace a resolver error's message and the same text in its stack.
|
|
777
|
+
* Node's module-hooks thread serializes errors and returns the stack accessor as a read-only configurable
|
|
778
|
+
* data property, so a rejected stack assignment redefines the property value.
|
|
779
|
+
* @param error - resolver error to update in place.
|
|
780
|
+
* @param message - replacement message.
|
|
781
|
+
*/
|
|
782
|
+
function replaceErrorMessage(error: Error, message: string): void {
|
|
783
|
+
const { message: originalMessage, stack } = error
|
|
784
|
+
error.message = message
|
|
785
|
+
/* v8 ignore next -- Node's resolver errors always carry a stack */
|
|
786
|
+
if (stack === undefined) return
|
|
787
|
+
const replaced = stack.replace(originalMessage, message)
|
|
788
|
+
if (!Reflect.set(error, 'stack', replaced)) Object.defineProperty(error, 'stack', { value: replaced })
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function throwWithImporter(error: unknown, routedParent: string, parent: string): never {
|
|
792
|
+
const code = (error as NodeJS.ErrnoException).code
|
|
793
|
+
if (error instanceof Error && (code === 'ERR_MODULE_NOT_FOUND' || code === 'ERR_PACKAGE_PATH_NOT_EXPORTED')) {
|
|
794
|
+
const routedPath = fileURLToPath(routedParent)
|
|
795
|
+
const parentPath = fileURLToPath(parent)
|
|
796
|
+
replaceErrorMessage(error, error.message.replaceAll(routedParent, parent).replaceAll(routedPath, parentPath))
|
|
797
|
+
}
|
|
798
|
+
throw error
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function throwWithoutCjsAnchor(error: unknown, anchor: string): never {
|
|
802
|
+
const resolved = error as NodeJS.ErrnoException & { requireStack?: string[] }
|
|
803
|
+
const requireStack = resolved.requireStack
|
|
804
|
+
if (error instanceof Error
|
|
805
|
+
&& resolved.code === 'MODULE_NOT_FOUND'
|
|
806
|
+
&& requireStack?.[0] !== undefined
|
|
807
|
+
&& sameResolution(requireStack[0], anchor)) {
|
|
808
|
+
const originalMessage = error.message
|
|
809
|
+
const originalBlock = `\nRequire stack:\n${requireStack.map(path => `- ${path}`).join('\n')}`
|
|
810
|
+
const remaining = requireStack.slice(1)
|
|
811
|
+
/* v8 ignore next -- routed calls always retain the original importing module */
|
|
812
|
+
const replacement = remaining.length === 0
|
|
813
|
+
? ''
|
|
814
|
+
: `\nRequire stack:\n${remaining.map(path => `- ${path}`).join('\n')}`
|
|
815
|
+
error.message = originalMessage.replace(originalBlock, replacement)
|
|
816
|
+
resolved.requireStack = remaining
|
|
817
|
+
const stack = error.stack
|
|
818
|
+
/* v8 ignore next -- Node's resolver errors always carry a stack */
|
|
819
|
+
if (stack !== undefined) error.stack = stack.replace(originalMessage, error.message)
|
|
820
|
+
}
|
|
821
|
+
throw error
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Install one runtime resolution as the interception on Node's default ESM and CommonJS resolvers.
|
|
826
|
+
* @param resolution - complete package table and profile scope.
|
|
827
|
+
* @returns an interception that publishes a successor or restores the native methods.
|
|
828
|
+
*/
|
|
829
|
+
export function installRuntimeInterception(
|
|
830
|
+
resolution: RuntimeResolution,
|
|
831
|
+
): RuntimeInterception {
|
|
832
|
+
const { esm, esmDefaultResolve, esmConditions, cjs, cjsConditions, modern } = internalModules()
|
|
833
|
+
const router = new ResolutionRouter(resolution, directory => cjs._nodeModulePaths(directory))
|
|
834
|
+
let delegatedEsm: { parent: string | undefined; request: string } | undefined
|
|
835
|
+
|
|
836
|
+
const adaptEsm = (native: EsmResolve): EsmResolve => {
|
|
837
|
+
const adapted: EsmResolve = (request, parent, attributes) => {
|
|
838
|
+
const delegated = delegatedEsm
|
|
839
|
+
/* v8 ignore next -- reentry requires a separate synchronous Node hook; supported launches install none */
|
|
840
|
+
if (delegated !== undefined && delegated.parent === parent && delegated.request === request) {
|
|
841
|
+
return native(request, parent, attributes)
|
|
842
|
+
}
|
|
843
|
+
if (parent === undefined || !router.hasInterceptionLayerForUrl(parent)) return native(request, parent, attributes)
|
|
844
|
+
const state = router.routeUrl(request, parent)
|
|
845
|
+
if (state === undefined) {
|
|
846
|
+
const target = request[0] === '#'
|
|
847
|
+
? packageImportsTarget(fileURLToPath(parent), request, esmConditions)
|
|
848
|
+
: undefined
|
|
849
|
+
if (target === undefined) return native(request, parent, attributes)
|
|
850
|
+
const restoreImporter = (error: unknown): never => throwWithImporter(error, target.parentURL, parent)
|
|
851
|
+
let expected: ResolveResult | Promise<ResolveResult>
|
|
852
|
+
try {
|
|
853
|
+
expected = adapted(target.specifier, target.parentURL, attributes)
|
|
854
|
+
/* v8 ignore next -- the current loader resolves synchronously; the private interface also permits Promise results */
|
|
855
|
+
if (expected instanceof Promise) expected = expected.catch(restoreImporter)
|
|
856
|
+
} catch (error) {
|
|
857
|
+
return restoreImporter(error)
|
|
858
|
+
}
|
|
859
|
+
return expected
|
|
860
|
+
}
|
|
861
|
+
const cacheable = attributes === EMPTY_ATTRIBUTES || Object.keys(attributes).length === 0
|
|
862
|
+
if (cacheable && state.esm !== undefined) return state.esm
|
|
863
|
+
const route = state.route
|
|
864
|
+
if (route.kind === 'native') {
|
|
865
|
+
const result = native(request, parent, attributes)
|
|
866
|
+
if (cacheable && !(result instanceof Promise)) state.esm = result
|
|
867
|
+
return result
|
|
868
|
+
}
|
|
869
|
+
const routedParent = pathToFileURL(route.kind === 'interception' ? route.entry.declarer : route.parent).href
|
|
870
|
+
const previous = delegatedEsm
|
|
871
|
+
delegatedEsm = { parent: routedParent, request }
|
|
872
|
+
const restoreImporter = (error: unknown): never => throwWithImporter(error, routedParent, parent)
|
|
873
|
+
try {
|
|
874
|
+
let result: ResolveResult | Promise<ResolveResult>
|
|
875
|
+
try {
|
|
876
|
+
result = native(request, routedParent, attributes)
|
|
877
|
+
} catch (error) {
|
|
878
|
+
return restoreImporter(error)
|
|
879
|
+
}
|
|
880
|
+
/* v8 ignore next -- the current loader resolves synchronously; the private interface also permits Promise results */
|
|
881
|
+
if (result instanceof Promise) return result.catch(restoreImporter)
|
|
882
|
+
if (cacheable) state.esm = result
|
|
883
|
+
return result
|
|
884
|
+
} finally {
|
|
885
|
+
delegatedEsm = previous
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Node's last real-path step, for routed and native requests alike, may follow a link it resolved earlier.
|
|
889
|
+
const relocate = (result: ResolveResult, parent: string | undefined): ResolveResult => {
|
|
890
|
+
if (!result.url.startsWith('file:')) return result
|
|
891
|
+
const url = new URL(result.url)
|
|
892
|
+
const filename = fileURLToPath(url)
|
|
893
|
+
const relocated = router.relocate(filename, parent?.startsWith('file:') === true ? fileURLToPath(parent) : undefined)
|
|
894
|
+
if (relocated === filename) return result
|
|
895
|
+
url.pathname = pathToFileURL(relocated).pathname
|
|
896
|
+
return { ...result, url: url.href }
|
|
897
|
+
}
|
|
898
|
+
return (request, parent, attributes) => {
|
|
899
|
+
const result = adapted(request, parent, attributes)
|
|
900
|
+
/* v8 ignore next -- the current loader resolves synchronously; the private interface also permits Promise results */
|
|
901
|
+
return result instanceof Promise ? result.then(resolved => relocate(resolved, parent)) : relocate(result, parent)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
let restoreEsm: () => void
|
|
906
|
+
/* v8 ignore else -- CI coverage runs Node 24 v2; the Node 24.0 matrix exercises the v1 adapter */
|
|
907
|
+
if (modern) {
|
|
908
|
+
const loader = esm as ModuleLoaderV2
|
|
909
|
+
const original = Reflect.get(loader, 'resolveSync')
|
|
910
|
+
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(
|
|
911
|
+
loader, parent as string, { specifier: request, attributes },
|
|
912
|
+
))
|
|
913
|
+
// Node 24.12+ adds skipSyncHooks after request. Its presence marks hook
|
|
914
|
+
// delegation whose outer call already selected a route.
|
|
915
|
+
const wrapped = (parent: string, request: { specifier: string; attributes?: ImportAttributes }, ...rest: unknown[]): ResolveResult => (
|
|
916
|
+
rest.length
|
|
917
|
+
? Reflect.apply(original, loader, [parent, request, ...rest]) as ResolveResult
|
|
918
|
+
: resolveRequest(request.specifier, parent, request.attributes ?? EMPTY_ATTRIBUTES) as ResolveResult
|
|
919
|
+
)
|
|
920
|
+
loader.resolveSync = wrapped
|
|
921
|
+
restoreEsm = () => {
|
|
922
|
+
/* v8 ignore else -- interceptions are disposed in reverse installation order */
|
|
923
|
+
if (loader.resolveSync === wrapped) loader.resolveSync = original
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
const loader = esm as ModuleLoaderV1
|
|
927
|
+
const original = Reflect.get(loader, 'resolve')
|
|
928
|
+
const originalSync = Reflect.get(loader, 'resolveSync')
|
|
929
|
+
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, request, parent as string, attributes))
|
|
930
|
+
const resolveRequestSync = adaptEsm((request, parent, attributes) => originalSync.call(loader, request, parent as string, attributes))
|
|
931
|
+
const wrapped = (request: string, parent: string, attributes: ImportAttributes = EMPTY_ATTRIBUTES): Promise<ResolveResult> => (
|
|
932
|
+
resolveRequest(request, parent, attributes) as Promise<ResolveResult>
|
|
933
|
+
)
|
|
934
|
+
const wrappedSync = (request: string, parent: string, attributes: ImportAttributes = EMPTY_ATTRIBUTES): ResolveResult => (
|
|
935
|
+
resolveRequestSync(request, parent, attributes) as ResolveResult
|
|
936
|
+
)
|
|
937
|
+
loader.resolve = wrapped
|
|
938
|
+
loader.resolveSync = wrappedSync
|
|
939
|
+
restoreEsm = () => {
|
|
940
|
+
if (loader.resolve === wrapped) loader.resolve = original
|
|
941
|
+
if (loader.resolveSync === wrappedSync) loader.resolveSync = originalSync
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const originalFilename = Reflect.get(cjs, '_resolveFilename')
|
|
946
|
+
let delegatedCjs = 0
|
|
947
|
+
const resolveRoutedCjs = (
|
|
948
|
+
request: string, routed: Exclude<ResolutionRoute, { kind: 'native' }>,
|
|
949
|
+
parent: CommonJsParent, main: boolean, options?: CommonJsOptions,
|
|
950
|
+
): string => {
|
|
951
|
+
const anchor = routed.kind === 'interception' ? routed.entry.declarer : routed.parent
|
|
952
|
+
const synthetic = new cjs(anchor)
|
|
953
|
+
// Late parent assignment preserves Node's require stack without publishing this routing anchor in parent.children.
|
|
954
|
+
synthetic.parent = parent
|
|
955
|
+
synthetic.filename = anchor
|
|
956
|
+
synthetic.paths = routed.kind === 'interception'
|
|
957
|
+
? packageSearchPaths(routed.entry, request, cjs)
|
|
958
|
+
: cjs._nodeModulePaths(dirname(anchor))
|
|
959
|
+
try {
|
|
960
|
+
return originalFilename.call(cjs, request, synthetic, main, options)
|
|
961
|
+
} catch (error) {
|
|
962
|
+
return throwWithoutCjsAnchor(error, anchor)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const resolveNativeCjs = (
|
|
966
|
+
request: string, searchPaths: readonly string[], parent: CommonJsParent,
|
|
967
|
+
parentFilename: string, main: boolean, conditions: ReadonlySet<string> | undefined,
|
|
968
|
+
): string => {
|
|
969
|
+
const synthetic = new cjs(parentFilename)
|
|
970
|
+
if (parent.parent !== undefined) synthetic.parent = parent.parent
|
|
971
|
+
synthetic.filename = parentFilename
|
|
972
|
+
synthetic.paths = [...searchPaths]
|
|
973
|
+
const options = conditions === undefined ? undefined : { conditions }
|
|
974
|
+
return originalFilename.call(cjs, request, synthetic, main, options)
|
|
975
|
+
}
|
|
976
|
+
const resolvePackageImportCjs = (
|
|
977
|
+
target: { specifier: string; parentURL: string }, conditions: Iterable<string>,
|
|
978
|
+
): string => {
|
|
979
|
+
const state = router.routeUrl(target.specifier, target.parentURL)
|
|
980
|
+
const resolveFrom = (parentURL: string): string => fileURLToPath(esmDefaultResolve(
|
|
981
|
+
target.specifier, { parentURL, conditions: [...conditions] },
|
|
982
|
+
).url)
|
|
983
|
+
/* v8 ignore next -- the target manifest was found inside the established profile scope */
|
|
984
|
+
if (state === undefined) return resolveFrom(target.parentURL)
|
|
985
|
+
if (state.route.kind === 'native') return resolveFrom(target.parentURL)
|
|
986
|
+
const route = state.route
|
|
987
|
+
if (route.kind === 'native-after-interception') return resolveFrom(pathToFileURL(route.parent).href)
|
|
988
|
+
return resolveFrom(pathToFileURL(route.entry.declarer).href)
|
|
989
|
+
}
|
|
990
|
+
const wrappedFilename: CommonJsModule['_resolveFilename'] = (request, parent, main, options) => {
|
|
991
|
+
if (delegatedCjs || !parent?.filename || options?.paths !== undefined) {
|
|
992
|
+
return originalFilename.call(cjs, request, parent, main, options)
|
|
993
|
+
}
|
|
994
|
+
const parentFilename = parent.filename
|
|
995
|
+
const cacheable = options?.conditions === undefined
|
|
996
|
+
const routedOptions = options?.conditions === undefined ? undefined : { conditions: options.conditions }
|
|
997
|
+
const state = router.routePath(request, parentFilename, {
|
|
998
|
+
cacheable,
|
|
999
|
+
resolveNative: searchPaths => resolveNativeCjs(request, searchPaths, parent, parentFilename, main, options?.conditions),
|
|
1000
|
+
resolveEntry: (entry, after) => {
|
|
1001
|
+
delegatedCjs++
|
|
1002
|
+
try {
|
|
1003
|
+
return resolveRoutedCjs(request, { kind: 'interception', entry, after }, parent, main, routedOptions)
|
|
1004
|
+
} finally {
|
|
1005
|
+
delegatedCjs--
|
|
1006
|
+
}
|
|
1007
|
+
},
|
|
1008
|
+
})
|
|
1009
|
+
if (state === undefined) {
|
|
1010
|
+
const scoped = router.hasInterceptionLayerForPath(parentFilename)
|
|
1011
|
+
const conditions = options?.conditions ?? cjsConditions
|
|
1012
|
+
const target = request[0] === '#' && scoped
|
|
1013
|
+
? packageImportsTarget(parentFilename, request, conditions)
|
|
1014
|
+
: undefined
|
|
1015
|
+
if (target === undefined) return originalFilename.call(cjs, request, parent, main, options)
|
|
1016
|
+
return resolvePackageImportCjs(target, conditions)
|
|
1017
|
+
}
|
|
1018
|
+
if (state.cjs !== undefined) return state.cjs
|
|
1019
|
+
const route = state.route
|
|
1020
|
+
if (route.kind === 'native') {
|
|
1021
|
+
const result = originalFilename.call(cjs, request, parent, main, options)
|
|
1022
|
+
if (cacheable) state.cjs = result
|
|
1023
|
+
return result
|
|
1024
|
+
}
|
|
1025
|
+
delegatedCjs++
|
|
1026
|
+
try {
|
|
1027
|
+
let expected: string
|
|
1028
|
+
try {
|
|
1029
|
+
expected = resolveRoutedCjs(request, route, parent, main, routedOptions)
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (route.kind !== 'interception' || !isUnselectedPackageMiss(error)) throw error
|
|
1032
|
+
expected = resolveRoutedCjs(
|
|
1033
|
+
request, { kind: 'native-after-interception', parent: route.after }, parent, main, routedOptions,
|
|
1034
|
+
)
|
|
1035
|
+
}
|
|
1036
|
+
if (cacheable) state.cjs = expected
|
|
1037
|
+
return expected
|
|
1038
|
+
} finally {
|
|
1039
|
+
delegatedCjs--
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const relocatedFilename: CommonJsModule['_resolveFilename'] = (request, parent, main, options) => {
|
|
1043
|
+
const filename = wrappedFilename(request, parent, main, options)
|
|
1044
|
+
return isAbsolute(filename) ? router.relocate(filename, parent?.filename ?? undefined) : filename
|
|
1045
|
+
}
|
|
1046
|
+
cjs._resolveFilename = relocatedFilename
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
packageDir(specifier, parentURL) {
|
|
1050
|
+
return router.packageDir(specifier, parentURL)
|
|
1051
|
+
},
|
|
1052
|
+
replace(next) { router.replace(next) },
|
|
1053
|
+
dispose() {
|
|
1054
|
+
/* v8 ignore else -- interceptions are disposed in reverse installation order */
|
|
1055
|
+
if (cjs._resolveFilename === relocatedFilename) cjs._resolveFilename = originalFilename
|
|
1056
|
+
restoreEsm()
|
|
1057
|
+
},
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Publish one runtime resolution for Harness-owned Workers.
|
|
1063
|
+
* @param resolution - complete package table and profile scope.
|
|
1064
|
+
* @returns a disposer restoring the previous thread environment data.
|
|
1065
|
+
*/
|
|
1066
|
+
export function registerWorkerResolution(
|
|
1067
|
+
resolution: RuntimeResolution,
|
|
1068
|
+
): () => void {
|
|
1069
|
+
const previous = getEnvironmentData(WORKER_RESOLUTION_KEY)
|
|
1070
|
+
setEnvironmentData(WORKER_RESOLUTION_KEY, { resolution })
|
|
1071
|
+
return () => { setEnvironmentData(WORKER_RESOLUTION_KEY, previous) }
|
|
1072
|
+
}
|