@deepseek-ai/dsh-app-boot 0.1.5-rc.2 → 0.1.6-alpha.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 +2 -2
- package/README.md +51 -16
- package/README.zh.md +54 -19
- package/lib/index.js +1453 -258
- package/lib/types/index.d.ts +81 -53
- package/lib/types/profile-context.d.ts +51 -0
- package/lib/types/profile-plugins.d.ts +60 -0
- package/lib/types/profile-resolution/legacy-links.d.ts +35 -0
- package/lib/types/profile-resolution/resolver.d.ts +43 -0
- package/lib/types/profile-resolution/service.d.ts +51 -0
- package/lib/types/profile-resolution/worker-bootstrap.d.ts +3 -0
- package/lib/types/profile-sanitize.d.ts +13 -0
- package/lib/types/profile.d.ts +63 -18
- package/lib/worker/profile-resolution-bootstrap.js +820 -0
- package/package.json +16 -18
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { createRequire } from "node:module";
|
|
1
|
+
import { createRequire, isBuiltin } from "node:module";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { parseEnv } from "node:util";
|
|
5
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import * as yaml from "js-yaml";
|
|
7
7
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
8
8
|
import Loader, { EntryGroup, EntryTree, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
|
|
@@ -12,7 +12,8 @@ import Group from "@deepseek-ai/cordis-plugin-group";
|
|
|
12
12
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
13
13
|
import { createLaunchEnvironmentSnapshot } from "@deepseek-ai/dsh-launch-environment";
|
|
14
14
|
import { withFileLock } from "@deepseek-ai/dsh-atomic-write";
|
|
15
|
-
import { resolve as resolve$1 } from "resolve.exports";
|
|
15
|
+
import { imports, resolve as resolve$1 } from "resolve.exports";
|
|
16
|
+
import { getEnvironmentData, setEnvironmentData } from "node:worker_threads";
|
|
16
17
|
//#region ../../../vendor/include/src/index.ts
|
|
17
18
|
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
|
|
18
19
|
kind: "scalar",
|
|
@@ -45,8 +46,7 @@ function retryableWriteError(error) {
|
|
|
45
46
|
* Apply patch lists to an entry list — THE patch semantics of this include,
|
|
46
47
|
* shared by mounting (`applyPatches`) and offline config tooling
|
|
47
48
|
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
|
|
48
|
-
* is never mutated
|
|
49
|
-
* patches): patching or mounting shared entry objects would bake earlier
|
|
49
|
+
* is never mutated: patching shared entry objects would bake earlier patch
|
|
50
50
|
* values into the cached parse, so repeated application (config hot-reloads)
|
|
51
51
|
* could never revert a removed or changed patch. Inserted entries are indexed
|
|
52
52
|
* as they are added, so a later patch in the same list can target a row an
|
|
@@ -57,8 +57,8 @@ function retryableWriteError(error) {
|
|
|
57
57
|
* @returns a detached entry list with every applicable patch applied.
|
|
58
58
|
*/
|
|
59
59
|
function applyEntryPatches(data, patches, warn) {
|
|
60
|
+
if (!patches?.length) return [...data];
|
|
60
61
|
data = structuredClone(data);
|
|
61
|
-
if (!patches?.length) return data;
|
|
62
62
|
const entryMap = /* @__PURE__ */ new Map();
|
|
63
63
|
const buildMap = (entries) => {
|
|
64
64
|
for (const entry of entries) {
|
|
@@ -106,14 +106,6 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
106
106
|
}
|
|
107
107
|
return data;
|
|
108
108
|
}
|
|
109
|
-
var ConfigFileError = class extends Error {
|
|
110
|
-
stage;
|
|
111
|
-
constructor(stage, path, cause) {
|
|
112
|
-
super(`failed to ${stage} config file ${path}`, { cause });
|
|
113
|
-
this.stage = stage;
|
|
114
|
-
this.name = "ConfigFileError";
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
109
|
/** Loader entry tree backed by a YAML or JSON file. */
|
|
118
110
|
var Include = class extends EntryTree {
|
|
119
111
|
config;
|
|
@@ -127,7 +119,6 @@ var Include = class extends EntryTree {
|
|
|
127
119
|
writeTask;
|
|
128
120
|
pendingWrite;
|
|
129
121
|
writeQueue = Promise.resolve();
|
|
130
|
-
applyQueue = Promise.resolve();
|
|
131
122
|
constructor(ctx, config) {
|
|
132
123
|
super(ctx);
|
|
133
124
|
this.config = config;
|
|
@@ -138,29 +129,15 @@ var Include = class extends EntryTree {
|
|
|
138
129
|
this.type = writable[ext];
|
|
139
130
|
this.readonly = !this.type;
|
|
140
131
|
this.ctx.baseUrl = new URL(".", pathToFileURL(this.filename)).href;
|
|
141
|
-
ctx.on("internal/update",
|
|
132
|
+
ctx.on("internal/update", (config, _, next) => {
|
|
142
133
|
if (config.path !== this.config.path) return next();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.
|
|
134
|
+
this.config = config;
|
|
135
|
+
this.root.update(this.applyPatches(this.data, config.patches)).catch((error) => {
|
|
136
|
+
this.ctx.logger.warn("config update at %C failed", this.filename);
|
|
137
|
+
this.ctx.logger.warn(error);
|
|
147
138
|
});
|
|
148
139
|
});
|
|
149
140
|
}
|
|
150
|
-
/**
|
|
151
|
-
* Serialize one child-tree mutation behind every earlier one. The group's
|
|
152
|
-
* transactional `update` is not reentrant: two concurrent applies (the init
|
|
153
|
-
* apply racing an HMR-triggered refresh from the watcher's initial scan)
|
|
154
|
-
* interleave create and rollback on the same entries and strand the include
|
|
155
|
-
* fiber without settling, so every apply path funnels through this queue.
|
|
156
|
-
* A predecessor's failure is its own caller's outcome and never gates the
|
|
157
|
-
* next task.
|
|
158
|
-
*/
|
|
159
|
-
enqueue(task) {
|
|
160
|
-
const run = this.applyQueue.then(task, task);
|
|
161
|
-
this.applyQueue = run.then(() => {}, () => {});
|
|
162
|
-
return run;
|
|
163
|
-
}
|
|
164
141
|
async checkAccess() {
|
|
165
142
|
if (!this.type) return;
|
|
166
143
|
try {
|
|
@@ -170,77 +147,63 @@ var Include = class extends EntryTree {
|
|
|
170
147
|
}
|
|
171
148
|
}
|
|
172
149
|
async read(forced = false) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
content = await readFile(this.filename, "utf8");
|
|
176
|
-
} catch (error) {
|
|
177
|
-
throw new ConfigFileError("read", this.filename, error);
|
|
178
|
-
}
|
|
179
|
-
if (!forced && this.content === content) return;
|
|
150
|
+
const content = await readFile(this.filename, "utf8");
|
|
151
|
+
if (!forced && this.content === content) return false;
|
|
180
152
|
let data;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.filename
|
|
153
|
+
if (this.type === "application/yaml") data = yaml.load(content, { schema: entryListSchema });
|
|
154
|
+
else if (this.type === "application/json") data = JSON.parse(content);
|
|
155
|
+
else {
|
|
156
|
+
const module = await import(
|
|
157
|
+
/* @vite-ignore */
|
|
158
|
+
this.filename
|
|
188
159
|
);
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
throw new ConfigFileError("parse", this.filename, error);
|
|
160
|
+
data = module.default || module;
|
|
193
161
|
}
|
|
194
|
-
if (!Array.isArray(data)) throw new
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
162
|
+
if (!Array.isArray(data)) throw new TypeError(`config file must be a top-level array of entries: ${this.filename}`);
|
|
163
|
+
this.content = content;
|
|
164
|
+
this.data = data;
|
|
165
|
+
await this.checkAccess();
|
|
166
|
+
return true;
|
|
199
167
|
}
|
|
200
|
-
applyPatches(data, patches) {
|
|
168
|
+
applyPatches(data, patches = this.config.patches) {
|
|
201
169
|
return applyEntryPatches(data, patches, (message, ...args) => {
|
|
202
170
|
this.ctx.root.logger?.("loader").warn(message, ...args);
|
|
203
171
|
});
|
|
204
172
|
}
|
|
205
173
|
async *[Service.init]() {
|
|
206
|
-
let candidate;
|
|
207
174
|
try {
|
|
208
|
-
|
|
175
|
+
await this.read();
|
|
209
176
|
} catch (error) {
|
|
210
|
-
if (
|
|
177
|
+
if (error?.code !== "ENOENT") throw error;
|
|
211
178
|
if (this.config.initial) {
|
|
212
179
|
await this._writeFile(this.config.initial);
|
|
213
|
-
|
|
180
|
+
await this.read(true);
|
|
214
181
|
} else throw new Error(`config file not found: ${this.filename}`);
|
|
215
182
|
}
|
|
216
183
|
yield () => this.stop();
|
|
217
|
-
await this.
|
|
184
|
+
await this.root.update(this.applyPatches(this.data));
|
|
218
185
|
}
|
|
219
186
|
async stop() {
|
|
220
|
-
|
|
221
|
-
|
|
187
|
+
try {
|
|
188
|
+
await this.flushWrite();
|
|
189
|
+
} finally {
|
|
190
|
+
this.root.stop();
|
|
191
|
+
await this.flushWrite();
|
|
192
|
+
}
|
|
222
193
|
}
|
|
223
194
|
/**
|
|
224
|
-
* Re-read the file and
|
|
225
|
-
*
|
|
226
|
-
*
|
|
195
|
+
* Re-read the file and refresh child entries when content changed. An
|
|
196
|
+
* unreadable or unparsable file logs a warning and keeps the last good
|
|
197
|
+
* tree: a hot-reload of a live app must never take the process down.
|
|
227
198
|
*/
|
|
228
199
|
async refresh() {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return this.enqueue(() => this._apply(candidate));
|
|
237
|
-
}
|
|
238
|
-
async _apply(candidate) {
|
|
239
|
-
const data = this.applyPatches(candidate.data, this.config.patches);
|
|
240
|
-
await this.root.update(data);
|
|
241
|
-
this.content = candidate.content;
|
|
242
|
-
this.data = candidate.data;
|
|
243
|
-
await this.checkAccess();
|
|
200
|
+
try {
|
|
201
|
+
if (!await this.read()) return;
|
|
202
|
+
await this.root.update(this.applyPatches(this.data));
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this.ctx.logger.warn("config reload at %C failed; keeping the running tree", this.filename);
|
|
205
|
+
this.ctx.logger.warn(error);
|
|
206
|
+
}
|
|
244
207
|
}
|
|
245
208
|
async _writeFile(config) {
|
|
246
209
|
if (this.readonly) throw new Error(`cannot overwrite readonly config`);
|
|
@@ -283,6 +246,70 @@ var Include = class extends EntryTree {
|
|
|
283
246
|
}
|
|
284
247
|
};
|
|
285
248
|
//#endregion
|
|
249
|
+
//#region lib/types/profile-resolution/legacy-links.js
|
|
250
|
+
/** Legacy profile-link inspection shared by the disk materializer and runtime resolver. */
|
|
251
|
+
/** Profile-private package links projected into its pnpm-managed node_modules. */
|
|
252
|
+
const PROFILE_MODULE_FALLBACK_DIR = ".dsh-module-fallback";
|
|
253
|
+
/**
|
|
254
|
+
* Return whether the process reads application modules from pkg's virtual filesystem.
|
|
255
|
+
* @returns whether pkg owns the module filesystem.
|
|
256
|
+
*/
|
|
257
|
+
function isPackagedExecutable() {
|
|
258
|
+
return process.pkg !== void 0;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Resolve a directory through the active carrier's filesystem implementation.
|
|
262
|
+
* @param path - directory path to canonicalize.
|
|
263
|
+
* @returns the canonical directory path.
|
|
264
|
+
*/
|
|
265
|
+
function realModuleDirectory(path) {
|
|
266
|
+
return isPackagedExecutable() ? realpathSync(path) : realpathSync.native(path);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Resolve a link target without following the final path component.
|
|
270
|
+
* @param path - candidate path whose parent is canonicalized.
|
|
271
|
+
* @returns the canonical candidate, or undefined when its parent is absent.
|
|
272
|
+
*/
|
|
273
|
+
function canonicalLinkPath(path) {
|
|
274
|
+
try {
|
|
275
|
+
return join(realModuleDirectory(dirname(path)), basename(path));
|
|
276
|
+
} catch (error) {
|
|
277
|
+
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
|
278
|
+
if (error.code === "ENOENT") return void 0;
|
|
279
|
+
/* v8 ignore next -- see the host-filesystem exception above */
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Return whether a symlink or junction points at the same path as `target`.
|
|
285
|
+
* @param link - symlink or junction to inspect.
|
|
286
|
+
* @param target - expected target path.
|
|
287
|
+
* @returns whether both paths identify the same entry.
|
|
288
|
+
*/
|
|
289
|
+
function symlinkPointsTo(link, target) {
|
|
290
|
+
const canonicalActual = canonicalLinkPath(resolve(dirname(link), readlinkSync(link)));
|
|
291
|
+
const canonicalTarget = canonicalLinkPath(resolve(target));
|
|
292
|
+
return canonicalActual !== void 0 && canonicalActual === canonicalTarget;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Return whether an observed profile package must not claim local precedence.
|
|
296
|
+
* @param profileDir - profile directory containing the package projection.
|
|
297
|
+
* @param packageName - bare package name to inspect.
|
|
298
|
+
* @returns whether the entry is a managed fallback link or disappeared during inspection.
|
|
299
|
+
*/
|
|
300
|
+
function isProfileModuleFallbackLink(profileDir, packageName) {
|
|
301
|
+
const link = join(profileDir, "node_modules", packageName);
|
|
302
|
+
const target = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, "node_modules", packageName);
|
|
303
|
+
try {
|
|
304
|
+
return lstatSync(link).isSymbolicLink() && symlinkPointsTo(link, target);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
/* v8 ignore next 2 -- a vanished candidate cannot claim local precedence */
|
|
307
|
+
if (error.code === "ENOENT") return true;
|
|
308
|
+
/* v8 ignore next -- non-ENOENT lstat/readlink failures require a host filesystem fault */
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
//#endregion
|
|
286
313
|
//#region lib/types/profile.js
|
|
287
314
|
/**
|
|
288
315
|
* Profile discovery, initialization, and patch-layer composition for the
|
|
@@ -312,8 +339,6 @@ var Include = class extends EntryTree {
|
|
|
312
339
|
const PROFILES_DIR = "profiles";
|
|
313
340
|
/** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */
|
|
314
341
|
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
315
|
-
/** Profile-private package links projected into its pnpm-managed node_modules. */
|
|
316
|
-
const PROFILE_MODULE_FALLBACK_DIR = ".dsh-module-fallback";
|
|
317
342
|
/**
|
|
318
343
|
* Resolve a profile's directory under the Harness home.
|
|
319
344
|
* @param name - the profile name (`dsh --profile <name>`).
|
|
@@ -326,26 +351,11 @@ function resolveProfileDir(name, home = resolveDshHome()) {
|
|
|
326
351
|
}
|
|
327
352
|
/** The shipped profile templates auto-initialized on first use, by name. */
|
|
328
353
|
const PROFILE_TEMPLATES = {
|
|
329
|
-
acp: {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"],
|
|
335
|
-
patchReload: "live"
|
|
336
|
-
},
|
|
337
|
-
headless: {
|
|
338
|
-
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"],
|
|
339
|
-
patchReload: "startup"
|
|
340
|
-
},
|
|
341
|
-
sdk: {
|
|
342
|
-
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-sdk-app"],
|
|
343
|
-
patchReload: "startup"
|
|
344
|
-
},
|
|
345
|
-
"sdk-minimal": {
|
|
346
|
-
bundles: ["@deepseek-ai/dsh-sdk-minimal"],
|
|
347
|
-
patchReload: "startup"
|
|
348
|
-
}
|
|
354
|
+
acp: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-acp-app"] },
|
|
355
|
+
web: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] },
|
|
356
|
+
headless: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"] },
|
|
357
|
+
sdk: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-sdk-app"] },
|
|
358
|
+
"sdk-minimal": { bundles: ["@deepseek-ai/dsh-sdk-minimal"] }
|
|
349
359
|
};
|
|
350
360
|
/** Installation-owned bundle tuples normalized to the shipped template. */
|
|
351
361
|
const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
@@ -355,8 +365,13 @@ const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
|
355
365
|
] };
|
|
356
366
|
/** The bundle list a `dsh plugin` init uses for a name with no shipped template. */
|
|
357
367
|
const DEFAULT_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base"];
|
|
358
|
-
/**
|
|
359
|
-
|
|
368
|
+
/**
|
|
369
|
+
* The bundles the dsh installation ships for a person to switch on: each a
|
|
370
|
+
* runtime dependency of the installation that declares `dsh.bundle.patch`,
|
|
371
|
+
* selected by no shipped template, and offered switched off by the plugin
|
|
372
|
+
* manager ([rationale](../../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)).
|
|
373
|
+
*/
|
|
374
|
+
const OPTIONAL_BUNDLES = ["@deepseek-ai/dsh-experimental-agent-team-profile", "@deepseek-ai/dsh-experimental-agent-team-web-profile"];
|
|
360
375
|
const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
|
|
361
376
|
# a top-level YAML array of loader patch entries (id-targeted config
|
|
362
377
|
# overrides, disables, and insert lists; \`!!js\` expressions allowed).
|
|
@@ -374,9 +389,8 @@ autoInstallPeers: false
|
|
|
374
389
|
* so re-running is a no-op on an initialized profile.
|
|
375
390
|
* @param dir - the profile directory from {@link resolveProfileDir}.
|
|
376
391
|
* @param bundles - the initial `dsh.profile.bundles` layer list.
|
|
377
|
-
* @param patchReload - user patch-file lifecycle; custom profiles default to live reload.
|
|
378
392
|
*/
|
|
379
|
-
function initProfile(dir, bundles
|
|
393
|
+
function initProfile(dir, bundles) {
|
|
380
394
|
mkdirSync(dir, { recursive: true });
|
|
381
395
|
const manifestPath = join(dir, "package.json");
|
|
382
396
|
if (!existsSync(manifestPath)) {
|
|
@@ -384,10 +398,7 @@ function initProfile(dir, bundles, patchReload = DEFAULT_PROFILE_PATCH_RELOAD) {
|
|
|
384
398
|
name: `dsh-profile-${basename(dir)}`,
|
|
385
399
|
private: true,
|
|
386
400
|
dependencies: {},
|
|
387
|
-
dsh: { profile: {
|
|
388
|
-
bundles: [...bundles],
|
|
389
|
-
patchReload
|
|
390
|
-
} }
|
|
401
|
+
dsh: { profile: { bundles: [...bundles] } }
|
|
391
402
|
};
|
|
392
403
|
writeFileSync(manifestPath, JSON.stringify(manifest, void 0, 2) + "\n");
|
|
393
404
|
}
|
|
@@ -429,23 +440,6 @@ function ensureSymlink(link, target) {
|
|
|
429
440
|
if (error.code !== "EEXIST" || !lstatSync(link).isSymbolicLink() || !symlinkPointsTo(link, target)) throw error;
|
|
430
441
|
}
|
|
431
442
|
}
|
|
432
|
-
/** Resolve a link target without following the final path component. */
|
|
433
|
-
function canonicalLinkPath(path) {
|
|
434
|
-
try {
|
|
435
|
-
return join(realpathSync.native(dirname(path)), basename(path));
|
|
436
|
-
} catch (error) {
|
|
437
|
-
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
|
438
|
-
if (error.code === "ENOENT") return void 0;
|
|
439
|
-
/* v8 ignore next -- see the host-filesystem exception above */
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
/** Return whether a symlink or junction points at the same path as `target`. */
|
|
444
|
-
function symlinkPointsTo(link, target) {
|
|
445
|
-
const canonicalActual = canonicalLinkPath(resolve(dirname(link), readlinkSync(link)));
|
|
446
|
-
const canonicalTarget = canonicalLinkPath(resolve(target));
|
|
447
|
-
return canonicalActual !== void 0 && canonicalActual === canonicalTarget;
|
|
448
|
-
}
|
|
449
443
|
/** Add one profile-owned fallback link without replacing a pnpm-managed entry. */
|
|
450
444
|
function ensureProfileSymlink(link, target) {
|
|
451
445
|
try {
|
|
@@ -481,10 +475,6 @@ function removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName) {
|
|
|
481
475
|
if (error.code !== "ENOENT") throw error;
|
|
482
476
|
}
|
|
483
477
|
}
|
|
484
|
-
/** Return whether the process reads application modules from pkg's virtual filesystem. */
|
|
485
|
-
function isPackagedExecutable() {
|
|
486
|
-
return process.pkg !== void 0;
|
|
487
|
-
}
|
|
488
478
|
/** Resolve one available explicit package export under Node ESM import conditions. */
|
|
489
479
|
function packageEntryFromPackage(packageName, packageDir, declared, subpath) {
|
|
490
480
|
let candidates;
|
|
@@ -586,11 +576,17 @@ function profileDependencyNames(manifest) {
|
|
|
586
576
|
return [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {})];
|
|
587
577
|
}
|
|
588
578
|
/** Resolve the installation generation that every profile must find through the fallback directory. */
|
|
589
|
-
function resolveModuleFallbackEntries(installAnchor) {
|
|
579
|
+
function resolveModuleFallbackEntries(installAnchor, materialize = true) {
|
|
590
580
|
const appManifest = readModuleFallbackManifest(installAnchor);
|
|
591
581
|
const links = /* @__PURE__ */ new Map();
|
|
582
|
+
const declarers = /* @__PURE__ */ new Map();
|
|
583
|
+
const versions = /* @__PURE__ */ new Map();
|
|
592
584
|
/* v8 ignore next -- a real app manifest always declares its name */
|
|
593
|
-
if (appManifest.name !== void 0)
|
|
585
|
+
if (appManifest.name !== void 0) {
|
|
586
|
+
links.set(appManifest.name, dirname(installAnchor));
|
|
587
|
+
declarers.set(appManifest.name, installAnchor);
|
|
588
|
+
versions.set(appManifest.name, appManifest.version);
|
|
589
|
+
}
|
|
594
590
|
const queue = [{
|
|
595
591
|
anchor: installAnchor,
|
|
596
592
|
manifest: appManifest
|
|
@@ -602,14 +598,17 @@ function resolveModuleFallbackEntries(installAnchor) {
|
|
|
602
598
|
const dir = packageDirFromAnchor(next.anchor, dep);
|
|
603
599
|
if (dir === void 0) continue;
|
|
604
600
|
links.set(dep, dir);
|
|
601
|
+
declarers.set(dep, next.anchor);
|
|
605
602
|
const manifestPath = join(dir, "package.json");
|
|
603
|
+
const manifest = readModuleFallbackManifest(manifestPath);
|
|
604
|
+
versions.set(dep, manifest.version);
|
|
606
605
|
queue.push({
|
|
607
606
|
anchor: manifestPath,
|
|
608
|
-
manifest
|
|
607
|
+
manifest
|
|
609
608
|
});
|
|
610
609
|
}
|
|
611
610
|
return {
|
|
612
|
-
entries: !isPackagedExecutable() ? [...links].map(([packageName, packageDir]) => ({
|
|
611
|
+
entries: !materialize ? [] : !isPackagedExecutable() ? [...links].map(([packageName, packageDir]) => ({
|
|
613
612
|
kind: "symlink",
|
|
614
613
|
packageName,
|
|
615
614
|
packageDir
|
|
@@ -622,7 +621,10 @@ function resolveModuleFallbackEntries(installAnchor) {
|
|
|
622
621
|
targets: source.targets
|
|
623
622
|
}];
|
|
624
623
|
}),
|
|
625
|
-
packageNames: new Set(links.keys())
|
|
624
|
+
packageNames: new Set(links.keys()),
|
|
625
|
+
packageDirs: links,
|
|
626
|
+
declarers,
|
|
627
|
+
versions
|
|
626
628
|
};
|
|
627
629
|
}
|
|
628
630
|
/** Return whether one existing fallback entry already matches its resolved installation generation. */
|
|
@@ -652,18 +654,85 @@ function moduleFallbackCurrent(modulesDir, entries) {
|
|
|
652
654
|
* `node_modules`; pnpm-managed entries remain authoritative, and another
|
|
653
655
|
* profile's links cannot change its resolution.
|
|
654
656
|
* @param options - installation anchor, optional loaded profile, and Harness home.
|
|
655
|
-
* @returns
|
|
657
|
+
* @returns the computed fallback generation after optional materialization.
|
|
656
658
|
*/
|
|
657
659
|
async function healProfilesModuleFallback(options) {
|
|
658
|
-
const { installAnchor, profile, home = resolveDshHome() } = options;
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
660
|
+
const { installAnchor, profile, home = resolveDshHome(), materialize = true } = options;
|
|
661
|
+
const profilesDir = join(home, PROFILES_DIR);
|
|
662
|
+
const modulesDir = join(profilesDir, "node_modules");
|
|
663
|
+
if (materialize) mkdirSync(modulesDir, { recursive: true });
|
|
664
|
+
const { entries, packageNames, packageDirs, declarers, versions } = resolveModuleFallbackEntries(installAnchor, materialize);
|
|
665
|
+
if (materialize && !moduleFallbackCurrent(modulesDir, entries)) await withFileLock(modulesDir, () => {
|
|
663
666
|
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir);
|
|
664
667
|
return Promise.resolve();
|
|
665
668
|
});
|
|
666
|
-
|
|
669
|
+
const profileDeclarers = /* @__PURE__ */ new Map();
|
|
670
|
+
const profileVersions = /* @__PURE__ */ new Map();
|
|
671
|
+
const localPackageNames = profile === void 0 ? [] : installedProfilePackageNames(profile);
|
|
672
|
+
const profilePackages = profile === void 0 ? /* @__PURE__ */ new Map() : healProfileModuleFallback(profile, packageNames, materialize, profileDeclarers, profileVersions);
|
|
673
|
+
return Object.freeze({
|
|
674
|
+
profilesDir,
|
|
675
|
+
profileDir: profile?.dir,
|
|
676
|
+
localPackageNames: Object.freeze(localPackageNames),
|
|
677
|
+
entries: Object.freeze([...[...packageDirs].map(([name, packageDir]) => Object.freeze({
|
|
678
|
+
name,
|
|
679
|
+
packageDir,
|
|
680
|
+
version: versions.get(name),
|
|
681
|
+
declarer: declarers.get(name),
|
|
682
|
+
scope: "installation"
|
|
683
|
+
})), ...[...profilePackages].map(([name, packageDir]) => Object.freeze({
|
|
684
|
+
name,
|
|
685
|
+
packageDir,
|
|
686
|
+
version: profileVersions.get(name),
|
|
687
|
+
declarer: profileDeclarers.get(name),
|
|
688
|
+
scope: "profile"
|
|
689
|
+
}))])
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/** Return installed direct dependencies that Node resolves before profile fallback. */
|
|
693
|
+
function installedProfilePackageNames(profile) {
|
|
694
|
+
let manifest;
|
|
695
|
+
try {
|
|
696
|
+
manifest = readModuleFallbackManifest(join(profile.dir, "package.json"));
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (error.code === "ENOENT") return [];
|
|
699
|
+
throw error;
|
|
700
|
+
}
|
|
701
|
+
return profileDependencyNames(manifest).filter((name) => {
|
|
702
|
+
if (!existsSync(join(join(profile.dir, "node_modules", name), "package.json"))) return false;
|
|
703
|
+
return !isProfileModuleFallbackLink(profile.dir, name);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Compute a profile resolution generation without materializing links or proxies.
|
|
708
|
+
* @param options - installation anchor, profile, and optional Harness home.
|
|
709
|
+
* @returns the complete immutable generation.
|
|
710
|
+
*/
|
|
711
|
+
function createProfileResolutionGeneration(options) {
|
|
712
|
+
return healProfilesModuleFallback({
|
|
713
|
+
...options,
|
|
714
|
+
materialize: false
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Supply an application-owned profile with filesystem packages from its installation and selected bundles.
|
|
719
|
+
* All fallback links belong to the profile; no shared Harness-home directory is written.
|
|
720
|
+
* Existing pnpm-managed packages remain authoritative. The caller serializes profile mutations.
|
|
721
|
+
* @param options - owning installation package.json and the loaded application profile.
|
|
722
|
+
*/
|
|
723
|
+
function healIsolatedProfileModuleFallback(options) {
|
|
724
|
+
const installationLinks = resolveModuleFallbackEntries(options.installAnchor, false).packageDirs;
|
|
725
|
+
healProfileModuleFallback(options.profile, new Set(installationLinks.keys()), true, void 0, void 0, installationLinks);
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Detach this profile's fallback links before a package-manager mutation.
|
|
729
|
+
* Installed packages and links replaced by pnpm remain untouched; the next profile launch restores fallbacks.
|
|
730
|
+
* @param profileDir - profile directory whose package mutation is serialized by the caller.
|
|
731
|
+
*/
|
|
732
|
+
function unlinkProfileModuleFallback(profileDir) {
|
|
733
|
+
const ownedModulesDir = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, "node_modules");
|
|
734
|
+
if (!existsSync(ownedModulesDir)) return;
|
|
735
|
+
for (const name of ownedPackageNames(ownedModulesDir)) removeProfileSymlink(join(profileDir, "node_modules"), ownedModulesDir, name);
|
|
667
736
|
}
|
|
668
737
|
/** Heal one module-fallback generation while the cross-process writer lock is held. */
|
|
669
738
|
function healProfilesModuleFallbackLocked(entries, modulesDir) {
|
|
@@ -675,17 +744,19 @@ function healProfilesModuleFallbackLocked(entries, modulesDir) {
|
|
|
675
744
|
}
|
|
676
745
|
}
|
|
677
746
|
/** Collect the first resolvable package directory for each dependency name. */
|
|
678
|
-
function dependencyClosure(anchors, reserved, exclude) {
|
|
747
|
+
function dependencyClosure(anchors, reserved, exclude, declarers, versions) {
|
|
679
748
|
const links = /* @__PURE__ */ new Map();
|
|
680
749
|
const visited = new Set(reserved);
|
|
681
750
|
for (const anchor of anchors) {
|
|
682
|
-
const canonicalAnchor =
|
|
751
|
+
const canonicalAnchor = join(realModuleDirectory(dirname(anchor)), basename(anchor));
|
|
683
752
|
const manifest = readModuleFallbackManifest(canonicalAnchor);
|
|
684
753
|
/* v8 ignore next -- an installable package manifest always declares its name */
|
|
685
754
|
if (manifest.name === void 0) continue;
|
|
686
755
|
if (!visited.has(manifest.name)) {
|
|
687
756
|
visited.add(manifest.name);
|
|
688
757
|
links.set(manifest.name, dirname(canonicalAnchor));
|
|
758
|
+
declarers?.set(manifest.name, canonicalAnchor);
|
|
759
|
+
versions?.set(manifest.name, manifest.version);
|
|
689
760
|
}
|
|
690
761
|
const queue = [{
|
|
691
762
|
anchor: canonicalAnchor,
|
|
@@ -699,21 +770,26 @@ function dependencyClosure(anchors, reserved, exclude) {
|
|
|
699
770
|
if (dir === void 0) continue;
|
|
700
771
|
visited.add(dep);
|
|
701
772
|
links.set(dep, dir);
|
|
773
|
+
declarers?.set(dep, next.anchor);
|
|
702
774
|
const manifestPath = join(dir, "package.json");
|
|
775
|
+
const dependencyManifest = readModuleFallbackManifest(manifestPath);
|
|
776
|
+
versions?.set(dep, dependencyManifest.version);
|
|
703
777
|
queue.push({
|
|
704
778
|
anchor: manifestPath,
|
|
705
|
-
manifest:
|
|
779
|
+
manifest: dependencyManifest
|
|
706
780
|
});
|
|
707
781
|
}
|
|
708
782
|
}
|
|
709
783
|
return links;
|
|
710
784
|
}
|
|
711
785
|
/** Reconcile packages carried only by selected bundles into one profile. */
|
|
712
|
-
function healProfileModuleFallback(profile, installationPackageNames) {
|
|
786
|
+
function healProfileModuleFallback(profile, installationPackageNames, materialize = true, declarers, versions, installationLinks = /* @__PURE__ */ new Map()) {
|
|
713
787
|
const profileModulesDir = join(profile.dir, "node_modules");
|
|
714
788
|
const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, "node_modules");
|
|
715
|
-
|
|
716
|
-
|
|
789
|
+
if (materialize) {
|
|
790
|
+
mkdirSync(profileModulesDir, { recursive: true });
|
|
791
|
+
mkdirSync(ownedModulesDir, { recursive: true });
|
|
792
|
+
}
|
|
717
793
|
const bundleLinks = dependencyClosure(profile.layers.filter((layer) => !installationPackageNames.has(layer.packageName)).map((layer) => join(layer.packageDir, "package.json")), installationPackageNames, (candidate, packageName) => {
|
|
718
794
|
const profileLink = join(profileModulesDir, packageName);
|
|
719
795
|
if (canonicalLinkPath(candidate) !== canonicalLinkPath(profileLink)) return false;
|
|
@@ -725,10 +801,12 @@ function healProfileModuleFallback(profile, installationPackageNames) {
|
|
|
725
801
|
/* v8 ignore next -- see the host-filesystem exception above */
|
|
726
802
|
throw error;
|
|
727
803
|
}
|
|
728
|
-
});
|
|
804
|
+
}, declarers, versions);
|
|
729
805
|
for (const layer of profile.layers) bundleLinks.delete(layer.packageName);
|
|
730
|
-
|
|
731
|
-
|
|
806
|
+
const links = new Map([...installationLinks, ...bundleLinks]);
|
|
807
|
+
if (!materialize) return links;
|
|
808
|
+
for (const packageName of ownedPackageNames(ownedModulesDir)) if (!links.has(packageName)) removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName);
|
|
809
|
+
for (const [packageName, target] of links) {
|
|
732
810
|
const ownedLink = join(ownedModulesDir, packageName);
|
|
733
811
|
mkdirSync(dirname(ownedLink), { recursive: true });
|
|
734
812
|
ensureSymlink(ownedLink, target);
|
|
@@ -736,6 +814,7 @@ function healProfileModuleFallback(profile, installationPackageNames) {
|
|
|
736
814
|
mkdirSync(dirname(profileLink), { recursive: true });
|
|
737
815
|
ensureProfileSymlink(profileLink, ownedLink);
|
|
738
816
|
}
|
|
817
|
+
return links;
|
|
739
818
|
}
|
|
740
819
|
/**
|
|
741
820
|
* Read a profile's manifest.
|
|
@@ -769,27 +848,21 @@ function sameBundles(left, right) {
|
|
|
769
848
|
}
|
|
770
849
|
/**
|
|
771
850
|
* Normalize an exact installation-owned bundle tuple to its shipped template,
|
|
772
|
-
*
|
|
773
|
-
* is written back during profile loading while every other manifest field is
|
|
774
|
-
* preserved; any other bundle list is user-owned and remains untouched.
|
|
851
|
+
* preserving all other manifest fields. Other bundle lists remain untouched.
|
|
775
852
|
*/
|
|
776
853
|
function normalizeShippedProfile(name, dir, manifest) {
|
|
777
854
|
const installationOwned = INSTALLATION_OWNED_PROFILE_TUPLES[name];
|
|
778
855
|
const template = PROFILE_TEMPLATES[name];
|
|
779
856
|
const bundles = manifest.dsh?.profile?.bundles;
|
|
780
857
|
if (template === void 0 || bundles === void 0) return manifest;
|
|
781
|
-
|
|
782
|
-
const isCurrentTuple = sameBundles(bundles, template.bundles);
|
|
783
|
-
const needsReloadDefault = manifest.dsh?.profile?.patchReload === void 0 && isCurrentTuple;
|
|
784
|
-
if (!isRetiredTuple && !needsReloadDefault) return manifest;
|
|
858
|
+
if (!(installationOwned !== void 0 && sameBundles(bundles, installationOwned))) return manifest;
|
|
785
859
|
const normalized = {
|
|
786
860
|
...manifest,
|
|
787
861
|
dsh: {
|
|
788
862
|
...manifest.dsh,
|
|
789
863
|
profile: {
|
|
790
864
|
...manifest.dsh?.profile,
|
|
791
|
-
bundles: [...template.bundles]
|
|
792
|
-
patchReload: manifest.dsh?.profile?.patchReload ?? template.patchReload
|
|
865
|
+
bundles: [...template.bundles]
|
|
793
866
|
}
|
|
794
867
|
}
|
|
795
868
|
};
|
|
@@ -841,12 +914,7 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
|
|
|
841
914
|
* @returns the resolved bundle layers and optional user patch layer.
|
|
842
915
|
*/
|
|
843
916
|
function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
844
|
-
const
|
|
845
|
-
const bundles = manifest.dsh?.profile?.bundles ?? [];
|
|
846
|
-
const rawPatchReload = manifest.dsh?.profile?.patchReload;
|
|
847
|
-
if (rawPatchReload !== void 0 && rawPatchReload !== "live" && rawPatchReload !== "startup") throw new Error(`${binName}: profile manifest ${join(dir, "package.json")} dsh.profile.patchReload must be "live" or "startup"`);
|
|
848
|
-
const patchReload = rawPatchReload ?? "live";
|
|
849
|
-
const layers = bundles.map((packageName) => {
|
|
917
|
+
const layers = (readProfileManifest(binName, dir).dsh?.profile?.bundles ?? []).map((packageName) => {
|
|
850
918
|
const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir);
|
|
851
919
|
const declared = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")).dsh?.bundle?.patch;
|
|
852
920
|
if (declared === void 0) throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.bundle in its package.json`);
|
|
@@ -865,8 +933,7 @@ function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
|
865
933
|
dir,
|
|
866
934
|
layers,
|
|
867
935
|
patchPath,
|
|
868
|
-
patches
|
|
869
|
-
patchReload
|
|
936
|
+
patches
|
|
870
937
|
};
|
|
871
938
|
}
|
|
872
939
|
/**
|
|
@@ -888,7 +955,7 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
888
955
|
if (!existsSync(join(dir, "package.json"))) {
|
|
889
956
|
const template = PROFILE_TEMPLATES[name];
|
|
890
957
|
if (template === void 0) throw new Error(`${binName}: profile ${JSON.stringify(name)} does not exist; create it with 'dsh plugin --profile ${name} add <package>'`);
|
|
891
|
-
initProfile(dir, template.bundles
|
|
958
|
+
initProfile(dir, template.bundles);
|
|
892
959
|
}
|
|
893
960
|
normalizeShippedProfile(name, dir, readProfileManifest(binName, dir));
|
|
894
961
|
return loadProfileDirectory(binName, dir, installAnchor, options);
|
|
@@ -908,6 +975,961 @@ function composeEntries(layers, warn = () => {}) {
|
|
|
908
975
|
});
|
|
909
976
|
}
|
|
910
977
|
//#endregion
|
|
978
|
+
//#region lib/types/profile-context.js
|
|
979
|
+
/** Launcher-owned profile locations and composition inputs. */
|
|
980
|
+
const TELEMETRY_ROW_ID = "session-telemetry-otel";
|
|
981
|
+
/**
|
|
982
|
+
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
|
983
|
+
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
|
|
984
|
+
* off-by-mistake over on-by-mistake. A composition without the telemetry row
|
|
985
|
+
* exports nothing, so the switch is then trivially satisfied and no patch is
|
|
986
|
+
* generated — custom profiles need not mount telemetry to run with the
|
|
987
|
+
* switch set.
|
|
988
|
+
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
|
989
|
+
* @param hasRow - whether the composition carries the telemetry row.
|
|
990
|
+
* @returns the disable patch, or `undefined` when no hard-disable patch is required.
|
|
991
|
+
*/
|
|
992
|
+
function resolveTelemetryPatch(disabledEnv, hasRow) {
|
|
993
|
+
if ((disabledEnv ?? "") === "" || !hasRow) return void 0;
|
|
994
|
+
return {
|
|
995
|
+
id: TELEMETRY_ROW_ID,
|
|
996
|
+
disabled: true
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/** Read current bundle and user layers with the launch-time overlays.
|
|
1000
|
+
* @param binName Diagnostic prefix for malformed or missing configuration.
|
|
1001
|
+
* @param context Data supplied by the profile launcher.
|
|
1002
|
+
* @param initialProfile Already loaded startup profile; omitted reads the current files.
|
|
1003
|
+
* @returns Detached ordered patches; this function does not update the Loader.
|
|
1004
|
+
*/
|
|
1005
|
+
function readProfilePatches(binName, context, initialProfile) {
|
|
1006
|
+
const profile = initialProfile ?? loadProfileDirectory(binName, context.dir, context.installAnchor, { userLayer: false });
|
|
1007
|
+
const patches = structuredClone([
|
|
1008
|
+
...profile.layers.flatMap((layer) => layer.patches),
|
|
1009
|
+
...initialProfile?.patches ?? loadOptionalPatches(binName, context.patchPath) ?? [],
|
|
1010
|
+
...loadOptionalPatches(binName, join(context.home, "cordis.patch.yml")) ?? [],
|
|
1011
|
+
...context.overlays
|
|
1012
|
+
]);
|
|
1013
|
+
const telemetryPatch = resolveTelemetryPatch(context.telemetryDisabledEnv, composeEntries([patches]).some((row) => row.id === TELEMETRY_ROW_ID));
|
|
1014
|
+
if (telemetryPatch !== void 0) patches.push(telemetryPatch);
|
|
1015
|
+
return patches;
|
|
1016
|
+
}
|
|
1017
|
+
//#endregion
|
|
1018
|
+
//#region lib/types/profile-plugins.js
|
|
1019
|
+
/** Installed profile dependencies and their bundle activation after package-manager operations. */
|
|
1020
|
+
/** Unavailable installed metadata must not prevent listing or removing dependencies. */
|
|
1021
|
+
function optionalManifest(binName, packageDir) {
|
|
1022
|
+
try {
|
|
1023
|
+
return readProfileManifest(binName, packageDir);
|
|
1024
|
+
} catch {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/** Resolve activation metadata with the same installation precedence as profile loading. */
|
|
1029
|
+
function bundleManifest(location, name) {
|
|
1030
|
+
let packageDir;
|
|
1031
|
+
try {
|
|
1032
|
+
packageDir = resolveBundleDir(location.binName, name, location.installAnchor, location.profileDir);
|
|
1033
|
+
} catch {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
return optionalManifest(location.binName, packageDir);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Read installed versions and bundle declarations without requiring loadable plugin code.
|
|
1040
|
+
* Missing or unreadable installed metadata leaves the dependency visible for repair or removal.
|
|
1041
|
+
* @param location - profile and installation resolution inputs.
|
|
1042
|
+
* @returns dependency records in package.json order and the profile manifest.
|
|
1043
|
+
*/
|
|
1044
|
+
function readProfilePlugins(location) {
|
|
1045
|
+
const manifest = readProfileManifest(location.binName, location.profileDir);
|
|
1046
|
+
const bundles = manifest.dsh?.profile?.bundles ?? [];
|
|
1047
|
+
return {
|
|
1048
|
+
manifest,
|
|
1049
|
+
dependencies: Object.entries(manifest.dependencies ?? {}).map(([name, spec]) => {
|
|
1050
|
+
const installed = optionalManifest(location.binName, join(location.profileDir, "node_modules", name));
|
|
1051
|
+
return {
|
|
1052
|
+
name,
|
|
1053
|
+
version: typeof installed?.version === "string" ? installed.version : spec,
|
|
1054
|
+
bundle: bundleManifest(location, name)?.dsh?.bundle?.patch !== void 0,
|
|
1055
|
+
enabled: bundles.includes(name)
|
|
1056
|
+
};
|
|
1057
|
+
})
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Write a bundle list while preserving the supplied profile's other metadata.
|
|
1062
|
+
* @param profileDir - directory whose package.json is updated.
|
|
1063
|
+
* @param manifest - current profile manifest, read after the package operation when applicable.
|
|
1064
|
+
* @param bundles - ordered active bundle names, including any template entries.
|
|
1065
|
+
* @returns the written manifest.
|
|
1066
|
+
*/
|
|
1067
|
+
function writeProfileBundles(profileDir, manifest, bundles) {
|
|
1068
|
+
const updated = {
|
|
1069
|
+
...manifest,
|
|
1070
|
+
dsh: {
|
|
1071
|
+
...manifest.dsh,
|
|
1072
|
+
profile: {
|
|
1073
|
+
...manifest.dsh?.profile,
|
|
1074
|
+
bundles: [...bundles]
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
writeProfileManifest(profileDir, updated);
|
|
1079
|
+
return updated;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Reconcile installed bundle declarations after a successful package-manager operation.
|
|
1083
|
+
* Dependency-managed entries disappear when removed or when their package loses its declaration;
|
|
1084
|
+
* template entries retain their order and duplicates. New bundle dependencies activate automatically.
|
|
1085
|
+
* @param options - location, inventory captured before pnpm, and whether explicitly disabled bundles remain disabled.
|
|
1086
|
+
* @returns updated inventory and newly added ordinary dependencies for caller-owned warnings.
|
|
1087
|
+
*/
|
|
1088
|
+
function reconcileProfilePlugins(options) {
|
|
1089
|
+
const after = readProfilePlugins(options);
|
|
1090
|
+
const beforeNames = new Set(options.before.dependencies.map((dependency) => dependency.name));
|
|
1091
|
+
const afterNames = new Set(after.dependencies.map((dependency) => dependency.name));
|
|
1092
|
+
const bundleNames = new Set(after.dependencies.filter((dependency) => dependency.bundle).map((dependency) => dependency.name));
|
|
1093
|
+
const disabled = new Set(options.preserveDisabled ? options.before.dependencies.filter((dependency) => dependency.bundle && !dependency.enabled).map((dependency) => dependency.name) : []);
|
|
1094
|
+
const previous = after.manifest.dsh?.profile?.bundles ?? [];
|
|
1095
|
+
const bundles = previous.filter((name) => !(beforeNames.has(name) || afterNames.has(name)) || bundleNames.has(name));
|
|
1096
|
+
for (const dependency of after.dependencies) if (dependency.bundle && !disabled.has(dependency.name) && !bundles.includes(dependency.name)) bundles.push(dependency.name);
|
|
1097
|
+
return {
|
|
1098
|
+
plugins: {
|
|
1099
|
+
manifest: bundles.length !== previous.length || bundles.some((name, index) => name !== previous[index]) ? writeProfileBundles(options.profileDir, after.manifest, bundles) : after.manifest,
|
|
1100
|
+
dependencies: after.dependencies.map((dependency) => ({
|
|
1101
|
+
...dependency,
|
|
1102
|
+
enabled: bundles.includes(dependency.name)
|
|
1103
|
+
}))
|
|
1104
|
+
},
|
|
1105
|
+
addedPlainDependencies: after.dependencies.filter((dependency) => !dependency.bundle && !beforeNames.has(dependency.name)).map((dependency) => dependency.name)
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
1109
|
+
//#region lib/types/profile-sanitize.js
|
|
1110
|
+
/** Filesystem recovery for callers that own profile shutdown and write exclusion. */
|
|
1111
|
+
/**
|
|
1112
|
+
* Back up the profile patch and retain only the caller's recovery bundles.
|
|
1113
|
+
* The caller must stop the profile and exclude concurrent profile writes.
|
|
1114
|
+
* Installed packages and other manifest fields are preserved; patches are never parsed.
|
|
1115
|
+
* Failures propagate and may leave completed changes in place for a retry.
|
|
1116
|
+
* @param binName - Diagnostic prefix for invalid profile manifests.
|
|
1117
|
+
* @param profileDir - Profile directory to recover without loading its plugins.
|
|
1118
|
+
* @param bundles - Ordered bundles to enable after recovery.
|
|
1119
|
+
* @returns Backup path with a Unix millisecond timestamp and optional collision ordinal, or undefined if absent.
|
|
1120
|
+
*/
|
|
1121
|
+
function sanitizeProfile(binName, profileDir, bundles) {
|
|
1122
|
+
const manifest = existsSync(join(profileDir, "package.json")) ? readProfileManifest(binName, profileDir) : void 0;
|
|
1123
|
+
const patchPath = join(profileDir, PROFILE_PATCH_FILENAME);
|
|
1124
|
+
const backupBase = `${patchPath}.bak-${Date.now()}`;
|
|
1125
|
+
let backupPath = backupBase;
|
|
1126
|
+
let ordinal = 0;
|
|
1127
|
+
while (existsSync(backupPath)) backupPath = `${backupBase}-${++ordinal}`;
|
|
1128
|
+
try {
|
|
1129
|
+
renameSync(patchPath, backupPath);
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
1132
|
+
backupPath = void 0;
|
|
1133
|
+
}
|
|
1134
|
+
if (manifest !== void 0) writeProfileBundles(profileDir, manifest, bundles);
|
|
1135
|
+
return backupPath;
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region lib/types/profile-resolution/resolver.js
|
|
1139
|
+
/** In-memory profile package routing for Node's default ESM and CommonJS loaders. */
|
|
1140
|
+
const WORKER_RESOLUTION_KEY = "@deepseek-ai/dsh-app-boot/profile-resolution";
|
|
1141
|
+
const EMPTY_ATTRIBUTES = Object.freeze({});
|
|
1142
|
+
/**
|
|
1143
|
+
* Split a bare request into its package name without allocating path segments.
|
|
1144
|
+
* @param request - module specifier to classify.
|
|
1145
|
+
* @returns the bare package name, or undefined for non-package requests.
|
|
1146
|
+
*/
|
|
1147
|
+
function barePackageName(request) {
|
|
1148
|
+
if (!request || request[0] === "." || request[0] === "/" || request[0] === "\\" || request[0] === "#" || request.includes(":") || isBuiltin(request)) return;
|
|
1149
|
+
const first = request.indexOf("/");
|
|
1150
|
+
if (request[0] !== "@") return first < 0 ? request : request.slice(0, first);
|
|
1151
|
+
if (first < 0) return;
|
|
1152
|
+
const second = request.indexOf("/", first + 1);
|
|
1153
|
+
return second < 0 ? request : request.slice(0, second);
|
|
1154
|
+
}
|
|
1155
|
+
function canonicalPath(path) {
|
|
1156
|
+
try {
|
|
1157
|
+
return realpathSync(path);
|
|
1158
|
+
} catch {
|
|
1159
|
+
return resolve(path);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
function prefixes(path) {
|
|
1163
|
+
const configured = resolve(path) + sep;
|
|
1164
|
+
const canonical = canonicalPath(path) + sep;
|
|
1165
|
+
return canonical === configured ? [configured] : [configured, canonical];
|
|
1166
|
+
}
|
|
1167
|
+
function compileGeneration(generation) {
|
|
1168
|
+
const profilePaths = prefixes(generation.profilesDir);
|
|
1169
|
+
const profile = generation.profileDir === void 0 ? [] : prefixes(generation.profileDir);
|
|
1170
|
+
return {
|
|
1171
|
+
entries: new Map(generation.entries.map((entry) => [entry.name, entry])),
|
|
1172
|
+
profilesDir: generation.profilesDir,
|
|
1173
|
+
profileDir: generation.profileDir,
|
|
1174
|
+
profilePaths,
|
|
1175
|
+
profileUrls: profilePaths.map((path) => pathToFileURL(path).href),
|
|
1176
|
+
profile,
|
|
1177
|
+
activeProfileUrls: profile.map((path) => pathToFileURL(path).href),
|
|
1178
|
+
localPackageNames: new Set(generation.localPackageNames),
|
|
1179
|
+
shared: new Set(profilePaths.map((prefix) => join(prefix, "node_modules"))),
|
|
1180
|
+
esmRoutes: /* @__PURE__ */ new Map(),
|
|
1181
|
+
cjsRoutes: /* @__PURE__ */ new Map()
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function startsWithin(path, roots) {
|
|
1185
|
+
for (const root of roots) if (path.startsWith(root)) return true;
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
function nativePackageDir(parent, name) {
|
|
1189
|
+
for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
1190
|
+
const candidate = join(searchPath, name);
|
|
1191
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function localPackageCandidate(searchPath, name, flavor) {
|
|
1195
|
+
const candidate = join(searchPath, name);
|
|
1196
|
+
const stat = statSync(candidate, { throwIfNoEntry: false });
|
|
1197
|
+
return (flavor === "esm" ? stat?.isDirectory() === true : stat !== void 0 || [
|
|
1198
|
+
".js",
|
|
1199
|
+
".json",
|
|
1200
|
+
".node"
|
|
1201
|
+
].some((extension) => existsSync(candidate + extension))) ? {
|
|
1202
|
+
packageDir: candidate,
|
|
1203
|
+
canBeManagedLink: stat !== void 0
|
|
1204
|
+
} : void 0;
|
|
1205
|
+
}
|
|
1206
|
+
function selfReferenceName(parent) {
|
|
1207
|
+
let current = dirname(parent);
|
|
1208
|
+
while (true) {
|
|
1209
|
+
const manifestPath = join(current, "package.json");
|
|
1210
|
+
if (existsSync(manifestPath)) {
|
|
1211
|
+
let manifest;
|
|
1212
|
+
try {
|
|
1213
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1214
|
+
} catch (_error) {
|
|
1215
|
+
return null;
|
|
1216
|
+
}
|
|
1217
|
+
return typeof manifest.name === "string" && manifest.exports != null ? manifest.name : false;
|
|
1218
|
+
}
|
|
1219
|
+
/* v8 ignore next -- scoped module requests normally find an owning manifest before node_modules. */
|
|
1220
|
+
if (basename(current) === "node_modules") return false;
|
|
1221
|
+
const next = dirname(current);
|
|
1222
|
+
if (next === current) return false;
|
|
1223
|
+
current = next;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function packageImportsTarget(parent, request, conditions) {
|
|
1227
|
+
let current = dirname(parent);
|
|
1228
|
+
while (true) {
|
|
1229
|
+
const manifestPath = join(current, "package.json");
|
|
1230
|
+
if (existsSync(manifestPath)) {
|
|
1231
|
+
let manifest;
|
|
1232
|
+
try {
|
|
1233
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1234
|
+
} catch (_error) {
|
|
1235
|
+
/* v8 ignore next -- native resolution cannot report MODULE_NOT_FOUND after an invalid scope manifest */
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
try {
|
|
1239
|
+
const target = imports(manifest, request, {
|
|
1240
|
+
conditions: [...conditions],
|
|
1241
|
+
unsafe: true
|
|
1242
|
+
})?.[0];
|
|
1243
|
+
return target !== void 0 && barePackageName(target) !== void 0 ? {
|
|
1244
|
+
specifier: target,
|
|
1245
|
+
parentURL: pathToFileURL(manifestPath).href
|
|
1246
|
+
} : void 0;
|
|
1247
|
+
} catch (_error) {
|
|
1248
|
+
/* v8 ignore next -- native resolution cannot report MODULE_NOT_FOUND before selecting a valid mapping */
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (basename(current) === "node_modules") return void 0;
|
|
1253
|
+
const next = dirname(current);
|
|
1254
|
+
/* v8 ignore next -- a MODULE_NOT_FOUND package-import target always has an owning package scope */
|
|
1255
|
+
if (next === current) return void 0;
|
|
1256
|
+
current = next;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
function packageSearchPaths(entry, request, cjs) {
|
|
1260
|
+
const name = barePackageName(request);
|
|
1261
|
+
/* v8 ignore next -- fallback routes are created only for bare package requests */
|
|
1262
|
+
if (name === void 0) return cjs._nodeModulePaths(dirname(entry.declarer));
|
|
1263
|
+
const suffix = sep + name.split("/").join(sep);
|
|
1264
|
+
return entry.packageDir.endsWith(suffix) ? [entry.packageDir.slice(0, -suffix.length)] : cjs._nodeModulePaths(dirname(entry.declarer));
|
|
1265
|
+
}
|
|
1266
|
+
function localCandidateOwnsResolution(candidate, resolved, request, name) {
|
|
1267
|
+
if (startsWithin(resolved, prefixes(candidate))) return true;
|
|
1268
|
+
if (sameResolution(candidate, resolved)) return true;
|
|
1269
|
+
if ([
|
|
1270
|
+
".js",
|
|
1271
|
+
".json",
|
|
1272
|
+
".node"
|
|
1273
|
+
].some((extension) => sameResolution(candidate + extension, resolved))) return true;
|
|
1274
|
+
/* v8 ignore next -- a bounded native lookup can escape a candidate only through its root legacy main */
|
|
1275
|
+
if (request !== name) return false;
|
|
1276
|
+
try {
|
|
1277
|
+
const manifest = JSON.parse(readFileSync(join(candidate, "package.json"), "utf8"));
|
|
1278
|
+
/* v8 ignore next -- a bounded native lookup outside the package directory requires a legacy main */
|
|
1279
|
+
if (typeof manifest.main !== "string") return false;
|
|
1280
|
+
return sameResolution(createRequire(join(candidate, "package.json")).resolve(resolve(candidate, manifest.main)), resolved);
|
|
1281
|
+
} catch (_error) {
|
|
1282
|
+
/* v8 ignore next -- this helper runs only after the same native resolution succeeded */
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function isUnselectedPackageMiss(error) {
|
|
1287
|
+
const failure = error;
|
|
1288
|
+
return failure.code === "MODULE_NOT_FOUND" && failure.path === void 0;
|
|
1289
|
+
}
|
|
1290
|
+
function sameResolution(left, right) {
|
|
1291
|
+
if (left === right) return true;
|
|
1292
|
+
const leftPath = left.startsWith("file:") ? fileURLToPath(left) : left;
|
|
1293
|
+
const rightPath = right.startsWith("file:") ? fileURLToPath(right) : right;
|
|
1294
|
+
return canonicalPath(leftPath) === canonicalPath(rightPath);
|
|
1295
|
+
}
|
|
1296
|
+
/** One mutable pointer to immutable generation data. */
|
|
1297
|
+
var ResolutionRouter = class {
|
|
1298
|
+
current;
|
|
1299
|
+
constructor(generation) {
|
|
1300
|
+
this.current = compileGeneration(generation);
|
|
1301
|
+
}
|
|
1302
|
+
replace(generation) {
|
|
1303
|
+
const entries = new Map(generation.entries.map((entry) => [entry.name, entry]));
|
|
1304
|
+
if (generation.profilesDir !== this.current.profilesDir || generation.profileDir !== this.current.profileDir) throw new Error("profile resolution: a generation cannot change its profile scope");
|
|
1305
|
+
for (const [name, current] of this.current.entries) {
|
|
1306
|
+
const next = entries.get(name);
|
|
1307
|
+
if (next === void 0 || !sameResolution(current.packageDir, next.packageDir) || !sameResolution(current.declarer, next.declarer) || current.version !== next.version || current.scope !== next.scope) throw new Error(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`);
|
|
1308
|
+
}
|
|
1309
|
+
const localPackageNames = new Set(generation.localPackageNames);
|
|
1310
|
+
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`);
|
|
1311
|
+
for (const name of localPackageNames) if (!this.current.localPackageNames.has(name) && this.current.entries.has(name)) throw new Error(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`);
|
|
1312
|
+
this.current = compileGeneration(generation);
|
|
1313
|
+
}
|
|
1314
|
+
routeScoped(request, parentRoutes, generation, flavor, nativeResolve, cacheable = false) {
|
|
1315
|
+
const { parent, profilesDir, requests } = parentRoutes;
|
|
1316
|
+
const name = barePackageName(request);
|
|
1317
|
+
if (name === void 0) return void 0;
|
|
1318
|
+
if (parentRoutes.selfReferenceName === void 0) parentRoutes.selfReferenceName = selfReferenceName(parent);
|
|
1319
|
+
if (parentRoutes.selfReferenceName === name || parentRoutes.selfReferenceName === null) {
|
|
1320
|
+
const state = { route: { kind: "native" } };
|
|
1321
|
+
requests.set(request, state);
|
|
1322
|
+
return state;
|
|
1323
|
+
}
|
|
1324
|
+
const target = generation.entries.get(name);
|
|
1325
|
+
const candidates = [];
|
|
1326
|
+
const localSearchPaths = [];
|
|
1327
|
+
for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
1328
|
+
if (generation.shared.has(resolve(searchPath))) break;
|
|
1329
|
+
localSearchPaths.push(searchPath);
|
|
1330
|
+
const candidate = localPackageCandidate(searchPath, name, flavor);
|
|
1331
|
+
if (candidate !== void 0) {
|
|
1332
|
+
if (!(candidate.canBeManagedLink && generation.profile.some((prefix) => candidate.packageDir === join(prefix, "node_modules", name) && isProfileModuleFallbackLink(prefix.slice(0, -1), name)))) candidates.push(candidate);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (candidates.length > 0) if (flavor === "cjs" && nativeResolve !== void 0) try {
|
|
1336
|
+
const resolved = nativeResolve(localSearchPaths);
|
|
1337
|
+
const selected = candidates.find((candidate) => localCandidateOwnsResolution(candidate.packageDir, resolved, request, name));
|
|
1338
|
+
if (selected !== void 0) {
|
|
1339
|
+
const state = {
|
|
1340
|
+
route: {
|
|
1341
|
+
kind: "native",
|
|
1342
|
+
packageDir: selected.packageDir
|
|
1343
|
+
},
|
|
1344
|
+
packageDir: selected.packageDir,
|
|
1345
|
+
...cacheable ? { cjs: resolved } : {}
|
|
1346
|
+
};
|
|
1347
|
+
requests.set(request, state);
|
|
1348
|
+
return state;
|
|
1349
|
+
}
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
const selected = candidates[0];
|
|
1355
|
+
const state = {
|
|
1356
|
+
route: {
|
|
1357
|
+
kind: "native",
|
|
1358
|
+
packageDir: selected.packageDir
|
|
1359
|
+
},
|
|
1360
|
+
packageDir: selected.packageDir
|
|
1361
|
+
};
|
|
1362
|
+
requests.set(request, state);
|
|
1363
|
+
return state;
|
|
1364
|
+
}
|
|
1365
|
+
const eligible = target?.scope === "installation" || target?.scope === "profile" && parentRoutes.activeProfile;
|
|
1366
|
+
const after = join(dirname(profilesDir), "package.json");
|
|
1367
|
+
const route = eligible ? {
|
|
1368
|
+
kind: "fallback",
|
|
1369
|
+
entry: target,
|
|
1370
|
+
after
|
|
1371
|
+
} : {
|
|
1372
|
+
kind: "after-fallback",
|
|
1373
|
+
parent: after
|
|
1374
|
+
};
|
|
1375
|
+
const state = { route };
|
|
1376
|
+
if (route.kind === "fallback") requests.set(request, state);
|
|
1377
|
+
return state;
|
|
1378
|
+
}
|
|
1379
|
+
routeLocalPackage(request, parentRoutes, generation) {
|
|
1380
|
+
const name = barePackageName(request);
|
|
1381
|
+
if (name === void 0 || !parentRoutes.activeProfile || !generation.localPackageNames.has(name)) return void 0;
|
|
1382
|
+
const state = { route: { kind: "native" } };
|
|
1383
|
+
parentRoutes.requests.set(request, state);
|
|
1384
|
+
return state;
|
|
1385
|
+
}
|
|
1386
|
+
routeUrl(request, parentURL) {
|
|
1387
|
+
const generation = this.current;
|
|
1388
|
+
let parentRoutes = generation.esmRoutes.get(parentURL);
|
|
1389
|
+
if (parentRoutes === false) return void 0;
|
|
1390
|
+
const cached = parentRoutes?.requests.get(request);
|
|
1391
|
+
if (cached !== void 0) return cached;
|
|
1392
|
+
if (parentRoutes === void 0) {
|
|
1393
|
+
const profileIndex = generation.profileUrls.findIndex((prefix) => parentURL.startsWith(prefix));
|
|
1394
|
+
const activeProfileIndex = generation.activeProfileUrls.findIndex((prefix) => parentURL.startsWith(prefix));
|
|
1395
|
+
if (profileIndex < 0 && activeProfileIndex < 0) {
|
|
1396
|
+
generation.esmRoutes.set(parentURL, false);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
let parent;
|
|
1400
|
+
try {
|
|
1401
|
+
parent = fileURLToPath(parentURL);
|
|
1402
|
+
} catch {
|
|
1403
|
+
generation.esmRoutes.set(parentURL, false);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const profileRoot = profileIndex >= 0 ? generation.profilePaths[profileIndex] : generation.profile[activeProfileIndex];
|
|
1407
|
+
/* v8 ignore next -- one matching index was established above */
|
|
1408
|
+
if (profileRoot === void 0) return void 0;
|
|
1409
|
+
parentRoutes = {
|
|
1410
|
+
parent,
|
|
1411
|
+
profilesDir: profileRoot.slice(0, -1),
|
|
1412
|
+
activeProfile: activeProfileIndex >= 0,
|
|
1413
|
+
requests: /* @__PURE__ */ new Map()
|
|
1414
|
+
};
|
|
1415
|
+
generation.esmRoutes.set(parentURL, parentRoutes);
|
|
1416
|
+
}
|
|
1417
|
+
const local = this.routeLocalPackage(request, parentRoutes, generation);
|
|
1418
|
+
if (local !== void 0) return local;
|
|
1419
|
+
return this.routeScoped(request, parentRoutes, generation, "esm");
|
|
1420
|
+
}
|
|
1421
|
+
routePath(request, parent, nativeResolve, cacheable = false) {
|
|
1422
|
+
const generation = this.current;
|
|
1423
|
+
let parentRoutes = generation.cjsRoutes.get(parent);
|
|
1424
|
+
if (parentRoutes === false) return void 0;
|
|
1425
|
+
const cached = parentRoutes?.requests.get(request);
|
|
1426
|
+
if (cached !== void 0) return cached;
|
|
1427
|
+
if (parentRoutes === void 0) {
|
|
1428
|
+
const profilesDir = generation.profilePaths.find((prefix) => parent.startsWith(prefix));
|
|
1429
|
+
const activeProfile = generation.profile.find((prefix) => parent.startsWith(prefix));
|
|
1430
|
+
if (profilesDir !== void 0 || activeProfile !== void 0) {
|
|
1431
|
+
const profileRoot = profilesDir ?? activeProfile;
|
|
1432
|
+
/* v8 ignore next -- one matching root was established above */
|
|
1433
|
+
if (profileRoot === void 0) return void 0;
|
|
1434
|
+
parentRoutes = {
|
|
1435
|
+
parent,
|
|
1436
|
+
profilesDir: profileRoot.slice(0, -1),
|
|
1437
|
+
activeProfile: activeProfile !== void 0,
|
|
1438
|
+
requests: /* @__PURE__ */ new Map()
|
|
1439
|
+
};
|
|
1440
|
+
generation.cjsRoutes.set(parent, parentRoutes);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (parentRoutes === void 0) {
|
|
1444
|
+
generation.cjsRoutes.set(parent, false);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
return this.routeScoped(request, parentRoutes, generation, "cjs", nativeResolve, cacheable);
|
|
1448
|
+
}
|
|
1449
|
+
explicitRoute(request, paths) {
|
|
1450
|
+
if (barePackageName(request) === void 0) return void 0;
|
|
1451
|
+
for (const [index, path] of paths.entries()) {
|
|
1452
|
+
const parent = join(resolve(path), ".dsh-profile-resolution.cjs");
|
|
1453
|
+
if (startsWithin(parent, this.current.profilePaths) || startsWithin(parent, this.current.profile)) return {
|
|
1454
|
+
index,
|
|
1455
|
+
parent
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
nativeSelfReference(request, parent) {
|
|
1460
|
+
const name = barePackageName(request);
|
|
1461
|
+
if (name === void 0) return false;
|
|
1462
|
+
const self = selfReferenceName(parent);
|
|
1463
|
+
return self === name || self === null;
|
|
1464
|
+
}
|
|
1465
|
+
packageDir(specifier, parentURL) {
|
|
1466
|
+
const name = barePackageName(specifier);
|
|
1467
|
+
if (name === void 0) return void 0;
|
|
1468
|
+
const state = this.routeUrl(specifier, parentURL);
|
|
1469
|
+
if (state?.route.kind === "fallback") return state.route.entry.packageDir;
|
|
1470
|
+
if (state?.packageDir !== void 0) return state.packageDir;
|
|
1471
|
+
let parent;
|
|
1472
|
+
try {
|
|
1473
|
+
parent = state?.route.kind === "after-fallback" ? state.route.parent : fileURLToPath(parentURL);
|
|
1474
|
+
} catch {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const found = nativePackageDir(parent, name);
|
|
1478
|
+
if (state !== void 0 && found !== void 0) state.packageDir = found;
|
|
1479
|
+
return found;
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
function internalModules() {
|
|
1483
|
+
const addon = createRequire(import.meta.url)("node-addon-require-builtin");
|
|
1484
|
+
const esmModule = addon.requireBuiltin("internal/modules/esm/loader");
|
|
1485
|
+
const cjsModule = addon.requireBuiltin("internal/modules/cjs/loader");
|
|
1486
|
+
const cjsHelpers = addon.requireBuiltin("internal/modules/helpers");
|
|
1487
|
+
const esmUtils = addon.requireBuiltin("internal/modules/esm/utils");
|
|
1488
|
+
const esmResolve = addon.requireBuiltin("internal/modules/esm/resolve");
|
|
1489
|
+
const esm = esmModule.getOrInitializeCascadedLoader();
|
|
1490
|
+
const modern = "getOrCreateModuleJob" in esm;
|
|
1491
|
+
/* v8 ignore start -- the supported Node 22/24/26 matrix validates each available Internal interface */
|
|
1492
|
+
if (typeof esm.resolveSync !== "function" || typeof Reflect.get(esm, modern ? "getOrCreateModuleJob" : "getModuleJobForImport") !== "function" || !modern && typeof Reflect.get(esm, "resolve") !== "function" || typeof cjsModule.Module._resolveFilename !== "function" || typeof cjsHelpers.getCjsConditions !== "function" || typeof esmUtils.getDefaultConditions !== "function" || typeof esmResolve.defaultResolve !== "function") throw new Error("profile resolution: unsupported Node module loader");
|
|
1493
|
+
/* v8 ignore stop */
|
|
1494
|
+
return {
|
|
1495
|
+
esm,
|
|
1496
|
+
esmDefaultResolve: (specifier, context) => esmResolve.defaultResolve(specifier, context),
|
|
1497
|
+
esmConditions: esmUtils.getDefaultConditions(),
|
|
1498
|
+
cjs: cjsModule.Module,
|
|
1499
|
+
cjsConditions: cjsHelpers.getCjsConditions(),
|
|
1500
|
+
modern
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
function throwWithImporter(error, routedParent, parent) {
|
|
1504
|
+
const code = error.code;
|
|
1505
|
+
if (error instanceof Error && (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED")) {
|
|
1506
|
+
const routedPath = fileURLToPath(routedParent);
|
|
1507
|
+
const parentPath = fileURLToPath(parent);
|
|
1508
|
+
const originalMessage = error.message;
|
|
1509
|
+
const message = originalMessage.replaceAll(routedParent, parent).replaceAll(routedPath, parentPath);
|
|
1510
|
+
const stack = error.stack;
|
|
1511
|
+
error.message = message;
|
|
1512
|
+
/* v8 ignore next -- Node's resolver errors always carry a stack */
|
|
1513
|
+
if (stack !== void 0) error.stack = stack.replace(originalMessage, message);
|
|
1514
|
+
}
|
|
1515
|
+
throw error;
|
|
1516
|
+
}
|
|
1517
|
+
function throwWithoutCjsAnchor(error, anchor) {
|
|
1518
|
+
const resolved = error;
|
|
1519
|
+
const requireStack = resolved.requireStack;
|
|
1520
|
+
if (error instanceof Error && resolved.code === "MODULE_NOT_FOUND" && requireStack?.[0] !== void 0 && sameResolution(requireStack[0], anchor)) {
|
|
1521
|
+
const originalMessage = error.message;
|
|
1522
|
+
const originalBlock = `\nRequire stack:\n${requireStack.map((path) => `- ${path}`).join("\n")}`;
|
|
1523
|
+
const remaining = requireStack.slice(1);
|
|
1524
|
+
/* v8 ignore next -- routed calls always retain the original importing module */
|
|
1525
|
+
const replacement = remaining.length === 0 ? "" : `\nRequire stack:\n${remaining.map((path) => `- ${path}`).join("\n")}`;
|
|
1526
|
+
error.message = originalMessage.replace(originalBlock, replacement);
|
|
1527
|
+
resolved.requireStack = remaining;
|
|
1528
|
+
const stack = error.stack;
|
|
1529
|
+
/* v8 ignore next -- Node's resolver errors always carry a stack */
|
|
1530
|
+
if (stack !== void 0) error.stack = stack.replace(originalMessage, error.message);
|
|
1531
|
+
}
|
|
1532
|
+
throw error;
|
|
1533
|
+
}
|
|
1534
|
+
function assertEquivalent(actual, expected, request, parent) {
|
|
1535
|
+
if (sameResolution(actual, expected)) return;
|
|
1536
|
+
throw new Error(`profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk resolved ${actual}, generation resolved ${expected}`);
|
|
1537
|
+
}
|
|
1538
|
+
function assertOptionalEquivalent(actual, expected, request, parent) {
|
|
1539
|
+
if (actual === void 0 && expected === void 0) return;
|
|
1540
|
+
if (actual !== void 0 && expected !== void 0 && sameResolution(actual, expected)) return;
|
|
1541
|
+
throw new Error(`profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk selected ${actual ?? "nothing"}, generation selected ${expected ?? "nothing"}`);
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Install one profile generation on Node's default ESM and CommonJS resolvers.
|
|
1545
|
+
* @param generation - complete package table and profile scope.
|
|
1546
|
+
* @param behavior - enforce the generation, or verify a materialized generation.
|
|
1547
|
+
* @returns a registration that replaces the generation or restores the native methods.
|
|
1548
|
+
*/
|
|
1549
|
+
function installProfileResolution(generation, behavior = "enforce") {
|
|
1550
|
+
const router = new ResolutionRouter(generation);
|
|
1551
|
+
const { esm, esmDefaultResolve, esmConditions, cjs, cjsConditions, modern } = internalModules();
|
|
1552
|
+
const esmScope = /* @__PURE__ */ new Map();
|
|
1553
|
+
const profilePaths = [...prefixes(generation.profilesDir), ...generation.profileDir === void 0 ? [] : prefixes(generation.profileDir)];
|
|
1554
|
+
const profileUrls = profilePaths.map((path) => pathToFileURL(path).href);
|
|
1555
|
+
let recentEsmParent;
|
|
1556
|
+
let recentEsmScoped = false;
|
|
1557
|
+
let delegatedEsm;
|
|
1558
|
+
const adaptEsm = (native) => {
|
|
1559
|
+
const adapted = (request, parent, attributes) => {
|
|
1560
|
+
const delegated = delegatedEsm;
|
|
1561
|
+
/* v8 ignore next -- reentry requires a separate synchronous Node hook; supported launches install none */
|
|
1562
|
+
if (delegated !== void 0 && delegated.parent === parent && delegated.request === request) return native(request, parent, attributes);
|
|
1563
|
+
if (parent === void 0) return native(request, parent, attributes);
|
|
1564
|
+
let scoped = recentEsmParent === parent ? recentEsmScoped : esmScope.get(parent);
|
|
1565
|
+
if (scoped === void 0) {
|
|
1566
|
+
scoped = startsWithin(parent, profileUrls);
|
|
1567
|
+
esmScope.set(parent, scoped);
|
|
1568
|
+
}
|
|
1569
|
+
if (recentEsmParent !== parent) {
|
|
1570
|
+
recentEsmParent = parent;
|
|
1571
|
+
recentEsmScoped = scoped;
|
|
1572
|
+
}
|
|
1573
|
+
if (!scoped) return native(request, parent, attributes);
|
|
1574
|
+
const state = router.routeUrl(request, parent);
|
|
1575
|
+
if (state === void 0) {
|
|
1576
|
+
const target = request[0] === "#" ? packageImportsTarget(fileURLToPath(parent), request, esmConditions) : void 0;
|
|
1577
|
+
if (target === void 0) return native(request, parent, attributes);
|
|
1578
|
+
const restoreImporter = (error) => throwWithImporter(error, target.parentURL, parent);
|
|
1579
|
+
let expected;
|
|
1580
|
+
try {
|
|
1581
|
+
expected = adapted(target.specifier, target.parentURL, attributes);
|
|
1582
|
+
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1583
|
+
if (expected instanceof Promise) expected = expected.catch(restoreImporter);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return restoreImporter(error);
|
|
1586
|
+
}
|
|
1587
|
+
if (behavior === "enforce") return expected;
|
|
1588
|
+
const actual = native(request, parent, attributes);
|
|
1589
|
+
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
|
1590
|
+
if (expected instanceof Promise || actual instanceof Promise) return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
|
1591
|
+
assertEquivalent(resolved.url, wanted.url, request, parent);
|
|
1592
|
+
return resolved;
|
|
1593
|
+
});
|
|
1594
|
+
/* v8 ignore stop */
|
|
1595
|
+
assertEquivalent(actual.url, expected.url, request, parent);
|
|
1596
|
+
return actual;
|
|
1597
|
+
}
|
|
1598
|
+
const cacheable = attributes === EMPTY_ATTRIBUTES || Object.keys(attributes).length === 0;
|
|
1599
|
+
if (cacheable && state.esm !== void 0) return state.esm;
|
|
1600
|
+
const route = state.route;
|
|
1601
|
+
if (route.kind === "native") {
|
|
1602
|
+
const result = native(request, parent, attributes);
|
|
1603
|
+
if (cacheable && !(result instanceof Promise)) state.esm = result;
|
|
1604
|
+
return result;
|
|
1605
|
+
}
|
|
1606
|
+
const routedParent = pathToFileURL(route.kind === "fallback" ? route.entry.declarer : route.parent).href;
|
|
1607
|
+
if (behavior === "enforce") {
|
|
1608
|
+
const previous = delegatedEsm;
|
|
1609
|
+
delegatedEsm = {
|
|
1610
|
+
parent: routedParent,
|
|
1611
|
+
request
|
|
1612
|
+
};
|
|
1613
|
+
const restoreImporter = (error) => throwWithImporter(error, routedParent, parent);
|
|
1614
|
+
try {
|
|
1615
|
+
let result;
|
|
1616
|
+
try {
|
|
1617
|
+
result = native(request, routedParent, attributes);
|
|
1618
|
+
} catch (error) {
|
|
1619
|
+
return restoreImporter(error);
|
|
1620
|
+
}
|
|
1621
|
+
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1622
|
+
if (result instanceof Promise) return result.catch(restoreImporter);
|
|
1623
|
+
if (cacheable) state.esm = result;
|
|
1624
|
+
return result;
|
|
1625
|
+
} finally {
|
|
1626
|
+
delegatedEsm = previous;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
const actual = native(request, parent, attributes);
|
|
1630
|
+
const previous = delegatedEsm;
|
|
1631
|
+
delegatedEsm = {
|
|
1632
|
+
parent: routedParent,
|
|
1633
|
+
request
|
|
1634
|
+
};
|
|
1635
|
+
const restoreImporter = (error) => throwWithImporter(error, routedParent, parent);
|
|
1636
|
+
try {
|
|
1637
|
+
let expected;
|
|
1638
|
+
try {
|
|
1639
|
+
const result = native(request, routedParent, attributes);
|
|
1640
|
+
expected = result;
|
|
1641
|
+
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1642
|
+
if (result instanceof Promise) expected = result.catch(restoreImporter);
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
return restoreImporter(error);
|
|
1645
|
+
}
|
|
1646
|
+
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
|
1647
|
+
if (expected instanceof Promise || actual instanceof Promise) return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
|
1648
|
+
assertEquivalent(resolved.url, wanted.url, request, parent);
|
|
1649
|
+
if (cacheable) state.esm = resolved;
|
|
1650
|
+
return resolved;
|
|
1651
|
+
});
|
|
1652
|
+
/* v8 ignore stop */
|
|
1653
|
+
assertEquivalent(actual.url, expected.url, request, parent);
|
|
1654
|
+
if (cacheable) state.esm = actual;
|
|
1655
|
+
return actual;
|
|
1656
|
+
} finally {
|
|
1657
|
+
delegatedEsm = previous;
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
return adapted;
|
|
1661
|
+
};
|
|
1662
|
+
let restoreEsm;
|
|
1663
|
+
/* v8 ignore else -- CI coverage runs Node 24 v2; the Node 22 matrix exercises the v1 adapter */
|
|
1664
|
+
if (modern) {
|
|
1665
|
+
const loader = esm;
|
|
1666
|
+
const original = Reflect.get(loader, "resolveSync");
|
|
1667
|
+
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, parent, {
|
|
1668
|
+
specifier: request,
|
|
1669
|
+
attributes
|
|
1670
|
+
}));
|
|
1671
|
+
const wrapped = (parent, request, ...rest) => rest.length ? Reflect.apply(original, loader, [
|
|
1672
|
+
parent,
|
|
1673
|
+
request,
|
|
1674
|
+
...rest
|
|
1675
|
+
]) : resolveRequest(request.specifier, parent, request.attributes ?? EMPTY_ATTRIBUTES);
|
|
1676
|
+
loader.resolveSync = wrapped;
|
|
1677
|
+
restoreEsm = () => {
|
|
1678
|
+
/* v8 ignore else -- registrations are disposed in reverse installation order */
|
|
1679
|
+
if (loader.resolveSync === wrapped) loader.resolveSync = original;
|
|
1680
|
+
};
|
|
1681
|
+
} else {
|
|
1682
|
+
const loader = esm;
|
|
1683
|
+
const original = Reflect.get(loader, "resolve");
|
|
1684
|
+
const originalSync = Reflect.get(loader, "resolveSync");
|
|
1685
|
+
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, request, parent, attributes));
|
|
1686
|
+
const resolveRequestSync = adaptEsm((request, parent, attributes) => originalSync.call(loader, request, parent, attributes));
|
|
1687
|
+
const wrapped = (request, parent, attributes = EMPTY_ATTRIBUTES) => resolveRequest(request, parent, attributes);
|
|
1688
|
+
const wrappedSync = (request, parent, attributes = EMPTY_ATTRIBUTES) => resolveRequestSync(request, parent, attributes);
|
|
1689
|
+
loader.resolve = wrapped;
|
|
1690
|
+
loader.resolveSync = wrappedSync;
|
|
1691
|
+
restoreEsm = () => {
|
|
1692
|
+
if (loader.resolve === wrapped) loader.resolve = original;
|
|
1693
|
+
if (loader.resolveSync === wrappedSync) loader.resolveSync = originalSync;
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
const originalFilename = Reflect.get(cjs, "_resolveFilename");
|
|
1697
|
+
let delegatedCjs = 0;
|
|
1698
|
+
const resolveRoutedCjs = (request, routed, parent, main, options) => {
|
|
1699
|
+
const anchor = routed.kind === "fallback" ? routed.entry.declarer : routed.parent;
|
|
1700
|
+
const synthetic = new cjs(anchor);
|
|
1701
|
+
synthetic.parent = parent;
|
|
1702
|
+
synthetic.filename = anchor;
|
|
1703
|
+
synthetic.paths = routed.kind === "fallback" ? packageSearchPaths(routed.entry, request, cjs) : cjs._nodeModulePaths(dirname(anchor));
|
|
1704
|
+
try {
|
|
1705
|
+
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
return throwWithoutCjsAnchor(error, anchor);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
const resolveNativeCjs = (request, searchPaths, parent, parentFilename, main, conditions) => {
|
|
1711
|
+
const synthetic = new cjs(parentFilename);
|
|
1712
|
+
synthetic.parent = parent;
|
|
1713
|
+
synthetic.filename = parentFilename;
|
|
1714
|
+
synthetic.paths = [...searchPaths];
|
|
1715
|
+
const options = conditions === void 0 ? void 0 : { conditions };
|
|
1716
|
+
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1717
|
+
};
|
|
1718
|
+
const resolvePackageImportCjs = (target, conditions) => {
|
|
1719
|
+
const state = router.routeUrl(target.specifier, target.parentURL);
|
|
1720
|
+
const resolveFrom = (parentURL) => fileURLToPath(esmDefaultResolve(target.specifier, {
|
|
1721
|
+
parentURL,
|
|
1722
|
+
conditions: [...conditions]
|
|
1723
|
+
}).url);
|
|
1724
|
+
/* v8 ignore next -- the target manifest was found inside the established profile scope */
|
|
1725
|
+
if (state === void 0) return resolveFrom(target.parentURL);
|
|
1726
|
+
if (state.route.kind === "native") return resolveFrom(target.parentURL);
|
|
1727
|
+
const route = state.route;
|
|
1728
|
+
if (route.kind === "after-fallback") return resolveFrom(pathToFileURL(route.parent).href);
|
|
1729
|
+
return resolveFrom(pathToFileURL(route.entry.declarer).href);
|
|
1730
|
+
};
|
|
1731
|
+
const wrappedFilename = (request, parent, main, options) => {
|
|
1732
|
+
if (delegatedCjs || !parent?.filename) return originalFilename.call(cjs, request, parent, main, options);
|
|
1733
|
+
const parentFilename = parent.filename;
|
|
1734
|
+
const cacheable = options?.paths === void 0 && options?.conditions === void 0;
|
|
1735
|
+
const explicitPaths = Array.isArray(options?.paths) ? options.paths : void 0;
|
|
1736
|
+
if (explicitPaths !== void 0 && router.nativeSelfReference(request, parentFilename)) return originalFilename.call(cjs, request, parent, main, options);
|
|
1737
|
+
const explicit = explicitPaths === void 0 ? void 0 : router.explicitRoute(request, explicitPaths);
|
|
1738
|
+
if (options?.paths !== void 0 && explicit === void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1739
|
+
if (explicit !== void 0 && explicitPaths !== void 0 && explicit.index > 0) try {
|
|
1740
|
+
return originalFilename.call(cjs, request, parent, main, {
|
|
1741
|
+
...options,
|
|
1742
|
+
paths: explicitPaths.slice(0, explicit.index)
|
|
1743
|
+
});
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1746
|
+
}
|
|
1747
|
+
const state = router.routePath(request, explicit?.parent ?? parentFilename, (searchPaths) => resolveNativeCjs(request, searchPaths, parent, parentFilename, main, options?.conditions), explicit === void 0 && cacheable);
|
|
1748
|
+
if (state === void 0) {
|
|
1749
|
+
const scoped = startsWithin(parentFilename, profilePaths);
|
|
1750
|
+
const conditions = options?.conditions ?? cjsConditions;
|
|
1751
|
+
const target = request[0] === "#" && scoped ? packageImportsTarget(parentFilename, request, conditions) : void 0;
|
|
1752
|
+
if (target === void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1753
|
+
const expected = resolvePackageImportCjs(target, conditions);
|
|
1754
|
+
if (behavior === "enforce") return expected;
|
|
1755
|
+
const actual = originalFilename.call(cjs, request, parent, main, options);
|
|
1756
|
+
assertEquivalent(actual, expected, request, parentFilename);
|
|
1757
|
+
return actual;
|
|
1758
|
+
}
|
|
1759
|
+
if (cacheable && state.cjs !== void 0) return state.cjs;
|
|
1760
|
+
const route = state.route;
|
|
1761
|
+
if (route.kind === "native") {
|
|
1762
|
+
const result = originalFilename.call(cjs, request, parent, main, options);
|
|
1763
|
+
if (cacheable) state.cjs = result;
|
|
1764
|
+
return result;
|
|
1765
|
+
}
|
|
1766
|
+
if (route.kind === "after-fallback" && explicit !== void 0 && explicitPaths !== void 0) try {
|
|
1767
|
+
return originalFilename.call(cjs, request, parent, main, {
|
|
1768
|
+
...options,
|
|
1769
|
+
paths: [dirname(route.parent)]
|
|
1770
|
+
});
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1773
|
+
const remaining = explicitPaths.slice(explicit.index + 1);
|
|
1774
|
+
if (remaining.length === 0) throw error;
|
|
1775
|
+
return wrappedFilename(request, parent, main, {
|
|
1776
|
+
...options,
|
|
1777
|
+
paths: remaining
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
delegatedCjs++;
|
|
1781
|
+
try {
|
|
1782
|
+
const routedOptions = options?.conditions === void 0 ? void 0 : { conditions: options.conditions };
|
|
1783
|
+
let expected;
|
|
1784
|
+
try {
|
|
1785
|
+
expected = resolveRoutedCjs(request, route, parent, main, routedOptions);
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
if (route.kind !== "fallback" || !isUnselectedPackageMiss(error)) throw error;
|
|
1788
|
+
try {
|
|
1789
|
+
expected = resolveRoutedCjs(request, {
|
|
1790
|
+
kind: "after-fallback",
|
|
1791
|
+
parent: route.after
|
|
1792
|
+
}, parent, main, routedOptions);
|
|
1793
|
+
} catch (afterError) {
|
|
1794
|
+
const remaining = explicit === void 0 || explicitPaths === void 0 ? [] : explicitPaths.slice(explicit.index + 1);
|
|
1795
|
+
if (!isUnselectedPackageMiss(afterError) || remaining.length === 0) throw afterError;
|
|
1796
|
+
delegatedCjs--;
|
|
1797
|
+
try {
|
|
1798
|
+
return wrappedFilename(request, parent, main, {
|
|
1799
|
+
...options,
|
|
1800
|
+
paths: remaining
|
|
1801
|
+
});
|
|
1802
|
+
} finally {
|
|
1803
|
+
delegatedCjs++;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (behavior === "enforce") {
|
|
1808
|
+
if (cacheable) state.cjs = expected;
|
|
1809
|
+
return expected;
|
|
1810
|
+
}
|
|
1811
|
+
const actual = originalFilename.call(cjs, request, parent, main, options);
|
|
1812
|
+
assertEquivalent(actual, expected, request, parentFilename);
|
|
1813
|
+
if (cacheable) state.cjs = actual;
|
|
1814
|
+
return actual;
|
|
1815
|
+
} finally {
|
|
1816
|
+
delegatedCjs--;
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
cjs._resolveFilename = wrappedFilename;
|
|
1820
|
+
return {
|
|
1821
|
+
packageDir(specifier, parentURL) {
|
|
1822
|
+
const expected = router.packageDir(specifier, parentURL);
|
|
1823
|
+
if (behavior !== "verify" || !startsWithin(parentURL, profileUrls)) return expected;
|
|
1824
|
+
const name = barePackageName(specifier);
|
|
1825
|
+
if (name === void 0) return expected;
|
|
1826
|
+
let parent;
|
|
1827
|
+
try {
|
|
1828
|
+
parent = fileURLToPath(parentURL);
|
|
1829
|
+
} catch {
|
|
1830
|
+
return expected;
|
|
1831
|
+
}
|
|
1832
|
+
assertOptionalEquivalent(nativePackageDir(parent, name), expected, specifier, parentURL);
|
|
1833
|
+
return expected;
|
|
1834
|
+
},
|
|
1835
|
+
replace(next) {
|
|
1836
|
+
router.replace(next);
|
|
1837
|
+
},
|
|
1838
|
+
dispose() {
|
|
1839
|
+
/* v8 ignore else -- registrations are disposed in reverse installation order */
|
|
1840
|
+
if (cjs._resolveFilename === wrappedFilename) cjs._resolveFilename = originalFilename;
|
|
1841
|
+
restoreEsm();
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Publish one generation for Harness-owned Workers.
|
|
1847
|
+
* @param generation - complete package table and profile scope.
|
|
1848
|
+
* @param behavior - enforce or verify the generation in newly created Workers.
|
|
1849
|
+
* @returns a disposer restoring the previous thread environment data.
|
|
1850
|
+
*/
|
|
1851
|
+
function registerWorkerResolution(generation, behavior = "enforce") {
|
|
1852
|
+
const previous = getEnvironmentData(WORKER_RESOLUTION_KEY);
|
|
1853
|
+
setEnvironmentData(WORKER_RESOLUTION_KEY, {
|
|
1854
|
+
generation,
|
|
1855
|
+
behavior
|
|
1856
|
+
});
|
|
1857
|
+
return () => {
|
|
1858
|
+
setEnvironmentData(WORKER_RESOLUTION_KEY, previous);
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
//#endregion
|
|
1862
|
+
//#region lib/types/profile-resolution/service.js
|
|
1863
|
+
/** Package metadata resolved through one profile resolution registration. */
|
|
1864
|
+
function readPackage(dir, fallbackName) {
|
|
1865
|
+
const manifestPath = join(dir, "package.json");
|
|
1866
|
+
if (!existsSync(manifestPath)) return void 0;
|
|
1867
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1868
|
+
const name = manifest.name;
|
|
1869
|
+
const version = manifest.version;
|
|
1870
|
+
return {
|
|
1871
|
+
name: typeof name === "string" ? name : fallbackName,
|
|
1872
|
+
version: typeof version === "string" ? version : void 0,
|
|
1873
|
+
dir,
|
|
1874
|
+
manifestPath,
|
|
1875
|
+
manifest
|
|
1876
|
+
};
|
|
1877
|
+
}
|
|
1878
|
+
/** Package lookup shared by metadata consumers in one profile process. */
|
|
1879
|
+
var PluginPackages = class extends Service {
|
|
1880
|
+
packages = /* @__PURE__ */ new Map();
|
|
1881
|
+
resolver;
|
|
1882
|
+
behavior;
|
|
1883
|
+
disposeWorkerResolution;
|
|
1884
|
+
constructor(ctx, config = {}) {
|
|
1885
|
+
super(ctx, "pluginPackages");
|
|
1886
|
+
this.behavior = config.behavior ?? "enforce";
|
|
1887
|
+
if (config.generation === void 0) return;
|
|
1888
|
+
const resolver = installProfileResolution(config.generation, this.behavior);
|
|
1889
|
+
this.disposeWorkerResolution = registerWorkerResolution(config.generation, this.behavior);
|
|
1890
|
+
this.resolver = resolver;
|
|
1891
|
+
ctx.effect(() => () => {
|
|
1892
|
+
this.disposeWorkerResolution?.();
|
|
1893
|
+
resolver.dispose();
|
|
1894
|
+
}, "profile package resolution");
|
|
1895
|
+
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Publish an additive generation for this process and subsequently created Workers.
|
|
1898
|
+
* @param generation - fully constructed successor generation.
|
|
1899
|
+
*/
|
|
1900
|
+
replace(generation) {
|
|
1901
|
+
if (this.resolver === void 0) throw new Error("plugin-packages: runtime resolution is not installed");
|
|
1902
|
+
this.resolver.replace(generation);
|
|
1903
|
+
this.packages = /* @__PURE__ */ new Map();
|
|
1904
|
+
this.disposeWorkerResolution?.();
|
|
1905
|
+
this.disposeWorkerResolution = registerWorkerResolution(generation, this.behavior);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Locate the package named by a specifier without requiring a package export.
|
|
1909
|
+
* @param specifier - module specifier whose package owns the requested module.
|
|
1910
|
+
* @param parentURL - URL whose Node lookup order applies.
|
|
1911
|
+
* @returns the parsed package, or undefined when no package owns the request.
|
|
1912
|
+
*/
|
|
1913
|
+
packageOf(specifier, parentURL) {
|
|
1914
|
+
const name = barePackageName(specifier);
|
|
1915
|
+
if (name === void 0) return void 0;
|
|
1916
|
+
const dir = this.resolver === void 0 ? packageDirFromParent(name, parentURL) : this.resolver.packageDir(name, parentURL);
|
|
1917
|
+
if (dir === void 0) return void 0;
|
|
1918
|
+
const key = JSON.stringify({
|
|
1919
|
+
dir,
|
|
1920
|
+
name
|
|
1921
|
+
});
|
|
1922
|
+
if (!this.packages.has(key)) this.packages.set(key, readPackage(dir, name));
|
|
1923
|
+
return this.packages.get(key);
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
function packageDirFromParent(name, parentURL) {
|
|
1927
|
+
for (const searchPath of createRequire(parentURL).resolve.paths(name)) {
|
|
1928
|
+
const candidate = join(searchPath, name);
|
|
1929
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
//#endregion
|
|
911
1933
|
//#region lib/types/index.js
|
|
912
1934
|
/**
|
|
913
1935
|
* Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored
|
|
@@ -1099,33 +2121,38 @@ function loadLayeredEnv(binName, cwd = process.cwd(), warn = (line) => void proc
|
|
|
1099
2121
|
}
|
|
1100
2122
|
const bootstrapIncludes = /* @__PURE__ */ new WeakMap();
|
|
1101
2123
|
const userPatchesSchema = entryListSchema;
|
|
1102
|
-
/**
|
|
1103
|
-
*
|
|
1104
|
-
* @param
|
|
1105
|
-
* @param
|
|
1106
|
-
* @
|
|
1107
|
-
* @
|
|
2124
|
+
/** Apply one complete patch generation and wait for Loader activation diagnostics.
|
|
2125
|
+
* @param ctx Booted root context.
|
|
2126
|
+
* @param patches Complete ordered patch list.
|
|
2127
|
+
* @param binName Diagnostic prefix.
|
|
2128
|
+
* @param requiredIds Explicit enablement targets whose existing failures also reject reconciliation.
|
|
2129
|
+
* @returns Diagnostics for unchanged pre-existing inactive entries; new or changed failures reject.
|
|
1108
2130
|
*/
|
|
1109
|
-
async function
|
|
1110
|
-
const { binName, filename, compose = (patches) => patches } = options;
|
|
1111
|
-
const hmr = ctx.get("hmr");
|
|
1112
|
-
if (hmr === void 0) throw new Error(`${binName}: user patch-layer watching requires the Cordis HMR service`);
|
|
2131
|
+
async function reconcileProfilePatches(ctx, patches, binName, requiredIds = []) {
|
|
1113
2132
|
const entry = bootstrapIncludes.get(ctx);
|
|
1114
|
-
if (entry === void 0) throw new Error(`${binName}:
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
2133
|
+
if (entry === void 0) throw new Error(`${binName}: profile reload requires the root Include entry`);
|
|
2134
|
+
const previousFailures = (await inactiveEntries(ctx)).map((failure) => ({
|
|
2135
|
+
...failure,
|
|
2136
|
+
diagnostic: inactiveDiagnostic(failure),
|
|
2137
|
+
fiber: failure.entry.fiber,
|
|
2138
|
+
options: JSON.stringify(failure.entry.options)
|
|
2139
|
+
}));
|
|
2140
|
+
const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [{
|
|
2141
|
+
fiber: row.fiber,
|
|
2142
|
+
failed: row.fiber.state === FIBER_FAILED || row.fiber.state === FIBER_DISPOSED
|
|
2143
|
+
}]);
|
|
2144
|
+
const { patches: _previous, ...includeConfig } = entry.options.config;
|
|
2145
|
+
await entry.update({ config: {
|
|
2146
|
+
...includeConfig,
|
|
2147
|
+
patches
|
|
2148
|
+
} });
|
|
2149
|
+
const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await()));
|
|
2150
|
+
await ctx.loader.await();
|
|
2151
|
+
const failures = await inactiveEntries(ctx);
|
|
2152
|
+
const introduced = failures.filter((failure) => requiredIds.includes(failure.entry.options.id) || !previousFailures.some((previous) => previous.entry === failure.entry && previous.fiber === failure.entry.fiber && previous.options === JSON.stringify(failure.entry.options) && previous.diagnostic === inactiveDiagnostic(failure)));
|
|
2153
|
+
if (introduced.length > 0) throw new Error(activationDiagnostic(binName, introduced).trimEnd());
|
|
2154
|
+
for (const [index, result] of results.entries()) if (result.status === "rejected" && !previousFibers[index]?.failed) throw result.reason;
|
|
2155
|
+
return failures.map(inactiveDiagnostic);
|
|
1129
2156
|
}
|
|
1130
2157
|
/**
|
|
1131
2158
|
* Load an optional patch-list file: a top-level YAML array of loader patch
|
|
@@ -1257,7 +2284,7 @@ function renderConfigDump(binName, absoluteConfigPath, layers, warn = (line) =>
|
|
|
1257
2284
|
};
|
|
1258
2285
|
let previous = base;
|
|
1259
2286
|
let previousWarnings = [];
|
|
1260
|
-
const
|
|
2287
|
+
const entryOrigins = base.map(() => ({
|
|
1261
2288
|
origin: baseLabel,
|
|
1262
2289
|
patchedBy: []
|
|
1263
2290
|
}));
|
|
@@ -1270,18 +2297,18 @@ function renderConfigDump(binName, absoluteConfigPath, layers, warn = (line) =>
|
|
|
1270
2297
|
composed = snapshot(count, warnings);
|
|
1271
2298
|
for (const line of warnings.slice(previousWarnings.length)) warn(`${binName}: [${layer.label}] ${line}`);
|
|
1272
2299
|
const before = previous.map((entry) => JSON.stringify(entry));
|
|
1273
|
-
for (let index = 0; index < composed.length; index += 1) if (index >= before.length)
|
|
2300
|
+
for (let index = 0; index < composed.length; index += 1) if (index >= before.length) entryOrigins.push({
|
|
1274
2301
|
origin: layer.label,
|
|
1275
2302
|
patchedBy: []
|
|
1276
2303
|
});
|
|
1277
|
-
else if (JSON.stringify(composed[index]) !== before[index])
|
|
2304
|
+
else if (JSON.stringify(composed[index]) !== before[index]) entryOrigins[index]?.patchedBy.push(layer.label);
|
|
1278
2305
|
previous = composed;
|
|
1279
2306
|
previousWarnings = warnings;
|
|
1280
2307
|
}
|
|
1281
|
-
return groupedDump(composed,
|
|
2308
|
+
return groupedDump(composed, entryOrigins);
|
|
1282
2309
|
}
|
|
1283
2310
|
/** Render the composed rows grouped under one source-and-patches comment per contiguous run. */
|
|
1284
|
-
function groupedDump(composed,
|
|
2311
|
+
function groupedDump(composed, entryOrigins) {
|
|
1285
2312
|
const lines = [];
|
|
1286
2313
|
let currentLabel;
|
|
1287
2314
|
let group = [];
|
|
@@ -1295,7 +2322,7 @@ function groupedDump(composed, provenance) {
|
|
|
1295
2322
|
group = [];
|
|
1296
2323
|
};
|
|
1297
2324
|
for (let index = 0; index < composed.length; index += 1) {
|
|
1298
|
-
const record =
|
|
2325
|
+
const record = entryOrigins[index];
|
|
1299
2326
|
/* v8 ignore next -- this array is index-aligned with composed by construction */
|
|
1300
2327
|
if (record === void 0) continue;
|
|
1301
2328
|
const label = record.patchedBy.length === 0 ? record.origin : `${record.origin}, patched by ${record.patchedBy.join(", ")}`;
|
|
@@ -1317,7 +2344,7 @@ function groupedDump(composed, provenance) {
|
|
|
1317
2344
|
* names; relative names continue to resolve beside the configuration file.
|
|
1318
2345
|
* @returns the created root Include entry, or `undefined` when a surface
|
|
1319
2346
|
* disposed the whole tree (taking the Loader service with it) while the
|
|
1320
|
-
*
|
|
2347
|
+
* entry creation was in flight.
|
|
1321
2348
|
*/
|
|
1322
2349
|
async function mountRootInclude(ctx, absoluteConfigPath, patches = [], bareModuleBaseUrl) {
|
|
1323
2350
|
ctx.loader.builtins.include = bareModuleBaseUrl === void 0 ? Include : class HostResolvedRootInclude extends Include {
|
|
@@ -1372,7 +2399,7 @@ const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
|
|
|
1372
2399
|
/**
|
|
1373
2400
|
* Install before boot to turn a late unhandled plugin-init rejection into one
|
|
1374
2401
|
* labelled stderr diagnostic and `exit(1)`. A rejection already included by
|
|
1375
|
-
* {@link
|
|
2402
|
+
* {@link auditStartupEntries} is ignored during its process checkpoint;
|
|
1376
2403
|
* every other rejection remains fatal. Stdout remains untouched for ACP; the
|
|
1377
2404
|
* returned function removes the handler.
|
|
1378
2405
|
*
|
|
@@ -1425,50 +2452,99 @@ function installFailLoud(binName, proc = process, release) {
|
|
|
1425
2452
|
return uninstall;
|
|
1426
2453
|
}
|
|
1427
2454
|
/**
|
|
1428
|
-
* After the tree settles, reject entries with no fiber and name every plugin
|
|
1429
|
-
* whose module failed to resolve. Disabled entries are the only valid
|
|
1430
|
-
* fiber-less state.
|
|
1431
|
-
* @param ctx - the settled context whose loader entries to audit.
|
|
1432
|
-
* @param binName - the diagnostic prefix on the thrown error.
|
|
1433
|
-
*/
|
|
1434
|
-
function assertEntriesLoaded(ctx, binName) {
|
|
1435
|
-
const failed = [...ctx.loader.entries()].filter((entry) => entry.fiber === void 0 && !entry.disabled);
|
|
1436
|
-
if (failed.length > 0) {
|
|
1437
|
-
const names = failed.map((entry) => entry.options.name).join(", ");
|
|
1438
|
-
throw new Error(`${binName}: plugin(s) failed to load: ${names}; Cordis startup failed because these plugin(s) could not be resolved (see the error(s) logged above)`);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
/**
|
|
1442
2455
|
* Value mirrors used because Cordis's const enum has no runtime object to import.
|
|
1443
|
-
* Keep aligned with `packages/
|
|
1444
|
-
* `packages/client/web/src/loader-status.ts`.
|
|
2456
|
+
* Keep aligned with `packages/client/web/src/loader-status.ts`.
|
|
1445
2457
|
*/
|
|
1446
2458
|
const FIBER_PENDING = 0;
|
|
1447
2459
|
const FIBER_ACTIVE = 2;
|
|
1448
2460
|
const FIBER_FAILED = 3;
|
|
1449
|
-
|
|
2461
|
+
const FIBER_DISPOSED = 4;
|
|
2462
|
+
/**
|
|
2463
|
+
* Entry ids whose presence defines a usable DSH application.
|
|
2464
|
+
*
|
|
2465
|
+
* The list is global rather than profile metadata. Missing or disabled ids do
|
|
2466
|
+
* not affect startup; an enabled listed entry must activate. The list covers
|
|
2467
|
+
* shared Agent execution, application endpoints, and Web bootstrap/transport.
|
|
2468
|
+
*/
|
|
2469
|
+
const requiredStartupEntryIds = new Set([
|
|
2470
|
+
"agent-loop",
|
|
2471
|
+
"webserver",
|
|
2472
|
+
"modules",
|
|
2473
|
+
"connection",
|
|
2474
|
+
"headless-runner",
|
|
2475
|
+
"acp",
|
|
2476
|
+
"sdk-jsonrpc-server"
|
|
2477
|
+
]);
|
|
2478
|
+
/** Render plugin stacks, nested causes, and aggregate member failures once per error. */
|
|
1450
2479
|
function formatActivationError(error) {
|
|
1451
|
-
|
|
2480
|
+
const details = [];
|
|
2481
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2482
|
+
function visit(value) {
|
|
2483
|
+
if (!(value instanceof Error)) {
|
|
2484
|
+
details.push(String(value));
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
if (seen.has(value)) return;
|
|
2488
|
+
seen.add(value);
|
|
2489
|
+
details.push(value.stack ?? value.message);
|
|
2490
|
+
if (value.cause !== void 0) visit(value.cause);
|
|
2491
|
+
if (value instanceof AggregateError) value.errors.forEach(visit);
|
|
2492
|
+
}
|
|
2493
|
+
visit(error);
|
|
2494
|
+
return details.join("\n");
|
|
1452
2495
|
}
|
|
2496
|
+
/** Startup audit failure with non-enumerable metadata and original failures as its cause. */
|
|
2497
|
+
var StartupError = class extends Error {
|
|
2498
|
+
entries;
|
|
2499
|
+
/** Root configuration and startup logs, attached by boot after disposal. */
|
|
2500
|
+
startup;
|
|
2501
|
+
/**
|
|
2502
|
+
* @param message - concise terminal diagnostic.
|
|
2503
|
+
* @param entries - inactive plugin metadata and original failure values.
|
|
2504
|
+
*/
|
|
2505
|
+
constructor(message, entries) {
|
|
2506
|
+
const failures = entries.flatMap(({ outcome }) => outcome.kind === "failed" ? [outcome.error] : []);
|
|
2507
|
+
super(message, failures.length > 0 ? { cause: new AggregateError(failures, "Plugin activation failures") } : void 0);
|
|
2508
|
+
this.entries = entries;
|
|
2509
|
+
Object.defineProperties(this, {
|
|
2510
|
+
entries: { enumerable: false },
|
|
2511
|
+
startup: { enumerable: false }
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
1453
2515
|
/**
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
* entries require no further wait; only failed fibers are awaited to recover
|
|
1458
|
-
* their private rejection reason.
|
|
1459
|
-
* @param ctx - the settled context whose Loader entries to audit.
|
|
1460
|
-
* @param binName - the diagnostic prefix on the thrown error.
|
|
1461
|
-
* @returns nothing when every enabled entry is active.
|
|
1462
|
-
* @throws after one process rejection checkpoint when an entry failed to
|
|
1463
|
-
* import, rejected during activation, or did not become active.
|
|
2516
|
+
* Collect Loader activation failures and disabled-expression errors. Failed
|
|
2517
|
+
* fibers are awaited to recover their recorded rejection reason and coalesce
|
|
2518
|
+
* duplicate Loader notifications through the next process rejection checkpoint.
|
|
1464
2519
|
*/
|
|
1465
|
-
async function
|
|
1466
|
-
assertEntriesLoaded(ctx, binName);
|
|
2520
|
+
async function inactiveEntries(ctx) {
|
|
1467
2521
|
const failures = [];
|
|
1468
2522
|
const rejectionReasons = [];
|
|
1469
2523
|
for (const entry of ctx.loader.entries()) {
|
|
2524
|
+
try {
|
|
2525
|
+
if (entry.disabled) continue;
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
failures.push({
|
|
2528
|
+
entry,
|
|
2529
|
+
outcome: {
|
|
2530
|
+
kind: "failed",
|
|
2531
|
+
error,
|
|
2532
|
+
phase: "disabled expression failed"
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
1470
2537
|
const fiber = entry.fiber;
|
|
1471
|
-
if (fiber === void 0
|
|
2538
|
+
if (fiber === void 0) {
|
|
2539
|
+
failures.push({
|
|
2540
|
+
entry,
|
|
2541
|
+
outcome: {
|
|
2542
|
+
kind: "failed",
|
|
2543
|
+
error: "failed to import"
|
|
2544
|
+
}
|
|
2545
|
+
});
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
1472
2548
|
const state = fiber.state;
|
|
1473
2549
|
if (state === FIBER_ACTIVE) continue;
|
|
1474
2550
|
if (state === FIBER_FAILED) {
|
|
@@ -1476,21 +2552,104 @@ async function assertEntriesActivated(ctx, binName) {
|
|
|
1476
2552
|
await fiber.await();
|
|
1477
2553
|
} catch (error) {
|
|
1478
2554
|
rejectionReasons.push(error);
|
|
1479
|
-
failures.push(
|
|
2555
|
+
failures.push({
|
|
2556
|
+
entry,
|
|
2557
|
+
outcome: {
|
|
2558
|
+
kind: "failed",
|
|
2559
|
+
error
|
|
2560
|
+
}
|
|
2561
|
+
});
|
|
1480
2562
|
}
|
|
1481
2563
|
continue;
|
|
1482
2564
|
}
|
|
1483
2565
|
if (state === FIBER_PENDING) {
|
|
1484
2566
|
const missing = Object.keys(fiber.inject).filter((service) => fiber.ctx.get(service) === void 0);
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2567
|
+
failures.push({
|
|
2568
|
+
entry,
|
|
2569
|
+
outcome: {
|
|
2570
|
+
kind: "pending",
|
|
2571
|
+
missing
|
|
2572
|
+
}
|
|
2573
|
+
});
|
|
2574
|
+
} else failures.push({
|
|
2575
|
+
entry,
|
|
2576
|
+
outcome: {
|
|
2577
|
+
kind: "failed",
|
|
2578
|
+
error: `fiber state ${String(state)}`
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
if (rejectionReasons.length > 0) await observeLoaderRejectionCheckpoint(rejectionReasons);
|
|
2583
|
+
return failures;
|
|
2584
|
+
}
|
|
2585
|
+
/** Render one failed plugin's original error and activation phase. */
|
|
2586
|
+
function failureDetail(outcome) {
|
|
2587
|
+
return `${outcome.phase === void 0 ? "" : `${outcome.phase}: `}${formatActivationError(outcome.error)}`;
|
|
2588
|
+
}
|
|
2589
|
+
/** Render optional-only warnings without changing startup policy. */
|
|
2590
|
+
function activationDiagnostic(binName, failures) {
|
|
2591
|
+
const noun = failures.length === 1 ? "entry" : "entries";
|
|
2592
|
+
return `${binName}: warning: ${String(failures.length)} ${noun} did not activate\n${failures.map(inactiveDiagnostic).join("\n")}\n`;
|
|
2593
|
+
}
|
|
2594
|
+
/** Stable per-entry text for reload comparisons and optional warnings. */
|
|
2595
|
+
function inactiveDiagnostic({ entry, outcome }) {
|
|
2596
|
+
const detail = outcome.kind === "failed" ? failureDetail(outcome) : `pending (waiting for ${outcome.missing.length === 1 ? "service" : "services"}: ${outcome.missing.join(", ") || "unknown"})`;
|
|
2597
|
+
return `${entry.options.id} (${entry.options.name}): ${detail}`;
|
|
2598
|
+
}
|
|
2599
|
+
/** Group startup failures and pending services, marking every required entry. */
|
|
2600
|
+
function startupDiagnostic(binName, failures, required) {
|
|
2601
|
+
const lines = [`${binName}: startup failed: ${String(required.size)} required ${required.size === 1 ? "plugin" : "plugins"} did not activate`];
|
|
2602
|
+
const failed = failures.flatMap(({ entry, outcome }) => outcome.kind === "failed" ? [{
|
|
2603
|
+
entry,
|
|
2604
|
+
outcome
|
|
2605
|
+
}] : []);
|
|
2606
|
+
const pending = failures.flatMap(({ entry, outcome }) => outcome.kind === "pending" ? [{
|
|
2607
|
+
entry,
|
|
2608
|
+
outcome
|
|
2609
|
+
}] : []);
|
|
2610
|
+
pending.sort((left, right) => Number(required.has(right.entry)) - Number(required.has(left.entry)));
|
|
2611
|
+
const label = (entry) => `${entry.options.id}${required.has(entry) ? " (required)" : ""}`;
|
|
2612
|
+
if (failed.length > 0) {
|
|
2613
|
+
lines.push("", `Failed plugins (${String(failed.length)}):`);
|
|
2614
|
+
for (const { entry, outcome } of failed) {
|
|
2615
|
+
lines.push(` ${label(entry)}`, ` Package: ${entry.options.name}`);
|
|
2616
|
+
lines.push(...failureDetail(outcome).split("\n").map((line) => ` ${line}`));
|
|
2617
|
+
}
|
|
1488
2618
|
}
|
|
1489
|
-
if (
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2619
|
+
if (pending.length > 0) {
|
|
2620
|
+
const width = Math.max(6, ...pending.map(({ entry }) => label(entry).length)) + 2;
|
|
2621
|
+
lines.push("", `Plugins waiting for services (${String(pending.length)}):`, ` ${"Plugin".padEnd(width)}Missing services`);
|
|
2622
|
+
for (const { entry, outcome } of pending) lines.push(` ${label(entry).padEnd(width)}${outcome.missing.join(", ") || "unknown"}`);
|
|
1493
2623
|
}
|
|
2624
|
+
return lines.join("\n");
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Apply DSH startup policy to a settled Loader tree.
|
|
2628
|
+
*
|
|
2629
|
+
* Inactive entries from the global required list reject startup. Other
|
|
2630
|
+
* inactive entries join that failure diagnostic, or produce one warning when
|
|
2631
|
+
* no required entry failed and leave successful siblings running.
|
|
2632
|
+
* Required ids absent from the tree, and disabled required entries, are ignored.
|
|
2633
|
+
* A throwing disabled expression is an entry failure, not a disabled entry.
|
|
2634
|
+
* The bootstrap Include must activate so unreadable or invalid root config is fatal.
|
|
2635
|
+
* @param ctx - the settled context whose Loader entries to audit.
|
|
2636
|
+
* @param binName - the prefix on startup diagnostics.
|
|
2637
|
+
* @param warn - sink for optional-entry warnings.
|
|
2638
|
+
* @returns after optional warnings if required startup checks pass.
|
|
2639
|
+
* @throws {@link StartupError} when the bootstrap Include or a required entry is inactive or its disabled expression throws;
|
|
2640
|
+
* its message includes optional failures too.
|
|
2641
|
+
*/
|
|
2642
|
+
async function auditStartupEntries(ctx, binName, warn = (line) => void process.stderr.write(line)) {
|
|
2643
|
+
const failures = await inactiveEntries(ctx);
|
|
2644
|
+
const required = new Set(failures.filter(({ entry }) => entry === bootstrapIncludes.get(ctx) || requiredStartupEntryIds.has(entry.options.id)).map(({ entry }) => entry));
|
|
2645
|
+
if (required.size > 0) throw new StartupError(startupDiagnostic(binName, failures, required), failures.map(({ entry, outcome }) => ({
|
|
2646
|
+
id: entry.options.id,
|
|
2647
|
+
module: entry.options.name,
|
|
2648
|
+
required: required.has(entry),
|
|
2649
|
+
fiberState: entry.fiber?.state,
|
|
2650
|
+
outcome
|
|
2651
|
+
})));
|
|
2652
|
+
if (failures.length > 0) warn(activationDiagnostic(binName, failures));
|
|
1494
2653
|
}
|
|
1495
2654
|
/**
|
|
1496
2655
|
* Boot the Loader against `absoluteConfigPath` and return only after the whole
|
|
@@ -1500,12 +2659,11 @@ async function assertEntriesActivated(ctx, binName) {
|
|
|
1500
2659
|
* is statically imported and mounted as the `cordis:include` builtin, loading
|
|
1501
2660
|
* through the ambient module pipeline (vite/tsx/plain ESM). The package build
|
|
1502
2661
|
* embeds Include while leaving Loader external, so the built include tree and
|
|
1503
|
-
* host share one Loader peer. Loader
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
* covered by {@link installFailLoud}. Built bins need the Loader's native
|
|
2662
|
+
* host share one Loader peer. Loader settlement drains entry work without
|
|
2663
|
+
* rejecting the whole tree. The final {@link auditStartupEntries} call rejects
|
|
2664
|
+
* failures in the global required list and warns about other failed, missing,
|
|
2665
|
+
* and pending entries while successful siblings remain active. Later unhandled
|
|
2666
|
+
* rejections remain covered by {@link installFailLoud}. Built bins need the Loader's native
|
|
1509
2667
|
* helper for bare plugin specifiers; relative specifiers do not.
|
|
1510
2668
|
* @param binName - the diagnostic prefix for load-failure errors.
|
|
1511
2669
|
* @param absoluteConfigPath - the config to include; must already be absolute
|
|
@@ -1516,33 +2674,70 @@ async function assertEntriesActivated(ctx, binName) {
|
|
|
1516
2674
|
* @param bareModuleBaseUrl - optional installed-host base for bare package
|
|
1517
2675
|
* names; use it when the host, rather than the configuration project, owns the
|
|
1518
2676
|
* complete plugin set.
|
|
1519
|
-
* @returns the root context
|
|
2677
|
+
* @returns the root context after the initial startup audit, or as soon as a
|
|
1520
2678
|
* surface disposed the tree while startup was still in flight.
|
|
1521
|
-
* @throws
|
|
2679
|
+
* @throws {@link StartupError} for an inactive required entry, including all inactive plugins in its message;
|
|
2680
|
+
* otherwise a labelled error after disposing the partial context — `host
|
|
1522
2681
|
* preparation failed` when `prepare` threw before any config-tree entry
|
|
1523
|
-
* mounted, `plugin tree failed to load` afterwards.
|
|
2682
|
+
* mounted, `plugin tree failed to load` afterwards. Cyclic causes terminate
|
|
2683
|
+
* diagnostic traversal without replacing the original cause.
|
|
1524
2684
|
*/
|
|
1525
2685
|
async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBaseUrl) {
|
|
1526
2686
|
const ctx = new Context();
|
|
2687
|
+
const startupLogs = [];
|
|
2688
|
+
const diagnostics = new Context();
|
|
2689
|
+
diagnostics.logger = ctx.logger;
|
|
2690
|
+
diagnostics.logger.exporter({
|
|
2691
|
+
levels: { default: 2 },
|
|
2692
|
+
export: ({ ts, name, type, args }) => {
|
|
2693
|
+
if (type === "warn" || type === "error") startupLogs.push({
|
|
2694
|
+
ts,
|
|
2695
|
+
name,
|
|
2696
|
+
type,
|
|
2697
|
+
args
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
});
|
|
1527
2701
|
let stage = "host preparation failed";
|
|
1528
2702
|
try {
|
|
1529
2703
|
ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + "/";
|
|
1530
2704
|
ctx.provide("dshHomePath", dshHomePath);
|
|
2705
|
+
ctx.on("internal/update", (_config, _noSave, next) => {
|
|
2706
|
+
Promise.resolve(next()).catch((error) => {
|
|
2707
|
+
ctx.logger.error(error);
|
|
2708
|
+
});
|
|
2709
|
+
}, {
|
|
2710
|
+
global: true,
|
|
2711
|
+
prepend: true
|
|
2712
|
+
});
|
|
1531
2713
|
await ctx.plugin(Loader);
|
|
1532
2714
|
await prepare?.(ctx);
|
|
1533
2715
|
stage = "plugin tree failed to load";
|
|
1534
2716
|
await mountRootInclude(ctx, absoluteConfigPath, patches, bareModuleBaseUrl);
|
|
1535
2717
|
await ctx.get("loader")?.await();
|
|
1536
2718
|
if (ctx.get("loader") === void 0) return ctx;
|
|
1537
|
-
await
|
|
2719
|
+
await auditStartupEntries(ctx, binName);
|
|
1538
2720
|
return ctx;
|
|
1539
2721
|
} catch (cause) {
|
|
1540
2722
|
await ctx.fiber.dispose();
|
|
2723
|
+
if (cause instanceof StartupError) {
|
|
2724
|
+
cause.startup = {
|
|
2725
|
+
configurationPath: absoluteConfigPath,
|
|
2726
|
+
messages: startupLogs
|
|
2727
|
+
};
|
|
2728
|
+
throw cause;
|
|
2729
|
+
}
|
|
1541
2730
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
1542
2731
|
let deepest = cause;
|
|
1543
|
-
|
|
2732
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2733
|
+
while (deepest instanceof Error && !seen.has(deepest) && deepest.cause !== void 0) {
|
|
2734
|
+
seen.add(deepest);
|
|
2735
|
+
deepest = deepest.cause;
|
|
2736
|
+
}
|
|
1544
2737
|
const stack = deepest instanceof AggregateError ? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map(formatActivationError).join("\n")}` : deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : "";
|
|
1545
2738
|
throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause });
|
|
2739
|
+
} finally {
|
|
2740
|
+
await diagnostics.fiber.dispose();
|
|
1546
2741
|
}
|
|
1547
2742
|
}
|
|
1548
2743
|
/** Prompt-section name for the harness-source location line an app bin adds after boot. */
|
|
@@ -1572,4 +2767,4 @@ function addHarnessSourceSection(ctx, sourceRoot) {
|
|
|
1572
2767
|
});
|
|
1573
2768
|
}
|
|
1574
2769
|
//#endregion
|
|
1575
|
-
export { DEFAULT_PROFILE_BUNDLES,
|
|
2770
|
+
export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, StartupError, addHarnessSourceSection, auditStartupEntries, boot, composeEntries, createProfileResolutionGeneration, healIsolatedProfileModuleFallback, healProfilesModuleFallback, initProfile, installFailLoud, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, loadProfileDirectory, mountRootInclude, readProfileManifest, readProfilePatches, readProfilePlugins, reconcileProfilePatches, reconcileProfilePlugins, renderConfigDump, resolveBundleDir, resolveConfigPath, resolveProfileDir, resolveTelemetryPatch, sanitizeProfile, unlinkProfileModuleFallback, writeProfileBundles, writeProfileManifest };
|