@deepseek-ai/dsh-app-boot 0.1.6-alpha.2 → 0.1.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +34 -14
- package/README.zh.md +34 -14
- package/lib/index.js +1683 -693
- 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 +34 -9
- package/lib/types/package-meta.d.ts +25 -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.d.ts +71 -63
- package/lib/worker/profile-resolution-bootstrap.js +255 -357
- package/package.json +20 -18
package/lib/index.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { createRequire, isBuiltin } from "node:module";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { existsSync,
|
|
4
|
-
import { parseEnv } from "node:util";
|
|
5
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { inspect, parseEnv } from "node:util";
|
|
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, rename, writeFile } from "node:fs/promises";
|
|
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 { withFileLock } from "@deepseek-ai/dsh-atomic-write";
|
|
15
|
-
import { imports, resolve as resolve$1 } from "resolve.exports";
|
|
16
14
|
import { getEnvironmentData, setEnvironmentData } from "node:worker_threads";
|
|
15
|
+
import { imports } from "resolve.exports";
|
|
17
16
|
//#region ../../../vendor/include/src/index.ts
|
|
18
17
|
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
|
|
19
18
|
kind: "scalar",
|
|
@@ -247,9 +246,7 @@ var Include = class extends EntryTree {
|
|
|
247
246
|
};
|
|
248
247
|
//#endregion
|
|
249
248
|
//#region lib/types/profile-resolution/legacy-links.js
|
|
250
|
-
/**
|
|
251
|
-
/** Profile-private package links projected into its pnpm-managed node_modules. */
|
|
252
|
-
const PROFILE_MODULE_FALLBACK_DIR = ".dsh-module-fallback";
|
|
249
|
+
/** Package directory canonicalization through the active runtime carrier's filesystem. */
|
|
253
250
|
/**
|
|
254
251
|
* Return whether the process reads application modules from pkg's virtual filesystem.
|
|
255
252
|
* @returns whether pkg owns the module filesystem.
|
|
@@ -265,50 +262,6 @@ function isPackagedExecutable() {
|
|
|
265
262
|
function realModuleDirectory(path) {
|
|
266
263
|
return isPackagedExecutable() ? realpathSync(path) : realpathSync.native(path);
|
|
267
264
|
}
|
|
268
|
-
/**
|
|
269
|
-
* Resolve a link target without following the final path component.
|
|
270
|
-
* @param path - candidate path whose parent is canonicalized.
|
|
271
|
-
* @returns the canonical candidate, or undefined when its parent is absent.
|
|
272
|
-
*/
|
|
273
|
-
function canonicalLinkPath(path) {
|
|
274
|
-
try {
|
|
275
|
-
return join(realModuleDirectory(dirname(path)), basename(path));
|
|
276
|
-
} catch (error) {
|
|
277
|
-
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
|
278
|
-
if (error.code === "ENOENT") return void 0;
|
|
279
|
-
/* v8 ignore next -- see the host-filesystem exception above */
|
|
280
|
-
throw error;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Return whether a symlink or junction points at the same path as `target`.
|
|
285
|
-
* @param link - symlink or junction to inspect.
|
|
286
|
-
* @param target - expected target path.
|
|
287
|
-
* @returns whether both paths identify the same entry.
|
|
288
|
-
*/
|
|
289
|
-
function symlinkPointsTo(link, target) {
|
|
290
|
-
const canonicalActual = canonicalLinkPath(resolve(dirname(link), readlinkSync(link)));
|
|
291
|
-
const canonicalTarget = canonicalLinkPath(resolve(target));
|
|
292
|
-
return canonicalActual !== void 0 && canonicalActual === canonicalTarget;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Return whether an observed profile package must not claim local precedence.
|
|
296
|
-
* @param profileDir - profile directory containing the package projection.
|
|
297
|
-
* @param packageName - bare package name to inspect.
|
|
298
|
-
* @returns whether the entry is a managed fallback link or disappeared during inspection.
|
|
299
|
-
*/
|
|
300
|
-
function isProfileModuleFallbackLink(profileDir, packageName) {
|
|
301
|
-
const link = join(profileDir, "node_modules", packageName);
|
|
302
|
-
const target = join(profileDir, PROFILE_MODULE_FALLBACK_DIR, "node_modules", packageName);
|
|
303
|
-
try {
|
|
304
|
-
return lstatSync(link).isSymbolicLink() && symlinkPointsTo(link, target);
|
|
305
|
-
} catch (error) {
|
|
306
|
-
/* v8 ignore next 2 -- a vanished candidate cannot claim local precedence */
|
|
307
|
-
if (error.code === "ENOENT") return true;
|
|
308
|
-
/* v8 ignore next -- non-ENOENT lstat/readlink failures require a host filesystem fault */
|
|
309
|
-
throw error;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
265
|
//#endregion
|
|
313
266
|
//#region lib/types/profile.js
|
|
314
267
|
/**
|
|
@@ -320,19 +273,17 @@ function isProfileModuleFallbackLink(profileDir, packageName) {
|
|
|
320
273
|
* `dsh.profile` with its ordered `bundles` list) and a `cordis.patch.yml`
|
|
321
274
|
* (the user's own patch layer, applied after every bundle layer). Bundles are
|
|
322
275
|
* npm packages whose manifest declares
|
|
323
|
-
* `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
|
|
324
|
-
* composed by applying each bundle's patch
|
|
325
|
-
* an empty entry list, then the
|
|
326
|
-
* 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).
|
|
327
281
|
*
|
|
328
282
|
* Module resolution is two-anchor by construction: a bundle name resolves
|
|
329
283
|
* first from the dsh installation (the launcher's own package), then from the
|
|
330
284
|
* profile directory. Pnpm-managed entries in the profile's `node_modules`
|
|
331
|
-
* resolve first.
|
|
332
|
-
* bundles
|
|
333
|
-
* dependency closure through Node's ordinary parent-walk. Plain Node uses
|
|
334
|
-
* symlinks for that shared fallback; packaged executables use ESM proxies so
|
|
335
|
-
* 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.
|
|
336
287
|
* @module @deepseek-ai/dsh-app-boot/profile
|
|
337
288
|
*/
|
|
338
289
|
/** Directory under the Harness home holding every profile. */
|
|
@@ -340,6 +291,28 @@ const PROFILES_DIR = "profiles";
|
|
|
340
291
|
/** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */
|
|
341
292
|
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
342
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
|
+
/**
|
|
343
316
|
* Resolve a profile's directory under the Harness home.
|
|
344
317
|
* @param name - the profile name (`dsh --profile <name>`).
|
|
345
318
|
* @param home - the Harness home; defaults to {@link resolveDshHome}.
|
|
@@ -371,7 +344,7 @@ const DEFAULT_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base"];
|
|
|
371
344
|
* selected by no shipped template, and offered switched off by the plugin
|
|
372
345
|
* manager ([rationale](../../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)).
|
|
373
346
|
*/
|
|
374
|
-
const OPTIONAL_BUNDLES = ["@deepseek-ai/dsh-experimental-
|
|
347
|
+
const OPTIONAL_BUNDLES = ["@deepseek-ai/dsh-experimental-voice-input-bundle", "@deepseek-ai/dsh-experimental-agent-team-profile"];
|
|
375
348
|
const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
|
|
376
349
|
# a top-level YAML array of loader patch entries (id-targeted config
|
|
377
350
|
# overrides, disables, and insert lists; \`!!js\` expressions allowed).
|
|
@@ -407,188 +380,108 @@ function initProfile(dir, bundles) {
|
|
|
407
380
|
const workspacePath = join(dir, "pnpm-workspace.yaml");
|
|
408
381
|
if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE);
|
|
409
382
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
});
|
|
416
401
|
}
|
|
417
|
-
/**
|
|
418
|
-
function
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (!stat.isSymbolicLink()) {
|
|
427
|
-
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`);
|
|
428
|
-
rmSync(link, { recursive: true });
|
|
429
|
-
stat = void 0;
|
|
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));
|
|
430
411
|
}
|
|
431
|
-
if (stat !== void 0) {
|
|
432
|
-
if (symlinkPointsTo(link, target)) return;
|
|
433
|
-
unlinkSync(link);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
try {
|
|
437
|
-
symlinkSync(target, link, "junction");
|
|
438
|
-
} catch (error) {
|
|
439
|
-
/* v8 ignore next 4 */
|
|
440
|
-
if (error.code !== "EEXIST" || !lstatSync(link).isSymbolicLink() || !symlinkPointsTo(link, target)) throw error;
|
|
441
412
|
}
|
|
413
|
+
return links;
|
|
442
414
|
}
|
|
443
|
-
/**
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
ensureSymlink(link, target);
|
|
453
|
-
}
|
|
454
|
-
/** Package names represented by owned symlinks below one fallback node_modules. */
|
|
455
|
-
function ownedPackageNames(modulesDir) {
|
|
456
|
-
return readdirSync(modulesDir, { withFileTypes: true }).flatMap((entry) => {
|
|
457
|
-
if (entry.name.startsWith("@") && entry.isDirectory()) return readdirSync(join(modulesDir, entry.name), { withFileTypes: true }).filter((child) => child.isSymbolicLink()).map((child) => `${entry.name}/${child.name}`);
|
|
458
|
-
return entry.isSymbolicLink() ? [entry.name] : [];
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
/** Remove an obsolete owned target and its profile projection when still connected. */
|
|
462
|
-
function removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName) {
|
|
463
|
-
const ownedLink = join(ownedModulesDir, packageName);
|
|
464
|
-
const profileLink = join(profileModulesDir, packageName);
|
|
465
|
-
try {
|
|
466
|
-
if (lstatSync(profileLink).isSymbolicLink() && symlinkPointsTo(profileLink, ownedLink)) unlinkSync(profileLink);
|
|
467
|
-
} catch (error) {
|
|
468
|
-
/* v8 ignore next -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
469
|
-
if (error.code !== "ENOENT") throw error;
|
|
470
|
-
}
|
|
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;
|
|
471
424
|
try {
|
|
472
|
-
|
|
425
|
+
tree = realModuleDirectory(profilesDir) + sep;
|
|
473
426
|
} catch (error) {
|
|
474
|
-
/* v8 ignore next -- concurrent identical cleanup may remove the link first */
|
|
475
427
|
if (error.code !== "ENOENT") throw error;
|
|
428
|
+
tree = resolve(profilesDir) + sep;
|
|
476
429
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
try {
|
|
482
|
-
candidates = resolve$1({
|
|
483
|
-
name: packageName,
|
|
484
|
-
exports: declared
|
|
485
|
-
}, subpath);
|
|
486
|
-
} catch (error) {
|
|
487
|
-
if (error.message.startsWith("No known conditions for ")) return void 0;
|
|
488
|
-
const specifier = subpath === "." ? packageName : packageName + subpath.slice(1);
|
|
489
|
-
throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error });
|
|
490
|
-
}
|
|
491
|
-
for (const candidate of candidates ?? []) {
|
|
492
|
-
const target = candidate;
|
|
493
|
-
const entry = resolve(packageDir, target);
|
|
494
|
-
const relativeEntry = relative(packageDir, entry);
|
|
495
|
-
if (!target.startsWith("./") || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`);
|
|
496
|
-
if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */
|
|
500
|
-
function packageProxySource(packageName, packageDir) {
|
|
501
|
-
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
502
|
-
if (typeof manifest.version !== "string" || manifest.version.length === 0) throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`);
|
|
503
|
-
const declared = manifest.exports;
|
|
504
|
-
if (declared === void 0) {
|
|
505
|
-
const main = typeof manifest.main === "string" && manifest.main.length > 0 ? manifest.main : void 0;
|
|
506
|
-
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;
|
|
507
434
|
try {
|
|
508
|
-
|
|
509
|
-
return {
|
|
510
|
-
version: manifest.version,
|
|
511
|
-
targets: { ".": pathToFileURL(resolved).href }
|
|
512
|
-
};
|
|
435
|
+
realPath = realModuleDirectory(linkPath);
|
|
513
436
|
} catch (error) {
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
targets: {}
|
|
517
|
-
};
|
|
518
|
-
throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error });
|
|
437
|
+
if (error.code !== "ENOENT") throw error;
|
|
438
|
+
continue;
|
|
519
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
|
+
});
|
|
520
445
|
}
|
|
521
|
-
|
|
522
|
-
const targets = {};
|
|
523
|
-
for (const subpath of subpaths) {
|
|
524
|
-
const target = packageEntryFromPackage(packageName, packageDir, declared, subpath);
|
|
525
|
-
if (target !== void 0) targets[subpath] = target;
|
|
526
|
-
}
|
|
527
|
-
return {
|
|
528
|
-
version: manifest.version,
|
|
529
|
-
targets
|
|
530
|
-
};
|
|
446
|
+
return roots.sort((left, right) => left.name.localeCompare(right.name));
|
|
531
447
|
}
|
|
532
|
-
/**
|
|
533
|
-
|
|
534
|
-
* URL. Files outside the executable cannot traverse a symlink into
|
|
535
|
-
* `/snapshot`, while an ESM re-export can import that URL and preserves the
|
|
536
|
-
* executable's single module instance for out-of-tree plugin peers.
|
|
537
|
-
*/
|
|
538
|
-
function ensureModuleProxy(link, packageName, version, targets) {
|
|
539
|
-
const manifest = {
|
|
540
|
-
name: packageName,
|
|
541
|
-
version,
|
|
542
|
-
private: true,
|
|
543
|
-
type: "module",
|
|
544
|
-
exports: Object.fromEntries(Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`])),
|
|
545
|
-
dsh: { moduleFallback: { targets } }
|
|
546
|
-
};
|
|
547
|
-
let stat;
|
|
448
|
+
/** Whether a symlink's target directory is `root` or lies below it. */
|
|
449
|
+
function pointsInto(link, root) {
|
|
548
450
|
try {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const existing = readModuleProxyRecord(link);
|
|
559
|
-
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`);
|
|
560
|
-
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;
|
|
561
|
-
rmSync(link, { recursive: true });
|
|
562
|
-
}
|
|
563
|
-
mkdirSync(link, { recursive: true });
|
|
564
|
-
writeFileSync(join(link, "package.json"), JSON.stringify(manifest, void 0, 2) + "\n");
|
|
565
|
-
for (const [index, target] of Object.values(targets).entries()) {
|
|
566
|
-
const specifier = JSON.stringify(target);
|
|
567
|
-
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;
|
|
568
460
|
}
|
|
569
461
|
}
|
|
570
|
-
/** Read one package manifest
|
|
571
|
-
function
|
|
462
|
+
/** Read one package manifest while traversing a dependency graph. */
|
|
463
|
+
function readPackageManifest(anchor) {
|
|
572
464
|
return JSON.parse(readFileSync(anchor, "utf8"));
|
|
573
465
|
}
|
|
574
466
|
/** Return dependency names that may be imported by a loader-visible plugin. */
|
|
575
467
|
function profileDependencyNames(manifest) {
|
|
576
468
|
return [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {})];
|
|
577
469
|
}
|
|
578
|
-
/** Resolve the installation
|
|
579
|
-
function
|
|
580
|
-
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);
|
|
581
474
|
const links = /* @__PURE__ */ new Map();
|
|
582
475
|
const declarers = /* @__PURE__ */ new Map();
|
|
583
476
|
const versions = /* @__PURE__ */ new Map();
|
|
584
477
|
/* v8 ignore next -- a real app manifest always declares its name */
|
|
585
478
|
if (appManifest.name !== void 0) {
|
|
586
479
|
links.set(appManifest.name, dirname(installAnchor));
|
|
587
|
-
declarers.set(appManifest.name,
|
|
480
|
+
declarers.set(appManifest.name, canonicalAnchor);
|
|
588
481
|
versions.set(appManifest.name, appManifest.version);
|
|
589
482
|
}
|
|
590
483
|
const queue = [{
|
|
591
|
-
anchor:
|
|
484
|
+
anchor: canonicalAnchor,
|
|
592
485
|
manifest: appManifest
|
|
593
486
|
}];
|
|
594
487
|
for (let next = queue.shift(); next !== void 0; next = queue.shift())
|
|
@@ -597,10 +490,16 @@ function resolveModuleFallbackEntries(installAnchor, materialize = true) {
|
|
|
597
490
|
if (links.has(dep)) continue;
|
|
598
491
|
const dir = packageDirFromAnchor(next.anchor, dep);
|
|
599
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
|
+
}
|
|
600
501
|
links.set(dep, dir);
|
|
601
502
|
declarers.set(dep, next.anchor);
|
|
602
|
-
const manifestPath = join(dir, "package.json");
|
|
603
|
-
const manifest = readModuleFallbackManifest(manifestPath);
|
|
604
503
|
versions.set(dep, manifest.version);
|
|
605
504
|
queue.push({
|
|
606
505
|
anchor: manifestPath,
|
|
@@ -608,72 +507,32 @@ function resolveModuleFallbackEntries(installAnchor, materialize = true) {
|
|
|
608
507
|
});
|
|
609
508
|
}
|
|
610
509
|
return {
|
|
611
|
-
entries: !materialize ? [] : !isPackagedExecutable() ? [...links].map(([packageName, packageDir]) => ({
|
|
612
|
-
kind: "symlink",
|
|
613
|
-
packageName,
|
|
614
|
-
packageDir
|
|
615
|
-
})) : [...links].flatMap(([packageName, packageDir]) => {
|
|
616
|
-
const source = packageProxySource(packageName, packageDir);
|
|
617
|
-
return Object.keys(source.targets).length === 0 ? [] : [{
|
|
618
|
-
kind: "proxy",
|
|
619
|
-
packageName,
|
|
620
|
-
version: source.version,
|
|
621
|
-
targets: source.targets
|
|
622
|
-
}];
|
|
623
|
-
}),
|
|
624
510
|
packageNames: new Set(links.keys()),
|
|
625
511
|
packageDirs: links,
|
|
626
512
|
declarers,
|
|
627
513
|
versions
|
|
628
514
|
};
|
|
629
515
|
}
|
|
630
|
-
/** Return whether one existing fallback entry already matches its resolved installation generation. */
|
|
631
|
-
function moduleFallbackEntryCurrent(modulesDir, entry) {
|
|
632
|
-
const link = join(modulesDir, entry.packageName);
|
|
633
|
-
try {
|
|
634
|
-
const stat = lstatSync(link);
|
|
635
|
-
if (entry.kind === "symlink") return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir;
|
|
636
|
-
if (!stat.isDirectory()) return false;
|
|
637
|
-
const existing = readModuleProxyRecord(link);
|
|
638
|
-
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`)));
|
|
639
|
-
} catch {
|
|
640
|
-
return false;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
/** Return whether every required fallback entry is already ready for this installation. */
|
|
644
|
-
function moduleFallbackCurrent(modulesDir, entries) {
|
|
645
|
-
return entries.every((entry) => moduleFallbackEntryCurrent(modulesDir, entry));
|
|
646
|
-
}
|
|
647
516
|
/**
|
|
648
|
-
*
|
|
649
|
-
* `$DSH_HOME/profiles/node_modules` mirrors the dsh installation dependency
|
|
650
|
-
* closure. Plain Node writes symlinks; a packaged executable writes ESM
|
|
651
|
-
* proxies under a cross-process lock because operating-system links cannot
|
|
652
|
-
* enter pkg's virtual filesystem. Missing packages carried only by selected
|
|
653
|
-
* bundles are linked through a profile-owned directory into that profile's
|
|
654
|
-
* `node_modules`; pnpm-managed entries remain authoritative, and another
|
|
655
|
-
* profile's links cannot change its resolution.
|
|
517
|
+
* Compute the runtime resolution without writing module-resolution files.
|
|
656
518
|
* @param options - installation anchor, optional loaded profile, and Harness home.
|
|
657
|
-
* @returns the
|
|
519
|
+
* @returns the complete immutable runtime resolution.
|
|
658
520
|
*/
|
|
659
|
-
async function
|
|
660
|
-
const { installAnchor, profile, home = resolveDshHome()
|
|
521
|
+
async function createRuntimeResolution(options) {
|
|
522
|
+
const { installAnchor, profile, home = resolveDshHome() } = options;
|
|
661
523
|
const profilesDir = join(home, PROFILES_DIR);
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
const { entries, packageNames, packageDirs, declarers, versions } = resolveModuleFallbackEntries(installAnchor, materialize);
|
|
665
|
-
if (materialize && !moduleFallbackCurrent(modulesDir, entries)) await withFileLock(modulesDir, () => {
|
|
666
|
-
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir);
|
|
667
|
-
return Promise.resolve();
|
|
668
|
-
});
|
|
524
|
+
const manifest = readOptionalProfileManifest(profile);
|
|
525
|
+
const { packageNames, packageDirs, declarers, versions } = collectInstallationScopePackages(installAnchor, skippedProfileBundles(profile, manifest));
|
|
669
526
|
const profileDeclarers = /* @__PURE__ */ new Map();
|
|
670
527
|
const profileVersions = /* @__PURE__ */ new Map();
|
|
671
|
-
const localPackageNames = profile === void 0 ? [] : installedProfilePackageNames(profile);
|
|
672
|
-
const profilePackages = profile === void 0 ? /* @__PURE__ */ new Map() :
|
|
673
|
-
|
|
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({
|
|
674
532
|
profilesDir,
|
|
675
533
|
profileDir: profile?.dir,
|
|
676
534
|
localPackageNames: Object.freeze(localPackageNames),
|
|
535
|
+
linkedRoots: Object.freeze(linkedRoots.map((root) => Object.freeze(root))),
|
|
677
536
|
entries: Object.freeze([...[...packageDirs].map(([name, packageDir]) => Object.freeze({
|
|
678
537
|
name,
|
|
679
538
|
packageDir,
|
|
@@ -687,69 +546,41 @@ async function healProfilesModuleFallback(options) {
|
|
|
687
546
|
declarer: profileDeclarers.get(name),
|
|
688
547
|
scope: "profile"
|
|
689
548
|
}))])
|
|
690
|
-
});
|
|
549
|
+
}));
|
|
691
550
|
}
|
|
692
|
-
/**
|
|
693
|
-
function
|
|
694
|
-
|
|
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;
|
|
695
554
|
try {
|
|
696
|
-
|
|
555
|
+
return readPackageManifest(join(profile.dir, "package.json"));
|
|
697
556
|
} catch (error) {
|
|
698
|
-
if (error.code === "ENOENT") return
|
|
557
|
+
if (error.code === "ENOENT") return void 0;
|
|
699
558
|
throw error;
|
|
700
559
|
}
|
|
701
|
-
return profileDependencyNames(manifest).filter((name) => {
|
|
702
|
-
if (!existsSync(join(join(profile.dir, "node_modules", name), "package.json"))) return false;
|
|
703
|
-
return !isProfileModuleFallbackLink(profile.dir, name);
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Compute a profile resolution generation without materializing links or proxies.
|
|
708
|
-
* @param options - installation anchor, profile, and optional Harness home.
|
|
709
|
-
* @returns the complete immutable generation.
|
|
710
|
-
*/
|
|
711
|
-
function createProfileResolutionGeneration(options) {
|
|
712
|
-
return healProfilesModuleFallback({
|
|
713
|
-
...options,
|
|
714
|
-
materialize: false
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
/**
|
|
718
|
-
* Supply an application-owned profile with filesystem packages from its installation and selected bundles.
|
|
719
|
-
* All fallback links belong to the profile; no shared Harness-home directory is written.
|
|
720
|
-
* Existing pnpm-managed packages remain authoritative. The caller serializes profile mutations.
|
|
721
|
-
* @param options - owning installation package.json and the loaded application profile.
|
|
722
|
-
*/
|
|
723
|
-
function healIsolatedProfileModuleFallback(options) {
|
|
724
|
-
const installationLinks = resolveModuleFallbackEntries(options.installAnchor, false).packageDirs;
|
|
725
|
-
healProfileModuleFallback(options.profile, new Set(installationLinks.keys()), true, void 0, void 0, installationLinks);
|
|
726
560
|
}
|
|
727
561
|
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
* @param
|
|
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.
|
|
731
566
|
*/
|
|
732
|
-
function
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
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)));
|
|
736
571
|
}
|
|
737
|
-
/**
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
mkdirSync(dirname(link), { recursive: true });
|
|
742
|
-
if (entry.kind === "proxy") ensureModuleProxy(link, entry.packageName, entry.version, entry.targets);
|
|
743
|
-
else ensureSymlink(link, entry.packageDir);
|
|
744
|
-
}
|
|
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")));
|
|
745
576
|
}
|
|
746
577
|
/** Collect the first resolvable package directory for each dependency name. */
|
|
747
|
-
function dependencyClosure(anchors, reserved,
|
|
578
|
+
function dependencyClosure(anchors, reserved, declarers, versions) {
|
|
748
579
|
const links = /* @__PURE__ */ new Map();
|
|
749
580
|
const visited = new Set(reserved);
|
|
750
581
|
for (const anchor of anchors) {
|
|
751
582
|
const canonicalAnchor = join(realModuleDirectory(dirname(anchor)), basename(anchor));
|
|
752
|
-
const manifest =
|
|
583
|
+
const manifest = readPackageManifest(canonicalAnchor);
|
|
753
584
|
/* v8 ignore next -- an installable package manifest always declares its name */
|
|
754
585
|
if (manifest.name === void 0) continue;
|
|
755
586
|
if (!visited.has(manifest.name)) {
|
|
@@ -766,13 +597,13 @@ function dependencyClosure(anchors, reserved, exclude, declarers, versions) {
|
|
|
766
597
|
/* v8 ignore next -- an installable package manifest always declares dependencies or peers */
|
|
767
598
|
for (const dep of profileDependencyNames(next.manifest)) {
|
|
768
599
|
if (visited.has(dep)) continue;
|
|
769
|
-
const dir = packageDirFromAnchor(next.anchor, dep
|
|
600
|
+
const dir = packageDirFromAnchor(next.anchor, dep);
|
|
770
601
|
if (dir === void 0) continue;
|
|
771
602
|
visited.add(dep);
|
|
772
603
|
links.set(dep, dir);
|
|
773
604
|
declarers?.set(dep, next.anchor);
|
|
774
|
-
const manifestPath = join(dir, "package.json");
|
|
775
|
-
const dependencyManifest =
|
|
605
|
+
const manifestPath = join(realModuleDirectory(dir), "package.json");
|
|
606
|
+
const dependencyManifest = readPackageManifest(manifestPath);
|
|
776
607
|
versions?.set(dep, dependencyManifest.version);
|
|
777
608
|
queue.push({
|
|
778
609
|
anchor: manifestPath,
|
|
@@ -782,39 +613,11 @@ function dependencyClosure(anchors, reserved, exclude, declarers, versions) {
|
|
|
782
613
|
}
|
|
783
614
|
return links;
|
|
784
615
|
}
|
|
785
|
-
/**
|
|
786
|
-
function
|
|
787
|
-
const
|
|
788
|
-
const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, "node_modules");
|
|
789
|
-
if (materialize) {
|
|
790
|
-
mkdirSync(profileModulesDir, { recursive: true });
|
|
791
|
-
mkdirSync(ownedModulesDir, { recursive: true });
|
|
792
|
-
}
|
|
793
|
-
const bundleLinks = dependencyClosure(profile.layers.filter((layer) => !installationPackageNames.has(layer.packageName)).map((layer) => join(layer.packageDir, "package.json")), installationPackageNames, (candidate, packageName) => {
|
|
794
|
-
const profileLink = join(profileModulesDir, packageName);
|
|
795
|
-
if (canonicalLinkPath(candidate) !== canonicalLinkPath(profileLink)) return false;
|
|
796
|
-
try {
|
|
797
|
-
return lstatSync(profileLink).isSymbolicLink() && symlinkPointsTo(profileLink, join(ownedModulesDir, packageName));
|
|
798
|
-
} catch (error) {
|
|
799
|
-
/* v8 ignore next 2 -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
800
|
-
if (error.code === "ENOENT") return true;
|
|
801
|
-
/* v8 ignore next -- see the host-filesystem exception above */
|
|
802
|
-
throw error;
|
|
803
|
-
}
|
|
804
|
-
}, 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);
|
|
805
619
|
for (const layer of profile.layers) bundleLinks.delete(layer.packageName);
|
|
806
|
-
|
|
807
|
-
if (!materialize) return links;
|
|
808
|
-
for (const packageName of ownedPackageNames(ownedModulesDir)) if (!links.has(packageName)) removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName);
|
|
809
|
-
for (const [packageName, target] of links) {
|
|
810
|
-
const ownedLink = join(ownedModulesDir, packageName);
|
|
811
|
-
mkdirSync(dirname(ownedLink), { recursive: true });
|
|
812
|
-
ensureSymlink(ownedLink, target);
|
|
813
|
-
const profileLink = join(profileModulesDir, packageName);
|
|
814
|
-
mkdirSync(dirname(profileLink), { recursive: true });
|
|
815
|
-
ensureProfileSymlink(profileLink, ownedLink);
|
|
816
|
-
}
|
|
817
|
-
return links;
|
|
620
|
+
return bundleLinks;
|
|
818
621
|
}
|
|
819
622
|
/**
|
|
820
623
|
* Read a profile's manifest.
|
|
@@ -877,11 +680,11 @@ function normalizeShippedProfile(name, dir, manifest) {
|
|
|
877
680
|
* matches what the Loader would import from the same anchor, and
|
|
878
681
|
* `existsSync` follows the symlinks pnpm's isolated layout uses.
|
|
879
682
|
*/
|
|
880
|
-
function packageDirFromAnchor(anchor, packageName
|
|
683
|
+
function packageDirFromAnchor(anchor, packageName) {
|
|
881
684
|
/* v8 ignore next */
|
|
882
685
|
for (const searchPath of createRequire(anchor).resolve.paths(packageName) ?? []) {
|
|
883
686
|
const candidate = join(searchPath, packageName);
|
|
884
|
-
if (existsSync(join(candidate, "package.json"))
|
|
687
|
+
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
885
688
|
}
|
|
886
689
|
}
|
|
887
690
|
/**
|
|
@@ -907,25 +710,31 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
|
|
|
907
710
|
* Load an already initialized profile directory without resolving it through
|
|
908
711
|
* the shared Harness home. This is used by application-owned profiles whose
|
|
909
712
|
* package project and lifecycle belong to that application.
|
|
713
|
+
* Unreadable bundles are reported on stderr and skipped without changing the manifest.
|
|
910
714
|
* @param binName - the diagnostic prefix on thrown errors.
|
|
911
715
|
* @param dir - absolute profile package directory.
|
|
912
716
|
* @param installAnchor - absolute path of the owning dsh app's package.json.
|
|
913
717
|
* @param options - `userLayer: false` skips reading `cordis.patch.yml`.
|
|
914
|
-
* @returns the
|
|
718
|
+
* @returns the successfully loaded bundle layers and optional user patch layer.
|
|
915
719
|
*/
|
|
916
720
|
function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
917
|
-
const
|
|
721
|
+
const bundles = readProfileManifest(binName, dir).dsh?.profile?.bundles ?? [];
|
|
722
|
+
const layers = [];
|
|
723
|
+
for (const packageName of bundles) try {
|
|
918
724
|
const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir);
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
921
|
-
const
|
|
922
|
-
|
|
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({
|
|
923
730
|
packageName,
|
|
924
731
|
packageDir,
|
|
925
|
-
|
|
926
|
-
patches
|
|
927
|
-
};
|
|
928
|
-
})
|
|
732
|
+
patchPaths,
|
|
733
|
+
patches
|
|
734
|
+
});
|
|
735
|
+
} catch (error) {
|
|
736
|
+
process.stderr.write(`${binName}: skipping profile bundle ${JSON.stringify(packageName)}: ${String(error)}\n`);
|
|
737
|
+
}
|
|
929
738
|
const patchPath = join(dir, PROFILE_PATCH_FILENAME);
|
|
930
739
|
const patches = options.userLayer !== false && existsSync(patchPath) ? loadOverlayPatches(binName, patchPath) : [];
|
|
931
740
|
return {
|
|
@@ -938,9 +747,8 @@ function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
|
938
747
|
}
|
|
939
748
|
/**
|
|
940
749
|
* Load a profile: resolve every `dsh.profile.bundles` entry to its patch
|
|
941
|
-
* layer and parse the profile's own patch file.
|
|
942
|
-
*
|
|
943
|
-
* 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.
|
|
944
752
|
* @param binName - the diagnostic prefix on thrown errors.
|
|
945
753
|
* @param name - the profile name.
|
|
946
754
|
* @param installAnchor - absolute path of the dsh app's package.json (first resolution anchor).
|
|
@@ -957,6 +765,7 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
957
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>'`);
|
|
958
766
|
initProfile(dir, template.bundles);
|
|
959
767
|
}
|
|
768
|
+
removeLinkProjections(dir);
|
|
960
769
|
normalizeShippedProfile(name, dir, readProfileManifest(binName, dir));
|
|
961
770
|
return loadProfileDirectory(binName, dir, installAnchor, options);
|
|
962
771
|
}
|
|
@@ -1164,27 +973,68 @@ function prefixes(path) {
|
|
|
1164
973
|
const canonical = canonicalPath(path) + sep;
|
|
1165
974
|
return canonical === configured ? [configured] : [configured, canonical];
|
|
1166
975
|
}
|
|
1167
|
-
function
|
|
1168
|
-
const profilePaths = prefixes(generation.profilesDir);
|
|
1169
|
-
const profile = generation.profileDir === void 0 ? [] : prefixes(generation.profileDir);
|
|
976
|
+
function compileResolution(resolution) {
|
|
1170
977
|
return {
|
|
1171
|
-
entries: new Map(
|
|
1172
|
-
profilesDir:
|
|
1173
|
-
profileDir:
|
|
1174
|
-
profilePaths,
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
localPackageNames: new Set(
|
|
1179
|
-
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),
|
|
1180
986
|
esmRoutes: /* @__PURE__ */ new Map(),
|
|
1181
987
|
cjsRoutes: /* @__PURE__ */ new Map()
|
|
1182
988
|
};
|
|
1183
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
|
+
}
|
|
1184
1023
|
function startsWithin(path, roots) {
|
|
1185
1024
|
for (const root of roots) if (path.startsWith(root)) return true;
|
|
1186
1025
|
return false;
|
|
1187
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
|
+
}
|
|
1188
1038
|
function nativePackageDir(parent, name) {
|
|
1189
1039
|
for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
1190
1040
|
const candidate = join(searchPath, name);
|
|
@@ -1198,10 +1048,7 @@ function localPackageCandidate(searchPath, name, flavor) {
|
|
|
1198
1048
|
".js",
|
|
1199
1049
|
".json",
|
|
1200
1050
|
".node"
|
|
1201
|
-
].some((extension) => existsSync(candidate + extension))) ?
|
|
1202
|
-
packageDir: candidate,
|
|
1203
|
-
canBeManagedLink: stat !== void 0
|
|
1204
|
-
} : void 0;
|
|
1051
|
+
].some((extension) => existsSync(candidate + extension))) ? candidate : void 0;
|
|
1205
1052
|
}
|
|
1206
1053
|
function selfReferenceName(parent) {
|
|
1207
1054
|
let current = dirname(parent);
|
|
@@ -1258,7 +1105,7 @@ function packageImportsTarget(parent, request, conditions) {
|
|
|
1258
1105
|
}
|
|
1259
1106
|
function packageSearchPaths(entry, request, cjs) {
|
|
1260
1107
|
const name = barePackageName(request);
|
|
1261
|
-
/* v8 ignore next --
|
|
1108
|
+
/* v8 ignore next -- interception routes are created only for bare package requests */
|
|
1262
1109
|
if (name === void 0) return cjs._nodeModulePaths(dirname(entry.declarer));
|
|
1263
1110
|
const suffix = sep + name.split("/").join(sep);
|
|
1264
1111
|
return entry.packageDir.endsWith(suffix) ? [entry.packageDir.slice(0, -suffix.length)] : cjs._nodeModulePaths(dirname(entry.declarer));
|
|
@@ -1289,62 +1136,100 @@ function isUnselectedPackageMiss(error) {
|
|
|
1289
1136
|
}
|
|
1290
1137
|
function sameResolution(left, right) {
|
|
1291
1138
|
if (left === right) return true;
|
|
1292
|
-
|
|
1293
|
-
const rightPath = right.startsWith("file:") ? fileURLToPath(right) : right;
|
|
1294
|
-
return canonicalPath(leftPath) === canonicalPath(rightPath);
|
|
1139
|
+
return canonicalPath(left) === canonicalPath(right);
|
|
1295
1140
|
}
|
|
1296
|
-
/** One mutable pointer to immutable
|
|
1141
|
+
/** One mutable pointer to the immutable runtime resolution. */
|
|
1297
1142
|
var ResolutionRouter = class {
|
|
1143
|
+
nodeModulePaths;
|
|
1298
1144
|
current;
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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");
|
|
1305
1154
|
for (const [name, current] of this.current.entries) {
|
|
1306
1155
|
const next = entries.get(name);
|
|
1307
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`);
|
|
1308
1157
|
}
|
|
1309
|
-
const localPackageNames = new Set(
|
|
1158
|
+
const localPackageNames = new Set(successor.localPackageNames);
|
|
1310
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`);
|
|
1311
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`);
|
|
1312
|
-
|
|
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;
|
|
1313
1168
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
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;
|
|
1198
|
+
}
|
|
1199
|
+
routeScoped(request, parentRoutes, resolution, flavor, cjs) {
|
|
1200
|
+
const { parent, layer, requests } = parentRoutes;
|
|
1201
|
+
const memo = layer.kind === "profile" && (cjs === void 0 || cjs.cacheable);
|
|
1316
1202
|
const name = barePackageName(request);
|
|
1317
1203
|
if (name === void 0) return void 0;
|
|
1318
|
-
if (parentRoutes.selfReferenceName === void 0) parentRoutes.selfReferenceName = selfReferenceName(parent);
|
|
1204
|
+
if (parentRoutes.selfReferenceName === void 0 || !memo) parentRoutes.selfReferenceName = selfReferenceName(parent);
|
|
1319
1205
|
if (parentRoutes.selfReferenceName === name || parentRoutes.selfReferenceName === null) {
|
|
1320
1206
|
const state = { route: { kind: "native" } };
|
|
1321
|
-
requests.set(request, state);
|
|
1207
|
+
if (memo) requests.set(request, state);
|
|
1322
1208
|
return state;
|
|
1323
1209
|
}
|
|
1324
|
-
|
|
1210
|
+
if (layer.kind === "linked") return this.routeLinked(request, name, parent, resolution, flavor, cjs);
|
|
1211
|
+
const target = resolution.entries.get(name);
|
|
1325
1212
|
const candidates = [];
|
|
1326
1213
|
const localSearchPaths = [];
|
|
1327
1214
|
for (const searchPath of createRequire(parent).resolve.paths(name)) {
|
|
1328
|
-
if (
|
|
1215
|
+
if (!searchPath.startsWith(layer.localPrefix)) break;
|
|
1329
1216
|
localSearchPaths.push(searchPath);
|
|
1330
1217
|
const candidate = localPackageCandidate(searchPath, name, flavor);
|
|
1331
|
-
if (candidate !== void 0)
|
|
1332
|
-
if (!(candidate.canBeManagedLink && generation.profile.some((prefix) => candidate.packageDir === join(prefix, "node_modules", name) && isProfileModuleFallbackLink(prefix.slice(0, -1), name)))) candidates.push(candidate);
|
|
1333
|
-
}
|
|
1218
|
+
if (candidate !== void 0) candidates.push(candidate);
|
|
1334
1219
|
}
|
|
1335
|
-
if (candidates.length > 0) if (
|
|
1336
|
-
const resolved =
|
|
1337
|
-
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));
|
|
1338
1223
|
if (selected !== void 0) {
|
|
1339
1224
|
const state = {
|
|
1340
1225
|
route: {
|
|
1341
1226
|
kind: "native",
|
|
1342
|
-
packageDir: selected
|
|
1227
|
+
packageDir: selected
|
|
1343
1228
|
},
|
|
1344
|
-
packageDir: selected
|
|
1345
|
-
|
|
1229
|
+
packageDir: selected,
|
|
1230
|
+
cjs: resolved
|
|
1346
1231
|
};
|
|
1347
|
-
requests.set(request, state);
|
|
1232
|
+
if (memo) requests.set(request, state);
|
|
1348
1233
|
return state;
|
|
1349
1234
|
}
|
|
1350
1235
|
} catch (error) {
|
|
@@ -1355,122 +1240,120 @@ var ResolutionRouter = class {
|
|
|
1355
1240
|
const state = {
|
|
1356
1241
|
route: {
|
|
1357
1242
|
kind: "native",
|
|
1358
|
-
packageDir: selected
|
|
1243
|
+
packageDir: selected
|
|
1359
1244
|
},
|
|
1360
|
-
packageDir: selected
|
|
1245
|
+
packageDir: selected
|
|
1361
1246
|
};
|
|
1362
1247
|
requests.set(request, state);
|
|
1363
1248
|
return state;
|
|
1364
1249
|
}
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
const route = eligible ? {
|
|
1368
|
-
kind: "fallback",
|
|
1250
|
+
const route = target !== void 0 && (target.scope === "installation" || layer.active) ? {
|
|
1251
|
+
kind: "interception",
|
|
1369
1252
|
entry: target,
|
|
1370
|
-
after
|
|
1253
|
+
after: layer.after
|
|
1371
1254
|
} : {
|
|
1372
|
-
kind: "after-
|
|
1373
|
-
parent:
|
|
1255
|
+
kind: "native-after-interception",
|
|
1256
|
+
parent: layer.nativeAfter
|
|
1374
1257
|
};
|
|
1375
1258
|
const state = { route };
|
|
1376
|
-
if (route.kind === "
|
|
1259
|
+
if (route.kind === "interception" && memo) requests.set(request, state);
|
|
1377
1260
|
return state;
|
|
1378
1261
|
}
|
|
1379
|
-
|
|
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) {
|
|
1380
1318
|
const name = barePackageName(request);
|
|
1381
|
-
|
|
1319
|
+
const { layer } = parentRoutes;
|
|
1320
|
+
if (name === void 0 || layer.kind !== "profile" || !layer.active || !resolution.localPackageNames.has(name)) return void 0;
|
|
1382
1321
|
const state = { route: { kind: "native" } };
|
|
1383
1322
|
parentRoutes.requests.set(request, state);
|
|
1384
1323
|
return state;
|
|
1385
1324
|
}
|
|
1386
1325
|
routeUrl(request, parentURL) {
|
|
1387
|
-
const
|
|
1388
|
-
|
|
1326
|
+
const resolution = this.current;
|
|
1327
|
+
const parentRoutes = this.getOrCreateParentRoutesForUrl(parentURL);
|
|
1389
1328
|
if (parentRoutes === false) return void 0;
|
|
1390
|
-
const cached = parentRoutes
|
|
1329
|
+
const cached = parentRoutes.requests.get(request);
|
|
1391
1330
|
if (cached !== void 0) return cached;
|
|
1392
|
-
|
|
1393
|
-
const profileIndex = generation.profileUrls.findIndex((prefix) => parentURL.startsWith(prefix));
|
|
1394
|
-
const activeProfileIndex = generation.activeProfileUrls.findIndex((prefix) => parentURL.startsWith(prefix));
|
|
1395
|
-
if (profileIndex < 0 && activeProfileIndex < 0) {
|
|
1396
|
-
generation.esmRoutes.set(parentURL, false);
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
let parent;
|
|
1400
|
-
try {
|
|
1401
|
-
parent = fileURLToPath(parentURL);
|
|
1402
|
-
} catch {
|
|
1403
|
-
generation.esmRoutes.set(parentURL, false);
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
const profileRoot = profileIndex >= 0 ? generation.profilePaths[profileIndex] : generation.profile[activeProfileIndex];
|
|
1407
|
-
/* v8 ignore next -- one matching index was established above */
|
|
1408
|
-
if (profileRoot === void 0) return void 0;
|
|
1409
|
-
parentRoutes = {
|
|
1410
|
-
parent,
|
|
1411
|
-
profilesDir: profileRoot.slice(0, -1),
|
|
1412
|
-
activeProfile: activeProfileIndex >= 0,
|
|
1413
|
-
requests: /* @__PURE__ */ new Map()
|
|
1414
|
-
};
|
|
1415
|
-
generation.esmRoutes.set(parentURL, parentRoutes);
|
|
1416
|
-
}
|
|
1417
|
-
const local = this.routeLocalPackage(request, parentRoutes, generation);
|
|
1331
|
+
const local = this.routeLocalPackage(request, parentRoutes, resolution);
|
|
1418
1332
|
if (local !== void 0) return local;
|
|
1419
|
-
return this.routeScoped(request, parentRoutes,
|
|
1333
|
+
return this.routeScoped(request, parentRoutes, resolution, "esm");
|
|
1420
1334
|
}
|
|
1421
|
-
routePath(request, parent,
|
|
1422
|
-
const
|
|
1423
|
-
let parentRoutes =
|
|
1335
|
+
routePath(request, parent, cjs) {
|
|
1336
|
+
const resolution = this.current;
|
|
1337
|
+
let parentRoutes = resolution.cjsRoutes.get(parent);
|
|
1424
1338
|
if (parentRoutes === false) return void 0;
|
|
1425
|
-
const cached = parentRoutes?.requests.get(request);
|
|
1339
|
+
const cached = cjs.cacheable ? parentRoutes?.requests.get(request) : void 0;
|
|
1426
1340
|
if (cached !== void 0) return cached;
|
|
1427
1341
|
if (parentRoutes === void 0) {
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
if (
|
|
1431
|
-
const profileRoot = profilesDir ?? activeProfile;
|
|
1432
|
-
/* v8 ignore next -- one matching root was established above */
|
|
1433
|
-
if (profileRoot === void 0) return void 0;
|
|
1434
|
-
parentRoutes = {
|
|
1435
|
-
parent,
|
|
1436
|
-
profilesDir: profileRoot.slice(0, -1),
|
|
1437
|
-
activeProfile: activeProfile !== void 0,
|
|
1438
|
-
requests: /* @__PURE__ */ new Map()
|
|
1439
|
-
};
|
|
1440
|
-
generation.cjsRoutes.set(parent, parentRoutes);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
if (parentRoutes === void 0) {
|
|
1444
|
-
generation.cjsRoutes.set(parent, false);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
return this.routeScoped(request, parentRoutes, generation, "cjs", nativeResolve, cacheable);
|
|
1448
|
-
}
|
|
1449
|
-
explicitRoute(request, paths) {
|
|
1450
|
-
if (barePackageName(request) === void 0) return void 0;
|
|
1451
|
-
for (const [index, path] of paths.entries()) {
|
|
1452
|
-
const parent = join(resolve(path), ".dsh-profile-resolution.cjs");
|
|
1453
|
-
if (startsWithin(parent, this.current.profilePaths) || startsWithin(parent, this.current.profile)) return {
|
|
1454
|
-
index,
|
|
1455
|
-
parent
|
|
1456
|
-
};
|
|
1342
|
+
parentRoutes = this.createParentRoutes(parent, resolution) ?? false;
|
|
1343
|
+
resolution.cjsRoutes.set(parent, parentRoutes);
|
|
1344
|
+
if (parentRoutes === false) return void 0;
|
|
1457
1345
|
}
|
|
1458
|
-
|
|
1459
|
-
nativeSelfReference(request, parent) {
|
|
1460
|
-
const name = barePackageName(request);
|
|
1461
|
-
if (name === void 0) return false;
|
|
1462
|
-
const self = selfReferenceName(parent);
|
|
1463
|
-
return self === name || self === null;
|
|
1346
|
+
return this.routeScoped(request, parentRoutes, resolution, "cjs", cjs);
|
|
1464
1347
|
}
|
|
1465
1348
|
packageDir(specifier, parentURL) {
|
|
1466
1349
|
const name = barePackageName(specifier);
|
|
1467
1350
|
if (name === void 0) return void 0;
|
|
1468
|
-
const state = this.routeUrl(
|
|
1469
|
-
if (state?.route.kind === "
|
|
1351
|
+
const state = this.routeUrl(name, parentURL);
|
|
1352
|
+
if (state?.route.kind === "interception") return state.route.entry.packageDir;
|
|
1470
1353
|
if (state?.packageDir !== void 0) return state.packageDir;
|
|
1471
1354
|
let parent;
|
|
1472
1355
|
try {
|
|
1473
|
-
parent = state?.route.kind === "after-
|
|
1356
|
+
parent = state?.route.kind === "native-after-interception" ? state.route.parent : fileURLToPath(parentURL);
|
|
1474
1357
|
} catch {
|
|
1475
1358
|
return;
|
|
1476
1359
|
}
|
|
@@ -1531,46 +1414,21 @@ function throwWithoutCjsAnchor(error, anchor) {
|
|
|
1531
1414
|
}
|
|
1532
1415
|
throw error;
|
|
1533
1416
|
}
|
|
1534
|
-
function assertEquivalent(actual, expected, request, parent) {
|
|
1535
|
-
if (sameResolution(actual, expected)) return;
|
|
1536
|
-
throw new Error(`profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk resolved ${actual}, generation resolved ${expected}`);
|
|
1537
|
-
}
|
|
1538
|
-
function assertOptionalEquivalent(actual, expected, request, parent) {
|
|
1539
|
-
if (actual === void 0 && expected === void 0) return;
|
|
1540
|
-
if (actual !== void 0 && expected !== void 0 && sameResolution(actual, expected)) return;
|
|
1541
|
-
throw new Error(`profile resolution mismatch for ${JSON.stringify(request)} from ${parent}: disk selected ${actual ?? "nothing"}, generation selected ${expected ?? "nothing"}`);
|
|
1542
|
-
}
|
|
1543
1417
|
/**
|
|
1544
|
-
* Install one
|
|
1545
|
-
* @param
|
|
1546
|
-
* @
|
|
1547
|
-
* @returns a registration that replaces the generation or restores the native methods.
|
|
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.
|
|
1548
1421
|
*/
|
|
1549
|
-
function
|
|
1550
|
-
const router = new ResolutionRouter(generation);
|
|
1422
|
+
function installRuntimeInterception(resolution) {
|
|
1551
1423
|
const { esm, esmDefaultResolve, esmConditions, cjs, cjsConditions, modern } = internalModules();
|
|
1552
|
-
const
|
|
1553
|
-
const profilePaths = [...prefixes(generation.profilesDir), ...generation.profileDir === void 0 ? [] : prefixes(generation.profileDir)];
|
|
1554
|
-
const profileUrls = profilePaths.map((path) => pathToFileURL(path).href);
|
|
1555
|
-
let recentEsmParent;
|
|
1556
|
-
let recentEsmScoped = false;
|
|
1424
|
+
const router = new ResolutionRouter(resolution, (directory) => cjs._nodeModulePaths(directory));
|
|
1557
1425
|
let delegatedEsm;
|
|
1558
1426
|
const adaptEsm = (native) => {
|
|
1559
1427
|
const adapted = (request, parent, attributes) => {
|
|
1560
1428
|
const delegated = delegatedEsm;
|
|
1561
1429
|
/* v8 ignore next -- reentry requires a separate synchronous Node hook; supported launches install none */
|
|
1562
1430
|
if (delegated !== void 0 && delegated.parent === parent && delegated.request === request) return native(request, parent, attributes);
|
|
1563
|
-
if (parent === void 0) return native(request, parent, attributes);
|
|
1564
|
-
let scoped = recentEsmParent === parent ? recentEsmScoped : esmScope.get(parent);
|
|
1565
|
-
if (scoped === void 0) {
|
|
1566
|
-
scoped = startsWithin(parent, profileUrls);
|
|
1567
|
-
esmScope.set(parent, scoped);
|
|
1568
|
-
}
|
|
1569
|
-
if (recentEsmParent !== parent) {
|
|
1570
|
-
recentEsmParent = parent;
|
|
1571
|
-
recentEsmScoped = scoped;
|
|
1572
|
-
}
|
|
1573
|
-
if (!scoped) return native(request, parent, attributes);
|
|
1431
|
+
if (parent === void 0 || !router.hasInterceptionLayerForUrl(parent)) return native(request, parent, attributes);
|
|
1574
1432
|
const state = router.routeUrl(request, parent);
|
|
1575
1433
|
if (state === void 0) {
|
|
1576
1434
|
const target = request[0] === "#" ? packageImportsTarget(fileURLToPath(parent), request, esmConditions) : void 0;
|
|
@@ -1584,16 +1442,7 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1584
1442
|
} catch (error) {
|
|
1585
1443
|
return restoreImporter(error);
|
|
1586
1444
|
}
|
|
1587
|
-
|
|
1588
|
-
const actual = native(request, parent, attributes);
|
|
1589
|
-
/* v8 ignore start -- Node 22 is the asynchronous adapter and is covered by the external version matrix */
|
|
1590
|
-
if (expected instanceof Promise || actual instanceof Promise) return Promise.all([actual, expected]).then(([resolved, wanted]) => {
|
|
1591
|
-
assertEquivalent(resolved.url, wanted.url, request, parent);
|
|
1592
|
-
return resolved;
|
|
1593
|
-
});
|
|
1594
|
-
/* v8 ignore stop */
|
|
1595
|
-
assertEquivalent(actual.url, expected.url, request, parent);
|
|
1596
|
-
return actual;
|
|
1445
|
+
return expected;
|
|
1597
1446
|
}
|
|
1598
1447
|
const cacheable = attributes === EMPTY_ATTRIBUTES || Object.keys(attributes).length === 0;
|
|
1599
1448
|
if (cacheable && state.esm !== void 0) return state.esm;
|
|
@@ -1603,30 +1452,7 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1603
1452
|
if (cacheable && !(result instanceof Promise)) state.esm = result;
|
|
1604
1453
|
return result;
|
|
1605
1454
|
}
|
|
1606
|
-
const routedParent = pathToFileURL(route.kind === "
|
|
1607
|
-
if (behavior === "enforce") {
|
|
1608
|
-
const previous = delegatedEsm;
|
|
1609
|
-
delegatedEsm = {
|
|
1610
|
-
parent: routedParent,
|
|
1611
|
-
request
|
|
1612
|
-
};
|
|
1613
|
-
const restoreImporter = (error) => throwWithImporter(error, routedParent, parent);
|
|
1614
|
-
try {
|
|
1615
|
-
let result;
|
|
1616
|
-
try {
|
|
1617
|
-
result = native(request, routedParent, attributes);
|
|
1618
|
-
} catch (error) {
|
|
1619
|
-
return restoreImporter(error);
|
|
1620
|
-
}
|
|
1621
|
-
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1622
|
-
if (result instanceof Promise) return result.catch(restoreImporter);
|
|
1623
|
-
if (cacheable) state.esm = result;
|
|
1624
|
-
return result;
|
|
1625
|
-
} finally {
|
|
1626
|
-
delegatedEsm = previous;
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
const actual = native(request, parent, attributes);
|
|
1455
|
+
const routedParent = pathToFileURL(route.kind === "interception" ? route.entry.declarer : route.parent).href;
|
|
1630
1456
|
const previous = delegatedEsm;
|
|
1631
1457
|
delegatedEsm = {
|
|
1632
1458
|
parent: routedParent,
|
|
@@ -1634,25 +1460,16 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1634
1460
|
};
|
|
1635
1461
|
const restoreImporter = (error) => throwWithImporter(error, routedParent, parent);
|
|
1636
1462
|
try {
|
|
1637
|
-
let
|
|
1463
|
+
let result;
|
|
1638
1464
|
try {
|
|
1639
|
-
|
|
1640
|
-
expected = result;
|
|
1641
|
-
/* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
|
|
1642
|
-
if (result instanceof Promise) expected = result.catch(restoreImporter);
|
|
1465
|
+
result = native(request, routedParent, attributes);
|
|
1643
1466
|
} catch (error) {
|
|
1644
1467
|
return restoreImporter(error);
|
|
1645
1468
|
}
|
|
1646
|
-
/* v8 ignore
|
|
1647
|
-
if (
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
return resolved;
|
|
1651
|
-
});
|
|
1652
|
-
/* v8 ignore stop */
|
|
1653
|
-
assertEquivalent(actual.url, expected.url, request, parent);
|
|
1654
|
-
if (cacheable) state.esm = actual;
|
|
1655
|
-
return actual;
|
|
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;
|
|
1656
1473
|
} finally {
|
|
1657
1474
|
delegatedEsm = previous;
|
|
1658
1475
|
}
|
|
@@ -1675,7 +1492,7 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1675
1492
|
]) : resolveRequest(request.specifier, parent, request.attributes ?? EMPTY_ATTRIBUTES);
|
|
1676
1493
|
loader.resolveSync = wrapped;
|
|
1677
1494
|
restoreEsm = () => {
|
|
1678
|
-
/* v8 ignore else --
|
|
1495
|
+
/* v8 ignore else -- interceptions are disposed in reverse installation order */
|
|
1679
1496
|
if (loader.resolveSync === wrapped) loader.resolveSync = original;
|
|
1680
1497
|
};
|
|
1681
1498
|
} else {
|
|
@@ -1696,11 +1513,11 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1696
1513
|
const originalFilename = Reflect.get(cjs, "_resolveFilename");
|
|
1697
1514
|
let delegatedCjs = 0;
|
|
1698
1515
|
const resolveRoutedCjs = (request, routed, parent, main, options) => {
|
|
1699
|
-
const anchor = routed.kind === "
|
|
1516
|
+
const anchor = routed.kind === "interception" ? routed.entry.declarer : routed.parent;
|
|
1700
1517
|
const synthetic = new cjs(anchor);
|
|
1701
1518
|
synthetic.parent = parent;
|
|
1702
1519
|
synthetic.filename = anchor;
|
|
1703
|
-
synthetic.paths = routed.kind === "
|
|
1520
|
+
synthetic.paths = routed.kind === "interception" ? packageSearchPaths(routed.entry, request, cjs) : cjs._nodeModulePaths(dirname(anchor));
|
|
1704
1521
|
try {
|
|
1705
1522
|
return originalFilename.call(cjs, request, synthetic, main, options);
|
|
1706
1523
|
} catch (error) {
|
|
@@ -1709,7 +1526,7 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1709
1526
|
};
|
|
1710
1527
|
const resolveNativeCjs = (request, searchPaths, parent, parentFilename, main, conditions) => {
|
|
1711
1528
|
const synthetic = new cjs(parentFilename);
|
|
1712
|
-
synthetic.parent = parent;
|
|
1529
|
+
if (parent.parent !== void 0) synthetic.parent = parent.parent;
|
|
1713
1530
|
synthetic.filename = parentFilename;
|
|
1714
1531
|
synthetic.paths = [...searchPaths];
|
|
1715
1532
|
const options = conditions === void 0 ? void 0 : { conditions };
|
|
@@ -1725,93 +1542,58 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1725
1542
|
if (state === void 0) return resolveFrom(target.parentURL);
|
|
1726
1543
|
if (state.route.kind === "native") return resolveFrom(target.parentURL);
|
|
1727
1544
|
const route = state.route;
|
|
1728
|
-
if (route.kind === "after-
|
|
1545
|
+
if (route.kind === "native-after-interception") return resolveFrom(pathToFileURL(route.parent).href);
|
|
1729
1546
|
return resolveFrom(pathToFileURL(route.entry.declarer).href);
|
|
1730
1547
|
};
|
|
1731
1548
|
const wrappedFilename = (request, parent, main, options) => {
|
|
1732
|
-
if (delegatedCjs || !parent?.filename) return originalFilename.call(cjs, request, parent, main, options);
|
|
1549
|
+
if (delegatedCjs || !parent?.filename || options?.paths !== void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1733
1550
|
const parentFilename = parent.filename;
|
|
1734
|
-
const cacheable = options?.
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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
|
+
});
|
|
1748
1569
|
if (state === void 0) {
|
|
1749
|
-
const scoped =
|
|
1570
|
+
const scoped = router.hasInterceptionLayerForPath(parentFilename);
|
|
1750
1571
|
const conditions = options?.conditions ?? cjsConditions;
|
|
1751
1572
|
const target = request[0] === "#" && scoped ? packageImportsTarget(parentFilename, request, conditions) : void 0;
|
|
1752
1573
|
if (target === void 0) return originalFilename.call(cjs, request, parent, main, options);
|
|
1753
|
-
|
|
1754
|
-
if (behavior === "enforce") return expected;
|
|
1755
|
-
const actual = originalFilename.call(cjs, request, parent, main, options);
|
|
1756
|
-
assertEquivalent(actual, expected, request, parentFilename);
|
|
1757
|
-
return actual;
|
|
1574
|
+
return resolvePackageImportCjs(target, conditions);
|
|
1758
1575
|
}
|
|
1759
|
-
if (
|
|
1576
|
+
if (state.cjs !== void 0) return state.cjs;
|
|
1760
1577
|
const route = state.route;
|
|
1761
1578
|
if (route.kind === "native") {
|
|
1762
1579
|
const result = originalFilename.call(cjs, request, parent, main, options);
|
|
1763
1580
|
if (cacheable) state.cjs = result;
|
|
1764
1581
|
return result;
|
|
1765
1582
|
}
|
|
1766
|
-
if (route.kind === "after-fallback" && explicit !== void 0 && explicitPaths !== void 0) try {
|
|
1767
|
-
return originalFilename.call(cjs, request, parent, main, {
|
|
1768
|
-
...options,
|
|
1769
|
-
paths: [dirname(route.parent)]
|
|
1770
|
-
});
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
if (!isUnselectedPackageMiss(error)) throw error;
|
|
1773
|
-
const remaining = explicitPaths.slice(explicit.index + 1);
|
|
1774
|
-
if (remaining.length === 0) throw error;
|
|
1775
|
-
return wrappedFilename(request, parent, main, {
|
|
1776
|
-
...options,
|
|
1777
|
-
paths: remaining
|
|
1778
|
-
});
|
|
1779
|
-
}
|
|
1780
1583
|
delegatedCjs++;
|
|
1781
1584
|
try {
|
|
1782
|
-
const routedOptions = options?.conditions === void 0 ? void 0 : { conditions: options.conditions };
|
|
1783
1585
|
let expected;
|
|
1784
1586
|
try {
|
|
1785
1587
|
expected = resolveRoutedCjs(request, route, parent, main, routedOptions);
|
|
1786
1588
|
} catch (error) {
|
|
1787
|
-
if (route.kind !== "
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
}, parent, main, routedOptions);
|
|
1793
|
-
} catch (afterError) {
|
|
1794
|
-
const remaining = explicit === void 0 || explicitPaths === void 0 ? [] : explicitPaths.slice(explicit.index + 1);
|
|
1795
|
-
if (!isUnselectedPackageMiss(afterError) || remaining.length === 0) throw afterError;
|
|
1796
|
-
delegatedCjs--;
|
|
1797
|
-
try {
|
|
1798
|
-
return wrappedFilename(request, parent, main, {
|
|
1799
|
-
...options,
|
|
1800
|
-
paths: remaining
|
|
1801
|
-
});
|
|
1802
|
-
} finally {
|
|
1803
|
-
delegatedCjs++;
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
if (behavior === "enforce") {
|
|
1808
|
-
if (cacheable) state.cjs = expected;
|
|
1809
|
-
return expected;
|
|
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);
|
|
1810
1594
|
}
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
if (cacheable) state.cjs = actual;
|
|
1814
|
-
return actual;
|
|
1595
|
+
if (cacheable) state.cjs = expected;
|
|
1596
|
+
return expected;
|
|
1815
1597
|
} finally {
|
|
1816
1598
|
delegatedCjs--;
|
|
1817
1599
|
}
|
|
@@ -1819,48 +1601,1223 @@ function installProfileResolution(generation, behavior = "enforce") {
|
|
|
1819
1601
|
cjs._resolveFilename = wrappedFilename;
|
|
1820
1602
|
return {
|
|
1821
1603
|
packageDir(specifier, parentURL) {
|
|
1822
|
-
|
|
1823
|
-
if (behavior !== "verify" || !startsWithin(parentURL, profileUrls)) return expected;
|
|
1824
|
-
const name = barePackageName(specifier);
|
|
1825
|
-
if (name === void 0) return expected;
|
|
1826
|
-
let parent;
|
|
1827
|
-
try {
|
|
1828
|
-
parent = fileURLToPath(parentURL);
|
|
1829
|
-
} catch {
|
|
1830
|
-
return expected;
|
|
1831
|
-
}
|
|
1832
|
-
assertOptionalEquivalent(nativePackageDir(parent, name), expected, specifier, parentURL);
|
|
1833
|
-
return expected;
|
|
1604
|
+
return router.packageDir(specifier, parentURL);
|
|
1834
1605
|
},
|
|
1835
1606
|
replace(next) {
|
|
1836
1607
|
router.replace(next);
|
|
1837
1608
|
},
|
|
1838
1609
|
dispose() {
|
|
1839
|
-
/* v8 ignore else --
|
|
1610
|
+
/* v8 ignore else -- interceptions are disposed in reverse installation order */
|
|
1840
1611
|
if (cjs._resolveFilename === wrappedFilename) cjs._resolveFilename = originalFilename;
|
|
1841
1612
|
restoreEsm();
|
|
1842
1613
|
}
|
|
1843
1614
|
};
|
|
1844
1615
|
}
|
|
1845
1616
|
/**
|
|
1846
|
-
* Publish one
|
|
1847
|
-
* @param
|
|
1848
|
-
* @param behavior - enforce or verify the generation in newly created Workers.
|
|
1617
|
+
* Publish one runtime resolution for Harness-owned Workers.
|
|
1618
|
+
* @param resolution - complete package table and profile scope.
|
|
1849
1619
|
* @returns a disposer restoring the previous thread environment data.
|
|
1850
1620
|
*/
|
|
1851
|
-
function registerWorkerResolution(
|
|
1621
|
+
function registerWorkerResolution(resolution) {
|
|
1852
1622
|
const previous = getEnvironmentData(WORKER_RESOLUTION_KEY);
|
|
1853
|
-
setEnvironmentData(WORKER_RESOLUTION_KEY, {
|
|
1854
|
-
generation,
|
|
1855
|
-
behavior
|
|
1856
|
-
});
|
|
1623
|
+
setEnvironmentData(WORKER_RESOLUTION_KEY, { resolution });
|
|
1857
1624
|
return () => {
|
|
1858
1625
|
setEnvironmentData(WORKER_RESOLUTION_KEY, previous);
|
|
1859
1626
|
};
|
|
1860
1627
|
}
|
|
1861
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`);
|
|
2582
|
+
}
|
|
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);
|
|
2589
|
+
}
|
|
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;
|
|
2594
|
+
}
|
|
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;
|
|
2604
|
+
}
|
|
2605
|
+
/**
|
|
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.
|
|
2615
|
+
*/
|
|
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;
|
|
2684
|
+
}
|
|
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}`);
|
|
2693
|
+
}
|
|
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}`);
|
|
2699
|
+
}
|
|
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);
|
|
2713
|
+
}
|
|
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"
|
|
2724
|
+
};
|
|
2725
|
+
result.entries.push(entry);
|
|
2726
|
+
const row = value;
|
|
2727
|
+
try {
|
|
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;
|
|
2751
|
+
}
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
entry.status = "error";
|
|
2754
|
+
report(path, error);
|
|
2755
|
+
}
|
|
2756
|
+
try {
|
|
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);
|
|
2763
|
+
} catch (error) {
|
|
2764
|
+
report(path, error);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
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);
|
|
2777
|
+
}
|
|
2778
|
+
if (row.group && Array.isArray(row.config)) indexTargets(row.config);
|
|
2779
|
+
}
|
|
2780
|
+
};
|
|
2781
|
+
indexTargets(entries);
|
|
2782
|
+
return await buildConfigSchemaDocument(profile.name, result.entries, targets, result.diagnostics);
|
|
2783
|
+
} finally {
|
|
2784
|
+
interception.dispose();
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
//#endregion
|
|
2788
|
+
//#region lib/types/config-schema/index.js
|
|
2789
|
+
/** Profile schema generation: composition diagnostics, runtime resolution, and boot-free discovery. */
|
|
2790
|
+
/**
|
|
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.
|
|
2802
|
+
*/
|
|
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.`
|
|
2809
|
+
});
|
|
2810
|
+
return collectConfigSchemas(profile, composeEntries(layers, (message) => diagnostics.push({
|
|
2811
|
+
level: "warning",
|
|
2812
|
+
message
|
|
2813
|
+
})), await createRuntimeResolution({
|
|
2814
|
+
installAnchor,
|
|
2815
|
+
profile
|
|
2816
|
+
}), diagnostics);
|
|
2817
|
+
}
|
|
2818
|
+
//#endregion
|
|
1862
2819
|
//#region lib/types/profile-resolution/service.js
|
|
1863
|
-
/** Package metadata resolved through one
|
|
2820
|
+
/** Package metadata resolved through one runtime interception. */
|
|
1864
2821
|
function readPackage(dir, fallbackName) {
|
|
1865
2822
|
const manifestPath = join(dir, "package.json");
|
|
1866
2823
|
if (!existsSync(manifestPath)) return void 0;
|
|
@@ -1878,31 +2835,30 @@ function readPackage(dir, fallbackName) {
|
|
|
1878
2835
|
/** Package lookup shared by metadata consumers in one profile process. */
|
|
1879
2836
|
var PluginPackages = class extends Service {
|
|
1880
2837
|
packages = /* @__PURE__ */ new Map();
|
|
1881
|
-
|
|
1882
|
-
behavior;
|
|
2838
|
+
interception;
|
|
1883
2839
|
disposeWorkerResolution;
|
|
1884
2840
|
constructor(ctx, config = {}) {
|
|
1885
2841
|
super(ctx, "pluginPackages");
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
this.
|
|
1890
|
-
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;
|
|
1891
2846
|
ctx.effect(() => () => {
|
|
1892
2847
|
this.disposeWorkerResolution?.();
|
|
1893
|
-
|
|
2848
|
+
interception.dispose();
|
|
1894
2849
|
}, "profile package resolution");
|
|
1895
2850
|
}
|
|
1896
2851
|
/**
|
|
1897
|
-
* Publish
|
|
1898
|
-
*
|
|
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}.
|
|
1899
2855
|
*/
|
|
1900
|
-
replace(
|
|
1901
|
-
if (this.
|
|
1902
|
-
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);
|
|
1903
2859
|
this.packages = /* @__PURE__ */ new Map();
|
|
1904
2860
|
this.disposeWorkerResolution?.();
|
|
1905
|
-
this.disposeWorkerResolution = registerWorkerResolution(
|
|
2861
|
+
this.disposeWorkerResolution = registerWorkerResolution(successor);
|
|
1906
2862
|
}
|
|
1907
2863
|
/**
|
|
1908
2864
|
* Locate the package named by a specifier without requiring a package export.
|
|
@@ -1913,7 +2869,7 @@ var PluginPackages = class extends Service {
|
|
|
1913
2869
|
packageOf(specifier, parentURL) {
|
|
1914
2870
|
const name = barePackageName(specifier);
|
|
1915
2871
|
if (name === void 0) return void 0;
|
|
1916
|
-
const dir = this.
|
|
2872
|
+
const dir = this.interception === void 0 ? packageDirFromParent(name, parentURL) : this.interception.packageDir(name, parentURL);
|
|
1917
2873
|
if (dir === void 0) return void 0;
|
|
1918
2874
|
const key = JSON.stringify({
|
|
1919
2875
|
dir,
|
|
@@ -1922,6 +2878,15 @@ var PluginPackages = class extends Service {
|
|
|
1922
2878
|
if (!this.packages.has(key)) this.packages.set(key, readPackage(dir, name));
|
|
1923
2879
|
return this.packages.get(key);
|
|
1924
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
|
+
}
|
|
1925
2890
|
};
|
|
1926
2891
|
function packageDirFromParent(name, parentURL) {
|
|
1927
2892
|
for (const searchPath of createRequire(parentURL).resolve.paths(name)) {
|
|
@@ -2152,6 +3117,7 @@ async function reconcileProfilePatches(ctx, patches, binName, requiredIds = [])
|
|
|
2152
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)));
|
|
2153
3118
|
if (introduced.length > 0) throw new Error(activationDiagnostic(binName, introduced).trimEnd());
|
|
2154
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");
|
|
2155
3121
|
return failures.map(inactiveDiagnostic);
|
|
2156
3122
|
}
|
|
2157
3123
|
/**
|
|
@@ -2397,11 +3363,22 @@ async function observeLoaderRejectionCheckpoint(reasons) {
|
|
|
2397
3363
|
*/
|
|
2398
3364
|
const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
|
|
2399
3365
|
/**
|
|
2400
|
-
* Install before boot to turn
|
|
2401
|
-
*
|
|
2402
|
-
* {@link auditStartupEntries} is
|
|
2403
|
-
*
|
|
2404
|
-
*
|
|
3366
|
+
* Install before boot to turn an unhandled rejection or an uncaught exception,
|
|
3367
|
+
* at any point in the process lifetime, into one labelled stderr diagnostic and
|
|
3368
|
+
* `exit(1)`. A rejection already included by {@link auditStartupEntries} is
|
|
3369
|
+
* ignored during its process checkpoint; every other rejection and every
|
|
3370
|
+
* uncaught exception remains fatal. Control never returns to the failed
|
|
3371
|
+
* operation after either: only the throw site knows which state is intact, and
|
|
3372
|
+
* a listener that threw mid-update (a stream `'data'` handler, a half-applied
|
|
3373
|
+
* registry write) leaves silently wrong results behind if it were resumed. The
|
|
3374
|
+
* event loop keeps running only until the release hook settles or times out.
|
|
3375
|
+
* Stdout remains untouched for ACP; the returned function removes both handlers.
|
|
3376
|
+
*
|
|
3377
|
+
* The diagnostic is `util.inspect(err)`, not `err.stack`: a `node:fs` error's
|
|
3378
|
+
* `code`, `syscall`, and `path` and any `cause` chain are enumerable properties
|
|
3379
|
+
* that the stack line omits, and they are what a crash report needs. Once a
|
|
3380
|
+
* handler is installed Node prints nothing of its own, so this line is the
|
|
3381
|
+
* only record of the failure.
|
|
2405
3382
|
*
|
|
2406
3383
|
* The Loader mounts entries concurrently, so a surface that owns the terminal
|
|
2407
3384
|
* can already hold it when a sibling entry rejects. Exiting straight from the
|
|
@@ -2423,15 +3400,17 @@ const FAIL_LOUD_RELEASE_TIMEOUT_MS = 2e3;
|
|
|
2423
3400
|
* @param release - optional teardown awaited before exit, used by a
|
|
2424
3401
|
* terminal-owning surface to restore the terminal. Its own failure is
|
|
2425
3402
|
* swallowed because the pending fatal exit already owns the outcome.
|
|
2426
|
-
* @returns the uninstaller that removes
|
|
3403
|
+
* @returns the uninstaller that removes both handlers.
|
|
2427
3404
|
*/
|
|
2428
3405
|
function installFailLoud(binName, proc = process, release) {
|
|
2429
3406
|
let exiting = false;
|
|
2430
|
-
const
|
|
2431
|
-
if (assembledActivationRejections.has(err)) return;
|
|
3407
|
+
const report = (err, label) => {
|
|
2432
3408
|
if (exiting) return;
|
|
2433
3409
|
exiting = true;
|
|
2434
|
-
proc.stderr.write(`${binName}:
|
|
3410
|
+
proc.stderr.write(`${binName}: ${label}: ${inspect(err, {
|
|
3411
|
+
depth: 4,
|
|
3412
|
+
maxArrayLength: 50
|
|
3413
|
+
})}\n`);
|
|
2435
3414
|
if (release === void 0) {
|
|
2436
3415
|
proc.exit(1);
|
|
2437
3416
|
return;
|
|
@@ -2447,8 +3426,19 @@ function installFailLoud(binName, proc = process, release) {
|
|
|
2447
3426
|
proc.exit(1);
|
|
2448
3427
|
})();
|
|
2449
3428
|
};
|
|
2450
|
-
const
|
|
2451
|
-
|
|
3429
|
+
const onRejection = (err) => {
|
|
3430
|
+
if (assembledActivationRejections.has(err)) return;
|
|
3431
|
+
report(err, "fatal load failure");
|
|
3432
|
+
};
|
|
3433
|
+
const onException = (err) => {
|
|
3434
|
+
report(err, "fatal uncaught exception");
|
|
3435
|
+
};
|
|
3436
|
+
const uninstall = () => {
|
|
3437
|
+
proc.off("unhandledRejection", onRejection);
|
|
3438
|
+
proc.off("uncaughtException", onException);
|
|
3439
|
+
};
|
|
3440
|
+
proc.on("unhandledRejection", onRejection);
|
|
3441
|
+
proc.on("uncaughtException", onException);
|
|
2452
3442
|
return uninstall;
|
|
2453
3443
|
}
|
|
2454
3444
|
/**
|
|
@@ -2767,4 +3757,4 @@ function addHarnessSourceSection(ctx, sourceRoot) {
|
|
|
2767
3757
|
});
|
|
2768
3758
|
}
|
|
2769
3759
|
//#endregion
|
|
2770
|
-
export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, StartupError, addHarnessSourceSection, auditStartupEntries, boot, composeEntries,
|
|
3760
|
+
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 };
|