@jskit-ai/kernel 0.1.118 → 0.1.120

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.
@@ -19,6 +19,7 @@ const CLIENT_RUNTIME_DEDUPE_SPECIFIERS = Object.freeze([
19
19
  "vue-router",
20
20
  "vuetify"
21
21
  ]);
22
+ const LOCAL_PACKAGE_SOURCE_TYPES = new Set(["local-package", "app-local-package"]);
22
23
 
23
24
  function isLocalScopePackageId(value) {
24
25
  return String(value || "").trim().startsWith("@local/");
@@ -38,6 +39,93 @@ function hasClientExport(packageJson) {
38
39
  return Boolean(exportsMap["./client"]);
39
40
  }
40
41
 
42
+ function isPathInsideRoot(rootPath, candidatePath) {
43
+ const relativePath = path.relative(rootPath, candidatePath);
44
+ return (
45
+ relativePath === "" ||
46
+ (!relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath))
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Generated apps intentionally preserve package symlinks, so a bare import from an editable local
52
+ * package would otherwise keep its node_modules path. optimizeDeps.exclude only prevents prebundling;
53
+ * Vite can still append its dependency-wide ?v hash and serve that source as one-year immutable.
54
+ * That hash does not change when local source changes, which lets a browser reuse stale client code
55
+ * even after Vite restarts.
56
+ *
57
+ * Build this table from every lock-classified local package, not only packages with a ./client export.
58
+ * Resolving only the bootstrap entry is insufficient: a local client can use bare imports from another
59
+ * local package's root, shared entry, or exported subpath and accidentally re-enter node_modules caching.
60
+ */
61
+ async function resolveLocalPackageSources({ appRoot, lockPath }) {
62
+ const absoluteLockPath = path.resolve(appRoot, lockPath);
63
+ const lockPayload = await readJsonFile(absoluteLockPath, {});
64
+ const installedPackages = normalizeObject(lockPayload.installedPackages);
65
+ const packages = [];
66
+
67
+ for (const packageId of sortStrings(Object.keys(installedPackages))) {
68
+ const installedPackageState = normalizeObject(installedPackages[packageId]);
69
+ const source = normalizeObject(installedPackageState.source);
70
+ const sourceType = String(source.type || "").trim().toLowerCase();
71
+ const packagePath = String(source.packagePath || "").trim();
72
+ if (!LOCAL_PACKAGE_SOURCE_TYPES.has(sourceType) || !packagePath) {
73
+ continue;
74
+ }
75
+
76
+ packages.push(
77
+ Object.freeze({
78
+ packageId,
79
+ installedPackageRoot: path.resolve(appRoot, "node_modules", ...packageId.split("/")),
80
+ sourcePackageRoot: path.resolve(appRoot, packagePath)
81
+ })
82
+ );
83
+ }
84
+
85
+ return Object.freeze(packages);
86
+ }
87
+
88
+ function splitSpecifierSuffix(source) {
89
+ const normalizedSource = String(source || "");
90
+ const queryIndex = normalizedSource.indexOf("?");
91
+ const hashIndex = normalizedSource.indexOf("#");
92
+ const suffixIndexes = [queryIndex, hashIndex].filter((index) => index >= 0);
93
+ const suffixIndex = suffixIndexes.length > 0 ? Math.min(...suffixIndexes) : -1;
94
+ if (suffixIndex < 0) {
95
+ return [normalizedSource, ""];
96
+ }
97
+ return [normalizedSource.slice(0, suffixIndex), normalizedSource.slice(suffixIndex)];
98
+ }
99
+
100
+ function resolveLocalPackageForSpecifier(source, localPackages = []) {
101
+ const [specifier] = splitSpecifierSuffix(source);
102
+ return (
103
+ (Array.isArray(localPackages) ? localPackages : []).find(
104
+ (entry) => specifier === entry.packageId || specifier.startsWith(`${entry.packageId}/`)
105
+ ) || null
106
+ );
107
+ }
108
+
109
+ function resolveCanonicalLocalPackageId(resolvedId, localPackage) {
110
+ const [resolvedPath, suffix] = splitSpecifierSuffix(resolvedId);
111
+ if (!resolvedPath || resolvedPath.startsWith("\0")) {
112
+ return "";
113
+ }
114
+
115
+ if (isPathInsideRoot(localPackage.sourcePackageRoot, resolvedPath)) {
116
+ return `${resolvedPath}${suffix}`;
117
+ }
118
+ if (!isPathInsideRoot(localPackage.installedPackageRoot, resolvedPath)) {
119
+ return "";
120
+ }
121
+
122
+ const packageRelativePath = path.relative(localPackage.installedPackageRoot, resolvedPath);
123
+ const canonicalPath = path.resolve(localPackage.sourcePackageRoot, packageRelativePath);
124
+ return isPathInsideRoot(localPackage.sourcePackageRoot, canonicalPath)
125
+ ? `${canonicalPath}${suffix}`
126
+ : "";
127
+ }
128
+
41
129
  async function resolveInstalledClientModules({ appRoot, lockPath }) {
42
130
  const absoluteLockPath = path.resolve(appRoot, lockPath);
43
131
  const lockPayload = await readJsonFile(absoluteLockPath, {});
@@ -160,11 +248,10 @@ export { installedClientModules, bootInstalledClientModules };
160
248
  // Vite-only: this Set-based source-type filter exists solely to drive optimizeDeps include/exclude decisions.
161
249
  function resolveClientOptimizeExcludeSpecifiers(clientModules = []) {
162
250
  const moduleDescriptors = normalizeClientModuleDescriptors(clientModules);
163
- const localSourceTypes = new Set(["local-package", "app-local-package"]);
164
251
  return sortStrings(
165
252
  [
166
253
  ...moduleDescriptors
167
- .filter((entry) => localSourceTypes.has(entry.sourceType))
254
+ .filter((entry) => LOCAL_PACKAGE_SOURCE_TYPES.has(entry.sourceType))
168
255
  .flatMap((entry) => [entry.packageId, `${entry.packageId}/shared`, `${entry.packageId}/client`]),
169
256
  ...moduleDescriptors.flatMap((entry) => entry.descriptorClientOptimizeExcludeSpecifiers || [])
170
257
  ]
@@ -173,12 +260,11 @@ function resolveClientOptimizeExcludeSpecifiers(clientModules = []) {
173
260
 
174
261
  function resolveClientOptimizeIncludeSpecifiers(clientModules = [], excludeSpecifiers = []) {
175
262
  const moduleDescriptors = normalizeClientModuleDescriptors(clientModules);
176
- const localSourceTypes = new Set(["local-package", "app-local-package"]);
177
263
  const excluded = new Set(sortStrings(excludeSpecifiers));
178
264
  return sortStrings(
179
265
  [
180
266
  ...moduleDescriptors
181
- .filter((entry) => !localSourceTypes.has(entry.sourceType))
267
+ .filter((entry) => !LOCAL_PACKAGE_SOURCE_TYPES.has(entry.sourceType))
182
268
  .map((entry) => `${entry.packageId}/client`),
183
269
  ...moduleDescriptors.flatMap((entry) => entry.descriptorClientOptimizeIncludeSpecifiers || [])
184
270
  ].filter((specifier) => !excluded.has(specifier))
@@ -198,10 +284,16 @@ function resolveClientRuntimeDedupeSpecifiers(userResolveConfig = {}) {
198
284
  }
199
285
 
200
286
  function createJskitClientBootstrapPlugin({ lockPath = ".jskit/lock.json" } = {}) {
287
+ let appRoot = process.cwd();
288
+ let localPackages = Object.freeze([]);
289
+ let resolvePackageSpecifier = null;
290
+
201
291
  return {
202
292
  name: "jskit-client-bootstrap",
293
+ // This must run before Vite's normal package resolver turns a local bare import into node_modules.
294
+ enforce: "pre",
203
295
  async config(userConfig = {}) {
204
- const appRoot = process.cwd();
296
+ appRoot = process.cwd();
205
297
  const clientModules = await resolveInstalledClientModules({
206
298
  appRoot,
207
299
  lockPath
@@ -210,6 +302,11 @@ function createJskitClientBootstrapPlugin({ lockPath = ".jskit/lock.json" } = {}
210
302
  appRoot,
211
303
  lockPath
212
304
  });
305
+ const userResolve = normalizeObject(userConfig.resolve);
306
+ localPackages = await resolveLocalPackageSources({
307
+ appRoot,
308
+ lockPath
309
+ });
213
310
  const clientExcludeSpecifiers = resolveClientOptimizeExcludeSpecifiers(clientModules);
214
311
  const localScopeExcludeSpecifiers = resolveLocalScopeOptimizeExcludeSpecifiers(localScopePackageIds);
215
312
  const userOptimizeDeps = normalizeObject(userConfig.optimizeDeps);
@@ -218,7 +315,6 @@ function createJskitClientBootstrapPlugin({ lockPath = ".jskit/lock.json" } = {}
218
315
  const exclude = sortStrings([...userExclude, ...clientExcludeSpecifiers, ...localScopeExcludeSpecifiers]);
219
316
  const clientIncludeSpecifiers = resolveClientOptimizeIncludeSpecifiers(clientModules, exclude);
220
317
  const include = sortStrings([...userInclude, ...clientIncludeSpecifiers].filter((specifier) => !exclude.includes(specifier)));
221
- const userResolve = normalizeObject(userConfig.resolve);
222
318
  const dedupe = resolveClientRuntimeDedupeSpecifiers(userResolve);
223
319
 
224
320
  return {
@@ -233,11 +329,34 @@ function createJskitClientBootstrapPlugin({ lockPath = ".jskit/lock.json" } = {}
233
329
  }
234
330
  };
235
331
  },
236
- resolveId(source) {
332
+ configResolved(resolvedConfig) {
333
+ // Do not use `this.resolve()` for this lookup. That re-enters Vite's live plugin chain, where
334
+ // the dependency optimizer can turn an unlisted local subpath into node_modules/.vite (or add
335
+ // its dependency ?v hash) before JSKIT sees the selected file. The config resolver applies the
336
+ // app's aliases, exports conditions, and wildcard rules without registering an optimized dep.
337
+ resolvePackageSpecifier = resolvedConfig.createResolver({ scan: true });
338
+ },
339
+ async resolveId(source, importer) {
237
340
  if (source === CLIENT_BOOTSTRAP_VIRTUAL_ID) {
238
341
  return CLIENT_BOOTSTRAP_RESOLVED_ID;
239
342
  }
240
- return null;
343
+ const localPackage = resolveLocalPackageForSpecifier(source, localPackages);
344
+ if (!localPackage) {
345
+ return null;
346
+ }
347
+
348
+ const resolvedId = await resolvePackageSpecifier?.(source, importer);
349
+ if (!resolvedId) {
350
+ return null;
351
+ }
352
+
353
+ const canonicalId = resolveCanonicalLocalPackageId(resolvedId, localPackage);
354
+ if (!canonicalId) {
355
+ return resolvedId;
356
+ }
357
+
358
+ // The canonical absolute path receives editable-source watching and no-cache headers.
359
+ return canonicalId;
241
360
  },
242
361
  async load(id) {
243
362
  if (id !== CLIENT_BOOTSTRAP_RESOLVED_ID) {
@@ -245,7 +364,7 @@ function createJskitClientBootstrapPlugin({ lockPath = ".jskit/lock.json" } = {}
245
364
  }
246
365
 
247
366
  const clientModules = await resolveInstalledClientModules({
248
- appRoot: process.cwd(),
367
+ appRoot,
249
368
  lockPath
250
369
  });
251
370
 
@@ -260,6 +379,9 @@ export {
260
379
  createVirtualModuleSource,
261
380
  resolveClientOptimizeIncludeSpecifiers,
262
381
  resolveClientOptimizeExcludeSpecifiers,
382
+ resolveCanonicalLocalPackageId,
383
+ resolveLocalPackageForSpecifier,
384
+ resolveLocalPackageSources,
263
385
  resolveLocalScopeOptimizeExcludeSpecifiers,
264
386
  resolveInstalledClientPackageIds,
265
387
  resolveLocalScopePackageIds,
@@ -8,6 +8,9 @@ import {
8
8
  CLIENT_BOOTSTRAP_VIRTUAL_ID,
9
9
  createJskitClientBootstrapPlugin,
10
10
  createVirtualModuleSource,
11
+ resolveCanonicalLocalPackageId,
12
+ resolveLocalPackageForSpecifier,
13
+ resolveLocalPackageSources,
11
14
  resolveLocalScopeOptimizeExcludeSpecifiers,
12
15
  resolveClientOptimizeIncludeSpecifiers,
13
16
  resolveClientOptimizeExcludeSpecifiers,
@@ -24,6 +27,85 @@ async function writeDescriptor(filePath, descriptor) {
24
27
  await writeFile(filePath, `export default ${JSON.stringify(descriptor, null, 2)};\n`, "utf8");
25
28
  }
26
29
 
30
+ test("resolveLocalPackageSources includes every lock-classified local package regardless of scope or exports", async () => {
31
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-local-resolution-"));
32
+ await mkdir(path.join(tempRoot, ".jskit"), { recursive: true });
33
+ await writeJson(path.join(tempRoot, ".jskit", "lock.json"), {
34
+ lockVersion: 1,
35
+ installedPackages: {
36
+ "@local/main": {
37
+ source: {
38
+ type: "local-package",
39
+ packagePath: "packages/main"
40
+ }
41
+ },
42
+ "@local/feature": {
43
+ source: {
44
+ type: "app-local-package",
45
+ packagePath: "packages/feature"
46
+ }
47
+ },
48
+ "@example/local-utility": {
49
+ source: {
50
+ type: "local-package",
51
+ packagePath: "packages/utility"
52
+ }
53
+ },
54
+ "@example/published": {
55
+ source: {
56
+ type: "npm",
57
+ packagePath: "packages/published"
58
+ }
59
+ }
60
+ }
61
+ });
62
+
63
+ const localPackages = await resolveLocalPackageSources({
64
+ appRoot: tempRoot,
65
+ lockPath: ".jskit/lock.json"
66
+ });
67
+ assert.deepEqual(localPackages.map((entry) => entry.packageId), [
68
+ "@example/local-utility",
69
+ "@local/feature",
70
+ "@local/main"
71
+ ]);
72
+ const mainPackage = localPackages.find((entry) => entry.packageId === "@local/main");
73
+ assert.equal(
74
+ mainPackage.installedPackageRoot,
75
+ path.join(tempRoot, "node_modules", "@local", "main")
76
+ );
77
+ assert.equal(
78
+ mainPackage.sourcePackageRoot,
79
+ path.join(tempRoot, "packages", "main")
80
+ );
81
+ assert.equal(resolveLocalPackageForSpecifier("@local/main/client", localPackages), mainPackage);
82
+ assert.equal(
83
+ resolveLocalPackageForSpecifier("@example/local-utility/shared?raw", localPackages)?.packageId,
84
+ "@example/local-utility"
85
+ );
86
+ assert.equal(resolveLocalPackageForSpecifier("@example/published/client", localPackages), null);
87
+ });
88
+
89
+ test("resolveCanonicalLocalPackageId translates Vite's selected export target to source", () => {
90
+ const localPackage = {
91
+ installedPackageRoot: path.join("/app", "node_modules", "@local", "main"),
92
+ sourcePackageRoot: path.join("/app", "packages", "main")
93
+ };
94
+
95
+ assert.equal(
96
+ resolveCanonicalLocalPackageId(
97
+ "/app/node_modules/@local/main/browser/condition-entry.js?vue&type=script",
98
+ localPackage
99
+ ),
100
+ "/app/packages/main/browser/condition-entry.js?vue&type=script"
101
+ );
102
+ assert.equal(
103
+ resolveCanonicalLocalPackageId("/app/packages/main/browser/already-source.js", localPackage),
104
+ "/app/packages/main/browser/already-source.js"
105
+ );
106
+ assert.equal(resolveCanonicalLocalPackageId("/app/node_modules/published/index.js", localPackage), "");
107
+ });
108
+
27
109
  test("createVirtualModuleSource renders deterministic client module imports", () => {
28
110
  const source = createVirtualModuleSource([
29
111
  {
@@ -401,7 +483,7 @@ test("createJskitClientBootstrapPlugin resolves and loads virtual module", async
401
483
  process.chdir(tempRoot);
402
484
  const plugin = createJskitClientBootstrapPlugin();
403
485
 
404
- const resolvedId = plugin.resolveId(CLIENT_BOOTSTRAP_VIRTUAL_ID);
486
+ const resolvedId = await plugin.resolveId(CLIENT_BOOTSTRAP_VIRTUAL_ID);
405
487
  assert.equal(resolvedId, CLIENT_BOOTSTRAP_RESOLVED_ID);
406
488
 
407
489
  const source = await plugin.load(CLIENT_BOOTSTRAP_RESOLVED_ID);
@@ -412,6 +494,142 @@ test("createJskitClientBootstrapPlugin resolves and loads virtual module", async
412
494
  }
413
495
  });
414
496
 
497
+ test("createJskitClientBootstrapPlugin resolves multiple local packages before Vite dependency resolution", async () => {
498
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-plugin-local-resolution-"));
499
+ const previousCwd = process.cwd();
500
+
501
+ try {
502
+ await mkdir(path.join(tempRoot, ".jskit"), { recursive: true });
503
+ await writeJson(path.join(tempRoot, ".jskit", "lock.json"), {
504
+ lockVersion: 1,
505
+ installedPackages: {
506
+ "@local/main": {
507
+ source: {
508
+ type: "local-package",
509
+ packagePath: "packages/main"
510
+ }
511
+ },
512
+ "@local/feature": {
513
+ source: {
514
+ type: "app-local-package",
515
+ packagePath: "packages/feature"
516
+ }
517
+ },
518
+ "@example/local-utility": {
519
+ source: {
520
+ type: "local-package",
521
+ packagePath: "packages/utility"
522
+ }
523
+ },
524
+ "@example/published": {
525
+ source: {
526
+ type: "npm"
527
+ }
528
+ }
529
+ }
530
+ });
531
+
532
+ const packageFixtures = [
533
+ {
534
+ packageId: "@local/main",
535
+ packageDirectory: "main",
536
+ exports: {
537
+ "./client": "./custom/main-client.js",
538
+ "./shared": "./custom/main-shared.js"
539
+ }
540
+ },
541
+ {
542
+ packageId: "@local/feature",
543
+ packageDirectory: "feature",
544
+ exports: {
545
+ "./client": "./browser/feature-client.js",
546
+ "./shared": "./browser/feature-shared.js"
547
+ }
548
+ },
549
+ {
550
+ packageId: "@example/local-utility",
551
+ packageDirectory: "utility",
552
+ exports: {
553
+ ".": "./browser/utility.js",
554
+ "./shared": "./browser/utility-shared.js"
555
+ }
556
+ }
557
+ ];
558
+ for (const fixture of packageFixtures) {
559
+ const packageJson = {
560
+ name: fixture.packageId,
561
+ exports: fixture.exports
562
+ };
563
+ const sourcePackageRoot = path.join(tempRoot, "packages", fixture.packageDirectory);
564
+ const installedPackageRoot = path.join(tempRoot, "node_modules", ...fixture.packageId.split("/"));
565
+ await mkdir(sourcePackageRoot, { recursive: true });
566
+ await mkdir(installedPackageRoot, { recursive: true });
567
+ await writeJson(path.join(sourcePackageRoot, "package.json"), packageJson);
568
+ await writeJson(path.join(installedPackageRoot, "package.json"), packageJson);
569
+ }
570
+
571
+ const publishedPackageRoot = path.join(tempRoot, "node_modules", "@example", "published");
572
+ await mkdir(publishedPackageRoot, { recursive: true });
573
+ await writeJson(path.join(publishedPackageRoot, "package.json"), {
574
+ name: "@example/published",
575
+ exports: {
576
+ "./client": "./src/client/index.js"
577
+ }
578
+ });
579
+
580
+ process.chdir(tempRoot);
581
+ const plugin = createJskitClientBootstrapPlugin();
582
+ await plugin.config({}, { command: "serve", mode: "development" });
583
+ const viteResolvedIds = new Map([
584
+ ["@local/main/client", path.join(tempRoot, "node_modules", "@local", "main", "custom", "main-client.js")],
585
+ ["@local/main/shared", path.join(tempRoot, "node_modules", "@local", "main", "custom", "main-shared.js")],
586
+ ["@local/feature/client", path.join(tempRoot, "node_modules", "@local", "feature", "browser", "feature-client.js")],
587
+ ["@local/feature/shared", path.join(tempRoot, "node_modules", "@local", "feature", "browser", "feature-shared.js")],
588
+ ["@example/local-utility", path.join(tempRoot, "node_modules", "@example", "local-utility", "browser", "utility.js")],
589
+ ["@example/local-utility/shared", path.join(tempRoot, "node_modules", "@example", "local-utility", "browser", "utility-shared.js")]
590
+ ]);
591
+ plugin.configResolved({
592
+ createResolver(options) {
593
+ assert.equal(options.scan, true);
594
+ return async (source, importer) => {
595
+ assert.equal(importer, undefined);
596
+ return viteResolvedIds.get(source);
597
+ };
598
+ }
599
+ });
600
+ const resolveId = async (source) => plugin.resolveId(source);
601
+
602
+ assert.equal(plugin.enforce, "pre");
603
+ assert.equal(
604
+ await resolveId("@local/main/client"),
605
+ path.join(tempRoot, "packages", "main", "custom", "main-client.js")
606
+ );
607
+ assert.equal(
608
+ await resolveId("@local/main/shared"),
609
+ path.join(tempRoot, "packages", "main", "custom", "main-shared.js")
610
+ );
611
+ assert.equal(
612
+ await resolveId("@local/feature/client"),
613
+ path.join(tempRoot, "packages", "feature", "browser", "feature-client.js")
614
+ );
615
+ assert.equal(
616
+ await resolveId("@local/feature/shared"),
617
+ path.join(tempRoot, "packages", "feature", "browser", "feature-shared.js")
618
+ );
619
+ assert.equal(
620
+ await resolveId("@example/local-utility"),
621
+ path.join(tempRoot, "packages", "utility", "browser", "utility.js")
622
+ );
623
+ assert.equal(
624
+ await resolveId("@example/local-utility/shared"),
625
+ path.join(tempRoot, "packages", "utility", "browser", "utility-shared.js")
626
+ );
627
+ assert.equal(await resolveId("@example/published/client"), null);
628
+ } finally {
629
+ process.chdir(previousCwd);
630
+ }
631
+ });
632
+
415
633
  test("createJskitClientBootstrapPlugin config excludes installed client package specifiers from optimizeDeps", async () => {
416
634
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-config-"));
417
635
  const previousCwd = process.cwd();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/kernel",
3
- "version": "0.1.118",
3
+ "version": "0.1.120",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "json-rest-schema": "1.x.x"
@@ -22,6 +22,19 @@ function nowMilliseconds() {
22
22
  return Date.now();
23
23
  }
24
24
 
25
+ function createProviderLifecycleError(providerId, phase, cause) {
26
+ const message = `Provider \"${providerId}\" failed during ${phase}().`;
27
+ const causeMessage = String(cause?.message || cause || "").trim();
28
+ return new ProviderLifecycleError(
29
+ causeMessage ? `${message} ${causeMessage}` : message,
30
+ {
31
+ providerId,
32
+ phase,
33
+ cause
34
+ }
35
+ );
36
+ }
37
+
25
38
  class Application {
26
39
  constructor({ profile = "", strict = true, container = null } = {}) {
27
40
  this.profile = normalizeText(profile);
@@ -196,11 +209,7 @@ class Application {
196
209
  await entry.provider.register(this);
197
210
  }
198
211
  } catch (error) {
199
- throw new ProviderLifecycleError(`Provider \"${entry.id}\" failed during register().`, {
200
- providerId: entry.id,
201
- phase: "register",
202
- cause: error
203
- });
212
+ throw createProviderLifecycleError(entry.id, "register", error);
204
213
  } finally {
205
214
  this.diagnostics.timings.register[entry.id] = nowMilliseconds() - startedAt;
206
215
  }
@@ -222,11 +231,7 @@ class Application {
222
231
  await entry.provider.boot(this);
223
232
  }
224
233
  } catch (error) {
225
- throw new ProviderLifecycleError(`Provider \"${entry.id}\" failed during boot().`, {
226
- providerId: entry.id,
227
- phase: "boot",
228
- cause: error
229
- });
234
+ throw createProviderLifecycleError(entry.id, "boot", error);
230
235
  } finally {
231
236
  this.diagnostics.timings.boot[entry.id] = nowMilliseconds() - startedAt;
232
237
  }
@@ -255,11 +260,7 @@ class Application {
255
260
  await entry.provider.shutdown(this);
256
261
  }
257
262
  } catch (error) {
258
- throw new ProviderLifecycleError(`Provider \"${entry.id}\" failed during shutdown().`, {
259
- providerId: entry.id,
260
- phase: "shutdown",
261
- cause: error
262
- });
263
+ throw createProviderLifecycleError(entry.id, "shutdown", error);
263
264
  } finally {
264
265
  this.diagnostics.timings.shutdown[entry.id] = nowMilliseconds() - startedAt;
265
266
  }
@@ -1,5 +1,8 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { spawnSync } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
3
6
  import process from "node:process";
4
7
  import test from "node:test";
5
8
  import { fileURLToPath } from "node:url";
@@ -19,30 +22,35 @@ test("KernelError preserves structured details while exposing the standard cause
19
22
  assert.equal(error.details.providerId, "example.provider");
20
23
  });
21
24
 
22
- test("provider lifecycle failures expose their standard cause through Node error reporting", () => {
25
+ test("provider lifecycle failures expose their cause in Node test output", async (context) => {
26
+ const fixtureRoot = await mkdtemp(path.join(tmpdir(), "jskit-kernel-error-"));
27
+ context.after(() => rm(fixtureRoot, { recursive: true, force: true }));
28
+ const fixturePath = path.join(fixtureRoot, "provider-lifecycle-failure.test.mjs");
23
29
  const source = `
24
30
  import { Application } from ${JSON.stringify(APPLICATION_PATH)};
31
+ import test from "node:test";
25
32
 
26
- const app = new Application();
27
- app.configureProviders([{
28
- id: "json-rest-api.core",
29
- async boot() {
30
- throw new Error('DB_CLIENT is required. Use mysql2 or pg.');
31
- }
32
- }]);
33
-
34
- try {
33
+ test("generated server starts", async () => {
34
+ const app = new Application();
35
+ app.configureProviders([{
36
+ id: "json-rest-api.core",
37
+ async boot() {
38
+ throw new Error("DB_CLIENT is required. Use mysql2 or pg.");
39
+ }
40
+ }]);
35
41
  await app.bootProviders();
36
- } catch (error) {
37
- console.error("Failed to start generated server:", error);
38
- }
42
+ });
39
43
  `;
40
- const result = spawnSync(process.execPath, ["--input-type=module", "--eval", source], {
41
- encoding: "utf8"
44
+ await writeFile(fixturePath, source, "utf8");
45
+ const childEnvironment = { ...process.env };
46
+ delete childEnvironment.NODE_TEST_CONTEXT;
47
+ const result = spawnSync(process.execPath, ["--test", fixturePath], {
48
+ encoding: "utf8",
49
+ env: childEnvironment
42
50
  });
51
+ const output = `${String(result.stdout || "")}\n${String(result.stderr || "")}`;
43
52
 
44
- assert.equal(result.status, 0, String(result.stderr || ""));
45
- assert.match(result.stderr, /Failed to start generated server:/u);
46
- assert.match(result.stderr, /Provider "json-rest-api\.core" failed during boot\(\)\./u);
47
- assert.match(result.stderr, /DB_CLIENT is required\. Use mysql2 or pg\./u);
53
+ assert.equal(result.status, 1, output);
54
+ assert.match(output, /Provider "json-rest-api\.core" failed during boot\(\)\./u);
55
+ assert.match(output, /DB_CLIENT is required\. Use mysql2 or pg\./u);
48
56
  });