@elementspanel/epanel-plugin-instance 1.0.5 → 1.0.7

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.
@@ -226,9 +226,26 @@ function commandStringToArray(cmd) {
226
226
 
227
227
  Object.defineProperty(exports, "__esModule", ({ value: true }));
228
228
  exports.ChildProcessAdapter = void 0;
229
+ exports.instanceProcessHost = instanceProcessHost;
230
+ exports.spawnInstanceProcess = spawnInstanceProcess;
229
231
  exports.waitForSpawn = waitForSpawn;
230
232
  const events_1 = __webpack_require__("events");
231
233
  const mcsmanager_common_1 = __webpack_require__("../common/src/index.ts");
234
+ const INSTANCE_HOST = Symbol.for("elementspanel.instance-process-host");
235
+ function instanceProcessHost() {
236
+ return globalThis[INSTANCE_HOST];
237
+ }
238
+ function spawnInstanceProcess(instance, fallback, file, args, options, metadata = {}) {
239
+ const host = instanceProcessHost();
240
+ return host
241
+ ? host.spawn(instance.instanceUuid, file, args, options, {
242
+ startTimestamp: instance.startTimestamp,
243
+ startCount: instance.startCount,
244
+ autoRestartCount: instance.autoRestartCount,
245
+ ...metadata
246
+ })
247
+ : fallback(file, args, options);
248
+ }
232
249
  /** spawn() reports failures asynchronously, including a missing executable. */
233
250
  function waitForSpawn(process) {
234
251
  return new Promise((resolve, reject) => {
@@ -266,7 +283,9 @@ class ChildProcessAdapter extends events_1.EventEmitter {
266
283
  super();
267
284
  this.child = child;
268
285
  this.pid = child.pid;
269
- child.once("close", () => { this.closed = true; });
286
+ child.once("close", () => {
287
+ this.closed = true;
288
+ });
270
289
  // close follows the final stdout/stderr data; exit can arrive before it.
271
290
  child.on("close", this.onExit);
272
291
  child.on("error", this.onError);
@@ -292,6 +311,9 @@ class ChildProcessAdapter extends events_1.EventEmitter {
292
311
  if (this.pid)
293
312
  return (0, mcsmanager_common_1.killProcess)(this.pid, this.child, signal);
294
313
  }
314
+ snapshot(metadata) {
315
+ this.child.setMetadata?.(metadata);
316
+ }
295
317
  destroy() {
296
318
  if (this.disposed)
297
319
  return;
@@ -326,6 +348,7 @@ class ChildProcessAdapter extends events_1.EventEmitter {
326
348
  this.child.stdin?.destroy();
327
349
  this.child.stdout?.destroy();
328
350
  this.child.stderr?.destroy();
351
+ this.child.release?.();
329
352
  }
330
353
  }
331
354
  }
@@ -1030,20 +1053,19 @@ class GeneralStartCommand extends start_1.default {
1030
1053
  instance.println("INFO", (0, i18n_1.$t)("TXT_CODE_ba09da46", { name: runAsConfig.runAsName }));
1031
1054
  }
1032
1055
  // create child process
1033
- const subProcess = (0, child_process_1.spawn)(commandExeFile, commandParameters, {
1056
+ const subProcess = (0, process_adapter_1.spawnInstanceProcess)(instance, child_process_1.spawn, commandExeFile, commandParameters, {
1034
1057
  ...runAsConfig,
1035
1058
  cwd: instance.absoluteCwdPath(),
1036
1059
  stdio: "pipe",
1037
1060
  windowsHide: true,
1038
1061
  env: instance.generateEnv(),
1039
- // Do not detach the child process;
1040
- // otherwise, an abnormal exit of the parent process may cause the child process to continue running,
1041
- // leading to an abnormal instance state.
1042
1062
  detached: false
1043
1063
  });
1044
1064
  const processAdapter = new process_adapter_1.ChildProcessAdapter(subProcess);
1045
1065
  try {
1046
1066
  await (0, process_adapter_1.waitForSpawn)(subProcess);
1067
+ processAdapter.pid = subProcess.pid;
1068
+ subProcess.setMetadata?.({ pid: subProcess.pid });
1047
1069
  instance.started(processAdapter);
1048
1070
  }
1049
1071
  catch (error) {
@@ -1470,17 +1492,14 @@ class PtyStartCommand extends start_1.default {
1470
1492
  }
1471
1493
  instance.println("INFO", "> " + commandList.join(" "));
1472
1494
  // create pty child process
1473
- const subProcess = (0, child_process_1.spawn)(const_1.PTY_PATH, ptyParameter, {
1495
+ const subProcess = (0, process_adapter_1.spawnInstanceProcess)(instance, child_process_1.spawn, const_1.PTY_PATH, ptyParameter, {
1474
1496
  ...runAsConfig,
1475
1497
  cwd: path_1.default.dirname(const_1.PTY_PATH),
1476
1498
  stdio: "pipe",
1477
1499
  windowsHide: true,
1478
1500
  env: instance.generateEnv(),
1479
- // Do not detach the child process;
1480
- // otherwise, an abnormal exit of the parent process may cause the child process to continue running,
1481
- // leading to an abnormal instance state.
1482
1501
  detached: false
1483
- });
1502
+ }, { pipeName });
1484
1503
  const processAdapter = new GoPtyProcessAdapter(subProcess, subProcess.pid, pipeName);
1485
1504
  try {
1486
1505
  const [, config] = await Promise.all([
@@ -1488,6 +1507,7 @@ class PtyStartCommand extends start_1.default {
1488
1507
  this.readPtySubProcessConfig(subProcess)
1489
1508
  ]);
1490
1509
  processAdapter.pid = config.pid;
1510
+ subProcess.setMetadata?.({ pid: config.pid });
1491
1511
  if (subProcess.exitCode !== null || subProcess.signalCode !== null)
1492
1512
  throw new StartupError((0, i18n_1.$t)("TXT_CODE_pty_start.instanceStartErr"));
1493
1513
  instance.started(processAdapter);
@@ -1547,6 +1567,17 @@ class AbsStartCommand extends command_1.default {
1547
1567
  // Claim the state before the first await so two starts cannot both proceed.
1548
1568
  instance.setLock(true);
1549
1569
  instance.status(instance_1.default.STATUS_STARTING);
1570
+ const starting = this.start(instance);
1571
+ instance.starting = starting;
1572
+ try {
1573
+ return await starting;
1574
+ }
1575
+ finally {
1576
+ instance.starting = undefined;
1577
+ instance.setLock(false);
1578
+ }
1579
+ }
1580
+ async start(instance) {
1550
1581
  try {
1551
1582
  if (!fs_extra_1.default.existsSync(instance.absoluteCwdPath())) {
1552
1583
  await fs_extra_1.default.mkdirs(instance.absoluteCwdPath());
@@ -1595,9 +1626,6 @@ class AbsStartCommand extends command_1.default {
1595
1626
  instance.startTimestamp = 0;
1596
1627
  instance.failure(error);
1597
1628
  }
1598
- finally {
1599
- instance.setLock(false);
1600
- }
1601
1629
  }
1602
1630
  }
1603
1631
  exports["default"] = AbsStartCommand;
@@ -2138,6 +2166,7 @@ class Instance extends events_1.EventEmitter {
2138
2166
  };
2139
2167
  watchers = new Map();
2140
2168
  process;
2169
+ starting;
2141
2170
  outputLoopTask;
2142
2171
  outputBuffer;
2143
2172
  stopping;
@@ -2336,9 +2365,22 @@ class Instance extends events_1.EventEmitter {
2336
2365
  }
2337
2366
  // function that must be executed after the instance starts
2338
2367
  // Trigger the open event and bind the data and exit events, etc.
2339
- started(process) {
2340
- this.startTimestamp = this.config.lastDatetime = Date.now();
2341
- this.userRequestedStop = false;
2368
+ started(process, previous) {
2369
+ this.startTimestamp = this.config.lastDatetime = previous?.startTimestamp || Date.now();
2370
+ if (previous) {
2371
+ this.startCount = previous.startCount;
2372
+ this.autoRestartCount = previous.autoRestartCount;
2373
+ }
2374
+ this.userRequestedStop = previous?.userRequestedStop ?? false;
2375
+ if (this.userRequestedStop)
2376
+ this.ignoreEventTaskOnce();
2377
+ process.snapshot?.({
2378
+ pid: Number(process.pid),
2379
+ startTimestamp: this.startTimestamp,
2380
+ startCount: this.startCount,
2381
+ autoRestartCount: this.autoRestartCount,
2382
+ userRequestedStop: this.userRequestedStop
2383
+ });
2342
2384
  const outputCode = this.config.terminalOption.pty ? "utf-8" : this.config.oe;
2343
2385
  process.on("data", (text) => {
2344
2386
  this.pushOutput(iconv_lite_1.default.decode(text, outputCode));
@@ -2442,6 +2484,7 @@ class Instance extends events_1.EventEmitter {
2442
2484
  this.stopOutputLoop();
2443
2485
  this.reloading = true;
2444
2486
  this.watchers.clear();
2487
+ await this.starting?.catch(() => { });
2445
2488
  await this.lifeCycleTaskManager.clearLifeCycleTask();
2446
2489
  }
2447
2490
  async resumeAfterReload(dispatcher) {
@@ -2479,6 +2522,7 @@ class Instance extends events_1.EventEmitter {
2479
2522
  }
2480
2523
  setUserRequestedStop(value) {
2481
2524
  this.userRequestedStop = value;
2525
+ this.process?.snapshot?.({ userRequestedStop: value });
2482
2526
  }
2483
2527
  getUserRequestedStop() {
2484
2528
  return this.userRequestedStop;
@@ -2995,6 +3039,17 @@ function apply(ctx) {
2995
3039
  let preserving = false;
2996
3040
  let subsystem;
2997
3041
  let disposeSchedules;
3042
+ let initializing;
3043
+ ctx.on("process/starting", async () => {
3044
+ if (!initializing)
3045
+ throw new Error("Instance services failed to initialize.");
3046
+ await initializing;
3047
+ await subsystem?.waitForTakeover();
3048
+ });
3049
+ ctx.on("process/restarting", async () => {
3050
+ disposeSchedules?.();
3051
+ await subsystem?.prepareForRestart();
3052
+ });
2998
3053
  ctx.on("plugins/reloading", async () => {
2999
3054
  const inventory = ctx.get("plugins")?.inventory();
3000
3055
  preserving = inventory?.some((plugin) => plugin.id === "instance" && plugin.enabled) ?? true;
@@ -3009,7 +3064,11 @@ function apply(ctx) {
3009
3064
  // are available on the next turn rather than synchronously in this function.
3010
3065
  // Restoring instances before that point makes the legacy dispatcher read
3011
3066
  // missing `presets` and `instanceLifecycle` services and emit warnings.
3012
- ctx.inject(["tasks", "instanceLifecycle", "presets", "schedules"], async (instanceCtx) => {
3067
+ ctx.inject(["tasks", "instanceLifecycle", "presets", "schedules"], (instanceCtx) => {
3068
+ initializing = initialize(instanceCtx);
3069
+ return initializing;
3070
+ });
3071
+ async function initialize(instanceCtx) {
3013
3072
  // The modules are required only after the context has been set and the
3014
3073
  // instance registries are ready. Keeping these synchronous requires keeps
3015
3074
  // webpack's output self-contained and avoids a second copy of the context.
@@ -3063,7 +3122,12 @@ function apply(ctx) {
3063
3122
  await disposeInstanceEvents?.();
3064
3123
  InstanceControlSubsystem.dispose();
3065
3124
  router_1.routerApp.dispose();
3066
- if ((preserving || recovering) && !runtime.stopping && ctx.root.scope.isActive) {
3125
+ if (runtime.restarting) {
3126
+ // The launcher owns general/PTY processes; Docker is reattached at
3127
+ // startup. Disposing adapters here would terminate those live owners.
3128
+ await InstanceSubsystem.prepareForRestart();
3129
+ }
3130
+ else if ((preserving || recovering) && !runtime.stopping && ctx.root.scope.isActive) {
3067
3131
  await InstanceSubsystem.suspendForReload();
3068
3132
  retain();
3069
3133
  }
@@ -3161,7 +3225,7 @@ function apply(ctx) {
3161
3225
  // Also allow recovery by enabling/retrying only the instance plugin.
3162
3226
  instanceCtx.on("ready", resume);
3163
3227
  }
3164
- });
3228
+ }
3165
3229
  }
3166
3230
 
3167
3231
 
@@ -5551,14 +5615,19 @@ function setPluginContext(value) {
5551
5615
  context: value,
5552
5616
  settings: value.settings,
5553
5617
  storage: value.storage,
5554
- stopping: false
5618
+ stopping: false,
5619
+ restarting: false
5555
5620
  };
5556
5621
  host[INSTANCE_RUNTIME] = runtime = state;
5622
+ host.on("process/restarting", () => {
5623
+ state.restarting = true;
5624
+ });
5557
5625
  host.on("dispose", async () => {
5558
5626
  state.stopping = true;
5559
5627
  // A replacement may fail before it can claim the retained processes.
5560
5628
  // The daemon still owns them and must stop them during real shutdown.
5561
- await state.handover?.shutdown();
5629
+ if (!state.restarting)
5630
+ await state.handover?.shutdown();
5562
5631
  state.handover = undefined;
5563
5632
  });
5564
5633
  }
@@ -7356,6 +7425,7 @@ const sleep_1 = __webpack_require__("../../plugins/instance/daemon/src/backend/u
7356
7425
  const log_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/log.ts"));
7357
7426
  const system_instance_control_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/system_instance_control.ts"));
7358
7427
  const takeover_container_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/takeover_container.ts"));
7428
+ const takeover_processes_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/takeover_processes.ts"));
7359
7429
  class InstanceSubsystem extends events_1.default {
7360
7430
  GLOBAL_INSTANCE = "__MCSM_GLOBAL_INSTANCE__";
7361
7431
  GLOBAL_INSTANCE_UUID = "global0001";
@@ -7464,14 +7534,26 @@ class InstanceSubsystem extends events_1.default {
7464
7534
  processType: globalConfig.processType
7465
7535
  }, true, // allow persistence
7466
7536
  this.GLOBAL_INSTANCE_UUID);
7467
- this.takeover = (0, takeover_container_1.default)(() => !this.startupCancelled).catch((error) => {
7468
- const reason = error.message || error;
7469
- if (typeof reason == "string" && reason.includes("connect ENOENT")) {
7470
- return;
7537
+ this.takeover = (async () => {
7538
+ await (0, takeover_processes_1.default)(() => !this.startupCancelled);
7539
+ try {
7540
+ await (0, takeover_container_1.default)(() => !this.startupCancelled);
7471
7541
  }
7472
- log_1.default.error(`${(0, i18n_1.$t)("TXT_CODE_8d4c8f7e")}: ${reason}`);
7473
- });
7474
- void this.takeover.then(() => this.autoStart());
7542
+ catch (error) {
7543
+ const reason = error.message || error;
7544
+ if (typeof reason == "string" && reason.includes("connect ENOENT"))
7545
+ return;
7546
+ log_1.default.error(`${(0, i18n_1.$t)("TXT_CODE_8d4c8f7e")}: ${reason}`);
7547
+ }
7548
+ })();
7549
+ void this.takeover
7550
+ .then(() => {
7551
+ // A worker replacement must also preserve intentionally stopped
7552
+ // instances, even when their next full launcher startup is automatic.
7553
+ if (process.env.EPANEL_RESTARTED !== "1")
7554
+ return this.autoStart();
7555
+ })
7556
+ .catch((error) => log_1.default.error(error));
7475
7557
  }
7476
7558
  createInstance(cfg, persistence = true, uuid) {
7477
7559
  const newUuid = uuid || (0, uuid_1.v4)().replace(/-/gim, "");
@@ -7546,6 +7628,22 @@ class InstanceSubsystem extends events_1.default {
7546
7628
  await Promise.all([...this.instances.values()].map((instance) => instance.suspendForReload()));
7547
7629
  await this.takeover;
7548
7630
  }
7631
+ async waitForTakeover() {
7632
+ await this.takeover;
7633
+ }
7634
+ async prepareForRestart() {
7635
+ await this.suspendForReload();
7636
+ for (const instance of this.instances.values()) {
7637
+ system_storage_1.default.store("InstanceConfig", instance.instanceUuid, instance.config);
7638
+ instance.process?.snapshot?.({
7639
+ pid: Number(instance.process.pid),
7640
+ startTimestamp: instance.startTimestamp,
7641
+ startCount: instance.startCount,
7642
+ autoRestartCount: instance.autoRestartCount,
7643
+ userRequestedStop: instance.getUserRequestedStop()
7644
+ });
7645
+ }
7646
+ }
7549
7647
  /** Transfer the existing owners, including ordinary child/PTY/Docker handles. */
7550
7648
  adoptInstances(instances) {
7551
7649
  this.startupCancelled = true;
@@ -8131,6 +8229,46 @@ async function takeoverContainer(isActive = () => true) {
8131
8229
  }
8132
8230
 
8133
8231
 
8232
+ /***/ },
8233
+
8234
+ /***/ "../../plugins/instance/daemon/src/backend/service/takeover_processes.ts"
8235
+ (__unused_webpack_module, exports, __webpack_require__) {
8236
+
8237
+ "use strict";
8238
+
8239
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8240
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8241
+ };
8242
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
8243
+ exports["default"] = takeoverProcesses;
8244
+ const process_adapter_1 = __webpack_require__("../../plugins/instance/daemon/src/backend/entity/commands/base/process_adapter.ts");
8245
+ const pty_start_1 = __webpack_require__("../../plugins/instance/daemon/src/backend/entity/commands/pty/pty_start.ts");
8246
+ const system_instance_1 = __importDefault(__webpack_require__("../../plugins/instance/daemon/src/backend/service/system_instance.ts"));
8247
+ /** Reattach the launcher's live pipes; do not execute the instance start command. */
8248
+ async function takeoverProcesses(isActive = () => true) {
8249
+ const host = (0, process_adapter_1.instanceProcessHost)();
8250
+ if (!host)
8251
+ return;
8252
+ for (const snapshot of await host.list()) {
8253
+ if (!isActive())
8254
+ return;
8255
+ const instance = system_instance_1.default.getInstance(snapshot.id);
8256
+ const child = host.connect(snapshot);
8257
+ const { pipeName, pid } = snapshot.metadata;
8258
+ const adapter = pipeName
8259
+ ? new pty_start_1.GoPtyProcessAdapter(child, pid, pipeName)
8260
+ : new process_adapter_1.ChildProcessAdapter(child);
8261
+ if (!instance || instance.process) {
8262
+ await adapter.destroy();
8263
+ continue;
8264
+ }
8265
+ instance.started(adapter, snapshot.metadata);
8266
+ if (pipeName)
8267
+ adapter.attachOutput();
8268
+ }
8269
+ }
8270
+
8271
+
8134
8272
  /***/ },
8135
8273
 
8136
8274
  /***/ "../../plugins/instance/daemon/src/backend/service/version_adapter.ts"
@@ -8634,6 +8772,7 @@ Object.defineProperty(exports, "arrayUnique", ({ enumerable: true, get: function
8634
8772
  __exportStar(__webpack_require__("../common/src/plugin_contract.ts"), exports);
8635
8773
  __exportStar(__webpack_require__("../common/src/plugin_overrides.ts"), exports);
8636
8774
  __exportStar(__webpack_require__("../common/src/plugin_lifecycle.ts"), exports);
8775
+ __exportStar(__webpack_require__("../common/src/process_restart.ts"), exports);
8637
8776
  __exportStar(__webpack_require__("../common/src/plugin_revision.ts"), exports);
8638
8777
  __exportStar(__webpack_require__("../common/src/plugin_modules.ts"), exports);
8639
8778
  __exportStar(__webpack_require__("../common/src/plugin_watch.ts"), exports);
@@ -9529,6 +9668,39 @@ async function loadPluginModule(entry, directory, revision) {
9529
9668
 
9530
9669
  "use strict";
9531
9670
 
9671
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9672
+ if (k2 === undefined) k2 = k;
9673
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9674
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9675
+ desc = { enumerable: true, get: function() { return m[k]; } };
9676
+ }
9677
+ Object.defineProperty(o, k2, desc);
9678
+ }) : (function(o, m, k, k2) {
9679
+ if (k2 === undefined) k2 = k;
9680
+ o[k2] = m[k];
9681
+ }));
9682
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
9683
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
9684
+ }) : function(o, v) {
9685
+ o["default"] = v;
9686
+ });
9687
+ var __importStar = (this && this.__importStar) || (function () {
9688
+ var ownKeys = function(o) {
9689
+ ownKeys = Object.getOwnPropertyNames || function (o) {
9690
+ var ar = [];
9691
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
9692
+ return ar;
9693
+ };
9694
+ return ownKeys(o);
9695
+ };
9696
+ return function (mod) {
9697
+ if (mod && mod.__esModule) return mod;
9698
+ var result = {};
9699
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
9700
+ __setModuleDefault(result, mod);
9701
+ return result;
9702
+ };
9703
+ })();
9532
9704
  var __importDefault = (this && this.__importDefault) || function (mod) {
9533
9705
  return (mod && mod.__esModule) ? mod : { "default": mod };
9534
9706
  };
@@ -9547,6 +9719,8 @@ const path_1 = __importDefault(__webpack_require__("path"));
9547
9719
  const module_1 = __webpack_require__("module");
9548
9720
  const plugin_manifest_1 = __webpack_require__("../common/src/plugin_manifest.ts");
9549
9721
  const plugin_contract_1 = __webpack_require__("../common/src/plugin_contract.ts");
9722
+ // @ts-ignore Shared by the source runtime and the published npm launcher.
9723
+ const npmPlugins = __importStar(__webpack_require__("../../scripts/runtime/npm-plugins.cjs"));
9550
9724
  const PLUGIN_ID = /^[a-z][a-z0-9_-]{1,63}$/;
9551
9725
  const PACKAGE_NAME = /^(?:@[a-z0-9_.-]+\/)?epanel-plugin-[a-z0-9_.-]+$/;
9552
9726
  const VERSION = /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}$/;
@@ -9589,9 +9763,18 @@ function currentProjectRoot(packageRoot = currentPackageRoot()) {
9589
9763
  }
9590
9764
  function npmPluginRoots(options = {}) {
9591
9765
  const packageRoot = path_1.default.resolve(options.packageRoot ?? currentPackageRoot());
9766
+ const projectRoot = path_1.default.resolve(options.projectRoot ?? currentProjectRoot(packageRoot));
9767
+ const host = readJson(path_1.default.join(packageRoot, "package.json"));
9768
+ const side = options.side ||
9769
+ stringValue(objectValue(host?.elementspanel)?.side) ||
9770
+ path_1.default.basename(packageRoot);
9592
9771
  return {
9593
9772
  packageRoot,
9594
- projectRoot: path_1.default.resolve(options.projectRoot ?? currentProjectRoot(packageRoot))
9773
+ projectRoot,
9774
+ dependencyRoot: path_1.default.dirname(npmPlugins.projectManifestFile(packageRoot, side, {
9775
+ projectRoot,
9776
+ runtimeDirectory: process.env.EPANEL_RUNTIME_ROOT
9777
+ }))
9595
9778
  };
9596
9779
  }
9597
9780
  function projectManifestFile(projectRoot) {
@@ -9619,15 +9802,10 @@ function pluginRecord(pluginId, name, source, packageRoot, directory, folder, ve
9619
9802
  function readPackageEntry(packageRoot, name) {
9620
9803
  // Read package boundaries directly: Node caches require.resolve() paths even
9621
9804
  // after npm moves an installation between nested and hoisted node_modules.
9622
- let manifestPath;
9623
- for (const root of [packageRoot, currentProjectRoot(packageRoot)]) {
9624
- const localRequire = (0, module_1.createRequire)(path_1.default.join(root, "package.json"));
9625
- manifestPath = (localRequire.resolve.paths(name) || [])
9626
- .map((directory) => path_1.default.join(directory, name, "package.json"))
9627
- .find((filename) => fs_extra_1.default.existsSync(filename));
9628
- if (manifestPath)
9629
- break;
9630
- }
9805
+ const localRequire = (0, module_1.createRequire)(path_1.default.join(packageRoot, "package.json"));
9806
+ let manifestPath = (localRequire.resolve.paths(name) || [])
9807
+ .map((directory) => path_1.default.join(directory, name, "package.json"))
9808
+ .find((filename) => fs_extra_1.default.existsSync(filename));
9631
9809
  if (!manifestPath)
9632
9810
  throw new Error(`Missing npm plugin package: ${name}.`);
9633
9811
  manifestPath = fs_extra_1.default.realpathSync(manifestPath);
@@ -9674,83 +9852,26 @@ function recordFromPackage(pluginId, name, source, packageRoot, metadata, side)
9674
9852
  /**
9675
9853
  * Every npm plugin the runtime can load for a side, built-in and project-installed.
9676
9854
  *
9677
- * Built-in package names win over a project mapping for the same id. An explicit
9678
- * project dependency on that same package selects the user's installed version.
9679
- * A project package that does not describe the requested side is skipped instead of failing, which
9680
- * is what lets a two-sided project share one dependency list between sides.
9855
+ * Built-in package names reserve their plugin ids. The side manifest selects
9856
+ * the installed versions; a package without that side is skipped.
9681
9857
  */
9682
9858
  function listNpmPlugins(side, options = {}) {
9683
- const { packageRoot, projectRoot } = npmPluginRoots(options);
9684
- const host = readJson(path_1.default.join(packageRoot, "package.json"));
9685
- if (!host)
9686
- throw new Error(`Missing runtime package metadata: ${packageRoot}`);
9687
- const project = path_1.default.resolve(projectRoot) === path_1.default.resolve(packageRoot)
9688
- ? null
9689
- : readProjectManifest(projectRoot);
9690
- const hostMappings = mappingsOf(host);
9691
- const projectMappings = { ...mappingsOf(project) };
9692
- for (const name of Object.keys(dependenciesOf(project))) {
9693
- if (!PACKAGE_NAME.test(name) || Object.values(projectMappings).includes(name))
9694
- continue;
9695
- try {
9696
- const entry = readPackageEntry(projectRoot, name);
9697
- const id = stringValue(objectValue(entry.metadata.elementspanel)?.id);
9698
- if (PLUGIN_ID.test(id) && projectMappings[id] === undefined)
9699
- projectMappings[id] = name;
9700
- }
9701
- catch {
9702
- /* npm may still be writing the package. */
9703
- }
9704
- }
9705
- const mappings = new Map();
9706
- for (const [pluginId, name] of Object.entries(projectMappings)) {
9707
- if (!PLUGIN_ID.test(pluginId) || typeof name !== "string" || !PACKAGE_NAME.test(name))
9708
- continue;
9709
- mappings.set(pluginId, { name, source: "project" });
9710
- }
9711
- for (const [pluginId, name] of Object.entries(hostMappings)) {
9712
- if (!PLUGIN_ID.test(pluginId) || typeof name !== "string" || !PACKAGE_NAME.test(name))
9713
- continue;
9714
- if (dependenciesOf(host)[name] === undefined)
9715
- continue;
9716
- mappings.set(pluginId, {
9717
- name,
9718
- source: dependenciesOf(project)[name] === undefined ? "bundled" : "project"
9719
- });
9720
- }
9721
- const hostDependencies = dependenciesOf(host);
9722
- const projectDependencies = dependenciesOf(project);
9723
- const folders = new Set();
9859
+ const { packageRoot, projectRoot, dependencyRoot } = npmPluginRoots({ ...options, side });
9724
9860
  const records = [];
9725
- for (const [pluginId, mapping] of mappings) {
9726
- const declared = hostDependencies[mapping.name] ?? projectDependencies[mapping.name];
9727
- if (declared === undefined) {
9728
- if (options.skipInvalid)
9729
- continue;
9730
- throw new Error(`Missing npm plugin dependency: ${mapping.name}. Run npm install in the project directory.`);
9731
- }
9732
- let entry;
9733
- try {
9734
- entry = readPackageEntry(mapping.source === "project" ? projectRoot : packageRoot, mapping.name);
9735
- }
9736
- catch (error) {
9737
- if (options.skipInvalid)
9738
- continue;
9739
- throw error;
9740
- }
9741
- const record = recordFromPackage(pluginId, mapping.name, mapping.source, entry.root, entry.metadata, side);
9861
+ for (const item of npmPlugins.resolveNpmPlugins(packageRoot, side, {
9862
+ projectRoot,
9863
+ runtimeDirectory: dependencyRoot,
9864
+ skipInvalid: options.skipInvalid
9865
+ })) {
9866
+ const metadata = readJson(path_1.default.join(item.packageRoot, "package.json"));
9867
+ if (!metadata)
9868
+ continue;
9869
+ const record = recordFromPackage(item.id, item.name, item.source, item.packageRoot, metadata, side);
9742
9870
  if (!record) {
9743
- if (options.skipInvalid || mapping.source === "project")
9744
- continue;
9745
- throw new Error(`Invalid ${side ?? "plugin"} entry in npm plugin ${mapping.name}.`);
9746
- }
9747
- const folder = record.folder;
9748
- if (folders.has(folder)) {
9749
9871
  if (options.skipInvalid)
9750
9872
  continue;
9751
- throw new Error(`Duplicate npm plugin asset directory: ${folder}.`);
9873
+ throw new Error(`Invalid ${side ?? "plugin"} entry in npm plugin ${item.name}.`);
9752
9874
  }
9753
- folders.add(folder);
9754
9875
  records.push(record);
9755
9876
  }
9756
9877
  return records;
@@ -9847,10 +9968,9 @@ async function acquireProjectLock(projectRoot) {
9847
9968
  }
9848
9969
  }
9849
9970
  }
9850
- function npmPluginOperationPending() {
9851
- const { packageRoot, projectRoot } = npmPluginRoots();
9852
- return (operations.size > 0 ||
9853
- [packageRoot, projectRoot].some((root) => fs_extra_1.default.existsSync(path_1.default.join(root, LOCK_NAME))));
9971
+ function npmPluginOperationPending(side) {
9972
+ const { dependencyRoot } = npmPluginRoots({ side });
9973
+ return operations.size > 0 || fs_extra_1.default.existsSync(path_1.default.join(dependencyRoot, LOCK_NAME));
9854
9974
  }
9855
9975
  function withProjectLock(projectRoot, operation) {
9856
9976
  const key = process.platform === "win32" ? projectRoot.toLowerCase() : projectRoot;
@@ -9885,18 +10005,15 @@ async function installNpmPlugin(options) {
9885
10005
  throw new Error(`Invalid plugin id: ${options.pluginId}`);
9886
10006
  if (!VERSION.test(version))
9887
10007
  throw new Error(`Invalid plugin version: ${version}`);
9888
- const { packageRoot, projectRoot } = npmPluginRoots(options);
9889
- return withProjectLock(projectRoot, async () => {
10008
+ const { packageRoot, projectRoot, dependencyRoot } = npmPluginRoots(options);
10009
+ return withProjectLock(dependencyRoot, async () => {
9890
10010
  const host = readJson(path_1.default.join(packageRoot, "package.json"));
9891
10011
  if (!host)
9892
10012
  throw new Error(`Missing runtime package metadata: ${packageRoot}`);
9893
- const hostId = Object.entries(mappingsOf(host)).find(([, value]) => value === name && dependenciesOf(host)[name] !== undefined)?.[0];
9894
- const targetRoot = hostId && dependenciesOf(readProjectManifest(projectRoot))[name] === undefined
9895
- ? packageRoot
9896
- : projectRoot;
10013
+ const hostId = Object.entries(mappingsOf(host)).find(([, value]) => value === name)?.[0];
10014
+ const targetRoot = dependencyRoot;
9897
10015
  if (options.pluginId &&
9898
10016
  mappingsOf(host)[options.pluginId] &&
9899
- dependenciesOf(host)[String(mappingsOf(host)[options.pluginId])] !== undefined &&
9900
10017
  mappingsOf(host)[options.pluginId] !== name)
9901
10018
  throw new Error(`BUNDLED:${options.pluginId}`);
9902
10019
  const original = readProjectManifest(targetRoot);
@@ -9925,24 +10042,33 @@ async function installNpmPlugin(options) {
9925
10042
  "--save-exact",
9926
10043
  "--no-fund",
9927
10044
  "--no-audit",
9928
- "--ignore-scripts"
10045
+ "--ignore-scripts",
10046
+ "--workspaces=false"
9929
10047
  ], targetRoot);
9930
10048
  const entry = readPackageEntry(targetRoot, name);
9931
10049
  const pluginId = options.pluginId || stringValue(objectValue(entry.metadata.elementspanel)?.id);
9932
10050
  if (!PLUGIN_ID.test(pluginId))
9933
10051
  throw new Error(`Invalid plugin id in ${name}.`);
9934
- if (mappingsOf(host)[pluginId] &&
9935
- dependenciesOf(host)[String(mappingsOf(host)[pluginId])] !== undefined &&
9936
- mappingsOf(host)[pluginId] !== name)
10052
+ if (mappingsOf(host)[pluginId] && mappingsOf(host)[pluginId] !== name)
9937
10053
  throw new Error(`Plugin id ${pluginId} belongs to a bundled dependency.`);
9938
10054
  if (hostId && pluginId !== hostId)
9939
10055
  throw new Error(`The package ${name} changed its plugin id from ${hostId} to ${pluginId}.`);
9940
10056
  const mapped = mappingsOf(readProjectManifest(targetRoot))[pluginId];
9941
10057
  if (mapped !== undefined && mapped !== name)
9942
10058
  throw new Error(`Plugin id ${pluginId} is already installed as ${String(mapped)}.`);
9943
- const record = recordFromPackage(pluginId, name, targetRoot === packageRoot ? "bundled" : "project", entry.root, entry.metadata, side);
10059
+ const record = recordFromPackage(pluginId, name, hostId ? "bundled" : "project", entry.root, entry.metadata, side);
9944
10060
  if (!record)
9945
10061
  throw new Error(`The package ${name} does not provide a ${side} plugin named ${pluginId}.`);
10062
+ if (side === "panel") {
10063
+ const expected = objectValue(objectValue(entry.metadata.elementspanel)?.panel)
10064
+ ?.frontendAssets;
10065
+ const available = objectValue(host.elementspanel)?.frontendAssets;
10066
+ if ((hostId || expected !== undefined) &&
10067
+ (!Array.isArray(expected) ||
10068
+ !Array.isArray(available) ||
10069
+ expected.some((file) => typeof file !== "string" || !available.includes(file))))
10070
+ throw new Error(`Frontend assets in ${name} do not match this panel. Update the panel or install a matching plugin version.`);
10071
+ }
9946
10072
  if (/^[0-9]/.test(version) && record.version !== version)
9947
10073
  throw new Error(`The installed package ${name} is ${record.version}, expected ${version}.`);
9948
10074
  if (!process.env.EPANEL_PACKAGE_ROOT &&
@@ -9986,7 +10112,8 @@ async function installNpmPlugin(options) {
9986
10112
  "--save",
9987
10113
  "--no-fund",
9988
10114
  "--no-audit",
9989
- "--ignore-scripts"
10115
+ "--ignore-scripts",
10116
+ "--workspaces=false"
9990
10117
  ]
9991
10118
  : [
9992
10119
  "--prefix",
@@ -9995,7 +10122,8 @@ async function installNpmPlugin(options) {
9995
10122
  name,
9996
10123
  "--no-fund",
9997
10124
  "--no-audit",
9998
- "--ignore-scripts"
10125
+ "--ignore-scripts",
10126
+ "--workspaces=false"
9999
10127
  ], targetRoot);
10000
10128
  }
10001
10129
  catch (rollback) {
@@ -10019,22 +10147,29 @@ async function uninstallNpmPlugin(pluginId, options = {}) {
10019
10147
  throw new Error(`Invalid plugin id: ${pluginId}`);
10020
10148
  if (isEssentialNpmPlugin(pluginId))
10021
10149
  throw new Error(`The essential plugin "${pluginId}" cannot be uninstalled.`);
10022
- const { packageRoot, projectRoot } = npmPluginRoots(options);
10023
- return withProjectLock(projectRoot, async () => {
10024
- const host = readJson(path_1.default.join(packageRoot, "package.json"));
10025
- const bundledName = mappingsOf(host)[pluginId];
10026
- const targetRoot = typeof bundledName === "string" &&
10027
- dependenciesOf(host)[bundledName] !== undefined &&
10028
- dependenciesOf(readProjectManifest(projectRoot))[bundledName] === undefined
10029
- ? packageRoot
10030
- : projectRoot;
10150
+ const { packageRoot, projectRoot, dependencyRoot } = npmPluginRoots(options);
10151
+ return withProjectLock(dependencyRoot, async () => {
10152
+ const targetRoot = dependencyRoot;
10031
10153
  const manifest = readProjectManifest(targetRoot);
10032
10154
  const name = mappingsOf(manifest)[pluginId] ??
10033
- listNpmPlugins(undefined, { packageRoot, projectRoot, skipInvalid: true }).find((item) => item.pluginId === pluginId)?.name;
10155
+ listNpmPlugins(options.side, { packageRoot, projectRoot, skipInvalid: true }).find((item) => item.pluginId === pluginId)?.name;
10034
10156
  if (typeof name !== "string" || !PACKAGE_NAME.test(name))
10035
10157
  return null;
10036
- const record = listNpmPlugins(undefined, { packageRoot, projectRoot, skipInvalid: true }).find((item) => item.pluginId === pluginId);
10037
- await runNpm(["--prefix", targetRoot, "uninstall", name, "--no-fund", "--no-audit", "--ignore-scripts"], targetRoot);
10158
+ const record = listNpmPlugins(options.side, {
10159
+ packageRoot,
10160
+ projectRoot,
10161
+ skipInvalid: true
10162
+ }).find((item) => item.pluginId === pluginId);
10163
+ await runNpm([
10164
+ "--prefix",
10165
+ targetRoot,
10166
+ "uninstall",
10167
+ name,
10168
+ "--no-fund",
10169
+ "--no-audit",
10170
+ "--ignore-scripts",
10171
+ "--workspaces=false"
10172
+ ], targetRoot);
10038
10173
  const updated = readProjectManifest(targetRoot);
10039
10174
  const plugins = { ...mappingsOf(updated) };
10040
10175
  delete plugins[pluginId];
@@ -10208,11 +10343,11 @@ const plugin_npm_1 = __webpack_require__("../common/src/plugin_npm.ts");
10208
10343
  /** Poll package boundaries, allowing npm to finish replacing files before reconciliation. */
10209
10344
  function watchNpmPlugins(ctx, side, reload, changed) {
10210
10345
  const stamp = () => {
10211
- const { packageRoot, projectRoot } = (0, plugin_npm_1.npmPluginRoots)();
10346
+ const { packageRoot, dependencyRoot } = (0, plugin_npm_1.npmPluginRoots)({ side });
10212
10347
  if (!node_fs_1.default.existsSync(node_path_1.default.join(packageRoot, "package.json")))
10213
10348
  return "";
10214
10349
  const files = [];
10215
- for (const root of [packageRoot, projectRoot])
10350
+ for (const root of [packageRoot, dependencyRoot])
10216
10351
  for (const name of [
10217
10352
  "package.json",
10218
10353
  "package-lock.json",
@@ -10236,7 +10371,7 @@ function watchNpmPlugins(ctx, side, reload, changed) {
10236
10371
  let candidate = applied;
10237
10372
  let busy = false;
10238
10373
  ctx.setInterval(() => {
10239
- if (busy || (0, plugin_npm_1.npmPluginOperationPending)())
10374
+ if (busy || (0, plugin_npm_1.npmPluginOperationPending)(side))
10240
10375
  return;
10241
10376
  try {
10242
10377
  const next = stamp();
@@ -10267,6 +10402,50 @@ function watchNpmPlugins(ctx, side, reload, changed) {
10267
10402
  }
10268
10403
 
10269
10404
 
10405
+ /***/ },
10406
+
10407
+ /***/ "../common/src/process_restart.ts"
10408
+ (__unused_webpack_module, exports) {
10409
+
10410
+ "use strict";
10411
+
10412
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10413
+ exports.isProcessRestartPending = isProcessRestartPending;
10414
+ exports.requestProcessRestart = requestProcessRestart;
10415
+ const RESTART = Symbol.for("elementspanel.process.restart");
10416
+ function isProcessRestartPending(ctx) {
10417
+ return ctx.root[RESTART]?.pending === true;
10418
+ }
10419
+ /** The CLI retains instance processes and starts a fresh worker after this one exits. */
10420
+ function requestProcessRestart(ctx, settle, ready = async () => { }) {
10421
+ if (isProcessRestartPending(ctx))
10422
+ return true;
10423
+ if (process.env.EPANEL_SUPERVISED !== "1" || !process.connected || !process.send)
10424
+ return false;
10425
+ const state = { pending: true };
10426
+ ctx.root[RESTART] = state;
10427
+ const retainProcesses = () => new Promise((resolve, reject) => process.send({ type: "elementspanel:prepare-restart" }, (error) => error ? reject(error) : resolve()));
10428
+ setImmediate(() => {
10429
+ void (async () => {
10430
+ await ready();
10431
+ // Detach output consumers before cleanup so output and exits during the
10432
+ // restart are retained for the next daemon generation.
10433
+ await retainProcesses();
10434
+ await ctx.parallel("process/restarting");
10435
+ // A pending start can finish while restart hooks are awaiting it.
10436
+ await retainProcesses();
10437
+ await ctx.stop();
10438
+ await settle();
10439
+ process.send({ type: "elementspanel:restart" }, (error) => process.exit(error ? 1 : 0));
10440
+ })().catch((error) => {
10441
+ ctx.logger.error("Failed to restart the process:", error);
10442
+ process.exit(1);
10443
+ });
10444
+ });
10445
+ return true;
10446
+ }
10447
+
10448
+
10270
10449
  /***/ },
10271
10450
 
10272
10451
  /***/ "../common/src/process_tools.ts"
@@ -11277,6 +11456,226 @@ module.exports = require("stream/promises");
11277
11456
  "use strict";
11278
11457
  module.exports = require("util");
11279
11458
 
11459
+ /***/ },
11460
+
11461
+ /***/ "../../scripts/runtime/npm-plugins.cjs"
11462
+ (module, __unused_webpack_exports, __webpack_require__) {
11463
+
11464
+ const fs = __webpack_require__("node:fs");
11465
+ const path = __webpack_require__("node:path");
11466
+ const { createRequire } = __webpack_require__("node:module");
11467
+
11468
+ const readJson = (filename) => JSON.parse(fs.readFileSync(filename, "utf8"));
11469
+ const readOptional = (filename) => {
11470
+ try {
11471
+ return readJson(filename);
11472
+ } catch (error) {
11473
+ if (error.code === "ENOENT") return null;
11474
+ throw error;
11475
+ }
11476
+ };
11477
+ const validId = /^[a-z][a-z0-9_-]{1,63}$/;
11478
+ const validName = /^(?:@[a-z0-9_.-]+\/)?epanel-plugin-[a-z0-9_.-]+$/;
11479
+
11480
+ function packageManifest(root, name) {
11481
+ const localRequire = createRequire(path.join(root, "package.json"));
11482
+ const filename = (localRequire.resolve.paths(name) || [])
11483
+ .map((directory) => path.join(directory, name, "package.json"))
11484
+ .find((candidate) => fs.existsSync(candidate));
11485
+ if (!filename) throw new Error(`Missing npm package: ${name}`);
11486
+ return fs.realpathSync(filename);
11487
+ }
11488
+
11489
+ function within(root, filename) {
11490
+ const relative = path.relative(root, filename);
11491
+ return (
11492
+ relative === "" ||
11493
+ (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
11494
+ );
11495
+ }
11496
+
11497
+ function projectRoot(packageRoot) {
11498
+ if (process.env.EPANEL_PROJECT_ROOT) return path.resolve(process.env.EPANEL_PROJECT_ROOT);
11499
+ const parent = path.resolve(packageRoot, "..");
11500
+ if (path.basename(parent) === "core") return path.dirname(parent);
11501
+ // An installed runtime sits at <project>/node_modules/@scope/<side>; the
11502
+ // project itself is three levels above the package. A development checkout
11503
+ // sits below core/ and is handled above.
11504
+ const scope = parent;
11505
+ const nodeModules = path.dirname(scope);
11506
+ if (path.basename(nodeModules) === "node_modules" && path.basename(scope).startsWith("@")) {
11507
+ return path.dirname(nodeModules);
11508
+ }
11509
+ return parent;
11510
+ }
11511
+
11512
+ function projectManifestFile(packageRoot, side, options = {}) {
11513
+ const selectedSide =
11514
+ side ||
11515
+ readJson(path.join(packageRoot, "package.json")).elementspanel?.side ||
11516
+ path.basename(packageRoot);
11517
+ if (!["panel", "daemon"].includes(selectedSide)) throw new Error("Missing runtime side.");
11518
+ const directory =
11519
+ options.runtimeDirectory ||
11520
+ (!options.projectRoot && process.env.EPANEL_RUNTIME_ROOT) ||
11521
+ path.join(options.projectRoot || projectRoot(packageRoot), selectedSide);
11522
+ return path.join(path.resolve(directory), "package.json");
11523
+ }
11524
+
11525
+ function mappingOf(manifest) {
11526
+ const plugins = manifest?.elementspanel?.plugins;
11527
+ return plugins && typeof plugins === "object" && !Array.isArray(plugins) ? plugins : {};
11528
+ }
11529
+
11530
+ function dependenciesOf(manifest) {
11531
+ const dependencies = manifest?.dependencies;
11532
+ return dependencies && typeof dependencies === "object" ? dependencies : {};
11533
+ }
11534
+
11535
+ /**
11536
+ * Resolve plugins declared in the side's package.json, including hoisted npm
11537
+ * installations. The host catalogue reserves built-in ids without owning their
11538
+ * versions. Other explicitly mapped plugins remain dormant until enabled.
11539
+ * Runtime startup passes skipInvalid so an optional package being removed does
11540
+ * not stop the host; the caller separately validates the small set of core
11541
+ * plugins.
11542
+ */
11543
+ function resolveNpmPlugins(packageRoot, side, options = {}) {
11544
+ const skipInvalid = options.skipInvalid === true;
11545
+ const host = readJson(path.join(packageRoot, "package.json"));
11546
+ const selectedSide = side || host.elementspanel?.side || path.basename(packageRoot);
11547
+ const projectFile = projectManifestFile(packageRoot, selectedSide, options);
11548
+ const project = readOptional(projectFile);
11549
+ const dependencyRoot = path.dirname(projectFile);
11550
+
11551
+ // The side manifest owns every dependency; the host only reserves identities.
11552
+ const configured = new Map();
11553
+ for (const [id, name] of Object.entries(mappingOf(project)))
11554
+ configured.set(id, { name, source: "project" });
11555
+ for (const name of Object.keys(dependenciesOf(project))) {
11556
+ if (!validName.test(name) || [...configured.values()].some((item) => item.name === name))
11557
+ continue;
11558
+ try {
11559
+ const metadata = readJson(packageManifest(dependencyRoot, name));
11560
+ const id = metadata.elementspanel?.id;
11561
+ if (validId.test(id) && !configured.has(id))
11562
+ configured.set(id, { name, source: "project", automatic: true });
11563
+ } catch {
11564
+ /* Ignore incomplete npm installations until the next scan. */
11565
+ }
11566
+ }
11567
+ for (const [id, name] of Object.entries(mappingOf(host))) {
11568
+ if (dependenciesOf(project)[name] !== undefined)
11569
+ configured.set(id, {
11570
+ name,
11571
+ source: "bundled",
11572
+ automatic: true
11573
+ });
11574
+ else if (configured.has(id)) {
11575
+ if (!skipInvalid) throw new Error(`Plugin id ${id} belongs to built-in package ${name}.`);
11576
+ configured.delete(id);
11577
+ }
11578
+ }
11579
+
11580
+ const projectDependencies = dependenciesOf(project);
11581
+ const folders = new Set();
11582
+ const records = [];
11583
+
11584
+ for (const [id, mapping] of configured) {
11585
+ const name = mapping.name;
11586
+ if (
11587
+ !validId.test(id) ||
11588
+ typeof name !== "string" ||
11589
+ !validName.test(name) ||
11590
+ projectDependencies[name] === undefined
11591
+ ) {
11592
+ if (skipInvalid) continue;
11593
+ throw new Error(`Invalid npm plugin dependency: ${id}.`);
11594
+ }
11595
+
11596
+ let manifestPath;
11597
+ let failure;
11598
+ try {
11599
+ manifestPath = packageManifest(dependencyRoot, name);
11600
+ } catch (error) {
11601
+ failure = error;
11602
+ }
11603
+ if (!manifestPath) {
11604
+ if (skipInvalid) continue;
11605
+ throw new Error(`Missing npm plugin ${name}. Run npm install in ${dependencyRoot}.`, {
11606
+ cause: failure
11607
+ });
11608
+ }
11609
+ let metadata;
11610
+ try {
11611
+ metadata = readJson(manifestPath);
11612
+ } catch (error) {
11613
+ if (skipInvalid) continue;
11614
+ throw error;
11615
+ }
11616
+ const definition = metadata.elementspanel?.[selectedSide];
11617
+ // A project dependency may only ship one side; the other side simply does
11618
+ // not see it. A built-in plugin declared by the runtime must carry this side.
11619
+ if (!definition) {
11620
+ if (mapping.source === "project" || skipInvalid) continue;
11621
+ throw new Error(`Invalid ${selectedSide} entry in npm plugin ${name}.`);
11622
+ }
11623
+ const pluginRoot = path.dirname(manifestPath);
11624
+ if (
11625
+ typeof definition.directory !== "string" ||
11626
+ typeof definition.folder !== "string" ||
11627
+ !validId.test(definition.folder) ||
11628
+ path.isAbsolute(definition.directory)
11629
+ ) {
11630
+ if (skipInvalid) continue;
11631
+ throw new Error(`Npm plugin entry escapes its package: ${name}.`);
11632
+ }
11633
+ const directory = path.resolve(pluginRoot, definition.directory);
11634
+ if (directory === pluginRoot || !within(pluginRoot, directory)) {
11635
+ if (skipInvalid) continue;
11636
+ throw new Error(`Npm plugin entry escapes its package: ${name}.`);
11637
+ }
11638
+ let entries;
11639
+ try {
11640
+ entries = readJson(path.join(directory, "plugin.json"));
11641
+ } catch (error) {
11642
+ if (skipInvalid) continue;
11643
+ throw error;
11644
+ }
11645
+ if (
11646
+ metadata.elementspanel?.id !== id ||
11647
+ typeof metadata.version !== "string" ||
11648
+ !metadata.version.trim() ||
11649
+ !entries ||
11650
+ typeof entries !== "object" ||
11651
+ Array.isArray(entries)
11652
+ ) {
11653
+ if (skipInvalid) continue;
11654
+ throw new Error(`Invalid npm plugin identity or version in package.json: ${name}.`);
11655
+ }
11656
+ if (folders.has(definition.folder)) {
11657
+ if (skipInvalid) continue;
11658
+ throw new Error(`Duplicate npm plugin asset directory: ${definition.folder}.`);
11659
+ }
11660
+ folders.add(definition.folder);
11661
+ records.push({
11662
+ id,
11663
+ name,
11664
+ version: metadata.version,
11665
+ source: mapping.source,
11666
+ defaultEnabled: mapping.automatic === true || mapping.source !== "project",
11667
+ packageRoot: pluginRoot,
11668
+ directory,
11669
+ folder: definition.folder,
11670
+ dependencies: Object.keys(metadata.dependencies || {})
11671
+ });
11672
+ }
11673
+ return records;
11674
+ }
11675
+
11676
+ module.exports = { resolveNpmPlugins, projectManifestFile };
11677
+
11678
+
11280
11679
  /***/ },
11281
11680
 
11282
11681
  /***/ "../../scripts/workspace-layout.cjs"