@deepseek-ai/dsh-app-boot 0.1.6-alpha.1 → 0.1.7-alpha.1
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 +46 -17
- package/README.zh.md +45 -16
- package/lib/index.js +2122 -995
- package/lib/types/config-schema/collect.d.ts +16 -0
- package/lib/types/config-schema/document.d.ts +13 -0
- package/lib/types/config-schema/index.d.ts +20 -0
- package/lib/types/config-schema/native.d.ts +9 -0
- package/lib/types/config-schema/pattern.d.ts +8 -0
- package/lib/types/config-schema/projector.d.ts +20 -0
- package/lib/types/config-schema/types.d.ts +95 -0
- package/lib/types/index.d.ts +73 -28
- package/lib/types/package-meta.d.ts +25 -0
- 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 +1 -28
- package/lib/types/profile-resolution/resolver.d.ts +18 -19
- package/lib/types/profile-resolution/service.d.ts +17 -12
- package/lib/types/profile-resolution/worker-bootstrap.d.ts +1 -1
- package/lib/types/profile-sanitize.d.ts +13 -0
- package/lib/types/profile.d.ts +79 -55
- package/lib/worker/profile-resolution-bootstrap.js +255 -357
- package/package.json +20 -26
- package/lib/types/watch-config.d.ts +0 -13
package/lib/index.js
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { createRequire, isBuiltin } from "node:module";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { existsSync,
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { parseEnv } from "node:util";
|
|
5
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
6
6
|
import * as yaml from "js-yaml";
|
|
7
7
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
8
|
-
import Loader, { EntryGroup, EntryTree, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
|
|
9
|
-
import { access, constants, readFile, realpath, rename,
|
|
8
|
+
import Loader, { EntryGroup, EntryTree, ModuleLoader, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
|
|
9
|
+
import { access, constants, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
10
10
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
11
11
|
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
|
-
import { watch } from "chokidar";
|
|
15
|
-
import { withFileLock } from "@deepseek-ai/dsh-atomic-write";
|
|
16
|
-
import { imports, resolve as resolve$1 } from "resolve.exports";
|
|
17
14
|
import { getEnvironmentData, setEnvironmentData } from "node:worker_threads";
|
|
15
|
+
import { imports } from "resolve.exports";
|
|
18
16
|
//#region ../../../vendor/include/src/index.ts
|
|
19
17
|
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
|
|
20
18
|
kind: "scalar",
|
|
@@ -247,104 +245,8 @@ var Include = class extends EntryTree {
|
|
|
247
245
|
}
|
|
248
246
|
};
|
|
249
247
|
//#endregion
|
|
250
|
-
//#region lib/types/watch-config.js
|
|
251
|
-
/** Exact-path watching for live profile patch files outside Cordis module roots. */
|
|
252
|
-
const registrations = /* @__PURE__ */ new WeakMap();
|
|
253
|
-
async function findWatchRoot(filename) {
|
|
254
|
-
let root = dirname(filename);
|
|
255
|
-
let depth = 0;
|
|
256
|
-
while (true) try {
|
|
257
|
-
if (!(await stat(root)).isDirectory()) throw new Error(`config watch parent is not a directory: ${root}`);
|
|
258
|
-
const canonicalRoot = await realpath(root);
|
|
259
|
-
return {
|
|
260
|
-
filename: resolve(canonicalRoot, relative(root, filename)),
|
|
261
|
-
root: canonicalRoot,
|
|
262
|
-
depth
|
|
263
|
-
};
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (error.code !== "ENOENT") throw error;
|
|
266
|
-
const parent = dirname(root);
|
|
267
|
-
if (parent === root) throw error;
|
|
268
|
-
root = parent;
|
|
269
|
-
depth += 1;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Watch one patch path, including missing parents, and serialize refresh callbacks.
|
|
274
|
-
* @param ctx Context that owns watcher disposal and receives refresh failures.
|
|
275
|
-
* @param filename Absolute patch-file path.
|
|
276
|
-
* @param options Deployment watcher options inherited from the HMR configuration.
|
|
277
|
-
* @param refresh Callback for additions, changes, and removals.
|
|
278
|
-
* @returns A disposer that closes the watcher and drains its current refresh.
|
|
279
|
-
* @throws When path resolution, watcher startup, or effect registration fails.
|
|
280
|
-
*/
|
|
281
|
-
async function watchConfig(ctx, filename, options, refresh) {
|
|
282
|
-
const target = await findWatchRoot(filename);
|
|
283
|
-
const paths = registrations.get(ctx) ?? /* @__PURE__ */ new Set();
|
|
284
|
-
registrations.set(ctx, paths);
|
|
285
|
-
if (paths.has(target.filename)) throw new Error(`config path already registered: ${filename}`);
|
|
286
|
-
const { cwd: _cwd, ignored: _ignored, ...watchOptions } = options;
|
|
287
|
-
const watcher = watch(target.root, {
|
|
288
|
-
...watchOptions,
|
|
289
|
-
depth: target.depth,
|
|
290
|
-
ignoreInitial: false
|
|
291
|
-
});
|
|
292
|
-
paths.add(target.filename);
|
|
293
|
-
const state = { dirty: false };
|
|
294
|
-
let running;
|
|
295
|
-
const onChange = (path) => {
|
|
296
|
-
const observed = resolve(path);
|
|
297
|
-
if (observed !== filename && observed !== target.filename) return;
|
|
298
|
-
state.dirty = true;
|
|
299
|
-
if (running) return;
|
|
300
|
-
running = (async () => {
|
|
301
|
-
while (state.dirty) {
|
|
302
|
-
state.dirty = false;
|
|
303
|
-
try {
|
|
304
|
-
await refresh();
|
|
305
|
-
} catch (reason) {
|
|
306
|
-
const error = reason instanceof Error ? reason : new Error(String(reason), { cause: reason });
|
|
307
|
-
ctx.logger.warn("config reload at %C failed", filename);
|
|
308
|
-
ctx.logger.warn(error);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
})().finally(() => {
|
|
312
|
-
running = void 0;
|
|
313
|
-
});
|
|
314
|
-
};
|
|
315
|
-
watcher.on("add", onChange);
|
|
316
|
-
watcher.on("change", onChange);
|
|
317
|
-
watcher.on("unlink", onChange);
|
|
318
|
-
const ready = Promise.withResolvers();
|
|
319
|
-
let pending = true;
|
|
320
|
-
watcher.once("ready", () => {
|
|
321
|
-
pending = false;
|
|
322
|
-
ready.resolve();
|
|
323
|
-
});
|
|
324
|
-
watcher.on("error", (error) => {
|
|
325
|
-
if (pending) {
|
|
326
|
-
pending = false;
|
|
327
|
-
ready.reject(error);
|
|
328
|
-
} else ctx.logger.warn(error);
|
|
329
|
-
});
|
|
330
|
-
const dispose = async () => {
|
|
331
|
-
await watcher.close();
|
|
332
|
-
paths.delete(target.filename);
|
|
333
|
-
await running;
|
|
334
|
-
};
|
|
335
|
-
try {
|
|
336
|
-
await ready.promise;
|
|
337
|
-
return ctx.effect(() => dispose, "app-boot.watchConfig()");
|
|
338
|
-
} catch (error) {
|
|
339
|
-
await dispose();
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
//#endregion
|
|
344
248
|
//#region lib/types/profile-resolution/legacy-links.js
|
|
345
|
-
/**
|
|
346
|
-
/** Profile-private package links projected into its pnpm-managed node_modules. */
|
|
347
|
-
const PROFILE_MODULE_FALLBACK_DIR = ".dsh-module-fallback";
|
|
249
|
+
/** Package directory canonicalization through the active runtime carrier's filesystem. */
|
|
348
250
|
/**
|
|
349
251
|
* Return whether the process reads application modules from pkg's virtual filesystem.
|
|
350
252
|
* @returns whether pkg owns the module filesystem.
|
|
@@ -360,50 +262,6 @@ function isPackagedExecutable() {
|
|
|
360
262
|
function realModuleDirectory(path) {
|
|
361
263
|
return isPackagedExecutable() ? realpathSync(path) : realpathSync.native(path);
|
|
362
264
|
}
|
|
363
|
-
/**
|
|
364
|
-
* Resolve a link target without following the final path component.
|
|
365
|
-
* @param path - candidate path whose parent is canonicalized.
|
|
366
|
-
* @returns the canonical candidate, or undefined when its parent is absent.
|
|
367
|
-
*/
|
|
368
|
-
function canonicalLinkPath(path) {
|
|
369
|
-
try {
|
|
370
|
-
return join(realModuleDirectory(dirname(path)), basename(path));
|
|
371
|
-
} catch (error) {
|
|
372
|
-
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
|
373
|
-
if (error.code === "ENOENT") return void 0;
|
|
374
|
-
/* v8 ignore next -- see the host-filesystem exception above */
|
|
375
|
-
throw error;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Return whether a symlink or junction points at the same path as `target`.
|
|
380
|
-
* @param link - symlink or junction to inspect.
|
|
381
|
-
* @param target - expected target path.
|
|
382
|
-
* @returns whether both paths identify the same entry.
|
|
383
|
-
*/
|
|
384
|
-
function symlinkPointsTo(link, target) {
|
|
385
|
-
const canonicalActual = canonicalLinkPath(resolve(dirname(link), readlinkSync(link)));
|
|
386
|
-
const canonicalTarget = canonicalLinkPath(resolve(target));
|
|
387
|
-
return canonicalActual !== void 0 && canonicalActual === canonicalTarget;
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Return whether an observed profile package must not claim local precedence.
|
|
391
|
-
* @param profileDir - profile directory containing the package projection.
|
|
392
|
-
* @param packageName - bare package name to inspect.
|
|
393
|
-
* @returns whether the entry is a managed fallback link or disappeared during inspection.
|
|
394
|
-
*/
|
|
395
|
-
function isProfileModuleFallbackLink(profileDir, packageName) {
|
|
396
|
-
const link = join(profileDir, "node_modules", packageName);
|
|
397
|
-
const target = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, "node_modules", packageName);
|
|
398
|
-
try {
|
|
399
|
-
return lstatSync(link).isSymbolicLink() && symlinkPointsTo(link, target);
|
|
400
|
-
} catch (error) {
|
|
401
|
-
/* v8 ignore next 2 -- a vanished candidate cannot claim local precedence */
|
|
402
|
-
if (error.code === "ENOENT") return true;
|
|
403
|
-
/* v8 ignore next -- non-ENOENT lstat/readlink failures require a host filesystem fault */
|
|
404
|
-
throw error;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
265
|
//#endregion
|
|
408
266
|
//#region lib/types/profile.js
|
|
409
267
|
/**
|
|
@@ -415,19 +273,17 @@ function isProfileModuleFallbackLink(profileDir, packageName) {
|
|
|
415
273
|
* `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml`
|
|
416
274
|
* (the user's own patch layer, applied after every bundle layer). Bundles are
|
|
417
275
|
* npm packages whose manifest declares
|
|
418
|
-
* `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
|
419
|
-
* composed by applying each bundle's patch
|
|
420
|
-
* an empty entry list, then the
|
|
421
|
-
* layers (`--patch` files and
|
|
276
|
+
* `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` (one file, or an
|
|
277
|
+
* ordered list of files); the tree is composed by applying each bundle's patch
|
|
278
|
+
* lists in `dsh.profile.bundles` order over an empty entry list, then the
|
|
279
|
+
* profile's own patches, then any launcher layers (`--patch` files and
|
|
280
|
+
* flag-derived patches).
|
|
422
281
|
*
|
|
423
282
|
* Module resolution is two-anchor by construction: a bundle name resolves
|
|
424
283
|
* first from the dsh installation (the launcher's own package), then from the
|
|
425
284
|
* profile directory. Pnpm-managed entries in the profile's `node_modules`
|
|
426
|
-
* resolve first.
|
|
427
|
-
* bundles
|
|
428
|
-
* dependency closure through Node's ordinary parent-walk. Plain Node uses
|
|
429
|
-
* symlinks for that shared fallback; packaged executables use ESM proxies so
|
|
430
|
-
* external plugins retain the installation's module instances.
|
|
285
|
+
* resolve first. The runtime resolution supplies packages carried by the
|
|
286
|
+
* installation and selected bundles to Node's ESM and CommonJS resolvers.
|
|
431
287
|
* @module @deepseek-ai/dsh-app-boot/profile
|
|
432
288
|
*/
|
|
433
289
|
/** Directory under the Harness home holding every profile. */
|
|
@@ -435,6 +291,28 @@ const PROFILES_DIR = "profiles";
|
|
|
435
291
|
/** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */
|
|
436
292
|
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
437
293
|
/**
|
|
294
|
+
* The patch files a bundle declares, as written: one file for a string
|
|
295
|
+
* `patch`, the listed files in order for an array.
|
|
296
|
+
* @param bundle - the bundle's `dsh.bundle` declaration, as read from package.json.
|
|
297
|
+
* @returns the package-relative patch file paths in application order.
|
|
298
|
+
* @throws {Error} when `patch` is neither a string nor a list of strings.
|
|
299
|
+
*/
|
|
300
|
+
function bundlePatchFiles(bundle) {
|
|
301
|
+
const declared = typeof bundle.patch === "string" ? [bundle.patch] : bundle.patch;
|
|
302
|
+
if (!Array.isArray(declared) || !declared.every((file) => typeof file === "string")) throw new Error("dsh.bundle.patch must be a file path or a list of file paths");
|
|
303
|
+
return declared;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Resolve a bundle declaration to its ordered absolute patch files.
|
|
307
|
+
* @param packageDir - absolute directory of the bundle package.
|
|
308
|
+
* @param bundle - the bundle's `dsh.bundle` declaration, as read from package.json.
|
|
309
|
+
* @returns the absolute patch file paths in application order.
|
|
310
|
+
* @throws {Error} when `patch` is neither a string nor a list of strings.
|
|
311
|
+
*/
|
|
312
|
+
function bundlePatchPaths(packageDir, bundle) {
|
|
313
|
+
return bundlePatchFiles(bundle).map((file) => join(packageDir, file));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
438
316
|
* Resolve a profile's directory under the Harness home.
|
|
439
317
|
* @param name - the profile name (`dsh --profile <name>`).
|
|
440
318
|
* @param home - the Harness home; defaults to {@link resolveDshHome}.
|
|
@@ -446,26 +324,11 @@ function resolveProfileDir(name, home = resolveDshHome()) {
|
|
|
446
324
|
}
|
|
447
325
|
/** The shipped profile templates auto-initialized on first use, by name. */
|
|
448
326
|
const PROFILE_TEMPLATES = {
|
|
449
|
-
acp: {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
},
|
|
453
|
-
|
|
454
|
-
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"],
|
|
455
|
-
patchReload: "live"
|
|
456
|
-
},
|
|
457
|
-
headless: {
|
|
458
|
-
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"],
|
|
459
|
-
patchReload: "startup"
|
|
460
|
-
},
|
|
461
|
-
sdk: {
|
|
462
|
-
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-sdk-app"],
|
|
463
|
-
patchReload: "startup"
|
|
464
|
-
},
|
|
465
|
-
"sdk-minimal": {
|
|
466
|
-
bundles: ["@deepseek-ai/dsh-sdk-minimal"],
|
|
467
|
-
patchReload: "startup"
|
|
468
|
-
}
|
|
327
|
+
acp: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-acp-app"] },
|
|
328
|
+
web: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] },
|
|
329
|
+
headless: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"] },
|
|
330
|
+
sdk: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-sdk-app"] },
|
|
331
|
+
"sdk-minimal": { bundles: ["@deepseek-ai/dsh-sdk-minimal"] }
|
|
469
332
|
};
|
|
470
333
|
/** Installation-owned bundle tuples normalized to the shipped template. */
|
|
471
334
|
const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
@@ -475,8 +338,13 @@ const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
|
475
338
|
] };
|
|
476
339
|
/** The bundle list a `dsh plugin` init uses for a name with no shipped template. */
|
|
477
340
|
const DEFAULT_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base"];
|
|
478
|
-
/**
|
|
479
|
-
|
|
341
|
+
/**
|
|
342
|
+
* The bundles the dsh installation ships for a person to switch on: each a
|
|
343
|
+
* runtime dependency of the installation that declares `dsh.bundle.patch`,
|
|
344
|
+
* selected by no shipped template, and offered switched off by the plugin
|
|
345
|
+
* manager ([rationale](../../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)).
|
|
346
|
+
*/
|
|
347
|
+
const OPTIONAL_BUNDLES = ["@deepseek-ai/dsh-experimental-voice-input-bundle", "@deepseek-ai/dsh-experimental-agent-team-profile"];
|
|
480
348
|
const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
|
|
481
349
|
# a top-level YAML array of loader patch entries (id-targeted config
|
|
482
350
|
# overrides, disables, and insert lists; \`!!js\` expressions allowed).
|
|
@@ -494,9 +362,8 @@ autoInstallPeers: false
|
|
|
494
362
|
* so re-running is a no-op on an initialized profile.
|
|
495
363
|
* @param dir - the profile directory from {@link resolveProfileDir}.
|
|
496
364
|
* @param bundles - the initial `dsh.profile.bundles` layer list.
|
|
497
|
-
* @param patchReload - user patch-file lifecycle; custom profiles default to live reload.
|
|
498
365
|
*/
|
|
499
|
-
function initProfile(dir, bundles
|
|
366
|
+
function initProfile(dir, bundles) {
|
|
500
367
|
mkdirSync(dir, { recursive: true });
|
|
501
368
|
const manifestPath = join(dir, "package.json");
|
|
502
369
|
if (!existsSync(manifestPath)) {
|
|
@@ -504,10 +371,7 @@ function initProfile(dir, bundles, patchReload = DEFAULT_PROFILE_PATCH_RELOAD) {
|
|
|
504
371
|
name: `dsh-profile-${basename(dir)}`,
|
|
505
372
|
private: true,
|
|
506
373
|
dependencies: {},
|
|
507
|
-
dsh: { profile: {
|
|
508
|
-
bundles: [...bundles],
|
|
509
|
-
patchReload
|
|
510
|
-
} }
|
|
374
|
+
dsh: { profile: { bundles: [...bundles] } }
|
|
511
375
|
};
|
|
512
376
|
writeFileSync(manifestPath, JSON.stringify(manifest, void 0, 2) + "\n");
|
|
513
377
|
}
|
|
@@ -516,188 +380,108 @@ function initProfile(dir, bundles, patchReload = DEFAULT_PROFILE_PATCH_RELOAD) {
|
|
|
516
380
|
const workspacePath = join(dir, "pnpm-workspace.yaml");
|
|
517
381
|
if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE);
|
|
518
382
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
383
|
+
/** Directory where the link backend of the dsh 0.1.5 releases projected bundle-carried packages into a profile. */
|
|
384
|
+
const LINK_PROJECTION_DIR = ".dsh-module-fallback";
|
|
385
|
+
/**
|
|
386
|
+
* Remove the package projections a link-backend launch left in a profile.
|
|
387
|
+
* Only symlinks under the profile's `node_modules` whose target lies inside
|
|
388
|
+
* `<profile>/.dsh-module-fallback/node_modules` are unlinked, then that directory is removed;
|
|
389
|
+
* pnpm-installed packages and every other symlink stay. A profile without the directory is untouched.
|
|
390
|
+
* @param dir - the profile directory.
|
|
391
|
+
*/
|
|
392
|
+
function removeLinkProjections(dir) {
|
|
393
|
+
const owned = join(dir, LINK_PROJECTION_DIR);
|
|
394
|
+
if (!existsSync(owned)) return;
|
|
395
|
+
const ownedModules = join(owned, "node_modules");
|
|
396
|
+
for (const link of symlinksUnder(join(dir, "node_modules"))) if (pointsInto(link, ownedModules)) unlinkSync(link);
|
|
397
|
+
rmSync(owned, {
|
|
398
|
+
recursive: true,
|
|
399
|
+
force: true
|
|
400
|
+
});
|
|
525
401
|
}
|
|
526
|
-
/**
|
|
527
|
-
function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (!stat.isSymbolicLink()) {
|
|
536
|
-
if ((stat.isDirectory() ? readModuleProxyRecord(link) : void 0)?.dsh?.moduleFallback?.targets === void 0) throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`);
|
|
537
|
-
rmSync(link, { recursive: true });
|
|
538
|
-
stat = void 0;
|
|
539
|
-
}
|
|
540
|
-
if (stat !== void 0) {
|
|
541
|
-
if (symlinkPointsTo(link, target)) return;
|
|
542
|
-
unlinkSync(link);
|
|
402
|
+
/** Top-level and scoped entries under a node_modules directory that are symlinks or junctions. */
|
|
403
|
+
function symlinksUnder(modules) {
|
|
404
|
+
const links = [];
|
|
405
|
+
if (!existsSync(modules)) return links;
|
|
406
|
+
for (const entry of readdirSync(modules, { withFileTypes: true })) {
|
|
407
|
+
const path = join(modules, entry.name);
|
|
408
|
+
if (entry.isSymbolicLink()) links.push(path);
|
|
409
|
+
else if (entry.name.startsWith("@") && entry.isDirectory()) {
|
|
410
|
+
for (const child of readdirSync(path, { withFileTypes: true })) if (child.isSymbolicLink()) links.push(join(path, child.name));
|
|
543
411
|
}
|
|
544
412
|
}
|
|
545
|
-
|
|
546
|
-
symlinkSync(target, link, "junction");
|
|
547
|
-
} catch (error) {
|
|
548
|
-
/* v8 ignore next 4 */
|
|
549
|
-
if (error.code !== "EEXIST" || !lstatSync(link).isSymbolicLink() || !symlinkPointsTo(link, target)) throw error;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
/** Add one profile-owned fallback link without replacing a pnpm-managed entry. */
|
|
553
|
-
function ensureProfileSymlink(link, target) {
|
|
554
|
-
try {
|
|
555
|
-
lstatSync(link);
|
|
556
|
-
return;
|
|
557
|
-
} catch (error) {
|
|
558
|
-
/* v8 ignore next -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
559
|
-
if (error.code !== "ENOENT") throw error;
|
|
560
|
-
}
|
|
561
|
-
ensureSymlink(link, target);
|
|
562
|
-
}
|
|
563
|
-
/** Package names represented by owned symlinks below one fallback node_modules. */
|
|
564
|
-
function ownedPackageNames(modulesDir) {
|
|
565
|
-
return readdirSync(modulesDir, { withFileTypes: true }).flatMap((entry) => {
|
|
566
|
-
if (entry.name.startsWith("@") && entry.isDirectory()) return readdirSync(join(modulesDir, entry.name), { withFileTypes: true }).filter((child) => child.isSymbolicLink()).map((child) => `${entry.name}/${child.name}`);
|
|
567
|
-
return entry.isSymbolicLink() ? [entry.name] : [];
|
|
568
|
-
});
|
|
413
|
+
return links;
|
|
569
414
|
}
|
|
570
|
-
/**
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
415
|
+
/**
|
|
416
|
+
* Active profile `node_modules` entries linked outside the shared profiles tree and the active profile.
|
|
417
|
+
* Missing targets and files are not linked roots; invalid link chains retain Node's diagnostic.
|
|
418
|
+
*/
|
|
419
|
+
function linkedProfileRoots(profile, profilesDir) {
|
|
420
|
+
const modules = join(profile.dir, "node_modules");
|
|
421
|
+
const links = symlinksUnder(modules);
|
|
422
|
+
if (links.length === 0) return [];
|
|
423
|
+
let tree;
|
|
580
424
|
try {
|
|
581
|
-
|
|
425
|
+
tree = realModuleDirectory(profilesDir) + sep;
|
|
582
426
|
} catch (error) {
|
|
583
|
-
/* v8 ignore next -- concurrent identical cleanup may remove the link first */
|
|
584
427
|
if (error.code !== "ENOENT") throw error;
|
|
428
|
+
tree = resolve(profilesDir) + sep;
|
|
585
429
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
try {
|
|
591
|
-
candidates = resolve$1({
|
|
592
|
-
name: packageName,
|
|
593
|
-
exports: declared
|
|
594
|
-
}, subpath);
|
|
595
|
-
} catch (error) {
|
|
596
|
-
if (error.message.startsWith("No known conditions for ")) return void 0;
|
|
597
|
-
const specifier = subpath === "." ? packageName : packageName + subpath.slice(1);
|
|
598
|
-
throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error });
|
|
599
|
-
}
|
|
600
|
-
for (const candidate of candidates ?? []) {
|
|
601
|
-
const target = candidate;
|
|
602
|
-
const entry = resolve(packageDir, target);
|
|
603
|
-
const relativeEntry = relative(packageDir, entry);
|
|
604
|
-
if (!target.startsWith("./") || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`);
|
|
605
|
-
if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */
|
|
609
|
-
function packageProxySource(packageName, packageDir) {
|
|
610
|
-
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
611
|
-
if (typeof manifest.version !== "string" || manifest.version.length === 0) throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`);
|
|
612
|
-
const declared = manifest.exports;
|
|
613
|
-
if (declared === void 0) {
|
|
614
|
-
const main = typeof manifest.main === "string" && manifest.main.length > 0 ? manifest.main : void 0;
|
|
615
|
-
const entry = join(packageDir, main ?? "index");
|
|
430
|
+
const excludedTrees = [tree, realModuleDirectory(profile.dir) + sep];
|
|
431
|
+
const roots = [];
|
|
432
|
+
for (const linkPath of links) {
|
|
433
|
+
let realPath;
|
|
616
434
|
try {
|
|
617
|
-
|
|
618
|
-
return {
|
|
619
|
-
version: manifest.version,
|
|
620
|
-
targets: { ".": pathToFileURL(resolved).href }
|
|
621
|
-
};
|
|
435
|
+
realPath = realModuleDirectory(linkPath);
|
|
622
436
|
} catch (error) {
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
targets: {}
|
|
626
|
-
};
|
|
627
|
-
throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error });
|
|
437
|
+
if (error.code !== "ENOENT") throw error;
|
|
438
|
+
continue;
|
|
628
439
|
}
|
|
440
|
+
if (excludedTrees.some((prefix) => realPath + sep === prefix || realPath.startsWith(prefix)) || !statSync(realPath).isDirectory()) continue;
|
|
441
|
+
roots.push({
|
|
442
|
+
name: relative(modules, linkPath).split(sep).join("/"),
|
|
443
|
+
realPath
|
|
444
|
+
});
|
|
629
445
|
}
|
|
630
|
-
|
|
631
|
-
const targets = {};
|
|
632
|
-
for (const subpath of subpaths) {
|
|
633
|
-
const target = packageEntryFromPackage(packageName, packageDir, declared, subpath);
|
|
634
|
-
if (target !== void 0) targets[subpath] = target;
|
|
635
|
-
}
|
|
636
|
-
return {
|
|
637
|
-
version: manifest.version,
|
|
638
|
-
targets
|
|
639
|
-
};
|
|
446
|
+
return roots.sort((left, right) => left.name.localeCompare(right.name));
|
|
640
447
|
}
|
|
641
|
-
/**
|
|
642
|
-
|
|
643
|
-
* URL. Files outside the executable cannot traverse a symlink into
|
|
644
|
-
* `/snapshot`, while an ESM re-export can import that URL and preserves the
|
|
645
|
-
* executable's single module instance for out-of-tree plugin peers.
|
|
646
|
-
*/
|
|
647
|
-
function ensureModuleProxy(link, packageName, version, targets) {
|
|
648
|
-
const manifest = {
|
|
649
|
-
name: packageName,
|
|
650
|
-
version,
|
|
651
|
-
private: true,
|
|
652
|
-
type: "module",
|
|
653
|
-
exports: Object.fromEntries(Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`])),
|
|
654
|
-
dsh: { moduleFallback: { targets } }
|
|
655
|
-
};
|
|
656
|
-
let stat;
|
|
448
|
+
/** Whether a symlink's target directory is `root` or lies below it. */
|
|
449
|
+
function pointsInto(link, root) {
|
|
657
450
|
try {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const existing = readModuleProxyRecord(link);
|
|
668
|
-
if (existing?.dsh?.moduleFallback?.targets === void 0) throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`);
|
|
669
|
-
if (existing.version === version && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets) && Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return;
|
|
670
|
-
rmSync(link, { recursive: true });
|
|
671
|
-
}
|
|
672
|
-
mkdirSync(link, { recursive: true });
|
|
673
|
-
writeFileSync(join(link, "package.json"), JSON.stringify(manifest, void 0, 2) + "\n");
|
|
674
|
-
for (const [index, target] of Object.values(targets).entries()) {
|
|
675
|
-
const specifier = JSON.stringify(target);
|
|
676
|
-
writeFileSync(join(link, `entry-${index}.js`), `export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`);
|
|
451
|
+
const target = resolve(dirname(link), readlinkSync(link));
|
|
452
|
+
const parent = realpathSync.native(dirname(target));
|
|
453
|
+
const rootPath = realpathSync.native(root);
|
|
454
|
+
return parent === rootPath || parent.startsWith(rootPath + sep);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
|
457
|
+
if (error.code === "ENOENT") return false;
|
|
458
|
+
/* v8 ignore next -- see the host-filesystem exception above */
|
|
459
|
+
throw error;
|
|
677
460
|
}
|
|
678
461
|
}
|
|
679
|
-
/** Read one package manifest
|
|
680
|
-
function
|
|
462
|
+
/** Read one package manifest while traversing a dependency graph. */
|
|
463
|
+
function readPackageManifest(anchor) {
|
|
681
464
|
return JSON.parse(readFileSync(anchor, "utf8"));
|
|
682
465
|
}
|
|
683
466
|
/** Return dependency names that may be imported by a loader-visible plugin. */
|
|
684
467
|
function profileDependencyNames(manifest) {
|
|
685
468
|
return [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {})];
|
|
686
469
|
}
|
|
687
|
-
/** Resolve the installation
|
|
688
|
-
function
|
|
689
|
-
const
|
|
470
|
+
/** Resolve the installation packages that the runtime resolver supplies to every profile. */
|
|
471
|
+
function collectInstallationScopePackages(installAnchor, skippedBundles) {
|
|
472
|
+
const canonicalAnchor = join(realModuleDirectory(dirname(installAnchor)), basename(installAnchor));
|
|
473
|
+
const appManifest = readPackageManifest(canonicalAnchor);
|
|
690
474
|
const links = /* @__PURE__ */ new Map();
|
|
691
475
|
const declarers = /* @__PURE__ */ new Map();
|
|
692
476
|
const versions = /* @__PURE__ */ new Map();
|
|
693
477
|
/* v8 ignore next -- a real app manifest always declares its name */
|
|
694
478
|
if (appManifest.name !== void 0) {
|
|
695
479
|
links.set(appManifest.name, dirname(installAnchor));
|
|
696
|
-
declarers.set(appManifest.name,
|
|
480
|
+
declarers.set(appManifest.name, canonicalAnchor);
|
|
697
481
|
versions.set(appManifest.name, appManifest.version);
|
|
698
482
|
}
|
|
699
483
|
const queue = [{
|
|
700
|
-
anchor:
|
|
484
|
+
anchor: canonicalAnchor,
|
|
701
485
|
manifest: appManifest
|
|
702
486
|
}];
|
|
703
487
|
for (let next = queue.shift(); next !== void 0; next = queue.shift())
|
|
@@ -706,10 +490,16 @@ function resolveModuleFallbackEntries(installAnchor, materialize = true) {
|
|
|
706
490
|
if (links.has(dep)) continue;
|
|
707
491
|
const dir = packageDirFromAnchor(next.anchor, dep);
|
|
708
492
|
if (dir === void 0) continue;
|
|
493
|
+
const manifestPath = join(realModuleDirectory(dir), "package.json");
|
|
494
|
+
let manifest;
|
|
495
|
+
try {
|
|
496
|
+
manifest = skippedBundles.has(dep) ? readProfileManifest("dsh", dir) : readPackageManifest(manifestPath);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (!skippedBundles.has(dep)) throw error;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
709
501
|
links.set(dep, dir);
|
|
710
502
|
declarers.set(dep, next.anchor);
|
|
711
|
-
const manifestPath = join(dir, "package.json");
|
|
712
|
-
const manifest = readModuleFallbackManifest(manifestPath);
|
|
713
503
|
versions.set(dep, manifest.version);
|
|
714
504
|
queue.push({
|
|
715
505
|
anchor: manifestPath,
|
|
@@ -717,72 +507,32 @@ function resolveModuleFallbackEntries(installAnchor, materialize = true) {
|
|
|
717
507
|
});
|
|
718
508
|
}
|
|
719
509
|
return {
|
|
720
|
-
entries: !materialize ? [] : !isPackagedExecutable() ? [...links].map(([packageName, packageDir]) => ({
|
|
721
|
-
kind: "symlink",
|
|
722
|
-
packageName,
|
|
723
|
-
packageDir
|
|
724
|
-
})) : [...links].flatMap(([packageName, packageDir]) => {
|
|
725
|
-
const source = packageProxySource(packageName, packageDir);
|
|
726
|
-
return Object.keys(source.targets).length === 0 ? [] : [{
|
|
727
|
-
kind: "proxy",
|
|
728
|
-
packageName,
|
|
729
|
-
version: source.version,
|
|
730
|
-
targets: source.targets
|
|
731
|
-
}];
|
|
732
|
-
}),
|
|
733
510
|
packageNames: new Set(links.keys()),
|
|
734
511
|
packageDirs: links,
|
|
735
512
|
declarers,
|
|
736
513
|
versions
|
|
737
514
|
};
|
|
738
515
|
}
|
|
739
|
-
/** Return whether one existing fallback entry already matches its resolved installation generation. */
|
|
740
|
-
function moduleFallbackEntryCurrent(modulesDir, entry) {
|
|
741
|
-
const link = join(modulesDir, entry.packageName);
|
|
742
|
-
try {
|
|
743
|
-
const stat = lstatSync(link);
|
|
744
|
-
if (entry.kind === "symlink") return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir;
|
|
745
|
-
if (!stat.isDirectory()) return false;
|
|
746
|
-
const existing = readModuleProxyRecord(link);
|
|
747
|
-
return existing?.version === entry.version && JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets) && Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)));
|
|
748
|
-
} catch {
|
|
749
|
-
return false;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
/** Return whether every required fallback entry is already ready for this installation. */
|
|
753
|
-
function moduleFallbackCurrent(modulesDir, entries) {
|
|
754
|
-
return entries.every((entry) => moduleFallbackEntryCurrent(modulesDir, entry));
|
|
755
|
-
}
|
|
756
516
|
/**
|
|
757
|
-
*
|
|
758
|
-
* `$DSH_HOME/profiles/node_modules` mirrors the dsh installation dependency
|
|
759
|
-
* closure. Plain Node writes symlinks; a packaged executable writes ESM
|
|
760
|
-
* proxies under a cross-process lock because operating-system links cannot
|
|
761
|
-
* enter pkg's virtual filesystem. Missing packages carried only by selected
|
|
762
|
-
* bundles are linked through a profile-owned directory into that profile's
|
|
763
|
-
* `node_modules`; pnpm-managed entries remain authoritative, and another
|
|
764
|
-
* profile's links cannot change its resolution.
|
|
517
|
+
* Compute the runtime resolution without writing module-resolution files.
|
|
765
518
|
* @param options - installation anchor, optional loaded profile, and Harness home.
|
|
766
|
-
* @returns the
|
|
519
|
+
* @returns the complete immutable runtime resolution.
|
|
767
520
|
*/
|
|
768
|
-
async function
|
|
769
|
-
const { installAnchor, profile, home = resolveDshHome()
|
|
521
|
+
async function createRuntimeResolution(options) {
|
|
522
|
+
const { installAnchor, profile, home = resolveDshHome() } = options;
|
|
770
523
|
const profilesDir = join(home, PROFILES_DIR);
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
const { entries, packageNames, packageDirs, declarers, versions } = resolveModuleFallbackEntries(installAnchor, materialize);
|
|
774
|
-
if (materialize && !moduleFallbackCurrent(modulesDir, entries)) await withFileLock(modulesDir, () => {
|
|
775
|
-
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir);
|
|
776
|
-
return Promise.resolve();
|
|
777
|
-
});
|
|
524
|
+
const manifest = readOptionalProfileManifest(profile);
|
|
525
|
+
const { packageNames, packageDirs, declarers, versions } = collectInstallationScopePackages(installAnchor, skippedProfileBundles(profile, manifest));
|
|
778
526
|
const profileDeclarers = /* @__PURE__ */ new Map();
|
|
779
527
|
const profileVersions = /* @__PURE__ */ new Map();
|
|
780
|
-
const localPackageNames = profile === void 0 ? [] : installedProfilePackageNames(profile);
|
|
781
|
-
const profilePackages = profile === void 0 ? /* @__PURE__ */ new Map() :
|
|
782
|
-
|
|
528
|
+
const localPackageNames = profile === void 0 ? [] : installedProfilePackageNames(profile, manifest);
|
|
529
|
+
const profilePackages = profile === void 0 ? /* @__PURE__ */ new Map() : collectProfileScopePackages(profile, packageNames, profileDeclarers, profileVersions);
|
|
530
|
+
const linkedRoots = profile === void 0 ? [] : linkedProfileRoots(profile, profilesDir);
|
|
531
|
+
return await Promise.resolve(Object.freeze({
|
|
783
532
|
profilesDir,
|
|
784
533
|
profileDir: profile?.dir,
|
|
785
534
|
localPackageNames: Object.freeze(localPackageNames),
|
|
535
|
+
linkedRoots: Object.freeze(linkedRoots.map((root) => Object.freeze(root))),
|
|
786
536
|
entries: Object.freeze([...[...packageDirs].map(([name, packageDir]) => Object.freeze({
|
|
787
537
|
name,
|
|
788
538
|
packageDir,
|
|
@@ -796,49 +546,41 @@ async function healProfilesModuleFallback(options) {
|
|
|
796
546
|
declarer: profileDeclarers.get(name),
|
|
797
547
|
scope: "profile"
|
|
798
548
|
}))])
|
|
799
|
-
});
|
|
549
|
+
}));
|
|
800
550
|
}
|
|
801
|
-
/**
|
|
802
|
-
function
|
|
803
|
-
|
|
551
|
+
/** Synthetic profiles used by direct callers may have no on-disk manifest. */
|
|
552
|
+
function readOptionalProfileManifest(profile) {
|
|
553
|
+
if (profile === void 0) return void 0;
|
|
804
554
|
try {
|
|
805
|
-
|
|
555
|
+
return readPackageManifest(join(profile.dir, "package.json"));
|
|
806
556
|
} catch (error) {
|
|
807
|
-
if (error.code === "ENOENT") return
|
|
557
|
+
if (error.code === "ENOENT") return void 0;
|
|
808
558
|
throw error;
|
|
809
559
|
}
|
|
810
|
-
return profileDependencyNames(manifest).filter((name) => {
|
|
811
|
-
if (!existsSync(join(join(profile.dir, "node_modules", name), "package.json"))) return false;
|
|
812
|
-
return !isProfileModuleFallbackLink(profile.dir, name);
|
|
813
|
-
});
|
|
814
560
|
}
|
|
815
561
|
/**
|
|
816
|
-
*
|
|
817
|
-
* @param
|
|
818
|
-
* @
|
|
562
|
+
* Identify selected bundles that did not produce a loaded layer.
|
|
563
|
+
* @param profile - loaded profile, when present.
|
|
564
|
+
* @param manifest - its parsed manifest, when present.
|
|
565
|
+
* @returns selected bundle names missing from the loaded layers, for resolution and diagnostics.
|
|
819
566
|
*/
|
|
820
|
-
function
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
});
|
|
567
|
+
function skippedProfileBundles(profile, manifest) {
|
|
568
|
+
const selected = manifest?.dsh?.profile?.bundles ?? [];
|
|
569
|
+
const loaded = new Set(profile?.layers.map((layer) => layer.packageName));
|
|
570
|
+
return new Set(selected.filter((name) => !loaded.has(name)));
|
|
825
571
|
}
|
|
826
|
-
/**
|
|
827
|
-
function
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
mkdirSync(dirname(link), { recursive: true });
|
|
831
|
-
if (entry.kind === "proxy") ensureModuleProxy(link, entry.packageName, entry.version, entry.targets);
|
|
832
|
-
else ensureSymlink(link, entry.packageDir);
|
|
833
|
-
}
|
|
572
|
+
/** Return installed direct dependencies that Node resolves before profile fallback. */
|
|
573
|
+
function installedProfilePackageNames(profile, manifest) {
|
|
574
|
+
if (manifest === void 0) return [];
|
|
575
|
+
return profileDependencyNames(manifest).filter((name) => existsSync(join(profile.dir, "node_modules", name, "package.json")));
|
|
834
576
|
}
|
|
835
577
|
/** Collect the first resolvable package directory for each dependency name. */
|
|
836
|
-
function dependencyClosure(anchors, reserved,
|
|
578
|
+
function dependencyClosure(anchors, reserved, declarers, versions) {
|
|
837
579
|
const links = /* @__PURE__ */ new Map();
|
|
838
580
|
const visited = new Set(reserved);
|
|
839
581
|
for (const anchor of anchors) {
|
|
840
582
|
const canonicalAnchor = join(realModuleDirectory(dirname(anchor)), basename(anchor));
|
|
841
|
-
const manifest =
|
|
583
|
+
const manifest = readPackageManifest(canonicalAnchor);
|
|
842
584
|
/* v8 ignore next -- an installable package manifest always declares its name */
|
|
843
585
|
if (manifest.name === void 0) continue;
|
|
844
586
|
if (!visited.has(manifest.name)) {
|
|
@@ -855,13 +597,13 @@ function dependencyClosure(anchors, reserved, exclude, declarers, versions) {
|
|
|
855
597
|
/* v8 ignore next -- an installable package manifest always declares dependencies or peers */
|
|
856
598
|
for (const dep of profileDependencyNames(next.manifest)) {
|
|
857
599
|
if (visited.has(dep)) continue;
|
|
858
|
-
const dir = packageDirFromAnchor(next.anchor, dep
|
|
600
|
+
const dir = packageDirFromAnchor(next.anchor, dep);
|
|
859
601
|
if (dir === void 0) continue;
|
|
860
602
|
visited.add(dep);
|
|
861
603
|
links.set(dep, dir);
|
|
862
604
|
declarers?.set(dep, next.anchor);
|
|
863
|
-
const manifestPath = join(dir, "package.json");
|
|
864
|
-
const dependencyManifest =
|
|
605
|
+
const manifestPath = join(realModuleDirectory(dir), "package.json");
|
|
606
|
+
const dependencyManifest = readPackageManifest(manifestPath);
|
|
865
607
|
versions?.set(dep, dependencyManifest.version);
|
|
866
608
|
queue.push({
|
|
867
609
|
anchor: manifestPath,
|
|
@@ -871,37 +613,10 @@ function dependencyClosure(anchors, reserved, exclude, declarers, versions) {
|
|
|
871
613
|
}
|
|
872
614
|
return links;
|
|
873
615
|
}
|
|
874
|
-
/**
|
|
875
|
-
function
|
|
876
|
-
const
|
|
877
|
-
const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, "node_modules");
|
|
878
|
-
if (materialize) {
|
|
879
|
-
mkdirSync(profileModulesDir, { recursive: true });
|
|
880
|
-
mkdirSync(ownedModulesDir, { recursive: true });
|
|
881
|
-
}
|
|
882
|
-
const bundleLinks = dependencyClosure(profile.layers.filter((layer) => !installationPackageNames.has(layer.packageName)).map((layer) => join(layer.packageDir, "package.json")), installationPackageNames, (candidate, packageName) => {
|
|
883
|
-
const profileLink = join(profileModulesDir, packageName);
|
|
884
|
-
if (canonicalLinkPath(candidate) !== canonicalLinkPath(profileLink)) return false;
|
|
885
|
-
try {
|
|
886
|
-
return lstatSync(profileLink).isSymbolicLink() && symlinkPointsTo(profileLink, join(ownedModulesDir, packageName));
|
|
887
|
-
} catch (error) {
|
|
888
|
-
/* v8 ignore next 2 -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
889
|
-
if (error.code === "ENOENT") return true;
|
|
890
|
-
/* v8 ignore next -- see the host-filesystem exception above */
|
|
891
|
-
throw error;
|
|
892
|
-
}
|
|
893
|
-
}, declarers, versions);
|
|
616
|
+
/** Collect packages carried by the profile's selected bundles that the installation does not supply. */
|
|
617
|
+
function collectProfileScopePackages(profile, installationPackageNames, declarers, versions) {
|
|
618
|
+
const bundleLinks = dependencyClosure(profile.layers.filter((layer) => !installationPackageNames.has(layer.packageName)).map((layer) => join(layer.packageDir, "package.json")), installationPackageNames, declarers, versions);
|
|
894
619
|
for (const layer of profile.layers) bundleLinks.delete(layer.packageName);
|
|
895
|
-
if (!materialize) return bundleLinks;
|
|
896
|
-
for (const packageName of ownedPackageNames(ownedModulesDir)) if (!bundleLinks.has(packageName)) removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName);
|
|
897
|
-
for (const [packageName, target] of bundleLinks) {
|
|
898
|
-
const ownedLink = join(ownedModulesDir, packageName);
|
|
899
|
-
mkdirSync(dirname(ownedLink), { recursive: true });
|
|
900
|
-
ensureSymlink(ownedLink, target);
|
|
901
|
-
const profileLink = join(profileModulesDir, packageName);
|
|
902
|
-
mkdirSync(dirname(profileLink), { recursive: true });
|
|
903
|
-
ensureProfileSymlink(profileLink, ownedLink);
|
|
904
|
-
}
|
|
905
620
|
return bundleLinks;
|
|
906
621
|
}
|
|
907
622
|
/**
|
|
@@ -936,27 +651,21 @@ function sameBundles(left, right) {
|
|
|
936
651
|
}
|
|
937
652
|
/**
|
|
938
653
|
* Normalize an exact installation-owned bundle tuple to its shipped template,
|
|
939
|
-
*
|
|
940
|
-
* is written back during profile loading while every other manifest field is
|
|
941
|
-
* preserved; any other bundle list is user-owned and remains untouched.
|
|
654
|
+
* preserving all other manifest fields. Other bundle lists remain untouched.
|
|
942
655
|
*/
|
|
943
656
|
function normalizeShippedProfile(name, dir, manifest) {
|
|
944
657
|
const installationOwned = INSTALLATION_OWNED_PROFILE_TUPLES[name];
|
|
945
658
|
const template = PROFILE_TEMPLATES[name];
|
|
946
659
|
const bundles = manifest.dsh?.profile?.bundles;
|
|
947
660
|
if (template === void 0 || bundles === void 0) return manifest;
|
|
948
|
-
|
|
949
|
-
const isCurrentTuple = sameBundles(bundles, template.bundles);
|
|
950
|
-
const needsReloadDefault = manifest.dsh?.profile?.patchReload === void 0 && isCurrentTuple;
|
|
951
|
-
if (!isRetiredTuple && !needsReloadDefault) return manifest;
|
|
661
|
+
if (!(installationOwned !== void 0 && sameBundles(bundles, installationOwned))) return manifest;
|
|
952
662
|
const normalized = {
|
|
953
663
|
...manifest,
|
|
954
664
|
dsh: {
|
|
955
665
|
...manifest.dsh,
|
|
956
666
|
profile: {
|
|
957
667
|
...manifest.dsh?.profile,
|
|
958
|
-
bundles: [...template.bundles]
|
|
959
|
-
patchReload: manifest.dsh?.profile?.patchReload ?? template.patchReload
|
|
668
|
+
bundles: [...template.bundles]
|
|
960
669
|
}
|
|
961
670
|
}
|
|
962
671
|
};
|
|
@@ -971,11 +680,11 @@ function normalizeShippedProfile(name, dir, manifest) {
|
|
|
971
680
|
* matches what the Loader would import from the same anchor, and
|
|
972
681
|
* `existsSync` follows the symlinks pnpm's isolated layout uses.
|
|
973
682
|
*/
|
|
974
|
-
function packageDirFromAnchor(anchor, packageName
|
|
683
|
+
function packageDirFromAnchor(anchor, packageName) {
|
|
975
684
|
/* v8 ignore next */
|
|
976
685
|
for (const searchPath of createRequire(anchor).resolve.paths(packageName) ?? []) {
|
|
977
686
|
const candidate = join(searchPath, packageName);
|
|
978
|
-
if (existsSync(join(candidate, "package.json"))
|
|
687
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
979
688
|
}
|
|
980
689
|
}
|
|
981
690
|
/**
|
|
@@ -1001,30 +710,31 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
|
|
|
1001
710
|
* Load an already initialized profile directory without resolving it through
|
|
1002
711
|
* the shared Harness home. This is used by application-owned profiles whose
|
|
1003
712
|
* package project and lifecycle belong to that application.
|
|
713
|
+
* Unreadable bundles are reported on stderr and skipped without changing the manifest.
|
|
1004
714
|
* @param binName - the diagnostic prefix on thrown errors.
|
|
1005
715
|
* @param dir - absolute profile package directory.
|
|
1006
716
|
* @param installAnchor - absolute path of the owning dsh app's package.json.
|
|
1007
717
|
* @param options - `userLayer: false` skips reading `cordis.patch.yml`.
|
|
1008
|
-
* @returns the
|
|
718
|
+
* @returns the successfully loaded bundle layers and optional user patch layer.
|
|
1009
719
|
*/
|
|
1010
720
|
function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
const
|
|
1014
|
-
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"`);
|
|
1015
|
-
const patchReload = rawPatchReload ?? "live";
|
|
1016
|
-
const layers = bundles.map((packageName) => {
|
|
721
|
+
const bundles = readProfileManifest(binName, dir).dsh?.profile?.bundles ?? [];
|
|
722
|
+
const layers = [];
|
|
723
|
+
for (const packageName of bundles) try {
|
|
1017
724
|
const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir);
|
|
1018
|
-
const
|
|
1019
|
-
if (
|
|
1020
|
-
const
|
|
1021
|
-
|
|
725
|
+
const bundle = readProfileManifest(binName, packageDir).dsh?.bundle;
|
|
726
|
+
if (bundle === void 0) throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.bundle in its package.json`);
|
|
727
|
+
const patchPaths = bundlePatchPaths(packageDir, bundle);
|
|
728
|
+
const patches = patchPaths.flatMap((patchPath) => loadOverlayPatches(binName, patchPath));
|
|
729
|
+
layers.push({
|
|
1022
730
|
packageName,
|
|
1023
731
|
packageDir,
|
|
1024
|
-
|
|
1025
|
-
patches
|
|
1026
|
-
};
|
|
1027
|
-
})
|
|
732
|
+
patchPaths,
|
|
733
|
+
patches
|
|
734
|
+
});
|
|
735
|
+
} catch (error) {
|
|
736
|
+
process.stderr.write(`${binName}: skipping profile bundle ${JSON.stringify(packageName)}: ${String(error)}\n`);
|
|
737
|
+
}
|
|
1028
738
|
const patchPath = join(dir, PROFILE_PATCH_FILENAME);
|
|
1029
739
|
const patches = options.userLayer !== false && existsSync(patchPath) ? loadOverlayPatches(binName, patchPath) : [];
|
|
1030
740
|
return {
|
|
@@ -1032,15 +742,13 @@ function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
|
1032
742
|
dir,
|
|
1033
743
|
layers,
|
|
1034
744
|
patchPath,
|
|
1035
|
-
patches
|
|
1036
|
-
patchReload
|
|
745
|
+
patches
|
|
1037
746
|
};
|
|
1038
747
|
}
|
|
1039
748
|
/**
|
|
1040
749
|
* Load a profile: resolve every `dsh.profile.bundles` entry to its patch
|
|
1041
|
-
* layer and parse the profile's own patch file.
|
|
1042
|
-
*
|
|
1043
|
-
* is a misconfiguration, not "no patches".
|
|
750
|
+
* layer and parse the profile's own patch file. Unreadable bundles are reported
|
|
751
|
+
* on stderr and skipped; profile manifest and user patch errors still throw.
|
|
1044
752
|
* @param binName - the diagnostic prefix on thrown errors.
|
|
1045
753
|
* @param name - the profile name.
|
|
1046
754
|
* @param installAnchor - absolute path of the dsh app's package.json (first resolution anchor).
|
|
@@ -1055,8 +763,9 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
1055
763
|
if (!existsSync(join(dir, "package.json"))) {
|
|
1056
764
|
const template = PROFILE_TEMPLATES[name];
|
|
1057
765
|
if (template === void 0) throw new Error(`${binName}: profile ${JSON.stringify(name)} does not exist; create it with 'dsh plugin --profile ${name} add <package>'`);
|
|
1058
|
-
initProfile(dir, template.bundles
|
|
766
|
+
initProfile(dir, template.bundles);
|
|
1059
767
|
}
|
|
768
|
+
removeLinkProjections(dir);
|
|
1060
769
|
normalizeShippedProfile(name, dir, readProfileManifest(binName, dir));
|
|
1061
770
|
return loadProfileDirectory(binName, dir, installAnchor, options);
|
|
1062
771
|
}
|
|
@@ -1075,6 +784,166 @@ function composeEntries(layers, warn = () => {}) {
|
|
|
1075
784
|
});
|
|
1076
785
|
}
|
|
1077
786
|
//#endregion
|
|
787
|
+
//#region lib/types/profile-context.js
|
|
788
|
+
/** Launcher-owned profile locations and composition inputs. */
|
|
789
|
+
const TELEMETRY_ROW_ID = "session-telemetry-otel";
|
|
790
|
+
/**
|
|
791
|
+
* Resolve the telemetry opt-out switch into its boot patch. ANY non-empty
|
|
792
|
+
* value (including `'0'`/`'false'`) disables: a privacy switch prefers
|
|
793
|
+
* off-by-mistake over on-by-mistake. A composition without the telemetry row
|
|
794
|
+
* exports nothing, so the switch is then trivially satisfied and no patch is
|
|
795
|
+
* generated — custom profiles need not mount telemetry to run with the
|
|
796
|
+
* switch set.
|
|
797
|
+
* @param disabledEnv - the raw `DSH_TELEMETRY_DISABLED` value (`undefined` when unset).
|
|
798
|
+
* @param hasRow - whether the composition carries the telemetry row.
|
|
799
|
+
* @returns the disable patch, or `undefined` when no hard-disable patch is required.
|
|
800
|
+
*/
|
|
801
|
+
function resolveTelemetryPatch(disabledEnv, hasRow) {
|
|
802
|
+
if ((disabledEnv ?? "") === "" || !hasRow) return void 0;
|
|
803
|
+
return {
|
|
804
|
+
id: TELEMETRY_ROW_ID,
|
|
805
|
+
disabled: true
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
/** Read current bundle and user layers with the launch-time overlays.
|
|
809
|
+
* @param binName Diagnostic prefix for malformed or missing configuration.
|
|
810
|
+
* @param context Data supplied by the profile launcher.
|
|
811
|
+
* @param initialProfile Already loaded startup profile; omitted reads the current files.
|
|
812
|
+
* @returns Detached ordered patches; this function does not update the Loader.
|
|
813
|
+
*/
|
|
814
|
+
function readProfilePatches(binName, context, initialProfile) {
|
|
815
|
+
const profile = initialProfile ?? loadProfileDirectory(binName, context.dir, context.installAnchor, { userLayer: false });
|
|
816
|
+
const patches = structuredClone([
|
|
817
|
+
...profile.layers.flatMap((layer) => layer.patches),
|
|
818
|
+
...initialProfile?.patches ?? loadOptionalPatches(binName, context.patchPath) ?? [],
|
|
819
|
+
...loadOptionalPatches(binName, join(context.home, "cordis.patch.yml")) ?? [],
|
|
820
|
+
...context.overlays
|
|
821
|
+
]);
|
|
822
|
+
const telemetryPatch = resolveTelemetryPatch(context.telemetryDisabledEnv, composeEntries([patches]).some((row) => row.id === TELEMETRY_ROW_ID));
|
|
823
|
+
if (telemetryPatch !== void 0) patches.push(telemetryPatch);
|
|
824
|
+
return patches;
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region lib/types/profile-plugins.js
|
|
828
|
+
/** Installed profile dependencies and their bundle activation after package-manager operations. */
|
|
829
|
+
/** Unavailable installed metadata must not prevent listing or removing dependencies. */
|
|
830
|
+
function optionalManifest(binName, packageDir) {
|
|
831
|
+
try {
|
|
832
|
+
return readProfileManifest(binName, packageDir);
|
|
833
|
+
} catch {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/** Resolve activation metadata with the same installation precedence as profile loading. */
|
|
838
|
+
function bundleManifest(location, name) {
|
|
839
|
+
let packageDir;
|
|
840
|
+
try {
|
|
841
|
+
packageDir = resolveBundleDir(location.binName, name, location.installAnchor, location.profileDir);
|
|
842
|
+
} catch {
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
return optionalManifest(location.binName, packageDir);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Read installed versions and bundle declarations without requiring loadable plugin code.
|
|
849
|
+
* Missing or unreadable installed metadata leaves the dependency visible for repair or removal.
|
|
850
|
+
* @param location - profile and installation resolution inputs.
|
|
851
|
+
* @returns dependency records in package.json order and the profile manifest.
|
|
852
|
+
*/
|
|
853
|
+
function readProfilePlugins(location) {
|
|
854
|
+
const manifest = readProfileManifest(location.binName, location.profileDir);
|
|
855
|
+
const bundles = manifest.dsh?.profile?.bundles ?? [];
|
|
856
|
+
return {
|
|
857
|
+
manifest,
|
|
858
|
+
dependencies: Object.entries(manifest.dependencies ?? {}).map(([name, spec]) => {
|
|
859
|
+
const installed = optionalManifest(location.binName, join(location.profileDir, "node_modules", name));
|
|
860
|
+
return {
|
|
861
|
+
name,
|
|
862
|
+
version: typeof installed?.version === "string" ? installed.version : spec,
|
|
863
|
+
bundle: bundleManifest(location, name)?.dsh?.bundle?.patch !== void 0,
|
|
864
|
+
enabled: bundles.includes(name)
|
|
865
|
+
};
|
|
866
|
+
})
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Write a bundle list while preserving the supplied profile's other metadata.
|
|
871
|
+
* @param profileDir - directory whose package.json is updated.
|
|
872
|
+
* @param manifest - current profile manifest, read after the package operation when applicable.
|
|
873
|
+
* @param bundles - ordered active bundle names, including any template entries.
|
|
874
|
+
* @returns the written manifest.
|
|
875
|
+
*/
|
|
876
|
+
function writeProfileBundles(profileDir, manifest, bundles) {
|
|
877
|
+
const updated = {
|
|
878
|
+
...manifest,
|
|
879
|
+
dsh: {
|
|
880
|
+
...manifest.dsh,
|
|
881
|
+
profile: {
|
|
882
|
+
...manifest.dsh?.profile,
|
|
883
|
+
bundles: [...bundles]
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
writeProfileManifest(profileDir, updated);
|
|
888
|
+
return updated;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Reconcile installed bundle declarations after a successful package-manager operation.
|
|
892
|
+
* Dependency-managed entries disappear when removed or when their package loses its declaration;
|
|
893
|
+
* template entries retain their order and duplicates. New bundle dependencies activate automatically.
|
|
894
|
+
* @param options - location, inventory captured before pnpm, and whether explicitly disabled bundles remain disabled.
|
|
895
|
+
* @returns updated inventory and newly added ordinary dependencies for caller-owned warnings.
|
|
896
|
+
*/
|
|
897
|
+
function reconcileProfilePlugins(options) {
|
|
898
|
+
const after = readProfilePlugins(options);
|
|
899
|
+
const beforeNames = new Set(options.before.dependencies.map((dependency) => dependency.name));
|
|
900
|
+
const afterNames = new Set(after.dependencies.map((dependency) => dependency.name));
|
|
901
|
+
const bundleNames = new Set(after.dependencies.filter((dependency) => dependency.bundle).map((dependency) => dependency.name));
|
|
902
|
+
const disabled = new Set(options.preserveDisabled ? options.before.dependencies.filter((dependency) => dependency.bundle && !dependency.enabled).map((dependency) => dependency.name) : []);
|
|
903
|
+
const previous = after.manifest.dsh?.profile?.bundles ?? [];
|
|
904
|
+
const bundles = previous.filter((name) => !(beforeNames.has(name) || afterNames.has(name)) || bundleNames.has(name));
|
|
905
|
+
for (const dependency of after.dependencies) if (dependency.bundle && !disabled.has(dependency.name) && !bundles.includes(dependency.name)) bundles.push(dependency.name);
|
|
906
|
+
return {
|
|
907
|
+
plugins: {
|
|
908
|
+
manifest: bundles.length !== previous.length || bundles.some((name, index) => name !== previous[index]) ? writeProfileBundles(options.profileDir, after.manifest, bundles) : after.manifest,
|
|
909
|
+
dependencies: after.dependencies.map((dependency) => ({
|
|
910
|
+
...dependency,
|
|
911
|
+
enabled: bundles.includes(dependency.name)
|
|
912
|
+
}))
|
|
913
|
+
},
|
|
914
|
+
addedPlainDependencies: after.dependencies.filter((dependency) => !dependency.bundle && !beforeNames.has(dependency.name)).map((dependency) => dependency.name)
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region lib/types/profile-sanitize.js
|
|
919
|
+
/** Filesystem recovery for callers that own profile shutdown and write exclusion. */
|
|
920
|
+
/**
|
|
921
|
+
* Back up the profile patch and retain only the caller's recovery bundles.
|
|
922
|
+
* The caller must stop the profile and exclude concurrent profile writes.
|
|
923
|
+
* Installed packages and other manifest fields are preserved; patches are never parsed.
|
|
924
|
+
* Failures propagate and may leave completed changes in place for a retry.
|
|
925
|
+
* @param binName - Diagnostic prefix for invalid profile manifests.
|
|
926
|
+
* @param profileDir - Profile directory to recover without loading its plugins.
|
|
927
|
+
* @param bundles - Ordered bundles to enable after recovery.
|
|
928
|
+
* @returns Backup path with a Unix millisecond timestamp and optional collision ordinal, or undefined if absent.
|
|
929
|
+
*/
|
|
930
|
+
function sanitizeProfile(binName, profileDir, bundles) {
|
|
931
|
+
const manifest = existsSync(join(profileDir, "package.json")) ? readProfileManifest(binName, profileDir) : void 0;
|
|
932
|
+
const patchPath = join(profileDir, PROFILE_PATCH_FILENAME);
|
|
933
|
+
const backupBase = `${patchPath}.bak-${Date.now()}`;
|
|
934
|
+
let backupPath = backupBase;
|
|
935
|
+
let ordinal = 0;
|
|
936
|
+
while (existsSync(backupPath)) backupPath = `${backupBase}-${++ordinal}`;
|
|
937
|
+
try {
|
|
938
|
+
renameSync(patchPath, backupPath);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
|
|
941
|
+
backupPath = void 0;
|
|
942
|
+
}
|
|
943
|
+
if (manifest !== void 0) writeProfileBundles(profileDir, manifest, bundles);
|
|
944
|
+
return backupPath;
|
|
945
|
+
}
|
|
946
|
+
//#endregion
|
|
1078
947
|
//#region lib/types/profile-resolution/resolver.js
|
|
1079
948
|
/** In-memory profile package routing for Node's default ESM and CommonJS loaders. */
|
|
1080
949
|
const WORKER_RESOLUTION_KEY = "@deepseek-ai/dsh-app-boot/profile-resolution";
|
|
@@ -1104,27 +973,68 @@ function prefixes(path) {
|
|
|
1104
973
|
const canonical = canonicalPath(path) + sep;
|
|
1105
974
|
return canonical === configured ? [configured] : [configured, canonical];
|
|
1106
975
|
}
|
|
1107
|
-
function
|
|
1108
|
-
const profilePaths = prefixes(generation.profilesDir);
|
|
1109
|
-
const profile = generation.profileDir === void 0 ? [] : prefixes(generation.profileDir);
|
|
976
|
+
function compileResolution(resolution) {
|
|
1110
977
|
return {
|
|
1111
|
-
entries: new Map(
|
|
1112
|
-
profilesDir:
|
|
1113
|
-
profileDir:
|
|
1114
|
-
profilePaths,
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
localPackageNames: new Set(
|
|
1119
|
-
shared: new Set(profilePaths.map((prefix) => join(prefix, "node_modules"))),
|
|
978
|
+
entries: new Map(resolution.entries.map((entry) => [entry.name, entry])),
|
|
979
|
+
profilesDir: resolution.profilesDir,
|
|
980
|
+
profileDir: resolution.profileDir,
|
|
981
|
+
profilePaths: prefixes(resolution.profilesDir),
|
|
982
|
+
profile: resolution.profileDir === void 0 ? [] : prefixes(resolution.profileDir),
|
|
983
|
+
installationPaths: [...new Set(resolution.entries.filter((entry) => entry.scope === "installation").flatMap((entry) => prefixes(entry.packageDir)))],
|
|
984
|
+
linkedPaths: resolution.linkedRoots.flatMap((root) => prefixes(root.realPath)),
|
|
985
|
+
localPackageNames: new Set(resolution.localPackageNames),
|
|
1120
986
|
esmRoutes: /* @__PURE__ */ new Map(),
|
|
1121
987
|
cjsRoutes: /* @__PURE__ */ new Map()
|
|
1122
988
|
};
|
|
1123
989
|
}
|
|
990
|
+
/**
|
|
991
|
+
* The interception layer of a profile is `<profileParent>/node_modules`: an entry occupies its name there, so its
|
|
992
|
+
* subpath misses continue above it, and a name without an entry continues at it.
|
|
993
|
+
*/
|
|
994
|
+
function computeProfileLayer(dir, active) {
|
|
995
|
+
const profileParent = dirname(dir);
|
|
996
|
+
return {
|
|
997
|
+
kind: "profile",
|
|
998
|
+
active,
|
|
999
|
+
localPrefix: dir + sep,
|
|
1000
|
+
nativeAfter: join(profileParent, "package.json"),
|
|
1001
|
+
after: join(dirname(profileParent), "package.json")
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
/** The interception layer of a module path: from its profile directory inside a profiles tree, or its linked root. */
|
|
1005
|
+
function findInterceptionLayer(path, resolution) {
|
|
1006
|
+
const treeRoot = resolution.profilePaths.find((prefix) => path.startsWith(prefix));
|
|
1007
|
+
const activeProfile = resolution.profile.find((prefix) => path.startsWith(prefix));
|
|
1008
|
+
const dir = treeRoot !== void 0 ? profileChild(path, treeRoot) : activeProfile?.slice(0, -1);
|
|
1009
|
+
if (dir !== void 0) return computeProfileLayer(dir, activeProfile !== void 0);
|
|
1010
|
+
if (!startsWithin(path, resolution.linkedPaths)) return void 0;
|
|
1011
|
+
if (startsWithin(path, resolution.installationPaths)) return void 0;
|
|
1012
|
+
return { kind: "linked" };
|
|
1013
|
+
}
|
|
1014
|
+
/** Package names a directory's current manifest lists as peers; an unreadable manifest lists none. */
|
|
1015
|
+
function readPeerNames(directory) {
|
|
1016
|
+
try {
|
|
1017
|
+
const peers = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).peerDependencies;
|
|
1018
|
+
return new Set(peers !== null && typeof peers === "object" ? Object.keys(peers) : []);
|
|
1019
|
+
} catch (_error) {
|
|
1020
|
+
return /* @__PURE__ */ new Set();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1124
1023
|
function startsWithin(path, roots) {
|
|
1125
1024
|
for (const root of roots) if (path.startsWith(root)) return true;
|
|
1126
1025
|
return false;
|
|
1127
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* The profile directory owning a module inside the profiles tree: the first path segment below the tree root.
|
|
1029
|
+
* @param path - module path below `treePrefix`.
|
|
1030
|
+
* @param treePrefix - profiles directory with a trailing separator.
|
|
1031
|
+
* @returns the profile directory without a trailing separator.
|
|
1032
|
+
*/
|
|
1033
|
+
function profileChild(path, treePrefix) {
|
|
1034
|
+
const rest = path.slice(treePrefix.length);
|
|
1035
|
+
const end = rest.indexOf(sep);
|
|
1036
|
+
return treePrefix + (end < 0 ? rest : rest.slice(0, end));
|
|
1037
|
+
}
|
|
1128
1038
|
function nativePackageDir(parent, name) {
|
|
1129
1039
|
for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
1130
1040
|
const candidate = join(searchPath, name);
|
|
@@ -1138,10 +1048,7 @@ function localPackageCandidate(searchPath, name, flavor) {
|
|
|
1138
1048
|
".js",
|
|
1139
1049
|
".json",
|
|
1140
1050
|
".node"
|
|
1141
|
-
].some((extension) => existsSync(candidate + extension))) ?
|
|
1142
|
-
packageDir: candidate,
|
|
1143
|
-
canBeManagedLink: stat !== void 0
|
|
1144
|
-
} : void 0;
|
|
1051
|
+
].some((extension) => existsSync(candidate + extension))) ? candidate : void 0;
|
|
1145
1052
|
}
|
|
1146
1053
|
function selfReferenceName(parent) {
|
|
1147
1054
|
let current = dirname(parent);
|
|
@@ -1198,7 +1105,7 @@ function packageImportsTarget(parent, request, conditions) {
|
|
|
1198
1105
|
}
|
|
1199
1106
|
function packageSearchPaths(entry, request, cjs) {
|
|
1200
1107
|
const name = barePackageName(request);
|
|
1201
|
-
/* v8 ignore next --
|
|
1108
|
+
/* v8 ignore next -- interception routes are created only for bare package requests */
|
|
1202
1109
|
if (name === void 0) return cjs._nodeModulePaths(dirname(entry.declarer));
|
|
1203
1110
|
const suffix = sep + name.split("/").join(sep);
|
|
1204
1111
|
return entry.packageDir.endsWith(suffix) ? [entry.packageDir.slice(0, -suffix.length)] : cjs._nodeModulePaths(dirname(entry.declarer));
|
|
@@ -1229,62 +1136,100 @@ function isUnselectedPackageMiss(error) {
|
|
|
1229
1136
|
}
|
|
1230
1137
|
function sameResolution(left, right) {
|
|
1231
1138
|
if (left === right) return true;
|
|
1232
|
-
|
|
1233
|
-
const rightPath = right.startsWith("file:") ? fileURLToPath(right) : right;
|
|
1234
|
-
return canonicalPath(leftPath) === canonicalPath(rightPath);
|
|
1139
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
1235
1140
|
}
|
|
1236
|
-
/** One mutable pointer to immutable
|
|
1141
|
+
/** One mutable pointer to the immutable runtime resolution. */
|
|
1237
1142
|
var ResolutionRouter = class {
|
|
1143
|
+
nodeModulePaths;
|
|
1238
1144
|
current;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1145
|
+
linkedTargets;
|
|
1146
|
+
constructor(resolution, nodeModulePaths) {
|
|
1147
|
+
this.nodeModulePaths = nodeModulePaths;
|
|
1148
|
+
this.current = compileResolution(resolution);
|
|
1149
|
+
this.linkedTargets = new Map(resolution.linkedRoots.map((root) => [root.name, root.realPath]));
|
|
1150
|
+
}
|
|
1151
|
+
replace(successor) {
|
|
1152
|
+
const entries = new Map(successor.entries.map((entry) => [entry.name, entry]));
|
|
1153
|
+
if (successor.profilesDir !== this.current.profilesDir || successor.profileDir !== this.current.profileDir) throw new Error("profile resolution: a successor cannot change its profile scope");
|
|
1245
1154
|
for (const [name, current] of this.current.entries) {
|
|
1246
1155
|
const next = entries.get(name);
|
|
1247
1156
|
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`);
|
|
1248
1157
|
}
|
|
1249
|
-
const localPackageNames = new Set(
|
|
1158
|
+
const localPackageNames = new Set(successor.localPackageNames);
|
|
1250
1159
|
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`);
|
|
1251
1160
|
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`);
|
|
1252
|
-
|
|
1161
|
+
for (const root of successor.linkedRoots) {
|
|
1162
|
+
const previous = this.linkedTargets.get(root.name);
|
|
1163
|
+
if (previous !== void 0 && !sameResolution(previous, root.realPath)) throw new Error(`profile resolution: relinking ${JSON.stringify(root.name)} requires a process restart`);
|
|
1164
|
+
}
|
|
1165
|
+
const next = compileResolution(successor);
|
|
1166
|
+
for (const { name, realPath } of successor.linkedRoots) if (!this.linkedTargets.has(name)) this.linkedTargets.set(name, realPath);
|
|
1167
|
+
this.current = next;
|
|
1168
|
+
}
|
|
1169
|
+
/** Whether a module path has an interception layer: it lies in the profiles tree, the active profile, or a linked root. */
|
|
1170
|
+
hasInterceptionLayerForPath(path) {
|
|
1171
|
+
return findInterceptionLayer(path, this.current) !== void 0;
|
|
1172
|
+
}
|
|
1173
|
+
/** Whether an importer URL is scoped; the answer is memoized with the importer's routes. */
|
|
1174
|
+
hasInterceptionLayerForUrl(parentURL) {
|
|
1175
|
+
return this.getOrCreateParentRoutesForUrl(parentURL) !== false;
|
|
1176
|
+
}
|
|
1177
|
+
createParentRoutes(parent, resolution) {
|
|
1178
|
+
const layer = findInterceptionLayer(parent, resolution);
|
|
1179
|
+
return layer === void 0 ? void 0 : {
|
|
1180
|
+
parent,
|
|
1181
|
+
layer,
|
|
1182
|
+
requests: /* @__PURE__ */ new Map()
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
getOrCreateParentRoutesForUrl(parentURL) {
|
|
1186
|
+
const resolution = this.current;
|
|
1187
|
+
let parentRoutes = resolution.esmRoutes.get(parentURL);
|
|
1188
|
+
if (parentRoutes !== void 0) return parentRoutes;
|
|
1189
|
+
let parent;
|
|
1190
|
+
try {
|
|
1191
|
+
parent = fileURLToPath(parentURL);
|
|
1192
|
+
} catch {
|
|
1193
|
+
parent = void 0;
|
|
1194
|
+
}
|
|
1195
|
+
parentRoutes = parent === void 0 ? false : this.createParentRoutes(parent, resolution) ?? false;
|
|
1196
|
+
resolution.esmRoutes.set(parentURL, parentRoutes);
|
|
1197
|
+
return parentRoutes;
|
|
1253
1198
|
}
|
|
1254
|
-
routeScoped(request, parentRoutes,
|
|
1255
|
-
const { parent,
|
|
1199
|
+
routeScoped(request, parentRoutes, resolution, flavor, cjs) {
|
|
1200
|
+
const { parent, layer, requests } = parentRoutes;
|
|
1201
|
+
const memo = layer.kind === "profile" && (cjs === void 0 || cjs.cacheable);
|
|
1256
1202
|
const name = barePackageName(request);
|
|
1257
1203
|
if (name === void 0) return void 0;
|
|
1258
|
-
if (parentRoutes.selfReferenceName === void 0) parentRoutes.selfReferenceName = selfReferenceName(parent);
|
|
1204
|
+
if (parentRoutes.selfReferenceName === void 0 || !memo) parentRoutes.selfReferenceName = selfReferenceName(parent);
|
|
1259
1205
|
if (parentRoutes.selfReferenceName === name || parentRoutes.selfReferenceName === null) {
|
|
1260
1206
|
const state = { route: { kind: "native" } };
|
|
1261
|
-
requests.set(request, state);
|
|
1207
|
+
if (memo) requests.set(request, state);
|
|
1262
1208
|
return state;
|
|
1263
1209
|
}
|
|
1264
|
-
|
|
1210
|
+
if (layer.kind === "linked") return this.routeLinked(request, name, parent, resolution, flavor, cjs);
|
|
1211
|
+
const target = resolution.entries.get(name);
|
|
1265
1212
|
const candidates = [];
|
|
1266
1213
|
const localSearchPaths = [];
|
|
1267
1214
|
for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
1268
|
-
if (
|
|
1215
|
+
if (!searchPath.startsWith(layer.localPrefix)) break;
|
|
1269
1216
|
localSearchPaths.push(searchPath);
|
|
1270
1217
|
const candidate = localPackageCandidate(searchPath, name, flavor);
|
|
1271
|
-
if (candidate !== void 0)
|
|
1272
|
-
if (!(candidate.canBeManagedLink && generation.profile.some((prefix) => candidate.packageDir === join(prefix, "node_modules", name) && isProfileModuleFallbackLink(prefix.slice(0, -1), name)))) candidates.push(candidate);
|
|
1273
|
-
}
|
|
1218
|
+
if (candidate !== void 0) candidates.push(candidate);
|
|
1274
1219
|
}
|
|
1275
|
-
if (candidates.length > 0) if (
|
|
1276
|
-
const resolved =
|
|
1277
|
-
const selected = candidates.find((candidate) => localCandidateOwnsResolution(candidate
|
|
1220
|
+
if (candidates.length > 0) if (cjs !== void 0) try {
|
|
1221
|
+
const resolved = cjs.resolveNative(localSearchPaths);
|
|
1222
|
+
const selected = candidates.find((candidate) => localCandidateOwnsResolution(candidate, resolved, request, name));
|
|
1278
1223
|
if (selected !== void 0) {
|
|
1279
1224
|
const state = {
|
|
1280
1225
|
route: {
|
|
1281
1226
|
kind: "native",
|
|
1282
|
-
packageDir: selected
|
|
1227
|
+
packageDir: selected
|
|
1283
1228
|
},
|
|
1284
|
-
packageDir: selected
|
|
1285
|
-
|
|
1229
|
+
packageDir: selected,
|
|
1230
|
+
cjs: resolved
|
|
1286
1231
|
};
|
|
1287
|
-
requests.set(request, state);
|
|
1232
|
+
if (memo) requests.set(request, state);
|
|
1288
1233
|
return state;
|
|
1289
1234
|
}
|
|
1290
1235
|
} catch (error) {
|
|
@@ -1295,122 +1240,120 @@ var ResolutionRouter = class {
|
|
|
1295
1240
|
const state = {
|
|
1296
1241
|
route: {
|
|
1297
1242
|
kind: "native",
|
|
1298
|
-
packageDir: selected
|
|
1243
|
+
packageDir: selected
|
|
1299
1244
|
},
|
|
1300
|
-
packageDir: selected
|
|
1245
|
+
packageDir: selected
|
|
1301
1246
|
};
|
|
1302
1247
|
requests.set(request, state);
|
|
1303
1248
|
return state;
|
|
1304
1249
|
}
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
const route = eligible ? {
|
|
1308
|
-
kind: "fallback",
|
|
1250
|
+
const route = target !== void 0 && (target.scope === "installation" || layer.active) ? {
|
|
1251
|
+
kind: "interception",
|
|
1309
1252
|
entry: target,
|
|
1310
|
-
after
|
|
1253
|
+
after: layer.after
|
|
1311
1254
|
} : {
|
|
1312
|
-
kind: "after-
|
|
1313
|
-
parent:
|
|
1255
|
+
kind: "native-after-interception",
|
|
1256
|
+
parent: layer.nativeAfter
|
|
1314
1257
|
};
|
|
1315
1258
|
const state = { route };
|
|
1316
|
-
if (route.kind === "
|
|
1259
|
+
if (route.kind === "interception" && memo) requests.set(request, state);
|
|
1317
1260
|
return state;
|
|
1318
1261
|
}
|
|
1319
|
-
|
|
1262
|
+
routeLinked(request, name, parent, resolution, flavor, cjs) {
|
|
1263
|
+
const target = resolution.entries.get(name);
|
|
1264
|
+
if (target === void 0) return { route: { kind: "native" } };
|
|
1265
|
+
const ancestors = this.nodeModulePaths(dirname(parent));
|
|
1266
|
+
const ancestorSet = new Set(ancestors);
|
|
1267
|
+
const searchPaths = flavor === "esm" ? ancestors : createRequire(parent).resolve.paths(name);
|
|
1268
|
+
for (const searchPath of searchPaths) {
|
|
1269
|
+
const directory = dirname(searchPath);
|
|
1270
|
+
if (ancestorSet.has(searchPath) && readPeerNames(directory).has(name)) {
|
|
1271
|
+
const route = {
|
|
1272
|
+
kind: "interception",
|
|
1273
|
+
entry: target,
|
|
1274
|
+
after: join(dirname(directory), "package.json")
|
|
1275
|
+
};
|
|
1276
|
+
if (cjs === void 0) return { route };
|
|
1277
|
+
try {
|
|
1278
|
+
const resolved = cjs.resolveEntry(target, route.after);
|
|
1279
|
+
if (localCandidateOwnsResolution(target.packageDir, resolved, request, name)) return {
|
|
1280
|
+
route,
|
|
1281
|
+
packageDir: target.packageDir,
|
|
1282
|
+
cjs: resolved
|
|
1283
|
+
};
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1286
|
+
}
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const candidate = localPackageCandidate(searchPath, name, flavor);
|
|
1290
|
+
if (candidate === void 0) continue;
|
|
1291
|
+
if (cjs === void 0) return {
|
|
1292
|
+
route: {
|
|
1293
|
+
kind: "native",
|
|
1294
|
+
packageDir: candidate
|
|
1295
|
+
},
|
|
1296
|
+
packageDir: candidate
|
|
1297
|
+
};
|
|
1298
|
+
try {
|
|
1299
|
+
const resolved = cjs.resolveNative([searchPath]);
|
|
1300
|
+
if (localCandidateOwnsResolution(candidate, resolved, request, name)) return {
|
|
1301
|
+
route: {
|
|
1302
|
+
kind: "native",
|
|
1303
|
+
packageDir: candidate
|
|
1304
|
+
},
|
|
1305
|
+
packageDir: candidate,
|
|
1306
|
+
cjs: resolved
|
|
1307
|
+
};
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
return cjs === void 0 ? { route: { kind: "native" } } : {
|
|
1313
|
+
route: { kind: "native" },
|
|
1314
|
+
cjs: cjs.resolveNative([])
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
routeLocalPackage(request, parentRoutes, resolution) {
|
|
1320
1318
|
const name = barePackageName(request);
|
|
1321
|
-
|
|
1319
|
+
const { layer } = parentRoutes;
|
|
1320
|
+
if (name === void 0 || layer.kind !== "profile" || !layer.active || !resolution.localPackageNames.has(name)) return void 0;
|
|
1322
1321
|
const state = { route: { kind: "native" } };
|
|
1323
1322
|
parentRoutes.requests.set(request, state);
|
|
1324
1323
|
return state;
|
|
1325
1324
|
}
|
|
1326
1325
|
routeUrl(request, parentURL) {
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1326
|
+
const resolution = this.current;
|
|
1327
|
+
const parentRoutes = this.getOrCreateParentRoutesForUrl(parentURL);
|
|
1329
1328
|
if (parentRoutes === false) return void 0;
|
|
1330
|
-
const cached = parentRoutes
|
|
1329
|
+
const cached = parentRoutes.requests.get(request);
|
|
1331
1330
|
if (cached !== void 0) return cached;
|
|
1332
|
-
|
|
1333
|
-
const profileIndex = generation.profileUrls.findIndex((prefix) => parentURL.startsWith(prefix));
|
|
1334
|
-
const activeProfileIndex = generation.activeProfileUrls.findIndex((prefix) => parentURL.startsWith(prefix));
|
|
1335
|
-
if (profileIndex < 0 && activeProfileIndex < 0) {
|
|
1336
|
-
generation.esmRoutes.set(parentURL, false);
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
let parent;
|
|
1340
|
-
try {
|
|
1341
|
-
parent = fileURLToPath(parentURL);
|
|
1342
|
-
} catch {
|
|
1343
|
-
generation.esmRoutes.set(parentURL, false);
|
|
1344
|
-
return;
|
|
1345
|
-
}
|
|
1346
|
-
const profileRoot = profileIndex >= 0 ? generation.profilePaths[profileIndex] : generation.profile[activeProfileIndex];
|
|
1347
|
-
/* v8 ignore next -- one matching index was established above */
|
|
1348
|
-
if (profileRoot === void 0) return void 0;
|
|
1349
|
-
parentRoutes = {
|
|
1350
|
-
parent,
|
|
1351
|
-
profilesDir: profileRoot.slice(0, -1),
|
|
1352
|
-
activeProfile: activeProfileIndex >= 0,
|
|
1353
|
-
requests: /* @__PURE__ */ new Map()
|
|
1354
|
-
};
|
|
1355
|
-
generation.esmRoutes.set(parentURL, parentRoutes);
|
|
1356
|
-
}
|
|
1357
|
-
const local = this.routeLocalPackage(request, parentRoutes, generation);
|
|
1331
|
+
const local = this.routeLocalPackage(request, parentRoutes, resolution);
|
|
1358
1332
|
if (local !== void 0) return local;
|
|
1359
|
-
return this.routeScoped(request, parentRoutes,
|
|
1333
|
+
return this.routeScoped(request, parentRoutes, resolution, "esm");
|
|
1360
1334
|
}
|
|
1361
|
-
routePath(request, parent,
|
|
1362
|
-
const
|
|
1363
|
-
let parentRoutes =
|
|
1335
|
+
routePath(request, parent, cjs) {
|
|
1336
|
+
const resolution = this.current;
|
|
1337
|
+
let parentRoutes = resolution.cjsRoutes.get(parent);
|
|
1364
1338
|
if (parentRoutes === false) return void 0;
|
|
1365
|
-
const cached = parentRoutes?.requests.get(request);
|
|
1339
|
+
const cached = cjs.cacheable ? parentRoutes?.requests.get(request) : void 0;
|
|
1366
1340
|
if (cached !== void 0) return cached;
|
|
1367
1341
|
if (parentRoutes === void 0) {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
if (
|
|
1371
|
-
const profileRoot = profilesDir ?? activeProfile;
|
|
1372
|
-
/* v8 ignore next -- one matching root was established above */
|
|
1373
|
-
if (profileRoot === void 0) return void 0;
|
|
1374
|
-
parentRoutes = {
|
|
1375
|
-
parent,
|
|
1376
|
-
profilesDir: profileRoot.slice(0, -1),
|
|
1377
|
-
activeProfile: activeProfile !== void 0,
|
|
1378
|
-
requests: /* @__PURE__ */ new Map()
|
|
1379
|
-
};
|
|
1380
|
-
generation.cjsRoutes.set(parent, parentRoutes);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
if (parentRoutes === void 0) {
|
|
1384
|
-
generation.cjsRoutes.set(parent, false);
|
|
1385
|
-
return;
|
|
1386
|
-
}
|
|
1387
|
-
return this.routeScoped(request, parentRoutes, generation, "cjs", nativeResolve, cacheable);
|
|
1388
|
-
}
|
|
1389
|
-
explicitRoute(request, paths) {
|
|
1390
|
-
if (barePackageName(request) === void 0) return void 0;
|
|
1391
|
-
for (const [index, path] of paths.entries()) {
|
|
1392
|
-
const parent = join(resolve(path), ".dsh-profile-resolution.cjs");
|
|
1393
|
-
if (startsWithin(parent, this.current.profilePaths) || startsWithin(parent, this.current.profile)) return {
|
|
1394
|
-
index,
|
|
1395
|
-
parent
|
|
1396
|
-
};
|
|
1342
|
+
parentRoutes = this.createParentRoutes(parent, resolution) ?? false;
|
|
1343
|
+
resolution.cjsRoutes.set(parent, parentRoutes);
|
|
1344
|
+
if (parentRoutes === false) return void 0;
|
|
1397
1345
|
}
|
|
1398
|
-
|
|
1399
|
-
nativeSelfReference(request, parent) {
|
|
1400
|
-
const name = barePackageName(request);
|
|
1401
|
-
if (name === void 0) return false;
|
|
1402
|
-
const self = selfReferenceName(parent);
|
|
1403
|
-
return self === name || self === null;
|
|
1346
|
+
return this.routeScoped(request, parentRoutes, resolution, "cjs", cjs);
|
|
1404
1347
|
}
|
|
1405
1348
|
packageDir(specifier, parentURL) {
|
|
1406
1349
|
const name = barePackageName(specifier);
|
|
1407
1350
|
if (name === void 0) return void 0;
|
|
1408
|
-
const state = this.routeUrl(
|
|
1409
|
-
if (state?.route.kind === "
|
|
1351
|
+
const state = this.routeUrl(name, parentURL);
|
|
1352
|
+
if (state?.route.kind === "interception") return state.route.entry.packageDir;
|
|
1410
1353
|
if (state?.packageDir !== void 0) return state.packageDir;
|
|
1411
1354
|
let parent;
|
|
1412
1355
|
try {
|
|
1413
|
-
parent = state?.route.kind === "after-
|
|
1356
|
+
parent = state?.route.kind === "native-after-interception" ? state.route.parent : fileURLToPath(parentURL);
|
|
1414
1357
|
} catch {
|
|
1415
1358
|
return;
|
|
1416
1359
|
}
|
|
@@ -1454,353 +1397,1427 @@ function throwWithImporter(error, routedParent, parent) {
|
|
|
1454
1397
|
}
|
|
1455
1398
|
throw error;
|
|
1456
1399
|
}
|
|
1457
|
-
function throwWithoutCjsAnchor(error, anchor) {
|
|
1458
|
-
const resolved = error;
|
|
1459
|
-
const requireStack = resolved.requireStack;
|
|
1460
|
-
if (error instanceof Error && resolved.code === "MODULE_NOT_FOUND" && requireStack?.[0] !== void 0 && sameResolution(requireStack[0], anchor)) {
|
|
1461
|
-
const originalMessage = error.message;
|
|
1462
|
-
const originalBlock = `\nRequire stack:\n${requireStack.map((path) => `- ${path}`).join("\n")}`;
|
|
1463
|
-
const remaining = requireStack.slice(1);
|
|
1464
|
-
/* v8 ignore next -- routed calls always retain the original importing module */
|
|
1465
|
-
const replacement = remaining.length === 0 ? "" : `\nRequire stack:\n${remaining.map((path) => `- ${path}`).join("\n")}`;
|
|
1466
|
-
error.message = originalMessage.replace(originalBlock, replacement);
|
|
1467
|
-
resolved.requireStack = remaining;
|
|
1468
|
-
const stack = error.stack;
|
|
1469
|
-
/* v8 ignore next -- Node's resolver errors always carry a stack */
|
|
1470
|
-
if (stack !== void 0) error.stack = stack.replace(originalMessage, error.message);
|
|
1400
|
+
function throwWithoutCjsAnchor(error, anchor) {
|
|
1401
|
+
const resolved = error;
|
|
1402
|
+
const requireStack = resolved.requireStack;
|
|
1403
|
+
if (error instanceof Error && resolved.code === "MODULE_NOT_FOUND" && requireStack?.[0] !== void 0 && sameResolution(requireStack[0], anchor)) {
|
|
1404
|
+
const originalMessage = error.message;
|
|
1405
|
+
const originalBlock = `\nRequire stack:\n${requireStack.map((path) => `- ${path}`).join("\n")}`;
|
|
1406
|
+
const remaining = requireStack.slice(1);
|
|
1407
|
+
/* v8 ignore next -- routed calls always retain the original importing module */
|
|
1408
|
+
const replacement = remaining.length === 0 ? "" : `\nRequire stack:\n${remaining.map((path) => `- ${path}`).join("\n")}`;
|
|
1409
|
+
error.message = originalMessage.replace(originalBlock, replacement);
|
|
1410
|
+
resolved.requireStack = remaining;
|
|
1411
|
+
const stack = error.stack;
|
|
1412
|
+
/* v8 ignore next -- Node's resolver errors always carry a stack */
|
|
1413
|
+
if (stack !== void 0) error.stack = stack.replace(originalMessage, error.message);
|
|
1414
|
+
}
|
|
1415
|
+
throw error;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Install one runtime resolution as the interception on Node's default ESM and CommonJS resolvers.
|
|
1419
|
+
* @param resolution - complete package table and profile scope.
|
|
1420
|
+
* @returns an interception that publishes a successor or restores the native methods.
|
|
1421
|
+
*/
|
|
1422
|
+
function installRuntimeInterception(resolution) {
|
|
1423
|
+
const { esm, esmDefaultResolve, esmConditions, cjs, cjsConditions, modern } = internalModules();
|
|
1424
|
+
const router = new ResolutionRouter(resolution, (directory) => cjs._nodeModulePaths(directory));
|
|
1425
|
+
let delegatedEsm;
|
|
1426
|
+
const adaptEsm = (native) => {
|
|
1427
|
+
const adapted = (request, parent, attributes) => {
|
|
1428
|
+
const delegated = delegatedEsm;
|
|
1429
|
+
/* v8 ignore next -- reentry requires a separate synchronous Node hook; supported launches install none */
|
|
1430
|
+
if (delegated !== void 0 && delegated.parent === parent && delegated.request === request) return native(request, parent, attributes);
|
|
1431
|
+
if (parent === void 0 || !router.hasInterceptionLayerForUrl(parent)) return native(request, parent, attributes);
|
|
1432
|
+
const state = router.routeUrl(request, parent);
|
|
1433
|
+
if (state === void 0) {
|
|
1434
|
+
const target = request[0] === "#" ? packageImportsTarget(fileURLToPath(parent), request, esmConditions) : void 0;
|
|
1435
|
+
if (target === void 0) return native(request, parent, attributes);
|
|
1436
|
+
const restoreImporter = (error) => throwWithImporter(error, target.parentURL, parent);
|
|
1437
|
+
let expected;
|
|
1438
|
+
try {
|
|
1439
|
+
expected = adapted(target.specifier, target.parentURL, attributes);
|
|
1440
|
+
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1441
|
+
if (expected instanceof Promise) expected = expected.catch(restoreImporter);
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
return restoreImporter(error);
|
|
1444
|
+
}
|
|
1445
|
+
return expected;
|
|
1446
|
+
}
|
|
1447
|
+
const cacheable = attributes === EMPTY_ATTRIBUTES || Object.keys(attributes).length === 0;
|
|
1448
|
+
if (cacheable && state.esm !== void 0) return state.esm;
|
|
1449
|
+
const route = state.route;
|
|
1450
|
+
if (route.kind === "native") {
|
|
1451
|
+
const result = native(request, parent, attributes);
|
|
1452
|
+
if (cacheable && !(result instanceof Promise)) state.esm = result;
|
|
1453
|
+
return result;
|
|
1454
|
+
}
|
|
1455
|
+
const routedParent = pathToFileURL(route.kind === "interception" ? route.entry.declarer : route.parent).href;
|
|
1456
|
+
const previous = delegatedEsm;
|
|
1457
|
+
delegatedEsm = {
|
|
1458
|
+
parent: routedParent,
|
|
1459
|
+
request
|
|
1460
|
+
};
|
|
1461
|
+
const restoreImporter = (error) => throwWithImporter(error, routedParent, parent);
|
|
1462
|
+
try {
|
|
1463
|
+
let result;
|
|
1464
|
+
try {
|
|
1465
|
+
result = native(request, routedParent, attributes);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return restoreImporter(error);
|
|
1468
|
+
}
|
|
1469
|
+
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1470
|
+
if (result instanceof Promise) return result.catch(restoreImporter);
|
|
1471
|
+
if (cacheable) state.esm = result;
|
|
1472
|
+
return result;
|
|
1473
|
+
} finally {
|
|
1474
|
+
delegatedEsm = previous;
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
return adapted;
|
|
1478
|
+
};
|
|
1479
|
+
let restoreEsm;
|
|
1480
|
+
/* v8 ignore else -- CI coverage runs Node 24 v2; the Node 22 matrix exercises the v1 adapter */
|
|
1481
|
+
if (modern) {
|
|
1482
|
+
const loader = esm;
|
|
1483
|
+
const original = Reflect.get(loader, "resolveSync");
|
|
1484
|
+
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, parent, {
|
|
1485
|
+
specifier: request,
|
|
1486
|
+
attributes
|
|
1487
|
+
}));
|
|
1488
|
+
const wrapped = (parent, request, ...rest) => rest.length ? Reflect.apply(original, loader, [
|
|
1489
|
+
parent,
|
|
1490
|
+
request,
|
|
1491
|
+
...rest
|
|
1492
|
+
]) : resolveRequest(request.specifier, parent, request.attributes ?? EMPTY_ATTRIBUTES);
|
|
1493
|
+
loader.resolveSync = wrapped;
|
|
1494
|
+
restoreEsm = () => {
|
|
1495
|
+
/* v8 ignore else -- interceptions are disposed in reverse installation order */
|
|
1496
|
+
if (loader.resolveSync === wrapped) loader.resolveSync = original;
|
|
1497
|
+
};
|
|
1498
|
+
} else {
|
|
1499
|
+
const loader = esm;
|
|
1500
|
+
const original = Reflect.get(loader, "resolve");
|
|
1501
|
+
const originalSync = Reflect.get(loader, "resolveSync");
|
|
1502
|
+
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, request, parent, attributes));
|
|
1503
|
+
const resolveRequestSync = adaptEsm((request, parent, attributes) => originalSync.call(loader, request, parent, attributes));
|
|
1504
|
+
const wrapped = (request, parent, attributes = EMPTY_ATTRIBUTES) => resolveRequest(request, parent, attributes);
|
|
1505
|
+
const wrappedSync = (request, parent, attributes = EMPTY_ATTRIBUTES) => resolveRequestSync(request, parent, attributes);
|
|
1506
|
+
loader.resolve = wrapped;
|
|
1507
|
+
loader.resolveSync = wrappedSync;
|
|
1508
|
+
restoreEsm = () => {
|
|
1509
|
+
if (loader.resolve === wrapped) loader.resolve = original;
|
|
1510
|
+
if (loader.resolveSync === wrappedSync) loader.resolveSync = originalSync;
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
const originalFilename = Reflect.get(cjs, "_resolveFilename");
|
|
1514
|
+
let delegatedCjs = 0;
|
|
1515
|
+
const resolveRoutedCjs = (request, routed, parent, main, options) => {
|
|
1516
|
+
const anchor = routed.kind === "interception" ? routed.entry.declarer : routed.parent;
|
|
1517
|
+
const synthetic = new cjs(anchor);
|
|
1518
|
+
synthetic.parent = parent;
|
|
1519
|
+
synthetic.filename = anchor;
|
|
1520
|
+
synthetic.paths = routed.kind === "interception" ? packageSearchPaths(routed.entry, request, cjs) : cjs._nodeModulePaths(dirname(anchor));
|
|
1521
|
+
try {
|
|
1522
|
+
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
return throwWithoutCjsAnchor(error, anchor);
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
const resolveNativeCjs = (request, searchPaths, parent, parentFilename, main, conditions) => {
|
|
1528
|
+
const synthetic = new cjs(parentFilename);
|
|
1529
|
+
if (parent.parent !== void 0) synthetic.parent = parent.parent;
|
|
1530
|
+
synthetic.filename = parentFilename;
|
|
1531
|
+
synthetic.paths = [...searchPaths];
|
|
1532
|
+
const options = conditions === void 0 ? void 0 : { conditions };
|
|
1533
|
+
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1534
|
+
};
|
|
1535
|
+
const resolvePackageImportCjs = (target, conditions) => {
|
|
1536
|
+
const state = router.routeUrl(target.specifier, target.parentURL);
|
|
1537
|
+
const resolveFrom = (parentURL) => fileURLToPath(esmDefaultResolve(target.specifier, {
|
|
1538
|
+
parentURL,
|
|
1539
|
+
conditions: [...conditions]
|
|
1540
|
+
}).url);
|
|
1541
|
+
/* v8 ignore next -- the target manifest was found inside the established profile scope */
|
|
1542
|
+
if (state === void 0) return resolveFrom(target.parentURL);
|
|
1543
|
+
if (state.route.kind === "native") return resolveFrom(target.parentURL);
|
|
1544
|
+
const route = state.route;
|
|
1545
|
+
if (route.kind === "native-after-interception") return resolveFrom(pathToFileURL(route.parent).href);
|
|
1546
|
+
return resolveFrom(pathToFileURL(route.entry.declarer).href);
|
|
1547
|
+
};
|
|
1548
|
+
const wrappedFilename = (request, parent, main, options) => {
|
|
1549
|
+
if (delegatedCjs || !parent?.filename || options?.paths !== void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1550
|
+
const parentFilename = parent.filename;
|
|
1551
|
+
const cacheable = options?.conditions === void 0;
|
|
1552
|
+
const routedOptions = options?.conditions === void 0 ? void 0 : { conditions: options.conditions };
|
|
1553
|
+
const state = router.routePath(request, parentFilename, {
|
|
1554
|
+
cacheable,
|
|
1555
|
+
resolveNative: (searchPaths) => resolveNativeCjs(request, searchPaths, parent, parentFilename, main, options?.conditions),
|
|
1556
|
+
resolveEntry: (entry, after) => {
|
|
1557
|
+
delegatedCjs++;
|
|
1558
|
+
try {
|
|
1559
|
+
return resolveRoutedCjs(request, {
|
|
1560
|
+
kind: "interception",
|
|
1561
|
+
entry,
|
|
1562
|
+
after
|
|
1563
|
+
}, parent, main, routedOptions);
|
|
1564
|
+
} finally {
|
|
1565
|
+
delegatedCjs--;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
if (state === void 0) {
|
|
1570
|
+
const scoped = router.hasInterceptionLayerForPath(parentFilename);
|
|
1571
|
+
const conditions = options?.conditions ?? cjsConditions;
|
|
1572
|
+
const target = request[0] === "#" && scoped ? packageImportsTarget(parentFilename, request, conditions) : void 0;
|
|
1573
|
+
if (target === void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1574
|
+
return resolvePackageImportCjs(target, conditions);
|
|
1575
|
+
}
|
|
1576
|
+
if (state.cjs !== void 0) return state.cjs;
|
|
1577
|
+
const route = state.route;
|
|
1578
|
+
if (route.kind === "native") {
|
|
1579
|
+
const result = originalFilename.call(cjs, request, parent, main, options);
|
|
1580
|
+
if (cacheable) state.cjs = result;
|
|
1581
|
+
return result;
|
|
1582
|
+
}
|
|
1583
|
+
delegatedCjs++;
|
|
1584
|
+
try {
|
|
1585
|
+
let expected;
|
|
1586
|
+
try {
|
|
1587
|
+
expected = resolveRoutedCjs(request, route, parent, main, routedOptions);
|
|
1588
|
+
} catch (error) {
|
|
1589
|
+
if (route.kind !== "interception" || !isUnselectedPackageMiss(error)) throw error;
|
|
1590
|
+
expected = resolveRoutedCjs(request, {
|
|
1591
|
+
kind: "native-after-interception",
|
|
1592
|
+
parent: route.after
|
|
1593
|
+
}, parent, main, routedOptions);
|
|
1594
|
+
}
|
|
1595
|
+
if (cacheable) state.cjs = expected;
|
|
1596
|
+
return expected;
|
|
1597
|
+
} finally {
|
|
1598
|
+
delegatedCjs--;
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
cjs._resolveFilename = wrappedFilename;
|
|
1602
|
+
return {
|
|
1603
|
+
packageDir(specifier, parentURL) {
|
|
1604
|
+
return router.packageDir(specifier, parentURL);
|
|
1605
|
+
},
|
|
1606
|
+
replace(next) {
|
|
1607
|
+
router.replace(next);
|
|
1608
|
+
},
|
|
1609
|
+
dispose() {
|
|
1610
|
+
/* v8 ignore else -- interceptions are disposed in reverse installation order */
|
|
1611
|
+
if (cjs._resolveFilename === wrappedFilename) cjs._resolveFilename = originalFilename;
|
|
1612
|
+
restoreEsm();
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* Publish one runtime resolution for Harness-owned Workers.
|
|
1618
|
+
* @param resolution - complete package table and profile scope.
|
|
1619
|
+
* @returns a disposer restoring the previous thread environment data.
|
|
1620
|
+
*/
|
|
1621
|
+
function registerWorkerResolution(resolution) {
|
|
1622
|
+
const previous = getEnvironmentData(WORKER_RESOLUTION_KEY);
|
|
1623
|
+
setEnvironmentData(WORKER_RESOLUTION_KEY, { resolution });
|
|
1624
|
+
return () => {
|
|
1625
|
+
setEnvironmentData(WORKER_RESOLUTION_KEY, previous);
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
//#endregion
|
|
1629
|
+
//#region lib/types/package-meta.js
|
|
1630
|
+
/** Read plugin display text and icons through exported resources without evaluating plugin code. */
|
|
1631
|
+
const LANGUAGE_ID = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/u;
|
|
1632
|
+
/** Maximum raw icon bytes admitted to plugin metadata. */
|
|
1633
|
+
const MAX_ICON_BYTES = 256 * 1024;
|
|
1634
|
+
const ICON_MEDIA_TYPES = new Map([
|
|
1635
|
+
[".svg", "image/svg+xml"],
|
|
1636
|
+
[".png", "image/png"],
|
|
1637
|
+
[".jpg", "image/jpeg"],
|
|
1638
|
+
[".jpeg", "image/jpeg"],
|
|
1639
|
+
[".webp", "image/webp"]
|
|
1640
|
+
]);
|
|
1641
|
+
function iconOf(value, manifestPath) {
|
|
1642
|
+
const icon = textOf(value, `${manifestPath}: icon`);
|
|
1643
|
+
if (icon === void 0) return void 0;
|
|
1644
|
+
if (isAbsolute(icon) || win32.isAbsolute(icon) || /^[A-Za-z][A-Za-z\d+.-]*:/u.test(icon)) throw new Error(`${manifestPath}: icon must be a relative file path`);
|
|
1645
|
+
const mediaType = ICON_MEDIA_TYPES.get(extname(icon).toLowerCase());
|
|
1646
|
+
if (mediaType === void 0) throw new Error(`${manifestPath}: icon must be SVG, PNG, JPEG, or WebP`);
|
|
1647
|
+
const directory = realpathSync(dirname(manifestPath));
|
|
1648
|
+
const file = realpathSync(resolve(directory, icon));
|
|
1649
|
+
const local = relative(directory, file);
|
|
1650
|
+
if (local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local)) throw new Error(`${manifestPath}: icon must remain inside its manifest directory`);
|
|
1651
|
+
const stat = statSync(file);
|
|
1652
|
+
if (!stat.isFile()) throw new Error(`${manifestPath}: icon must be a regular file`);
|
|
1653
|
+
if (stat.size > MAX_ICON_BYTES) throw new Error(`${manifestPath}: icon exceeds 256 KiB`);
|
|
1654
|
+
const bytes = readFileSync(file);
|
|
1655
|
+
if (bytes.length > MAX_ICON_BYTES) throw new Error(`${manifestPath}: icon exceeds 256 KiB`);
|
|
1656
|
+
return `data:${mediaType};base64,${bytes.toString("base64")}`;
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Resolve a plugin resource through the active Node ESM resolver without evaluating it.
|
|
1660
|
+
* @param specifier - complete resource module specifier, including its locale filename.
|
|
1661
|
+
* @param parentURL - owning module-resolution base.
|
|
1662
|
+
* @returns the local filesystem path selected by Node and the active profile.
|
|
1663
|
+
* @throws when the resolver is unavailable or the resource cannot resolve to a local file.
|
|
1664
|
+
*/
|
|
1665
|
+
function resolvePluginResource(specifier, parentURL) {
|
|
1666
|
+
const loader = ModuleLoader.fromInternal();
|
|
1667
|
+
if (loader === void 0) throw new Error("Plugin metadata requires the Node module resolver");
|
|
1668
|
+
return fileURLToPath((loader.version === "v2" ? loader.resolveSync(parentURL, {
|
|
1669
|
+
specifier,
|
|
1670
|
+
attributes: {}
|
|
1671
|
+
}) : loader.resolveSync(specifier, parentURL, {})).url);
|
|
1672
|
+
}
|
|
1673
|
+
function missingResource(error) {
|
|
1674
|
+
const code = error?.code;
|
|
1675
|
+
return code === "ERR_PACKAGE_PATH_NOT_EXPORTED" || code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "ENOENT" || code === "ENOTDIR";
|
|
1676
|
+
}
|
|
1677
|
+
function optionalResourcePath(specifier, parentURL) {
|
|
1678
|
+
try {
|
|
1679
|
+
return resolvePluginResource(specifier, parentURL);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
if (missingResource(error)) return void 0;
|
|
1682
|
+
throw error;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function objectOf(value, path) {
|
|
1686
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
|
|
1687
|
+
return value;
|
|
1688
|
+
}
|
|
1689
|
+
function textOf(value, path) {
|
|
1690
|
+
if (value === void 0) return void 0;
|
|
1691
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`${path} must be a non-empty string`);
|
|
1692
|
+
return value;
|
|
1693
|
+
}
|
|
1694
|
+
function readObject(file) {
|
|
1695
|
+
let contents;
|
|
1696
|
+
try {
|
|
1697
|
+
contents = JSON.parse(readFileSync(file, "utf8"));
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
throw new Error(`${file}: ${String(error)}`);
|
|
1700
|
+
}
|
|
1701
|
+
return objectOf(contents, file);
|
|
1702
|
+
}
|
|
1703
|
+
function fallbackText(value) {
|
|
1704
|
+
return typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1705
|
+
}
|
|
1706
|
+
function dictionariesOf(englishPath, specifier, parentURL) {
|
|
1707
|
+
const dictionaries = /* @__PURE__ */ new Map();
|
|
1708
|
+
for (const entry of readdirSync(dirname(englishPath), { withFileTypes: true })) {
|
|
1709
|
+
if (!entry.name.endsWith(".json")) continue;
|
|
1710
|
+
const resource = `${specifier}/locale/${entry.name}`;
|
|
1711
|
+
const language = entry.name.slice(0, -5);
|
|
1712
|
+
if (!LANGUAGE_ID.test(language)) throw new Error(`${resource} must use a language id as its filename`);
|
|
1713
|
+
const id = language.toLowerCase();
|
|
1714
|
+
if (dictionaries.has(id)) throw new Error(`${resource} duplicates locale ${id}`);
|
|
1715
|
+
const file = resolvePluginResource(resource, parentURL);
|
|
1716
|
+
if (dirname(file) !== dirname(englishPath)) throw new Error(`${resource}: ${file} must share the English locale directory ${dirname(englishPath)}`);
|
|
1717
|
+
const parsed = readObject(file);
|
|
1718
|
+
const meta = parsed.meta === void 0 ? void 0 : objectOf(parsed.meta, `${file}: meta`);
|
|
1719
|
+
dictionaries.set(id, {
|
|
1720
|
+
title: textOf(meta?.title, `${file}: meta.title`),
|
|
1721
|
+
description: textOf(meta?.description, `${file}: meta.description`)
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
return dictionaries;
|
|
1725
|
+
}
|
|
1726
|
+
function localizedText(field, dictionaries, fallback, finalFallback) {
|
|
1727
|
+
const entries = [...dictionaries].flatMap(([language, fields]) => {
|
|
1728
|
+
const value = fields[field];
|
|
1729
|
+
return value === void 0 ? [] : [[language, value]];
|
|
1730
|
+
});
|
|
1731
|
+
if (entries.length === 0) return fallback;
|
|
1732
|
+
return {
|
|
1733
|
+
en: fallback ?? finalFallback,
|
|
1734
|
+
...Object.fromEntries(entries)
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Read localized display text and the icon declared in a plugin's exported package.json.
|
|
1739
|
+
* Icons are manifest-relative SVG, PNG, JPEG, or WebP files of at most 256 KiB,
|
|
1740
|
+
* contained in the manifest directory after realpath resolution. Icon failures retain display text.
|
|
1741
|
+
* Non-package specifiers are skipped without invoking the resource resolver.
|
|
1742
|
+
* Language files share the directory containing the resolved English resource;
|
|
1743
|
+
* each file is resolved through the complete plugin specifier before reading.
|
|
1744
|
+
* Missing fields use the same address's package.json name/description. Translation
|
|
1745
|
+
* maps retain an English fallback, ultimately the full module specifier for titles and empty for descriptions.
|
|
1746
|
+
* @param specifier - configured plugin module name, including any package subpath.
|
|
1747
|
+
* @param parentURL - owning Loader tree's module-resolution base.
|
|
1748
|
+
* @returns display fields and any icon diagnostic, or undefined for non-package specifiers or absent metadata.
|
|
1749
|
+
*/
|
|
1750
|
+
function readPluginMeta(specifier, parentURL) {
|
|
1751
|
+
if (barePackageName(specifier) === void 0) return void 0;
|
|
1752
|
+
try {
|
|
1753
|
+
const englishPath = optionalResourcePath(`${specifier}/locale/en.json`, parentURL);
|
|
1754
|
+
const dictionaries = englishPath === void 0 ? /* @__PURE__ */ new Map() : dictionariesOf(englishPath, specifier, parentURL);
|
|
1755
|
+
const manifestPath = optionalResourcePath(`${specifier}/package.json`, parentURL);
|
|
1756
|
+
const manifest = manifestPath === void 0 ? void 0 : readObject(manifestPath);
|
|
1757
|
+
const title = localizedText("title", dictionaries, fallbackText(manifest?.name), specifier);
|
|
1758
|
+
const description = localizedText("description", dictionaries, fallbackText(manifest?.description), "");
|
|
1759
|
+
const text = {
|
|
1760
|
+
...title === void 0 ? {} : { title },
|
|
1761
|
+
...description === void 0 ? {} : { description }
|
|
1762
|
+
};
|
|
1763
|
+
let icon;
|
|
1764
|
+
try {
|
|
1765
|
+
icon = manifestPath === void 0 ? void 0 : iconOf(manifest?.icon, manifestPath);
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
return {
|
|
1768
|
+
...text,
|
|
1769
|
+
error: `Plugin metadata for ${specifier}: ${String(error)}`
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
if (title === void 0 && description === void 0 && icon === void 0) return void 0;
|
|
1773
|
+
return {
|
|
1774
|
+
...text,
|
|
1775
|
+
...icon === void 0 ? {} : { icon }
|
|
1776
|
+
};
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
return { error: `Plugin metadata for ${specifier}: ${String(error)}` };
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
//#endregion
|
|
1782
|
+
//#region lib/types/config-schema/native.js
|
|
1783
|
+
/** Identity checks at the plugin-export and lazy-builder boundaries. */
|
|
1784
|
+
/**
|
|
1785
|
+
* Recognize the native Schemastery graph protocol without invoking a validator or serialization hook.
|
|
1786
|
+
* @param value - plugin Config export or lazy-builder result.
|
|
1787
|
+
* @returns whether native schema fields can be inspected.
|
|
1788
|
+
*/
|
|
1789
|
+
function isNativeConfigSchema(value) {
|
|
1790
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") return false;
|
|
1791
|
+
const meta = Reflect.get(value, "meta");
|
|
1792
|
+
return Reflect.get(value, Symbol.for("schemastery")) === true && typeof Reflect.get(value, "type") === "string" && meta !== null && typeof meta === "object";
|
|
1793
|
+
}
|
|
1794
|
+
//#endregion
|
|
1795
|
+
//#region lib/types/config-schema/pattern.js
|
|
1796
|
+
/** Conservative compatibility checks between native RegExp validation and Unicode JSON Schema patterns. */
|
|
1797
|
+
function bmpOnly(node) {
|
|
1798
|
+
switch (node.type) {
|
|
1799
|
+
case "Character": return node.value <= 65535 && (node.value < 55296 || node.value > 57343);
|
|
1800
|
+
case "CharacterClassRange": return bmpOnly(node.min) && bmpOnly(node.max) && (node.max.value < 55296 || node.min.value > 57343);
|
|
1801
|
+
case "CharacterClass": return !node.negate && node.elements.every(bmpOnly);
|
|
1802
|
+
case "CharacterSet": return (node.kind === "digit" || node.kind === "space" || node.kind === "word") && !node.negate;
|
|
1803
|
+
case "Quantifier": return bmpOnly(node.element);
|
|
1804
|
+
case "Assertion": return [
|
|
1805
|
+
"start",
|
|
1806
|
+
"end",
|
|
1807
|
+
"word"
|
|
1808
|
+
].includes(node.kind);
|
|
1809
|
+
default: return false;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function broadSet(node) {
|
|
1813
|
+
return node.type === "CharacterSet" && (node.kind === "any" || (node.kind === "digit" || node.kind === "space" || node.kind === "word") && node.negate);
|
|
1814
|
+
}
|
|
1815
|
+
function portableAlternative(alternative) {
|
|
1816
|
+
if (alternative.elements.length === 1 && alternative.elements.some(broadSet)) return true;
|
|
1817
|
+
let broadRepetitions = 0;
|
|
1818
|
+
const wordAssertion = alternative.elements.some((element) => element.type === "Assertion" && element.kind === "word");
|
|
1819
|
+
return alternative.elements.every((element) => {
|
|
1820
|
+
if (element.type === "Quantifier" && element.max === Infinity && element.min <= 1 && broadSet(element.element)) return !wordAssertion && ++broadRepetitions === 1;
|
|
1821
|
+
return bmpOnly(element);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
function signature(pattern) {
|
|
1825
|
+
const incidental = new Set([
|
|
1826
|
+
"parent",
|
|
1827
|
+
"start",
|
|
1828
|
+
"end",
|
|
1829
|
+
"raw",
|
|
1830
|
+
"references",
|
|
1831
|
+
"resolved"
|
|
1832
|
+
]);
|
|
1833
|
+
return JSON.stringify(pattern, (key, value) => incidental.has(key) ? void 0 : value);
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Create a checker for patterns that can retain native acceptance under Unicode regex semantics.
|
|
1837
|
+
* Unrecognized flagless syntax stays explicitly partial rather than becoming a misleading constraint.
|
|
1838
|
+
* @returns the compatibility predicate; parsing never executes the regular expression against config values.
|
|
1839
|
+
*/
|
|
1840
|
+
async function createPatternCheck() {
|
|
1841
|
+
const { RegExpParser } = await import("@eslint-community/regexpp");
|
|
1842
|
+
const parser = new RegExpParser();
|
|
1843
|
+
return (source, flags = "") => {
|
|
1844
|
+
if (flags !== "" && flags !== "u") return false;
|
|
1845
|
+
try {
|
|
1846
|
+
const unicode = parser.parsePattern(source, 0, source.length, { unicode: true });
|
|
1847
|
+
if (flags === "u") return true;
|
|
1848
|
+
const native = parser.parsePattern(source, 0, source.length, { unicode: false });
|
|
1849
|
+
return signature(native) === signature(unicode) && native.alternatives.every(portableAlternative);
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
if (error instanceof SyntaxError) return false;
|
|
1852
|
+
throw error;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
//#endregion
|
|
1857
|
+
//#region lib/types/config-schema/projector.js
|
|
1858
|
+
/** Project native Config input constraints without executing their validators or transform callbacks. */
|
|
1859
|
+
/** Definition that projected value positions reference as `#/$defs/loaderExpression`; an enclosing document must define it. */
|
|
1860
|
+
const LOADER_EXPRESSION_SCHEMA = {
|
|
1861
|
+
type: "object",
|
|
1862
|
+
properties: { __jsExpr: { type: "string" } },
|
|
1863
|
+
required: ["__jsExpr"],
|
|
1864
|
+
description: "Inert representation of a YAML !!js scalar from the Cordis entry-list parser. Its result is evaluated and validated only at runtime. Extra marker-object fields are ignored by interpolation."
|
|
1865
|
+
};
|
|
1866
|
+
/** Copy JSON-compatible schema annotations without silently converting non-JSON defaults. */
|
|
1867
|
+
function jsonValue(value, nullifyUndefined = false) {
|
|
1868
|
+
const visit = (item, active) => {
|
|
1869
|
+
if (nullifyUndefined && item === void 0) return;
|
|
1870
|
+
if (item === null || typeof item === "string" || typeof item === "boolean") return;
|
|
1871
|
+
if (typeof item === "number" && Number.isFinite(item)) return;
|
|
1872
|
+
if (typeof item !== "object") throw new Error("schema annotation is not JSON-compatible");
|
|
1873
|
+
if (active.has(item)) throw new Error("schema annotation contains a cycle");
|
|
1874
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("schema annotation contains a non-JSON object");
|
|
1875
|
+
active.add(item);
|
|
1876
|
+
for (const child of Object.values(item)) visit(child, active);
|
|
1877
|
+
active.delete(item);
|
|
1878
|
+
};
|
|
1879
|
+
visit(value, /* @__PURE__ */ new Set());
|
|
1880
|
+
return JSON.parse(JSON.stringify(value, (_key, item) => nullifyUndefined && item === void 0 ? null : item));
|
|
1881
|
+
}
|
|
1882
|
+
/** Native constant equality treats omitted and null-valued object members alike. */
|
|
1883
|
+
function constant(value, objectComparison) {
|
|
1884
|
+
if (Array.isArray(value)) return {
|
|
1885
|
+
type: "array",
|
|
1886
|
+
minItems: value.length,
|
|
1887
|
+
maxItems: value.length,
|
|
1888
|
+
items: false,
|
|
1889
|
+
...value.length ? { prefixItems: Array.from(value, (item) => constant(item, objectComparison)) } : {}
|
|
1890
|
+
};
|
|
1891
|
+
if (value !== null && typeof value === "object") {
|
|
1892
|
+
objectComparison();
|
|
1893
|
+
const properties = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, constant(child, objectComparison)]));
|
|
1894
|
+
const inherited = Object.getOwnPropertyNames(Object.prototype);
|
|
1895
|
+
const required = Object.keys(properties).filter((key) => !inherited.includes(key) && Reflect.get(value, key) != null);
|
|
1896
|
+
return {
|
|
1897
|
+
type: "object",
|
|
1898
|
+
properties,
|
|
1899
|
+
additionalProperties: { type: "null" },
|
|
1900
|
+
patternProperties: { [`^(?:${inherited.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`]: {} },
|
|
1901
|
+
...required.length ? { required } : {}
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
return { const: value };
|
|
1905
|
+
}
|
|
1906
|
+
/** Add or remove null acceptance; a type list stays deduplicated because a wrapped projection may already accept null. */
|
|
1907
|
+
function nullable(core, accepts) {
|
|
1908
|
+
if (typeof core === "boolean") return accepts ? core || { type: "null" } : core && { not: { type: "null" } };
|
|
1909
|
+
const types = typeof core.type === "string" ? [core.type] : core.type;
|
|
1910
|
+
if (types === void 0) return accepts ? { anyOf: [core, { type: "null" }] } : {
|
|
1911
|
+
...core,
|
|
1912
|
+
not: { type: "null" }
|
|
1913
|
+
};
|
|
1914
|
+
const [single, ...rest] = accepts ? [...new Set([...types, "null"])] : types.filter((type) => type !== "null");
|
|
1915
|
+
if (single === void 0) return false;
|
|
1916
|
+
return {
|
|
1917
|
+
...core,
|
|
1918
|
+
type: rest.length ? [single, ...rest] : single
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
/** Add expression alternatives only at config VALUE positions, not property names or applicator branches. */
|
|
1922
|
+
function expressions(schema, valuePosition = true, enabled = true) {
|
|
1923
|
+
const result = { ...schema };
|
|
1924
|
+
if (schema.properties) result.properties = Object.fromEntries(Object.entries(schema.properties).map(([key, child]) => [key, expressions(child, true, enabled)]));
|
|
1925
|
+
for (const key of ["items", "additionalProperties"]) if (schema[key] !== void 0) result[key] = schema[key] === false ? false : expressions(schema[key], true, enabled);
|
|
1926
|
+
if (schema.propertyNames) result.propertyNames = expressions(schema.propertyNames, false, false);
|
|
1927
|
+
if (schema.prefixItems) result.prefixItems = schema.prefixItems.map((child) => expressions(child, true, enabled));
|
|
1928
|
+
if (schema.anyOf) result.anyOf = schema.anyOf.map((child) => expressions(child, false, enabled));
|
|
1929
|
+
if (schema.not !== void 0) result.not = expressions(schema.not, false, false);
|
|
1930
|
+
if (!valuePosition || !enabled) return result;
|
|
1931
|
+
const annotations = {};
|
|
1932
|
+
for (const key of [
|
|
1933
|
+
"title",
|
|
1934
|
+
"description",
|
|
1935
|
+
"default",
|
|
1936
|
+
"$comment",
|
|
1937
|
+
"x-cordis"
|
|
1938
|
+
]) if (Object.hasOwn(result, key)) {
|
|
1939
|
+
annotations[key] = result[key];
|
|
1940
|
+
Reflect.deleteProperty(result, key);
|
|
1941
|
+
}
|
|
1942
|
+
return {
|
|
1943
|
+
...annotations,
|
|
1944
|
+
anyOf: [result, { $ref: "#/$defs/loaderExpression" }]
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
/** Check native volatile containment without evaluating values or invoking unresolved lazy builders. */
|
|
1948
|
+
function validateVolatilePlacement(node, path, blocked = false, seen = /* @__PURE__ */ new Map()) {
|
|
1949
|
+
const states = seen.get(node) ?? /* @__PURE__ */ new Set();
|
|
1950
|
+
if (states.has(blocked)) return;
|
|
1951
|
+
states.add(blocked);
|
|
1952
|
+
seen.set(node, states);
|
|
1953
|
+
if (node.meta.volatile && blocked) throw new Error(`${path}: volatile fields require a fixed object path without an enclosing volatile field`);
|
|
1954
|
+
const nested = blocked || !!node.meta.volatile;
|
|
1955
|
+
for (const [key, child] of Object.entries(node.dict ?? {})) validateVolatilePlacement(child, `${path}/${key}`, nested, seen);
|
|
1956
|
+
if (node.sKey) validateVolatilePlacement(node.sKey, `${path}/keys`, true, seen);
|
|
1957
|
+
if (node.inner && (node.type !== "lazy" || isNativeConfigSchema(node.inner))) validateVolatilePlacement(node.inner, `${path}/inner`, true, seen);
|
|
1958
|
+
for (const [index, child] of (node.list ?? []).entries()) validateVolatilePlacement(child, `${path}/${index}`, true, seen);
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Create an invocation-local projector. Ajv checks only literal defaults against generated schemas;
|
|
1962
|
+
* it does not fill defaults, coerce input, or call native plugin validators. Opaque input or metadata effects
|
|
1963
|
+
* widen validation with explicit limitations instead of simulating native mutation.
|
|
1964
|
+
* @returns a function projecting one trusted native Config graph into the enclosing document's definitions.
|
|
1965
|
+
*/
|
|
1966
|
+
async function createConfigProjector() {
|
|
1967
|
+
const { Ajv2020 } = await import("ajv/dist/2020.js");
|
|
1968
|
+
const validator = new Ajv2020({
|
|
1969
|
+
strict: false,
|
|
1970
|
+
logger: false,
|
|
1971
|
+
validateFormats: false,
|
|
1972
|
+
addUsedSchema: false
|
|
1973
|
+
});
|
|
1974
|
+
const portablePattern = await createPatternCheck();
|
|
1975
|
+
const builderResults = /* @__PURE__ */ new Map();
|
|
1976
|
+
return (root, prefix) => {
|
|
1977
|
+
validateVolatilePlacement(root, "config");
|
|
1978
|
+
const completed = /* @__PURE__ */ new Map();
|
|
1979
|
+
const strictNodes = /* @__PURE__ */ new Map();
|
|
1980
|
+
const active = /* @__PURE__ */ new Set();
|
|
1981
|
+
const recursiveNames = /* @__PURE__ */ new Map();
|
|
1982
|
+
const definitions = {};
|
|
1983
|
+
const limitations = [];
|
|
1984
|
+
const metadataEffects = [];
|
|
1985
|
+
const limitation = (path, message) => {
|
|
1986
|
+
const text = `${path}: ${message}`;
|
|
1987
|
+
limitations.push(text);
|
|
1988
|
+
return text;
|
|
1989
|
+
};
|
|
1990
|
+
const visit = (node, path, strict = false) => {
|
|
1991
|
+
let key = node;
|
|
1992
|
+
if (strict) {
|
|
1993
|
+
key = strictNodes.get(node) ?? { ...node };
|
|
1994
|
+
strictNodes.set(node, key);
|
|
1995
|
+
}
|
|
1996
|
+
const cached = completed.get(key);
|
|
1997
|
+
if (cached) return cached;
|
|
1998
|
+
if (active.has(key)) {
|
|
1999
|
+
let name = recursiveNames.get(key);
|
|
2000
|
+
if (name === void 0) {
|
|
2001
|
+
name = `${prefix}Recursive${recursiveNames.size}`;
|
|
2002
|
+
recursiveNames.set(key, name);
|
|
2003
|
+
}
|
|
2004
|
+
return {
|
|
2005
|
+
schema: { $ref: `#/$defs/${name}` },
|
|
2006
|
+
acceptsMissing: node.type !== "lazy" && node.meta.required ? false : "unknown",
|
|
2007
|
+
exact: true,
|
|
2008
|
+
recursive: true,
|
|
2009
|
+
effectful: true,
|
|
2010
|
+
mutating: true
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
active.add(key);
|
|
2014
|
+
let projection;
|
|
2015
|
+
if (node.type === "lazy") {
|
|
2016
|
+
let inner = node;
|
|
2017
|
+
let meta = node.meta.volatile ? {
|
|
2018
|
+
...node.meta,
|
|
2019
|
+
volatile: false
|
|
2020
|
+
} : node.meta;
|
|
2021
|
+
const chain = /* @__PURE__ */ new Set();
|
|
2022
|
+
let looseProjection;
|
|
2023
|
+
while (inner.type === "lazy") {
|
|
2024
|
+
const cachedInner = isNativeConfigSchema(inner.inner);
|
|
2025
|
+
if (meta.loose) {
|
|
2026
|
+
const message = limitation(path, "lazy loose fallback can accept inputs rejected by its inner schema");
|
|
2027
|
+
if (!cachedInner) metadataEffects.push(limitation(path, "unresolved loose lazy metadata propagation may affect shared schemas"));
|
|
2028
|
+
looseProjection = {
|
|
2029
|
+
schema: { "x-cordis": {
|
|
2030
|
+
loose: true,
|
|
2031
|
+
limitations: [message]
|
|
2032
|
+
} },
|
|
2033
|
+
acceptsMissing: true,
|
|
2034
|
+
exact: false,
|
|
2035
|
+
recursive: false,
|
|
2036
|
+
effectful: true,
|
|
2037
|
+
mutating: false
|
|
2038
|
+
};
|
|
2039
|
+
break;
|
|
2040
|
+
}
|
|
2041
|
+
if (chain.has(inner)) throw new Error(`${path}: lazy cycle has no concrete schema`);
|
|
2042
|
+
chain.add(inner);
|
|
2043
|
+
let next = cachedInner ? inner.inner : builderResults.get(inner);
|
|
2044
|
+
if (next === void 0 && typeof inner.builder === "function") {
|
|
2045
|
+
next = Reflect.apply(inner.builder, inner, []);
|
|
2046
|
+
builderResults.set(inner, next);
|
|
2047
|
+
}
|
|
2048
|
+
if (!isNativeConfigSchema(next)) throw new Error(`${path}: lazy schema has no native builder result or resolved inner schema`);
|
|
2049
|
+
if (!cachedInner && [
|
|
2050
|
+
"required",
|
|
2051
|
+
"default",
|
|
2052
|
+
"min",
|
|
2053
|
+
"max",
|
|
2054
|
+
"step",
|
|
2055
|
+
"pattern",
|
|
2056
|
+
"loose",
|
|
2057
|
+
"volatile"
|
|
2058
|
+
].some((key) => Object.hasOwn(meta, key) && !Object.hasOwn(next.meta, key) && ([
|
|
2059
|
+
"required",
|
|
2060
|
+
"loose",
|
|
2061
|
+
"volatile"
|
|
2062
|
+
].includes(key) ? !!Reflect.get(meta, key) : Reflect.get(meta, key) != null))) metadataEffects.push(limitation(path, "lazy metadata propagation may affect shared schemas; validation requires native execution"));
|
|
2063
|
+
meta = cachedInner ? next.meta : {
|
|
2064
|
+
...meta,
|
|
2065
|
+
...next.meta
|
|
2066
|
+
};
|
|
2067
|
+
validateVolatilePlacement({
|
|
2068
|
+
...next,
|
|
2069
|
+
meta
|
|
2070
|
+
}, `${path}/lazy`, true);
|
|
2071
|
+
inner = next;
|
|
2072
|
+
}
|
|
2073
|
+
projection = looseProjection ?? visit({
|
|
2074
|
+
...inner,
|
|
2075
|
+
meta
|
|
2076
|
+
}, `${path}/lazy`, strict);
|
|
2077
|
+
if (node.meta.volatile) projection = {
|
|
2078
|
+
...projection,
|
|
2079
|
+
schema: {
|
|
2080
|
+
...projection.schema,
|
|
2081
|
+
"x-cordis": {
|
|
2082
|
+
...projection.schema["x-cordis"],
|
|
2083
|
+
volatile: true
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
} else {
|
|
2088
|
+
const ownLimitations = [];
|
|
2089
|
+
const inputLimitations = [];
|
|
2090
|
+
let unsupported = false;
|
|
2091
|
+
const inputLimitation = (message) => {
|
|
2092
|
+
inputLimitations.push(message);
|
|
2093
|
+
ownLimitations.push(limitation(path, message));
|
|
2094
|
+
};
|
|
2095
|
+
const annotation = (value, name) => {
|
|
2096
|
+
try {
|
|
2097
|
+
return {
|
|
2098
|
+
ok: true,
|
|
2099
|
+
value: jsonValue(value)
|
|
2100
|
+
};
|
|
2101
|
+
} catch (error) {
|
|
2102
|
+
ownLimitations.push(limitation(`${path}/${name}`, `annotation omitted: ${error instanceof Error ? error.message : String(error)}`));
|
|
2103
|
+
return { ok: false };
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
const children = [];
|
|
2107
|
+
const child = (value, suffix, childStrict = false) => {
|
|
2108
|
+
if (value === void 0) throw new Error(`${path}: ${node.type} schema is missing ${suffix}`);
|
|
2109
|
+
const result = visit(value, `${path}/${suffix}`, childStrict);
|
|
2110
|
+
children.push(result);
|
|
2111
|
+
return result;
|
|
2112
|
+
};
|
|
2113
|
+
const meta = { ...node.meta };
|
|
2114
|
+
for (const key of [
|
|
2115
|
+
"min",
|
|
2116
|
+
"max",
|
|
2117
|
+
"step"
|
|
2118
|
+
]) {
|
|
2119
|
+
if (meta[key] === void 0 || Number.isFinite(meta[key])) continue;
|
|
2120
|
+
if (!(key === "max" ? meta.max === Infinity : key === "min" && meta.min === -Infinity && !meta.step)) inputLimitation(`non-finite ${key} constraint is omitted; native validation is required`);
|
|
2121
|
+
Reflect.deleteProperty(meta, key);
|
|
2122
|
+
}
|
|
2123
|
+
let core;
|
|
2124
|
+
let dictionaryValidation;
|
|
2125
|
+
switch (node.type) {
|
|
2126
|
+
case "any":
|
|
2127
|
+
core = true;
|
|
2128
|
+
break;
|
|
2129
|
+
case "never":
|
|
2130
|
+
core = false;
|
|
2131
|
+
break;
|
|
2132
|
+
case "const":
|
|
2133
|
+
try {
|
|
2134
|
+
core = node.value == null ? false : constant(jsonValue(node.value, true), () => {
|
|
2135
|
+
inputLimitation("object constant inherited-member comparison requires native validation");
|
|
2136
|
+
});
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
core = true;
|
|
2139
|
+
inputLimitation(`constant constraint requires native validation: ${error instanceof Error ? error.message : String(error)}`);
|
|
2140
|
+
}
|
|
2141
|
+
break;
|
|
2142
|
+
case "boolean":
|
|
2143
|
+
core = { type: "boolean" };
|
|
2144
|
+
break;
|
|
2145
|
+
case "string":
|
|
2146
|
+
core = { type: "string" };
|
|
2147
|
+
if (meta.min !== void 0) if (meta.min <= 1) core.minLength = Math.max(0, Math.ceil(meta.min));
|
|
2148
|
+
else inputLimitation("UTF-16 minimum length requires native validation");
|
|
2149
|
+
if (meta.max !== void 0) if (meta.max < 0) core = false;
|
|
2150
|
+
else {
|
|
2151
|
+
const maximum = Math.floor(meta.max);
|
|
2152
|
+
core.maxLength = maximum;
|
|
2153
|
+
if (maximum > 0) inputLimitation("UTF-16 maximum length requires native validation");
|
|
2154
|
+
}
|
|
2155
|
+
if (meta.pattern && core !== false) if (portablePattern(meta.pattern.source, meta.pattern.flags)) core.pattern = meta.pattern.source;
|
|
2156
|
+
else inputLimitation("regular-expression syntax or Unicode semantics require native validation");
|
|
2157
|
+
break;
|
|
2158
|
+
case "number":
|
|
2159
|
+
core = { type: "number" };
|
|
2160
|
+
if (meta.min !== void 0) core.minimum = meta.min;
|
|
2161
|
+
if (meta.max !== void 0) core.maximum = meta.max;
|
|
2162
|
+
if (meta.step) {
|
|
2163
|
+
const step = Math.abs(meta.step);
|
|
2164
|
+
const origin = meta.min ?? 0;
|
|
2165
|
+
if (!Number.isInteger(step) || origin % step !== 0) inputLimitation("fractional or offset numeric step requires native validation");
|
|
2166
|
+
else if (step === 1) core.type = "integer";
|
|
2167
|
+
else core.multipleOf = step;
|
|
2168
|
+
}
|
|
2169
|
+
break;
|
|
2170
|
+
case "object": {
|
|
2171
|
+
const required = [];
|
|
2172
|
+
core = {
|
|
2173
|
+
type: "object",
|
|
2174
|
+
properties: Object.fromEntries(Object.entries(node.dict ?? {}).map(([key, field]) => {
|
|
2175
|
+
const value = child(field, key);
|
|
2176
|
+
if (value.acceptsMissing === false) required.push(key);
|
|
2177
|
+
return [key, value.schema];
|
|
2178
|
+
}))
|
|
2179
|
+
};
|
|
2180
|
+
if (required.length) core.required = required;
|
|
2181
|
+
break;
|
|
2182
|
+
}
|
|
2183
|
+
case "array":
|
|
2184
|
+
core = {
|
|
2185
|
+
type: "array",
|
|
2186
|
+
items: child(node.inner, "items").schema
|
|
2187
|
+
};
|
|
2188
|
+
if (meta.min !== void 0 && node.inner?.meta.default == null) core.minItems = Math.max(0, Math.ceil(meta.min));
|
|
2189
|
+
if (meta.max !== void 0) if (meta.max < 0) core = false;
|
|
2190
|
+
else core.maxItems = Math.floor(meta.max);
|
|
2191
|
+
break;
|
|
2192
|
+
case "dict": {
|
|
2193
|
+
const valueSchema = child(node.inner, "values").schema;
|
|
2194
|
+
core = {
|
|
2195
|
+
type: "object",
|
|
2196
|
+
additionalProperties: valueSchema
|
|
2197
|
+
};
|
|
2198
|
+
if (node.sKey) {
|
|
2199
|
+
const key = child(node.sKey, "keys");
|
|
2200
|
+
if (!(node.sKey.type === "string" && node.sKey.meta.min === void 0 && node.sKey.meta.max === void 0 && node.sKey.meta.pattern === void 0) && (strict || key.effectful)) {
|
|
2201
|
+
dictionaryValidation = {
|
|
2202
|
+
keySchema: key.schema,
|
|
2203
|
+
valueSchema
|
|
2204
|
+
};
|
|
2205
|
+
core = { type: "object" };
|
|
2206
|
+
inputLimitation(key.effectful ? "dictionary key normalization can overwrite unvalidated values; native validation is required" : "strict dictionary filtering and value validation require native validation");
|
|
2207
|
+
} else core.propertyNames = key.schema;
|
|
2208
|
+
}
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
case "tuple": {
|
|
2212
|
+
if (node.list === void 0) throw new Error(`${path}: tuple schema is missing its list`);
|
|
2213
|
+
const values = node.list.map((item, index) => child(item, String(index)));
|
|
2214
|
+
core = {
|
|
2215
|
+
type: "array",
|
|
2216
|
+
...values.length ? { prefixItems: values.map((item) => item.schema) } : {}
|
|
2217
|
+
};
|
|
2218
|
+
const minimum = values.reduce((length, value, index) => value.acceptsMissing === false ? index + 1 : length, 0);
|
|
2219
|
+
if (minimum) core.minItems = minimum;
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
case "union": {
|
|
2223
|
+
if (node.list === void 0) throw new Error(`${path}: union schema is missing its list`);
|
|
2224
|
+
const variants = node.list.map((item, index) => child(item, String(index), strict));
|
|
2225
|
+
if (variants.slice(0, -1).some((variant) => variant.mutating)) {
|
|
2226
|
+
core = true;
|
|
2227
|
+
inputLimitation("earlier union branches may alter later validation inputs; native validation is required");
|
|
2228
|
+
} else core = variants.length ? { anyOf: variants.map((variant) => variant.schema) } : false;
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
case "transform":
|
|
2232
|
+
core = child(node.inner, "input", true).schema;
|
|
2233
|
+
inputLimitation("transform callback validation and normalization are not executed or projected");
|
|
2234
|
+
break;
|
|
2235
|
+
default:
|
|
2236
|
+
core = true;
|
|
2237
|
+
unsupported = true;
|
|
2238
|
+
inputLimitation(`native schema type ${node.type} is not statically projected`);
|
|
2239
|
+
}
|
|
2240
|
+
if (meta.loose) {
|
|
2241
|
+
core = true;
|
|
2242
|
+
inputLimitation("loose validation can replace invalid values with defaults");
|
|
2243
|
+
}
|
|
2244
|
+
const recursive = children.some((value) => value.recursive);
|
|
2245
|
+
const container = [
|
|
2246
|
+
"object",
|
|
2247
|
+
"array",
|
|
2248
|
+
"dict",
|
|
2249
|
+
"tuple"
|
|
2250
|
+
].includes(node.type);
|
|
2251
|
+
const mutating = unsupported || children.some((value) => value.mutating || container && value.effectful);
|
|
2252
|
+
const exact = inputLimitations.length === 0 && children.every((value) => value.exact);
|
|
2253
|
+
const fallback = meta.default === void 0 ? void 0 : annotation(meta.default, "default");
|
|
2254
|
+
let acceptsMissing;
|
|
2255
|
+
if (meta.required) acceptsMissing = false;
|
|
2256
|
+
else if (unsupported) acceptsMissing = "unknown";
|
|
2257
|
+
else if (meta.default == null) acceptsMissing = true;
|
|
2258
|
+
else if (!fallback?.ok) acceptsMissing = "unknown";
|
|
2259
|
+
else if (recursive) {
|
|
2260
|
+
acceptsMissing = "unknown";
|
|
2261
|
+
inputLimitation("recursive default validation cannot be decided statically");
|
|
2262
|
+
} else if (!exact) acceptsMissing = "unknown";
|
|
2263
|
+
else acceptsMissing = validator.validate(core, fallback.value);
|
|
2264
|
+
const annotations = {};
|
|
2265
|
+
if (fallback?.ok) annotations.default = fallback.value;
|
|
2266
|
+
const native = {};
|
|
2267
|
+
if (typeof meta.description === "string") annotations.description = meta.description;
|
|
2268
|
+
else if (meta.description) {
|
|
2269
|
+
const descriptions = annotation(meta.description, "description");
|
|
2270
|
+
if (descriptions.ok) {
|
|
2271
|
+
native.descriptions = descriptions.value;
|
|
2272
|
+
const description = meta.description.en ?? meta.description[""] ?? Object.values(meta.description)[0];
|
|
2273
|
+
if (typeof description === "string") annotations.description = description;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (dictionaryValidation) native.dictionaryValidation = dictionaryValidation;
|
|
2277
|
+
const copy = (name, value) => {
|
|
2278
|
+
if (value === void 0) return;
|
|
2279
|
+
const result = annotation(value, name);
|
|
2280
|
+
if (result.ok) native[name] = result.value;
|
|
2281
|
+
};
|
|
2282
|
+
for (const key of [
|
|
2283
|
+
"role",
|
|
2284
|
+
"extra",
|
|
2285
|
+
"hidden",
|
|
2286
|
+
"disabled",
|
|
2287
|
+
"collapse",
|
|
2288
|
+
"link",
|
|
2289
|
+
"comment",
|
|
2290
|
+
"badges",
|
|
2291
|
+
"loose",
|
|
2292
|
+
"volatile"
|
|
2293
|
+
]) copy(key, meta[key]);
|
|
2294
|
+
if (meta.pattern?.flags) copy("patternFlags", meta.pattern.flags);
|
|
2295
|
+
if (node.type === "union") native.branchSelection = "first-success";
|
|
2296
|
+
if (acceptsMissing === "unknown") native.omissionValidation = "runtime";
|
|
2297
|
+
if (ownLimitations.length) {
|
|
2298
|
+
native.type = node.type;
|
|
2299
|
+
for (const key of [
|
|
2300
|
+
"min",
|
|
2301
|
+
"max",
|
|
2302
|
+
"step",
|
|
2303
|
+
"pattern"
|
|
2304
|
+
]) copy(key, meta[key]);
|
|
2305
|
+
native.limitations = ownLimitations;
|
|
2306
|
+
}
|
|
2307
|
+
if (Object.keys(native).length) annotations["x-cordis"] = native;
|
|
2308
|
+
const input = nullable(core, acceptsMissing !== false);
|
|
2309
|
+
projection = {
|
|
2310
|
+
schema: {
|
|
2311
|
+
...typeof input === "boolean" ? input ? {} : { not: {} } : input,
|
|
2312
|
+
...annotations
|
|
2313
|
+
},
|
|
2314
|
+
acceptsMissing,
|
|
2315
|
+
exact: exact && acceptsMissing !== "unknown",
|
|
2316
|
+
recursive,
|
|
2317
|
+
effectful: unsupported || node.type === "transform" || !!meta.loose || children.some((child) => child.effectful),
|
|
2318
|
+
mutating
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
active.delete(key);
|
|
2322
|
+
const name = recursiveNames.get(key);
|
|
2323
|
+
if (name !== void 0) {
|
|
2324
|
+
definitions[name] = projection.schema;
|
|
2325
|
+
projection = {
|
|
2326
|
+
...projection,
|
|
2327
|
+
schema: { $ref: `#/$defs/${name}` }
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
completed.set(key, projection);
|
|
2331
|
+
return projection;
|
|
2332
|
+
};
|
|
2333
|
+
const result = visit(root, "config");
|
|
2334
|
+
const schema = expressions(result.schema);
|
|
2335
|
+
return {
|
|
2336
|
+
schema: metadataEffects.length ? { anyOf: [schema, {}] } : schema,
|
|
2337
|
+
definitions: Object.fromEntries(Object.entries(definitions).map(([key, value]) => [key, expressions(value, false)])),
|
|
2338
|
+
acceptsMissing: metadataEffects.length ? "unknown" : result.acceptsMissing,
|
|
2339
|
+
limitations: [...new Set(limitations)]
|
|
2340
|
+
};
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
//#endregion
|
|
2344
|
+
//#region lib/types/config-schema/document.js
|
|
2345
|
+
/** Compose Loader entry/patch structure and discovered plugin input schemas into one JSON Schema document. */
|
|
2346
|
+
const ref = (name) => ({ $ref: `#/$defs/${name}` });
|
|
2347
|
+
function metadata() {
|
|
2348
|
+
return {
|
|
2349
|
+
id: {
|
|
2350
|
+
type: "string",
|
|
2351
|
+
description: "Entry id. Loader generates an id when an entry omits it; patches use the configured id."
|
|
2352
|
+
},
|
|
2353
|
+
name: {
|
|
2354
|
+
type: "string",
|
|
2355
|
+
description: "Plugin module specifier. Inserted relative plugin paths are anchored beside their patch file."
|
|
2356
|
+
},
|
|
2357
|
+
config: {},
|
|
2358
|
+
group: {
|
|
2359
|
+
type: ["boolean", "null"],
|
|
2360
|
+
description: "Allows patch indexing and insertion into an entry-list config; does not select the plugin implementation."
|
|
2361
|
+
},
|
|
2362
|
+
disabled: {
|
|
2363
|
+
anyOf: [{ type: ["boolean", "null"] }, ref("loaderExpression")],
|
|
2364
|
+
description: "Boolean or !!js expression. The Loader coerces other truthy values as disabled; this schema rejects them."
|
|
2365
|
+
},
|
|
2366
|
+
inject: { anyOf: [
|
|
2367
|
+
{
|
|
2368
|
+
type: "array",
|
|
2369
|
+
items: { type: "string" }
|
|
2370
|
+
},
|
|
2371
|
+
{ type: "object" },
|
|
2372
|
+
{ type: "null" }
|
|
2373
|
+
] },
|
|
2374
|
+
intercept: { type: ["object", "null"] },
|
|
2375
|
+
isolate: {
|
|
2376
|
+
type: ["object", "null"],
|
|
2377
|
+
additionalProperties: { anyOf: [{ const: true }, { type: "string" }] }
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
function patchStructure() {
|
|
2382
|
+
return {
|
|
2383
|
+
type: "object",
|
|
2384
|
+
allOf: [ref("entryMetadata")],
|
|
2385
|
+
properties: { insert: ref("entryList") },
|
|
2386
|
+
description: "An insert appends entries, optionally inside the group identified by id. Other patches replace supplied fields; config is replaced wholesale, not deep-merged. A truthy name asserts the existing plugin name rather than renaming it. Unknown targets and non-insert patches without a nonempty id are warned and skipped."
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Build a schema for the composed entry list and a separately addressable root-tree patch list.
|
|
2391
|
+
* Unknown plugin names remain open; only collected schemas supply plugin-specific constraints.
|
|
2392
|
+
* @param profile - selected profile name.
|
|
2393
|
+
* @param collected - declarations discovered without mounting plugins, including include descendants.
|
|
2394
|
+
* @param targets - last-id-wins patch targets from the root tree's patch index, excluding include descendants.
|
|
2395
|
+
* @param initialDiagnostics - composition and import diagnostics already collected.
|
|
2396
|
+
* @returns one JSON Schema document with explicit collection and projection annotations.
|
|
2397
|
+
*/
|
|
2398
|
+
async function buildConfigSchemaDocument(profile, collected, targets, initialDiagnostics) {
|
|
2399
|
+
const project = await createConfigProjector();
|
|
2400
|
+
const diagnostics = [...initialDiagnostics];
|
|
2401
|
+
const definitions = {
|
|
2402
|
+
loaderExpression: LOADER_EXPRESSION_SCHEMA,
|
|
2403
|
+
entryMetadata: {
|
|
2404
|
+
type: "object",
|
|
2405
|
+
properties: metadata()
|
|
2406
|
+
},
|
|
2407
|
+
entryList: {
|
|
2408
|
+
type: "array",
|
|
2409
|
+
items: ref("entry")
|
|
2410
|
+
},
|
|
2411
|
+
patchList: {
|
|
2412
|
+
type: "array",
|
|
2413
|
+
items: ref("patch")
|
|
2414
|
+
},
|
|
2415
|
+
unknownConfig: { $comment: "No projected Config schema is available; this is unknown configuration, not a prohibition on fields." },
|
|
2416
|
+
includeConfig: {
|
|
2417
|
+
type: "object",
|
|
2418
|
+
required: ["path"],
|
|
2419
|
+
properties: {
|
|
2420
|
+
path: {
|
|
2421
|
+
type: "string",
|
|
2422
|
+
description: "YAML/JSON filename resolved relative to the owning Loader tree. This field is literal, not a !!js expression."
|
|
2423
|
+
},
|
|
2424
|
+
initial: ref("entryList"),
|
|
2425
|
+
patches: {
|
|
2426
|
+
type: "array",
|
|
2427
|
+
items: ref("includePatch")
|
|
2428
|
+
},
|
|
2429
|
+
enableLogs: { type: "boolean" }
|
|
2430
|
+
},
|
|
2431
|
+
description: "Native Include configuration stays literal. initial is used only when the file is absent. Included files have their own module-resolution base and patch target index."
|
|
2432
|
+
},
|
|
2433
|
+
includePatch: patchStructure()
|
|
2434
|
+
};
|
|
2435
|
+
const entries = [];
|
|
2436
|
+
const outputByEntry = /* @__PURE__ */ new Map();
|
|
2437
|
+
const projections = /* @__PURE__ */ new Map();
|
|
2438
|
+
const trees = new Set(["#/$defs/entryList", "#/$defs/includeConfig"]);
|
|
2439
|
+
const requiredConfigs = new Set(trees);
|
|
2440
|
+
const names = new Map([["cordis:group", new Set(["#/$defs/entryList"])], ["cordis:include", new Set(["#/$defs/includeConfig"])]]);
|
|
2441
|
+
for (const entry of collected) {
|
|
2442
|
+
const { native, ...output } = entry;
|
|
2443
|
+
if (entry.tree) output.configRef = entry.tree === "group" ? "#/$defs/entryList" : "#/$defs/includeConfig";
|
|
2444
|
+
else if (native) {
|
|
2445
|
+
let projection = projections.get(native);
|
|
2446
|
+
if (projection === void 0) {
|
|
2447
|
+
const name = `config${projections.size}`;
|
|
2448
|
+
try {
|
|
2449
|
+
const result = project(native, name);
|
|
2450
|
+
definitions[name] = result.schema;
|
|
2451
|
+
Object.assign(definitions, result.definitions);
|
|
2452
|
+
projection = {
|
|
2453
|
+
reference: `#/$defs/${name}`,
|
|
2454
|
+
required: result.acceptsMissing === false,
|
|
2455
|
+
limitations: result.limitations
|
|
2456
|
+
};
|
|
2457
|
+
projections.set(native, projection);
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
output.status = "error";
|
|
2460
|
+
diagnostics.push({
|
|
2461
|
+
level: "error",
|
|
2462
|
+
path: entry.path,
|
|
2463
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
if (projection) {
|
|
2468
|
+
output.configRef = projection.reference;
|
|
2469
|
+
output.status = projection.limitations.length ? "partial" : "schema";
|
|
2470
|
+
if (projection.required) requiredConfigs.add(projection.reference);
|
|
2471
|
+
for (const message of projection.limitations) diagnostics.push({
|
|
2472
|
+
level: "warning",
|
|
2473
|
+
path: entry.path,
|
|
2474
|
+
message
|
|
2475
|
+
});
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
output.configRef ??= "#/$defs/unknownConfig";
|
|
2479
|
+
entries.push(output);
|
|
2480
|
+
outputByEntry.set(entry, output);
|
|
2481
|
+
if (entry.name !== void 0) {
|
|
2482
|
+
const choices = names.get(entry.name) ?? /* @__PURE__ */ new Set();
|
|
2483
|
+
choices.add(output.configRef);
|
|
2484
|
+
names.set(entry.name, choices);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
const dormant = {
|
|
2488
|
+
properties: {
|
|
2489
|
+
disabled: { anyOf: [{ const: true }, ref("loaderExpression")] },
|
|
2490
|
+
group: { not: { const: true } }
|
|
2491
|
+
},
|
|
2492
|
+
required: ["disabled"]
|
|
2493
|
+
};
|
|
2494
|
+
const entryRules = [];
|
|
2495
|
+
for (const [name, choices] of names) {
|
|
2496
|
+
const validated = { properties: { config: { anyOf: [...choices].map((reference) => ({ $ref: reference })) } } };
|
|
2497
|
+
let then = validated;
|
|
2498
|
+
if ([...choices].every((reference) => requiredConfigs.has(reference))) {
|
|
2499
|
+
then = {
|
|
2500
|
+
if: dormant,
|
|
2501
|
+
else: {
|
|
2502
|
+
...validated,
|
|
2503
|
+
required: ["config"]
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
if (![...choices].every((reference) => trees.has(reference))) then.then = validated;
|
|
2507
|
+
}
|
|
2508
|
+
entryRules.push({
|
|
2509
|
+
if: {
|
|
2510
|
+
properties: { name: { const: name } },
|
|
2511
|
+
required: ["name"]
|
|
2512
|
+
},
|
|
2513
|
+
then
|
|
2514
|
+
});
|
|
2515
|
+
if (choices.size > 1) diagnostics.push({
|
|
2516
|
+
level: "warning",
|
|
2517
|
+
message: `Plugin ${JSON.stringify(name)} has multiple collected schemas; entry validation accepts their union because module resolution depends on the owning tree.`
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
definitions.entry = {
|
|
2521
|
+
type: "object",
|
|
2522
|
+
required: ["name"],
|
|
2523
|
+
allOf: [ref("entryMetadata"), ...entryRules],
|
|
2524
|
+
description: "Loader entry. Unknown metadata and plugin names are accepted; plugin-specific validation is available only for the names collected in this profile. Distinct resolutions of one name use a union of their schemas."
|
|
2525
|
+
};
|
|
2526
|
+
const patchRules = [];
|
|
2527
|
+
for (const [id, target] of targets) {
|
|
2528
|
+
if (target.name === void 0) continue;
|
|
2529
|
+
const configRef = outputByEntry.get(target)?.configRef ?? "#/$defs/unknownConfig";
|
|
2530
|
+
patchRules.push({
|
|
2531
|
+
if: {
|
|
2532
|
+
required: ["id"],
|
|
2533
|
+
properties: { id: { const: id } },
|
|
2534
|
+
allOf: [{ not: { required: ["insert"] } }, { anyOf: [{ not: { required: ["name"] } }, { properties: { name: { enum: ["", target.name] } } }] }]
|
|
2535
|
+
},
|
|
2536
|
+
then: { properties: { config: { $ref: configRef } } }
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
definitions.patch = {
|
|
2540
|
+
...patchStructure(),
|
|
2541
|
+
allOf: [ref("entryMetadata"), ...patchRules]
|
|
2542
|
+
};
|
|
2543
|
+
const positions = new Map(entries.map((entry, index) => [entry.path, index]));
|
|
2544
|
+
diagnostics.sort((left, right) => (positions.get(left.path ?? "") ?? -1) - (positions.get(right.path ?? "") ?? -1));
|
|
2545
|
+
return {
|
|
2546
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
2547
|
+
title: `Cordis configuration for profile ${profile}`,
|
|
2548
|
+
description: "Describes the parsed entry-list YAML printed by --dump-config. Use $defs.patchList for a profile/home/CLI overlay. Parse !!js with the Cordis entry-list YAML dialect. Expression results, service dependencies, plugin startup checks, and sequence-dependent patch targets still require runtime validation.",
|
|
2549
|
+
$comment: "Bundle, profile, home, and CLI layers apply in that order. A patch config replaces the whole config. JSON Schema defaults are annotations; Schemastery validates a fallback on null/omission unless required rejects it first. Schemastery unions select the first successful branch. Root patch id constraints describe the current index, not ids introduced or changed by earlier patches. Include-local patches have a separate index. Relative plugin insertions require their source patch directory; this document does not infer future overlay locations.",
|
|
2550
|
+
type: "array",
|
|
2551
|
+
items: ref("entry"),
|
|
2552
|
+
$defs: definitions,
|
|
2553
|
+
"x-cordis": {
|
|
2554
|
+
profile,
|
|
2555
|
+
complete: !diagnostics.some((item) => item.level === "error") && !entries.some((entry) => [
|
|
2556
|
+
"partial",
|
|
2557
|
+
"unsupported",
|
|
2558
|
+
"error"
|
|
2559
|
+
].includes(entry.status)) && ![...names.values()].some((choices) => choices.size > 1),
|
|
2560
|
+
entries,
|
|
2561
|
+
diagnostics,
|
|
2562
|
+
patchSchema: "#/$defs/patchList"
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
//#endregion
|
|
2567
|
+
//#region lib/types/config-schema/collect.js
|
|
2568
|
+
/** Boot-free inspection of declared plugin Config schemas using profile module resolution. */
|
|
2569
|
+
function objectLike(value) {
|
|
2570
|
+
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
2571
|
+
}
|
|
2572
|
+
function record(value) {
|
|
2573
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2574
|
+
}
|
|
2575
|
+
function configOf(plugin) {
|
|
2576
|
+
return objectLike(plugin) ? Reflect.get(plugin, "Config") : void 0;
|
|
2577
|
+
}
|
|
2578
|
+
function validateMetadata(row) {
|
|
2579
|
+
for (const key of ["id", "name"]) {
|
|
2580
|
+
const value = Reflect.get(row, key);
|
|
2581
|
+
if (value !== void 0 && typeof value !== "string") throw new Error(`${key} must be a literal string`);
|
|
1471
2582
|
}
|
|
1472
|
-
|
|
2583
|
+
const group = Reflect.get(row, "group");
|
|
2584
|
+
if (group !== void 0 && group !== null && typeof group !== "boolean") throw new Error("group must be a literal boolean or null");
|
|
2585
|
+
}
|
|
2586
|
+
function validateEntry(value) {
|
|
2587
|
+
if (!record(value) || typeof value.name !== "string") throw new Error("each entry must be a mapping with a literal plugin name");
|
|
2588
|
+
validateMetadata(value);
|
|
1473
2589
|
}
|
|
1474
|
-
function
|
|
1475
|
-
if (
|
|
1476
|
-
throw new Error(
|
|
2590
|
+
function entryList(value) {
|
|
2591
|
+
if (value === void 0) throw new Error("entry list config is missing");
|
|
2592
|
+
if (!Array.isArray(value)) throw new Error("expected a literal entry list; config expressions are not evaluated");
|
|
2593
|
+
return value;
|
|
1477
2594
|
}
|
|
1478
|
-
function
|
|
1479
|
-
if (
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
2595
|
+
function includePatches(value) {
|
|
2596
|
+
if (value === void 0) return void 0;
|
|
2597
|
+
if (!Array.isArray(value)) throw new Error("include patches must be a literal patch list; config expressions are not evaluated");
|
|
2598
|
+
for (const patch of value) {
|
|
2599
|
+
if (!record(patch) || isJsExpr(patch)) throw new Error("include patches must be literal mappings; config expressions are not evaluated");
|
|
2600
|
+
validateMetadata(patch);
|
|
2601
|
+
if (patch.insert !== void 0) entryList(patch.insert);
|
|
2602
|
+
}
|
|
2603
|
+
return value;
|
|
1482
2604
|
}
|
|
1483
2605
|
/**
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
* @
|
|
2606
|
+
* Generate JSON Schema from parsed entry rows and root-tree patches without applying plugins or evaluating expressions.
|
|
2607
|
+
* Imports, Config getters, and lazy schema builders execute trusted plugin code; transform callbacks do not. Calls must not overlap
|
|
2608
|
+
* another profile-resolution interception; module imports remain cached after the interception is released.
|
|
2609
|
+
* @param profile - prepared profile whose directory anchors root module and include resolution.
|
|
2610
|
+
* @param entries - unvalidated rows from profile composition; malformed rows become positioned diagnostics without losing siblings.
|
|
2611
|
+
* @param resolution - the same immutable package resolution used for profile boot.
|
|
2612
|
+
* @param diagnostics - existing composition diagnostics; copied into the returned catalog.
|
|
2613
|
+
* @returns a JSON Schema document with partial-result diagnostics and Config references under `x-cordis`.
|
|
2614
|
+
* @throws when Node's profile module resolution cannot be installed.
|
|
1488
2615
|
*/
|
|
1489
|
-
function
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
if (
|
|
1506
|
-
|
|
1507
|
-
|
|
2616
|
+
async function collectConfigSchemas(profile, entries, resolution, diagnostics = []) {
|
|
2617
|
+
const result = {
|
|
2618
|
+
entries: [],
|
|
2619
|
+
diagnostics: [...diagnostics]
|
|
2620
|
+
};
|
|
2621
|
+
const byOptions = /* @__PURE__ */ new Map();
|
|
2622
|
+
const interception = installRuntimeInterception(resolution);
|
|
2623
|
+
try {
|
|
2624
|
+
const loader = ModuleLoader.fromInternal();
|
|
2625
|
+
if (loader === void 0) throw new Error("config schema dump requires the Node module loader used by profile resolution");
|
|
2626
|
+
const builtins = {
|
|
2627
|
+
group: Group,
|
|
2628
|
+
include: Include
|
|
2629
|
+
};
|
|
2630
|
+
const nativeCarriers = /* @__PURE__ */ new Map();
|
|
2631
|
+
const carrier = async (plugin, baseUrl) => {
|
|
2632
|
+
if (plugin === Group) return "group";
|
|
2633
|
+
if (plugin === Include) return "include";
|
|
2634
|
+
if (!objectLike(plugin) || !Reflect.get(plugin, EntryGroup.key)) return void 0;
|
|
2635
|
+
let resolved = nativeCarriers.get(baseUrl);
|
|
2636
|
+
if (resolved === void 0) {
|
|
2637
|
+
resolved = Promise.allSettled([loader.import("@deepseek-ai/cordis-plugin-group", baseUrl, {}), loader.import("@deepseek-ai/cordis-plugin-include", baseUrl, {})]).then(([group, include]) => {
|
|
2638
|
+
return {
|
|
2639
|
+
group: group.status === "fulfilled" ? Loader.prototype.unwrapExports(group.value) : void 0,
|
|
2640
|
+
include: include.status === "fulfilled" ? Loader.prototype.unwrapExports(include.value) : void 0
|
|
2641
|
+
};
|
|
2642
|
+
});
|
|
2643
|
+
nativeCarriers.set(baseUrl, resolved);
|
|
2644
|
+
}
|
|
2645
|
+
const native = await resolved;
|
|
2646
|
+
if (plugin === native.group) return "group";
|
|
2647
|
+
if (plugin === native.include) return "include";
|
|
2648
|
+
throw new Error("unrecognized Loader tree carrier; use cordis:group or cordis:include for native child collection");
|
|
2649
|
+
};
|
|
2650
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
2651
|
+
const report = (path, error) => {
|
|
2652
|
+
result.diagnostics.push({
|
|
2653
|
+
level: "error",
|
|
2654
|
+
path,
|
|
2655
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2656
|
+
});
|
|
2657
|
+
};
|
|
2658
|
+
const warn = (path) => (message, ...args) => {
|
|
2659
|
+
let index = 0;
|
|
2660
|
+
result.diagnostics.push({
|
|
2661
|
+
level: "warning",
|
|
2662
|
+
path,
|
|
2663
|
+
message: message.replace(/%C/g, () => JSON.stringify(args[index++]))
|
|
2664
|
+
});
|
|
2665
|
+
};
|
|
2666
|
+
const walkInclude = async (config, baseUrl, path) => {
|
|
2667
|
+
if (config === void 0) throw new Error("include config is missing");
|
|
2668
|
+
if (!record(config) || isJsExpr(config)) throw new Error("include config must be literal; config expressions are not evaluated");
|
|
2669
|
+
if (typeof config.path !== "string") throw new Error("include path must be literal; config expressions are not evaluated");
|
|
2670
|
+
const filename = fileURLToPath(new URL(config.path, baseUrl));
|
|
2671
|
+
const extension = extname(filename);
|
|
2672
|
+
if (![
|
|
2673
|
+
".json",
|
|
2674
|
+
".yaml",
|
|
2675
|
+
".yml"
|
|
2676
|
+
].includes(extension)) throw new Error(`include extension ${JSON.stringify(extension)} is not supported`);
|
|
2677
|
+
let source;
|
|
2678
|
+
let canonical;
|
|
2679
|
+
try {
|
|
2680
|
+
canonical = await realpath(filename);
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
if (error.code !== "ENOENT") throw error;
|
|
2683
|
+
canonical = filename;
|
|
1508
2684
|
}
|
|
1509
|
-
if (
|
|
1510
|
-
|
|
1511
|
-
|
|
2685
|
+
if (ancestors.has(canonical)) throw new Error(`include cycle at ${filename}`);
|
|
2686
|
+
let content;
|
|
2687
|
+
try {
|
|
2688
|
+
content = await readFile(filename, "utf8");
|
|
2689
|
+
} catch (error) {
|
|
2690
|
+
if (error.code !== "ENOENT") throw error;
|
|
2691
|
+
source = config.initial;
|
|
2692
|
+
if (source === void 0) throw new Error(`include file not found: ${filename}`);
|
|
1512
2693
|
}
|
|
1513
|
-
if (
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
const
|
|
1517
|
-
|
|
1518
|
-
const restoreImporter = (error) => throwWithImporter(error, target.parentURL, parent);
|
|
1519
|
-
let expected;
|
|
1520
|
-
try {
|
|
1521
|
-
expected = adapted(target.specifier, target.parentURL, attributes);
|
|
1522
|
-
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1523
|
-
if (expected instanceof Promise) expected = expected.catch(restoreImporter);
|
|
1524
|
-
} catch (error) {
|
|
1525
|
-
return restoreImporter(error);
|
|
1526
|
-
}
|
|
1527
|
-
if (behavior === "enforce") return expected;
|
|
1528
|
-
const actual = native(request, parent, attributes);
|
|
1529
|
-
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
|
1530
|
-
if (expected instanceof Promise || actual instanceof Promise) return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
|
1531
|
-
assertEquivalent(resolved.url, wanted.url, request, parent);
|
|
1532
|
-
return resolved;
|
|
1533
|
-
});
|
|
1534
|
-
/* v8 ignore stop */
|
|
1535
|
-
assertEquivalent(actual.url, expected.url, request, parent);
|
|
1536
|
-
return actual;
|
|
2694
|
+
if (content !== void 0) try {
|
|
2695
|
+
source = extension === ".json" ? JSON.parse(content) : yaml.load(content, { schema: entryListSchema });
|
|
2696
|
+
} catch (error) {
|
|
2697
|
+
const at = error instanceof yaml.YAMLException ? ` at line ${error.mark.line + 1}, column ${error.mark.column + 1}` : "";
|
|
2698
|
+
throw new Error(`invalid ${extension === ".json" ? "JSON" : "YAML"} include ${filename}${at}`);
|
|
1537
2699
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
2700
|
+
const patches = includePatches(config.patches);
|
|
2701
|
+
const rows = entryList(source);
|
|
2702
|
+
let children;
|
|
2703
|
+
try {
|
|
2704
|
+
children = applyEntryPatches(rows, patches, warn(path));
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
throw new Error(`include patches could not be composed for ${filename}; child declarations are unavailable`, { cause: error });
|
|
2707
|
+
}
|
|
2708
|
+
ancestors.add(canonical);
|
|
2709
|
+
try {
|
|
2710
|
+
await walk(children, new URL(".", pathToFileURL(filename)).href, `${path}/include`);
|
|
2711
|
+
} finally {
|
|
2712
|
+
ancestors.delete(canonical);
|
|
1545
2713
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
2714
|
+
};
|
|
2715
|
+
const walk = async (rows, baseUrl, prefix) => {
|
|
2716
|
+
for (const [index, value] of rows.entries()) {
|
|
2717
|
+
const path = `${prefix}/${index}`;
|
|
2718
|
+
const identity = record(value) ? value : void 0;
|
|
2719
|
+
const entry = {
|
|
2720
|
+
path,
|
|
2721
|
+
...typeof identity?.id === "string" ? { id: identity.id } : {},
|
|
2722
|
+
...typeof identity?.name === "string" ? { name: identity.name } : {},
|
|
2723
|
+
status: "error"
|
|
1552
2724
|
};
|
|
1553
|
-
|
|
2725
|
+
result.entries.push(entry);
|
|
2726
|
+
const row = value;
|
|
1554
2727
|
try {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
2728
|
+
validateEntry(row);
|
|
2729
|
+
} catch (error) {
|
|
2730
|
+
report(path, error);
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
entry.status = "absent";
|
|
2734
|
+
byOptions.set(row, entry);
|
|
2735
|
+
let plugin;
|
|
2736
|
+
try {
|
|
2737
|
+
let exports;
|
|
2738
|
+
if (row.name.startsWith("cordis:")) {
|
|
2739
|
+
const name = row.name.slice(7);
|
|
2740
|
+
if (!Object.hasOwn(builtins, name)) throw new Error(`unknown Cordis builtin ${JSON.stringify(row.name)}`);
|
|
2741
|
+
exports = builtins[name];
|
|
2742
|
+
} else exports = await loader.import(row.name, baseUrl, {});
|
|
2743
|
+
plugin = Loader.prototype.unwrapExports(exports);
|
|
2744
|
+
const schema = configOf(plugin);
|
|
2745
|
+
if (schema !== void 0 && schema !== null) if (!isNativeConfigSchema(schema)) {
|
|
2746
|
+
entry.status = "unsupported";
|
|
2747
|
+
report(path, "Config is not a native Schemastery schema");
|
|
2748
|
+
} else {
|
|
2749
|
+
entry.status = "schema";
|
|
2750
|
+
entry.native = schema;
|
|
1560
2751
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
return result;
|
|
1565
|
-
} finally {
|
|
1566
|
-
delegatedEsm = previous;
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
entry.status = "error";
|
|
2754
|
+
report(path, error);
|
|
1567
2755
|
}
|
|
1568
|
-
}
|
|
1569
|
-
const actual = native(request, parent, attributes);
|
|
1570
|
-
const previous = delegatedEsm;
|
|
1571
|
-
delegatedEsm = {
|
|
1572
|
-
parent: routedParent,
|
|
1573
|
-
request
|
|
1574
|
-
};
|
|
1575
|
-
const restoreImporter = (error) => throwWithImporter(error, routedParent, parent);
|
|
1576
|
-
try {
|
|
1577
|
-
let expected;
|
|
1578
2756
|
try {
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
if (
|
|
2757
|
+
const kind = await carrier(plugin, baseUrl);
|
|
2758
|
+
if (kind === void 0) continue;
|
|
2759
|
+
entry.tree = kind;
|
|
2760
|
+
if (row.config === void 0 && row.group !== true && (row.disabled === true || isJsExpr(row.disabled))) continue;
|
|
2761
|
+
if (kind === "group") await walk(entryList(row.config), baseUrl, `${path}/config`);
|
|
2762
|
+
else await walkInclude(row.config, baseUrl, path);
|
|
1583
2763
|
} catch (error) {
|
|
1584
|
-
|
|
2764
|
+
report(path, error);
|
|
1585
2765
|
}
|
|
1586
|
-
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
|
1587
|
-
if (expected instanceof Promise || actual instanceof Promise) return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
|
1588
|
-
assertEquivalent(resolved.url, wanted.url, request, parent);
|
|
1589
|
-
if (cacheable) state.esm = resolved;
|
|
1590
|
-
return resolved;
|
|
1591
|
-
});
|
|
1592
|
-
/* v8 ignore stop */
|
|
1593
|
-
assertEquivalent(actual.url, expected.url, request, parent);
|
|
1594
|
-
if (cacheable) state.esm = actual;
|
|
1595
|
-
return actual;
|
|
1596
|
-
} finally {
|
|
1597
|
-
delegatedEsm = previous;
|
|
1598
2766
|
}
|
|
1599
2767
|
};
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
attributes
|
|
1610
|
-
}));
|
|
1611
|
-
const wrapped = (parent, request, ...rest) => rest.length ? Reflect.apply(original, loader, [
|
|
1612
|
-
parent,
|
|
1613
|
-
request,
|
|
1614
|
-
...rest
|
|
1615
|
-
]) : resolveRequest(request.specifier, parent, request.attributes ?? EMPTY_ATTRIBUTES);
|
|
1616
|
-
loader.resolveSync = wrapped;
|
|
1617
|
-
restoreEsm = () => {
|
|
1618
|
-
/* v8 ignore else -- registrations are disposed in reverse installation order */
|
|
1619
|
-
if (loader.resolveSync === wrapped) loader.resolveSync = original;
|
|
1620
|
-
};
|
|
1621
|
-
} else {
|
|
1622
|
-
const loader = esm;
|
|
1623
|
-
const original = Reflect.get(loader, "resolve");
|
|
1624
|
-
const originalSync = Reflect.get(loader, "resolveSync");
|
|
1625
|
-
const resolveRequest = adaptEsm((request, parent, attributes) => original.call(loader, request, parent, attributes));
|
|
1626
|
-
const resolveRequestSync = adaptEsm((request, parent, attributes) => originalSync.call(loader, request, parent, attributes));
|
|
1627
|
-
const wrapped = (request, parent, attributes = EMPTY_ATTRIBUTES) => resolveRequest(request, parent, attributes);
|
|
1628
|
-
const wrappedSync = (request, parent, attributes = EMPTY_ATTRIBUTES) => resolveRequestSync(request, parent, attributes);
|
|
1629
|
-
loader.resolve = wrapped;
|
|
1630
|
-
loader.resolveSync = wrappedSync;
|
|
1631
|
-
restoreEsm = () => {
|
|
1632
|
-
if (loader.resolve === wrapped) loader.resolve = original;
|
|
1633
|
-
if (loader.resolveSync === wrappedSync) loader.resolveSync = originalSync;
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
const originalFilename = Reflect.get(cjs, "_resolveFilename");
|
|
1637
|
-
let delegatedCjs = 0;
|
|
1638
|
-
const resolveRoutedCjs = (request, routed, parent, main, options) => {
|
|
1639
|
-
const anchor = routed.kind === "fallback" ? routed.entry.declarer : routed.parent;
|
|
1640
|
-
const synthetic = new cjs(anchor);
|
|
1641
|
-
synthetic.parent = parent;
|
|
1642
|
-
synthetic.filename = anchor;
|
|
1643
|
-
synthetic.paths = routed.kind === "fallback" ? packageSearchPaths(routed.entry, request, cjs) : cjs._nodeModulePaths(dirname(anchor));
|
|
1644
|
-
try {
|
|
1645
|
-
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1646
|
-
} catch (error) {
|
|
1647
|
-
return throwWithoutCjsAnchor(error, anchor);
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1650
|
-
const resolveNativeCjs = (request, searchPaths, parent, parentFilename, main, conditions) => {
|
|
1651
|
-
const synthetic = new cjs(parentFilename);
|
|
1652
|
-
synthetic.parent = parent;
|
|
1653
|
-
synthetic.filename = parentFilename;
|
|
1654
|
-
synthetic.paths = [...searchPaths];
|
|
1655
|
-
const options = conditions === void 0 ? void 0 : { conditions };
|
|
1656
|
-
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1657
|
-
};
|
|
1658
|
-
const resolvePackageImportCjs = (target, conditions) => {
|
|
1659
|
-
const state = router.routeUrl(target.specifier, target.parentURL);
|
|
1660
|
-
const resolveFrom = (parentURL) => fileURLToPath(esmDefaultResolve(target.specifier, {
|
|
1661
|
-
parentURL,
|
|
1662
|
-
conditions: [...conditions]
|
|
1663
|
-
}).url);
|
|
1664
|
-
/* v8 ignore next -- the target manifest was found inside the established profile scope */
|
|
1665
|
-
if (state === void 0) return resolveFrom(target.parentURL);
|
|
1666
|
-
if (state.route.kind === "native") return resolveFrom(target.parentURL);
|
|
1667
|
-
const route = state.route;
|
|
1668
|
-
if (route.kind === "after-fallback") return resolveFrom(pathToFileURL(route.parent).href);
|
|
1669
|
-
return resolveFrom(pathToFileURL(route.entry.declarer).href);
|
|
1670
|
-
};
|
|
1671
|
-
const wrappedFilename = (request, parent, main, options) => {
|
|
1672
|
-
if (delegatedCjs || !parent?.filename) return originalFilename.call(cjs, request, parent, main, options);
|
|
1673
|
-
const parentFilename = parent.filename;
|
|
1674
|
-
const cacheable = options?.paths === void 0 && options?.conditions === void 0;
|
|
1675
|
-
const explicitPaths = Array.isArray(options?.paths) ? options.paths : void 0;
|
|
1676
|
-
if (explicitPaths !== void 0 && router.nativeSelfReference(request, parentFilename)) return originalFilename.call(cjs, request, parent, main, options);
|
|
1677
|
-
const explicit = explicitPaths === void 0 ? void 0 : router.explicitRoute(request, explicitPaths);
|
|
1678
|
-
if (options?.paths !== void 0 && explicit === void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1679
|
-
if (explicit !== void 0 && explicitPaths !== void 0 && explicit.index > 0) try {
|
|
1680
|
-
return originalFilename.call(cjs, request, parent, main, {
|
|
1681
|
-
...options,
|
|
1682
|
-
paths: explicitPaths.slice(0, explicit.index)
|
|
1683
|
-
});
|
|
1684
|
-
} catch (error) {
|
|
1685
|
-
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1686
|
-
}
|
|
1687
|
-
const state = router.routePath(request, explicit?.parent ?? parentFilename, (searchPaths) => resolveNativeCjs(request, searchPaths, parent, parentFilename, main, options?.conditions), explicit === void 0 && cacheable);
|
|
1688
|
-
if (state === void 0) {
|
|
1689
|
-
const scoped = startsWithin(parentFilename, profilePaths);
|
|
1690
|
-
const conditions = options?.conditions ?? cjsConditions;
|
|
1691
|
-
const target = request[0] === "#" && scoped ? packageImportsTarget(parentFilename, request, conditions) : void 0;
|
|
1692
|
-
if (target === void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1693
|
-
const expected = resolvePackageImportCjs(target, conditions);
|
|
1694
|
-
if (behavior === "enforce") return expected;
|
|
1695
|
-
const actual = originalFilename.call(cjs, request, parent, main, options);
|
|
1696
|
-
assertEquivalent(actual, expected, request, parentFilename);
|
|
1697
|
-
return actual;
|
|
1698
|
-
}
|
|
1699
|
-
if (cacheable && state.cjs !== void 0) return state.cjs;
|
|
1700
|
-
const route = state.route;
|
|
1701
|
-
if (route.kind === "native") {
|
|
1702
|
-
const result = originalFilename.call(cjs, request, parent, main, options);
|
|
1703
|
-
if (cacheable) state.cjs = result;
|
|
1704
|
-
return result;
|
|
1705
|
-
}
|
|
1706
|
-
if (route.kind === "after-fallback" && explicit !== void 0 && explicitPaths !== void 0) try {
|
|
1707
|
-
return originalFilename.call(cjs, request, parent, main, {
|
|
1708
|
-
...options,
|
|
1709
|
-
paths: [dirname(route.parent)]
|
|
1710
|
-
});
|
|
1711
|
-
} catch (error) {
|
|
1712
|
-
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1713
|
-
const remaining = explicitPaths.slice(explicit.index + 1);
|
|
1714
|
-
if (remaining.length === 0) throw error;
|
|
1715
|
-
return wrappedFilename(request, parent, main, {
|
|
1716
|
-
...options,
|
|
1717
|
-
paths: remaining
|
|
1718
|
-
});
|
|
1719
|
-
}
|
|
1720
|
-
delegatedCjs++;
|
|
1721
|
-
try {
|
|
1722
|
-
const routedOptions = options?.conditions === void 0 ? void 0 : { conditions: options.conditions };
|
|
1723
|
-
let expected;
|
|
1724
|
-
try {
|
|
1725
|
-
expected = resolveRoutedCjs(request, route, parent, main, routedOptions);
|
|
1726
|
-
} catch (error) {
|
|
1727
|
-
if (route.kind !== "fallback" || !isUnselectedPackageMiss(error)) throw error;
|
|
1728
|
-
try {
|
|
1729
|
-
expected = resolveRoutedCjs(request, {
|
|
1730
|
-
kind: "after-fallback",
|
|
1731
|
-
parent: route.after
|
|
1732
|
-
}, parent, main, routedOptions);
|
|
1733
|
-
} catch (afterError) {
|
|
1734
|
-
const remaining = explicit === void 0 || explicitPaths === void 0 ? [] : explicitPaths.slice(explicit.index + 1);
|
|
1735
|
-
if (!isUnselectedPackageMiss(afterError) || remaining.length === 0) throw afterError;
|
|
1736
|
-
delegatedCjs--;
|
|
1737
|
-
try {
|
|
1738
|
-
return wrappedFilename(request, parent, main, {
|
|
1739
|
-
...options,
|
|
1740
|
-
paths: remaining
|
|
1741
|
-
});
|
|
1742
|
-
} finally {
|
|
1743
|
-
delegatedCjs++;
|
|
1744
|
-
}
|
|
2768
|
+
await walk(entries, pathToFileURL(join(profile.dir, "cordis.yml")).href, "");
|
|
2769
|
+
const targets = /* @__PURE__ */ new Map();
|
|
2770
|
+
const indexTargets = (rows) => {
|
|
2771
|
+
for (const row of rows) {
|
|
2772
|
+
if (!record(row)) continue;
|
|
2773
|
+
if (typeof row.id === "string" && row.id) {
|
|
2774
|
+
const entry = byOptions.get(row);
|
|
2775
|
+
if (entry) targets.set(row.id, entry);
|
|
2776
|
+
else targets.delete(row.id);
|
|
1745
2777
|
}
|
|
2778
|
+
if (row.group && Array.isArray(row.config)) indexTargets(row.config);
|
|
1746
2779
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
if (cacheable) state.cjs = actual;
|
|
1754
|
-
return actual;
|
|
1755
|
-
} finally {
|
|
1756
|
-
delegatedCjs--;
|
|
1757
|
-
}
|
|
1758
|
-
};
|
|
1759
|
-
cjs._resolveFilename = wrappedFilename;
|
|
1760
|
-
return {
|
|
1761
|
-
packageDir(specifier, parentURL) {
|
|
1762
|
-
const expected = router.packageDir(specifier, parentURL);
|
|
1763
|
-
if (behavior !== "verify" || !startsWithin(parentURL, profileUrls)) return expected;
|
|
1764
|
-
const name = barePackageName(specifier);
|
|
1765
|
-
if (name === void 0) return expected;
|
|
1766
|
-
let parent;
|
|
1767
|
-
try {
|
|
1768
|
-
parent = fileURLToPath(parentURL);
|
|
1769
|
-
} catch {
|
|
1770
|
-
return expected;
|
|
1771
|
-
}
|
|
1772
|
-
assertOptionalEquivalent(nativePackageDir(parent, name), expected, specifier, parentURL);
|
|
1773
|
-
return expected;
|
|
1774
|
-
},
|
|
1775
|
-
replace(next) {
|
|
1776
|
-
router.replace(next);
|
|
1777
|
-
},
|
|
1778
|
-
dispose() {
|
|
1779
|
-
/* v8 ignore else -- registrations are disposed in reverse installation order */
|
|
1780
|
-
if (cjs._resolveFilename === wrappedFilename) cjs._resolveFilename = originalFilename;
|
|
1781
|
-
restoreEsm();
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
2780
|
+
};
|
|
2781
|
+
indexTargets(entries);
|
|
2782
|
+
return await buildConfigSchemaDocument(profile.name, result.entries, targets, result.diagnostics);
|
|
2783
|
+
} finally {
|
|
2784
|
+
interception.dispose();
|
|
2785
|
+
}
|
|
1784
2786
|
}
|
|
2787
|
+
//#endregion
|
|
2788
|
+
//#region lib/types/config-schema/index.js
|
|
2789
|
+
/** Profile schema generation: composition diagnostics, runtime resolution, and boot-free discovery. */
|
|
1785
2790
|
/**
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
*
|
|
2791
|
+
* Generate JSON Schema for a prepared profile's ordered patch layers without mounting plugins or evaluating expressions.
|
|
2792
|
+
* Reads the profile manifest; imports, Config getters, and lazy builders execute trusted code. Native validators and
|
|
2793
|
+
* transform callbacks are not executed. Calls must not overlap another profile-resolution interception; collection
|
|
2794
|
+
* releases its interception on success or rejection, while Node retains imported modules. Supplied layers are not mutated.
|
|
2795
|
+
* Profile preparation, layer selection, process streams, and exit policy belong to the caller.
|
|
2796
|
+
* @param binName - the diagnostic prefix on thrown manifest errors.
|
|
2797
|
+
* @param profile - prepared on-disk profile whose directory anchors root module and include resolution.
|
|
2798
|
+
* @param layers - already parsed patch lists in application order, including caller-selected home and argv overlays.
|
|
2799
|
+
* @param installAnchor - package manifest anchoring the installation's runtime dependencies.
|
|
2800
|
+
* @returns a JSON Schema document with declaration references, partial results, and diagnostics under `x-cordis`.
|
|
2801
|
+
* @throws when profile metadata, composition, or runtime resolution cannot be prepared.
|
|
1790
2802
|
*/
|
|
1791
|
-
function
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2803
|
+
async function generateConfigSchema(binName, profile, layers, installAnchor) {
|
|
2804
|
+
const diagnostics = [];
|
|
2805
|
+
const manifest = readProfileManifest(binName, profile.dir);
|
|
2806
|
+
for (const packageName of skippedProfileBundles(profile, manifest)) diagnostics.push({
|
|
2807
|
+
level: "error",
|
|
2808
|
+
message: `Selected profile bundle ${JSON.stringify(packageName)} could not be loaded; repair or remove its bundle selection.`
|
|
1796
2809
|
});
|
|
1797
|
-
return () => {
|
|
1798
|
-
|
|
1799
|
-
|
|
2810
|
+
return collectConfigSchemas(profile, composeEntries(layers, (message) => diagnostics.push({
|
|
2811
|
+
level: "warning",
|
|
2812
|
+
message
|
|
2813
|
+
})), await createRuntimeResolution({
|
|
2814
|
+
installAnchor,
|
|
2815
|
+
profile
|
|
2816
|
+
}), diagnostics);
|
|
1800
2817
|
}
|
|
1801
2818
|
//#endregion
|
|
1802
2819
|
//#region lib/types/profile-resolution/service.js
|
|
1803
|
-
/** Package metadata resolved through one
|
|
2820
|
+
/** Package metadata resolved through one runtime interception. */
|
|
1804
2821
|
function readPackage(dir, fallbackName) {
|
|
1805
2822
|
const manifestPath = join(dir, "package.json");
|
|
1806
2823
|
if (!existsSync(manifestPath)) return void 0;
|
|
@@ -1818,31 +2835,30 @@ function readPackage(dir, fallbackName) {
|
|
|
1818
2835
|
/** Package lookup shared by metadata consumers in one profile process. */
|
|
1819
2836
|
var PluginPackages = class extends Service {
|
|
1820
2837
|
packages = /* @__PURE__ */ new Map();
|
|
1821
|
-
|
|
1822
|
-
behavior;
|
|
2838
|
+
interception;
|
|
1823
2839
|
disposeWorkerResolution;
|
|
1824
2840
|
constructor(ctx, config = {}) {
|
|
1825
2841
|
super(ctx, "pluginPackages");
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
this.
|
|
1830
|
-
this.resolver = resolver;
|
|
2842
|
+
if (config.resolution === void 0) return;
|
|
2843
|
+
const interception = installRuntimeInterception(config.resolution);
|
|
2844
|
+
this.disposeWorkerResolution = registerWorkerResolution(config.resolution);
|
|
2845
|
+
this.interception = interception;
|
|
1831
2846
|
ctx.effect(() => () => {
|
|
1832
2847
|
this.disposeWorkerResolution?.();
|
|
1833
|
-
|
|
2848
|
+
interception.dispose();
|
|
1834
2849
|
}, "profile package resolution");
|
|
1835
2850
|
}
|
|
1836
2851
|
/**
|
|
1837
|
-
* Publish
|
|
1838
|
-
*
|
|
2852
|
+
* Publish a complete successor generation for this process and subsequently created Workers.
|
|
2853
|
+
* Linked roots may be removed without unloading modules or clearing Node caches.
|
|
2854
|
+
* @param successor - fully constructed generation accepted by {@link RuntimeInterception.replace}.
|
|
1839
2855
|
*/
|
|
1840
|
-
replace(
|
|
1841
|
-
if (this.
|
|
1842
|
-
this.
|
|
2856
|
+
replace(successor) {
|
|
2857
|
+
if (this.interception === void 0) throw new Error("plugin-packages: runtime resolution is not installed");
|
|
2858
|
+
this.interception.replace(successor);
|
|
1843
2859
|
this.packages = /* @__PURE__ */ new Map();
|
|
1844
2860
|
this.disposeWorkerResolution?.();
|
|
1845
|
-
this.disposeWorkerResolution = registerWorkerResolution(
|
|
2861
|
+
this.disposeWorkerResolution = registerWorkerResolution(successor);
|
|
1846
2862
|
}
|
|
1847
2863
|
/**
|
|
1848
2864
|
* Locate the package named by a specifier without requiring a package export.
|
|
@@ -1853,7 +2869,7 @@ var PluginPackages = class extends Service {
|
|
|
1853
2869
|
packageOf(specifier, parentURL) {
|
|
1854
2870
|
const name = barePackageName(specifier);
|
|
1855
2871
|
if (name === void 0) return void 0;
|
|
1856
|
-
const dir = this.
|
|
2872
|
+
const dir = this.interception === void 0 ? packageDirFromParent(name, parentURL) : this.interception.packageDir(name, parentURL);
|
|
1857
2873
|
if (dir === void 0) return void 0;
|
|
1858
2874
|
const key = JSON.stringify({
|
|
1859
2875
|
dir,
|
|
@@ -1862,6 +2878,15 @@ var PluginPackages = class extends Service {
|
|
|
1862
2878
|
if (!this.packages.has(key)) this.packages.set(key, readPackage(dir, name));
|
|
1863
2879
|
return this.packages.get(key);
|
|
1864
2880
|
}
|
|
2881
|
+
/**
|
|
2882
|
+
* Read display metadata without loading or activating the target plugin.
|
|
2883
|
+
* @param specifier - configured package module, including package subpaths.
|
|
2884
|
+
* @param parentURL - owning Loader tree's resolution base.
|
|
2885
|
+
* @returns local display metadata or its diagnostic; undefined for non-package requests or absent metadata.
|
|
2886
|
+
*/
|
|
2887
|
+
metaOf(specifier, parentURL) {
|
|
2888
|
+
return readPluginMeta(specifier, parentURL);
|
|
2889
|
+
}
|
|
1865
2890
|
};
|
|
1866
2891
|
function packageDirFromParent(name, parentURL) {
|
|
1867
2892
|
for (const searchPath of createRequire(parentURL).resolve.paths(name)) {
|
|
@@ -2061,37 +3086,39 @@ function loadLayeredEnv(binName, cwd = process.cwd(), warn = (line) => void proc
|
|
|
2061
3086
|
}
|
|
2062
3087
|
const bootstrapIncludes = /* @__PURE__ */ new WeakMap();
|
|
2063
3088
|
const userPatchesSchema = entryListSchema;
|
|
2064
|
-
/**
|
|
2065
|
-
*
|
|
2066
|
-
* @param
|
|
2067
|
-
* @param
|
|
2068
|
-
* @
|
|
2069
|
-
* @
|
|
3089
|
+
/** Apply one complete patch generation and wait for Loader activation diagnostics.
|
|
3090
|
+
* @param ctx Booted root context.
|
|
3091
|
+
* @param patches Complete ordered patch list.
|
|
3092
|
+
* @param binName Diagnostic prefix.
|
|
3093
|
+
* @param requiredIds Explicit enablement targets whose existing failures also reject reconciliation.
|
|
3094
|
+
* @returns Diagnostics for unchanged pre-existing inactive entries; new or changed failures reject.
|
|
2070
3095
|
*/
|
|
2071
|
-
async function
|
|
2072
|
-
const { binName, filename, compose = (patches) => patches } = options;
|
|
2073
|
-
const hmr = ctx.get("hmr");
|
|
2074
|
-
if (hmr === void 0) throw new Error(`${binName}: user patch-layer watching requires the Cordis HMR service`);
|
|
3096
|
+
async function reconcileProfilePatches(ctx, patches, binName, requiredIds = []) {
|
|
2075
3097
|
const entry = bootstrapIncludes.get(ctx);
|
|
2076
|
-
if (entry === void 0) throw new Error(`${binName}:
|
|
2077
|
-
const
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
3098
|
+
if (entry === void 0) throw new Error(`${binName}: profile reload requires the root Include entry`);
|
|
3099
|
+
const previousFailures = (await inactiveEntries(ctx)).map((failure) => ({
|
|
3100
|
+
...failure,
|
|
3101
|
+
diagnostic: inactiveDiagnostic(failure),
|
|
3102
|
+
fiber: failure.entry.fiber,
|
|
3103
|
+
options: JSON.stringify(failure.entry.options)
|
|
3104
|
+
}));
|
|
3105
|
+
const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [{
|
|
3106
|
+
fiber: row.fiber,
|
|
3107
|
+
failed: row.fiber.state === FIBER_FAILED || row.fiber.state === FIBER_DISPOSED
|
|
3108
|
+
}]);
|
|
3109
|
+
const { patches: _previous, ...includeConfig } = entry.options.config;
|
|
3110
|
+
await entry.update({ config: {
|
|
3111
|
+
...includeConfig,
|
|
3112
|
+
patches
|
|
3113
|
+
} });
|
|
3114
|
+
const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await()));
|
|
3115
|
+
await ctx.loader.await();
|
|
3116
|
+
const failures = await inactiveEntries(ctx);
|
|
3117
|
+
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)));
|
|
3118
|
+
if (introduced.length > 0) throw new Error(activationDiagnostic(binName, introduced).trimEnd());
|
|
3119
|
+
for (const [index, result] of results.entries()) if (result.status === "rejected" && !previousFibers[index]?.failed) throw result.reason;
|
|
3120
|
+
ctx.emit("app-boot/config-reload");
|
|
3121
|
+
return failures.map(inactiveDiagnostic);
|
|
2095
3122
|
}
|
|
2096
3123
|
/**
|
|
2097
3124
|
* Load an optional patch-list file: a top-level YAML array of loader patch
|
|
@@ -2392,12 +3419,12 @@ function installFailLoud(binName, proc = process, release) {
|
|
|
2392
3419
|
}
|
|
2393
3420
|
/**
|
|
2394
3421
|
* Value mirrors used because Cordis's const enum has no runtime object to import.
|
|
2395
|
-
* Keep aligned with `packages/
|
|
2396
|
-
* `packages/client/web/src/loader-status.ts`.
|
|
3422
|
+
* Keep aligned with `packages/client/web/src/loader-status.ts`.
|
|
2397
3423
|
*/
|
|
2398
3424
|
const FIBER_PENDING = 0;
|
|
2399
3425
|
const FIBER_ACTIVE = 2;
|
|
2400
3426
|
const FIBER_FAILED = 3;
|
|
3427
|
+
const FIBER_DISPOSED = 4;
|
|
2401
3428
|
/**
|
|
2402
3429
|
* Entry ids whose presence defines a usable DSH application.
|
|
2403
3430
|
*
|
|
@@ -2432,6 +3459,25 @@ function formatActivationError(error) {
|
|
|
2432
3459
|
visit(error);
|
|
2433
3460
|
return details.join("\n");
|
|
2434
3461
|
}
|
|
3462
|
+
/** Startup audit failure with non-enumerable metadata and original failures as its cause. */
|
|
3463
|
+
var StartupError = class extends Error {
|
|
3464
|
+
entries;
|
|
3465
|
+
/** Root configuration and startup logs, attached by boot after disposal. */
|
|
3466
|
+
startup;
|
|
3467
|
+
/**
|
|
3468
|
+
* @param message - concise terminal diagnostic.
|
|
3469
|
+
* @param entries - inactive plugin metadata and original failure values.
|
|
3470
|
+
*/
|
|
3471
|
+
constructor(message, entries) {
|
|
3472
|
+
const failures = entries.flatMap(({ outcome }) => outcome.kind === "failed" ? [outcome.error] : []);
|
|
3473
|
+
super(message, failures.length > 0 ? { cause: new AggregateError(failures, "Plugin activation failures") } : void 0);
|
|
3474
|
+
this.entries = entries;
|
|
3475
|
+
Object.defineProperties(this, {
|
|
3476
|
+
entries: { enumerable: false },
|
|
3477
|
+
startup: { enumerable: false }
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
};
|
|
2435
3481
|
/**
|
|
2436
3482
|
* Collect Loader activation failures and disabled-expression errors. Failed
|
|
2437
3483
|
* fibers are awaited to recover their recorded rejection reason and coalesce
|
|
@@ -2441,13 +3487,16 @@ async function inactiveEntries(ctx) {
|
|
|
2441
3487
|
const failures = [];
|
|
2442
3488
|
const rejectionReasons = [];
|
|
2443
3489
|
for (const entry of ctx.loader.entries()) {
|
|
2444
|
-
const subject = `${entry.options.id} (${entry.options.name})`;
|
|
2445
3490
|
try {
|
|
2446
3491
|
if (entry.disabled) continue;
|
|
2447
3492
|
} catch (error) {
|
|
2448
3493
|
failures.push({
|
|
2449
3494
|
entry,
|
|
2450
|
-
|
|
3495
|
+
outcome: {
|
|
3496
|
+
kind: "failed",
|
|
3497
|
+
error,
|
|
3498
|
+
phase: "disabled expression failed"
|
|
3499
|
+
}
|
|
2451
3500
|
});
|
|
2452
3501
|
continue;
|
|
2453
3502
|
}
|
|
@@ -2455,7 +3504,10 @@ async function inactiveEntries(ctx) {
|
|
|
2455
3504
|
if (fiber === void 0) {
|
|
2456
3505
|
failures.push({
|
|
2457
3506
|
entry,
|
|
2458
|
-
|
|
3507
|
+
outcome: {
|
|
3508
|
+
kind: "failed",
|
|
3509
|
+
error: "failed to import"
|
|
3510
|
+
}
|
|
2459
3511
|
});
|
|
2460
3512
|
continue;
|
|
2461
3513
|
}
|
|
@@ -2468,7 +3520,10 @@ async function inactiveEntries(ctx) {
|
|
|
2468
3520
|
rejectionReasons.push(error);
|
|
2469
3521
|
failures.push({
|
|
2470
3522
|
entry,
|
|
2471
|
-
|
|
3523
|
+
outcome: {
|
|
3524
|
+
kind: "failed",
|
|
3525
|
+
error
|
|
3526
|
+
}
|
|
2472
3527
|
});
|
|
2473
3528
|
}
|
|
2474
3529
|
continue;
|
|
@@ -2477,42 +3532,90 @@ async function inactiveEntries(ctx) {
|
|
|
2477
3532
|
const missing = Object.keys(fiber.inject).filter((service) => fiber.ctx.get(service) === void 0);
|
|
2478
3533
|
failures.push({
|
|
2479
3534
|
entry,
|
|
2480
|
-
|
|
3535
|
+
outcome: {
|
|
3536
|
+
kind: "pending",
|
|
3537
|
+
missing
|
|
3538
|
+
}
|
|
2481
3539
|
});
|
|
2482
3540
|
} else failures.push({
|
|
2483
3541
|
entry,
|
|
2484
|
-
|
|
3542
|
+
outcome: {
|
|
3543
|
+
kind: "failed",
|
|
3544
|
+
error: `fiber state ${String(state)}`
|
|
3545
|
+
}
|
|
2485
3546
|
});
|
|
2486
3547
|
}
|
|
2487
3548
|
if (rejectionReasons.length > 0) await observeLoaderRejectionCheckpoint(rejectionReasons);
|
|
2488
3549
|
return failures;
|
|
2489
3550
|
}
|
|
2490
|
-
/** Render
|
|
2491
|
-
function
|
|
3551
|
+
/** Render one failed plugin's original error and activation phase. */
|
|
3552
|
+
function failureDetail(outcome) {
|
|
3553
|
+
return `${outcome.phase === void 0 ? "" : `${outcome.phase}: `}${formatActivationError(outcome.error)}`;
|
|
3554
|
+
}
|
|
3555
|
+
/** Render optional-only warnings without changing startup policy. */
|
|
3556
|
+
function activationDiagnostic(binName, failures) {
|
|
2492
3557
|
const noun = failures.length === 1 ? "entry" : "entries";
|
|
2493
|
-
return `${binName
|
|
3558
|
+
return `${binName}: warning: ${String(failures.length)} ${noun} did not activate\n${failures.map(inactiveDiagnostic).join("\n")}\n`;
|
|
3559
|
+
}
|
|
3560
|
+
/** Stable per-entry text for reload comparisons and optional warnings. */
|
|
3561
|
+
function inactiveDiagnostic({ entry, outcome }) {
|
|
3562
|
+
const detail = outcome.kind === "failed" ? failureDetail(outcome) : `pending (waiting for ${outcome.missing.length === 1 ? "service" : "services"}: ${outcome.missing.join(", ") || "unknown"})`;
|
|
3563
|
+
return `${entry.options.id} (${entry.options.name}): ${detail}`;
|
|
3564
|
+
}
|
|
3565
|
+
/** Group startup failures and pending services, marking every required entry. */
|
|
3566
|
+
function startupDiagnostic(binName, failures, required) {
|
|
3567
|
+
const lines = [`${binName}: startup failed: ${String(required.size)} required ${required.size === 1 ? "plugin" : "plugins"} did not activate`];
|
|
3568
|
+
const failed = failures.flatMap(({ entry, outcome }) => outcome.kind === "failed" ? [{
|
|
3569
|
+
entry,
|
|
3570
|
+
outcome
|
|
3571
|
+
}] : []);
|
|
3572
|
+
const pending = failures.flatMap(({ entry, outcome }) => outcome.kind === "pending" ? [{
|
|
3573
|
+
entry,
|
|
3574
|
+
outcome
|
|
3575
|
+
}] : []);
|
|
3576
|
+
pending.sort((left, right) => Number(required.has(right.entry)) - Number(required.has(left.entry)));
|
|
3577
|
+
const label = (entry) => `${entry.options.id}${required.has(entry) ? " (required)" : ""}`;
|
|
3578
|
+
if (failed.length > 0) {
|
|
3579
|
+
lines.push("", `Failed plugins (${String(failed.length)}):`);
|
|
3580
|
+
for (const { entry, outcome } of failed) {
|
|
3581
|
+
lines.push(` ${label(entry)}`, ` Package: ${entry.options.name}`);
|
|
3582
|
+
lines.push(...failureDetail(outcome).split("\n").map((line) => ` ${line}`));
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
if (pending.length > 0) {
|
|
3586
|
+
const width = Math.max(6, ...pending.map(({ entry }) => label(entry).length)) + 2;
|
|
3587
|
+
lines.push("", `Plugins waiting for services (${String(pending.length)}):`, ` ${"Plugin".padEnd(width)}Missing services`);
|
|
3588
|
+
for (const { entry, outcome } of pending) lines.push(` ${label(entry).padEnd(width)}${outcome.missing.join(", ") || "unknown"}`);
|
|
3589
|
+
}
|
|
3590
|
+
return lines.join("\n");
|
|
2494
3591
|
}
|
|
2495
3592
|
/**
|
|
2496
3593
|
* Apply DSH startup policy to a settled Loader tree.
|
|
2497
3594
|
*
|
|
2498
3595
|
* Inactive entries from the global required list reject startup. Other
|
|
2499
|
-
* inactive entries
|
|
3596
|
+
* inactive entries join that failure diagnostic, or produce one warning when
|
|
3597
|
+
* no required entry failed and leave successful siblings running.
|
|
2500
3598
|
* Required ids absent from the tree, and disabled required entries, are ignored.
|
|
2501
3599
|
* A throwing disabled expression is an entry failure, not a disabled entry.
|
|
2502
3600
|
* The bootstrap Include must activate so unreadable or invalid root config is fatal.
|
|
2503
3601
|
* @param ctx - the settled context whose Loader entries to audit.
|
|
2504
|
-
* @param binName - the
|
|
3602
|
+
* @param binName - the prefix on startup diagnostics.
|
|
2505
3603
|
* @param warn - sink for optional-entry warnings.
|
|
2506
3604
|
* @returns after optional warnings if required startup checks pass.
|
|
2507
|
-
* @throws when the bootstrap Include or a required entry is inactive or its disabled expression throws
|
|
3605
|
+
* @throws {@link StartupError} when the bootstrap Include or a required entry is inactive or its disabled expression throws;
|
|
3606
|
+
* its message includes optional failures too.
|
|
2508
3607
|
*/
|
|
2509
3608
|
async function auditStartupEntries(ctx, binName, warn = (line) => void process.stderr.write(line)) {
|
|
2510
3609
|
const failures = await inactiveEntries(ctx);
|
|
2511
|
-
const required =
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
3610
|
+
const required = new Set(failures.filter(({ entry }) => entry === bootstrapIncludes.get(ctx) || requiredStartupEntryIds.has(entry.options.id)).map(({ entry }) => entry));
|
|
3611
|
+
if (required.size > 0) throw new StartupError(startupDiagnostic(binName, failures, required), failures.map(({ entry, outcome }) => ({
|
|
3612
|
+
id: entry.options.id,
|
|
3613
|
+
module: entry.options.name,
|
|
3614
|
+
required: required.has(entry),
|
|
3615
|
+
fiberState: entry.fiber?.state,
|
|
3616
|
+
outcome
|
|
3617
|
+
})));
|
|
3618
|
+
if (failures.length > 0) warn(activationDiagnostic(binName, failures));
|
|
2516
3619
|
}
|
|
2517
3620
|
/**
|
|
2518
3621
|
* Boot the Loader against `absoluteConfigPath` and return only after the whole
|
|
@@ -2539,13 +3642,28 @@ async function auditStartupEntries(ctx, binName, warn = (line) => void process.s
|
|
|
2539
3642
|
* complete plugin set.
|
|
2540
3643
|
* @returns the root context after the initial startup audit, or as soon as a
|
|
2541
3644
|
* surface disposed the tree while startup was still in flight.
|
|
2542
|
-
* @throws
|
|
3645
|
+
* @throws {@link StartupError} for an inactive required entry, including all inactive plugins in its message;
|
|
3646
|
+
* otherwise a labelled error after disposing the partial context — `host
|
|
2543
3647
|
* preparation failed` when `prepare` threw before any config-tree entry
|
|
2544
3648
|
* mounted, `plugin tree failed to load` afterwards. Cyclic causes terminate
|
|
2545
3649
|
* diagnostic traversal without replacing the original cause.
|
|
2546
3650
|
*/
|
|
2547
3651
|
async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBaseUrl) {
|
|
2548
3652
|
const ctx = new Context();
|
|
3653
|
+
const startupLogs = [];
|
|
3654
|
+
const diagnostics = new Context();
|
|
3655
|
+
diagnostics.logger = ctx.logger;
|
|
3656
|
+
diagnostics.logger.exporter({
|
|
3657
|
+
levels: { default: 2 },
|
|
3658
|
+
export: ({ ts, name, type, args }) => {
|
|
3659
|
+
if (type === "warn" || type === "error") startupLogs.push({
|
|
3660
|
+
ts,
|
|
3661
|
+
name,
|
|
3662
|
+
type,
|
|
3663
|
+
args
|
|
3664
|
+
});
|
|
3665
|
+
}
|
|
3666
|
+
});
|
|
2549
3667
|
let stage = "host preparation failed";
|
|
2550
3668
|
try {
|
|
2551
3669
|
ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + "/";
|
|
@@ -2568,6 +3686,13 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
2568
3686
|
return ctx;
|
|
2569
3687
|
} catch (cause) {
|
|
2570
3688
|
await ctx.fiber.dispose();
|
|
3689
|
+
if (cause instanceof StartupError) {
|
|
3690
|
+
cause.startup = {
|
|
3691
|
+
configurationPath: absoluteConfigPath,
|
|
3692
|
+
messages: startupLogs
|
|
3693
|
+
};
|
|
3694
|
+
throw cause;
|
|
3695
|
+
}
|
|
2571
3696
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
2572
3697
|
let deepest = cause;
|
|
2573
3698
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2577,6 +3702,8 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
2577
3702
|
}
|
|
2578
3703
|
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}` : "";
|
|
2579
3704
|
throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause });
|
|
3705
|
+
} finally {
|
|
3706
|
+
await diagnostics.fiber.dispose();
|
|
2580
3707
|
}
|
|
2581
3708
|
}
|
|
2582
3709
|
/** Prompt-section name for the harness-source location line an app bin adds after boot. */
|
|
@@ -2606,4 +3733,4 @@ function addHarnessSourceSection(ctx, sourceRoot) {
|
|
|
2606
3733
|
});
|
|
2607
3734
|
}
|
|
2608
3735
|
//#endregion
|
|
2609
|
-
export { DEFAULT_PROFILE_BUNDLES,
|
|
3736
|
+
export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, LOADER_EXPRESSION_SCHEMA, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, StartupError, addHarnessSourceSection, auditStartupEntries, boot, bundlePatchFiles, bundlePatchPaths, composeEntries, createConfigProjector, createRuntimeResolution, generateConfigSchema, initProfile, installFailLoud, isNativeConfigSchema, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, loadProfileDirectory, mountRootInclude, readPluginMeta, readProfileManifest, readProfilePatches, readProfilePlugins, reconcileProfilePatches, reconcileProfilePlugins, removeLinkProjections, renderConfigDump, resolveBundleDir, resolveConfigPath, resolveProfileDir, resolveTelemetryPatch, sanitizeProfile, writeProfileBundles, writeProfileManifest };
|