@jskit-ai/kernel 0.1.156 → 0.1.158

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.
@@ -4,7 +4,7 @@ import {
4
4
  collectRootDependencySpecifiers,
5
5
  discoverInstalledPackages
6
6
  } from "../../internal/node/installedPackages.js";
7
- import { normalizeObject } from "../../shared/support/normalize.js";
7
+ import { isRecord, normalizeObject } from "../../shared/support/normalize.js";
8
8
  import { sortStrings } from "../../shared/support/sorting.js";
9
9
  import {
10
10
  normalizePackageMetadataClientProviders,
@@ -132,8 +132,7 @@ function resolveCanonicalLocalPackageId(resolvedId, localPackage) {
132
132
  : "";
133
133
  }
134
134
 
135
- async function resolveInstalledClientModules({ appRoot }) {
136
- const installedPackages = await discoverInstalledPackages({ appRoot });
135
+ function resolveClientModulesFromInstalledPackages(installedPackages = []) {
137
136
  const modules = [];
138
137
  for (const installedPackage of installedPackages) {
139
138
  if (!hasClientExport(installedPackage.packageJson)) {
@@ -156,6 +155,85 @@ async function resolveInstalledClientModules({ appRoot }) {
156
155
  return Object.freeze(modules);
157
156
  }
158
157
 
158
+ async function resolveInstalledClientModules({ appRoot }) {
159
+ return resolveClientModulesFromInstalledPackages(
160
+ await discoverInstalledPackages({ appRoot })
161
+ );
162
+ }
163
+
164
+ function resolveInstalledViteProxyEntries(installedPackages = [], { proxyTarget = "" } = {}) {
165
+ const fallbackTarget = String(proxyTarget || "").trim();
166
+ const proxyEntries = {};
167
+ const ownerByPath = new Map();
168
+
169
+ for (const installedPackage of [...installedPackages].sort((left, right) =>
170
+ String(left?.packageId || "").localeCompare(String(right?.packageId || "")))) {
171
+ const packageId = String(installedPackage?.packageId || "").trim();
172
+ const vite = installedPackage?.packageMetadata?.vite;
173
+ if (vite == null) {
174
+ continue;
175
+ }
176
+ if (!isRecord(vite) || (vite.proxy != null && !isRecord(vite.proxy))) {
177
+ throw new Error(`${packageId} package.json#jskit.vite.proxy must be an object.`);
178
+ }
179
+ const proxy = normalizeObject(vite.proxy);
180
+
181
+ for (const routePath of sortStrings(Object.keys(proxy))) {
182
+ if (!routePath.startsWith("/") || routePath.startsWith("//")) {
183
+ throw new Error(`Vite proxy path ${JSON.stringify(routePath)} from ${packageId} must start with one "/".`);
184
+ }
185
+ if (ownerByPath.has(routePath)) {
186
+ throw new Error(
187
+ `Vite proxy path ${JSON.stringify(routePath)} is declared by both ${ownerByPath.get(routePath)} and ${packageId}.`
188
+ );
189
+ }
190
+
191
+ const config = proxy[routePath];
192
+ if (!isRecord(config)) {
193
+ throw new Error(
194
+ `Vite proxy path ${JSON.stringify(routePath)} from ${packageId} must be an object.`
195
+ );
196
+ }
197
+ const unknownFields = Object.keys(config)
198
+ .filter((field) => !["changeOrigin", "target", "ws"].includes(field))
199
+ .sort();
200
+ if (unknownFields.length > 0) {
201
+ throw new Error(
202
+ `Vite proxy path ${JSON.stringify(routePath)} from ${packageId} has unsupported ${unknownFields.length === 1 ? "field" : "fields"}: ${unknownFields.join(", ")}.`
203
+ );
204
+ }
205
+ for (const field of ["changeOrigin", "ws"]) {
206
+ if (Object.hasOwn(config, field) && typeof config[field] !== "boolean") {
207
+ throw new Error(
208
+ `Vite proxy path ${JSON.stringify(routePath)} from ${packageId} requires ${field} to be boolean.`
209
+ );
210
+ }
211
+ }
212
+ if (Object.hasOwn(config, "target") && typeof config.target !== "string") {
213
+ throw new Error(
214
+ `Vite proxy path ${JSON.stringify(routePath)} from ${packageId} requires target to be a string.`
215
+ );
216
+ }
217
+
218
+ const target = String(config.target || "").trim() || fallbackTarget;
219
+ if (!target) {
220
+ throw new Error(
221
+ `Vite proxy path ${JSON.stringify(routePath)} from ${packageId} requires target or createJskitClientBootstrapPlugin({ proxyTarget }).`
222
+ );
223
+ }
224
+
225
+ ownerByPath.set(routePath, packageId);
226
+ proxyEntries[routePath] = {
227
+ target,
228
+ ...(Object.hasOwn(config, "changeOrigin") ? { changeOrigin: config.changeOrigin === true } : {}),
229
+ ...(Object.hasOwn(config, "ws") ? { ws: config.ws === true } : {})
230
+ };
231
+ }
232
+ }
233
+
234
+ return proxyEntries;
235
+ }
236
+
159
237
  async function resolveInstalledClientPackageIds(options) {
160
238
  const modules = await resolveInstalledClientModules(options);
161
239
  return Object.freeze(modules.map((entry) => entry.packageId));
@@ -269,7 +347,7 @@ function resolveClientRuntimeDedupeSpecifiers(userResolveConfig = {}) {
269
347
  return sortStrings([...userDedupe, ...CLIENT_RUNTIME_DEDUPE_SPECIFIERS]);
270
348
  }
271
349
 
272
- function createJskitClientBootstrapPlugin() {
350
+ function createJskitClientBootstrapPlugin({ proxyTarget = "" } = {}) {
273
351
  let appRoot = process.cwd();
274
352
  let localPackages = Object.freeze([]);
275
353
  let resolvePackageSpecifier = null;
@@ -280,8 +358,10 @@ function createJskitClientBootstrapPlugin() {
280
358
  enforce: "pre",
281
359
  async config(userConfig = {}) {
282
360
  appRoot = process.cwd();
283
- const clientModules = await resolveInstalledClientModules({
284
- appRoot
361
+ const installedPackages = await discoverInstalledPackages({ appRoot });
362
+ const clientModules = resolveClientModulesFromInstalledPackages(installedPackages);
363
+ const installedProxyEntries = resolveInstalledViteProxyEntries(installedPackages, {
364
+ proxyTarget
285
365
  });
286
366
  const localScopePackageIds = await resolveLocalScopePackageIds({
287
367
  appRoot
@@ -299,6 +379,8 @@ function createJskitClientBootstrapPlugin() {
299
379
  const clientIncludeSpecifiers = resolveClientOptimizeIncludeSpecifiers(clientModules, exclude);
300
380
  const include = sortStrings([...userInclude, ...clientIncludeSpecifiers].filter((specifier) => !exclude.includes(specifier)));
301
381
  const dedupe = resolveClientRuntimeDedupeSpecifiers(userResolve);
382
+ const userServer = normalizeObject(userConfig.server);
383
+ const userProxyEntries = normalizeObject(userServer.proxy);
302
384
 
303
385
  return {
304
386
  optimizeDeps: {
@@ -309,6 +391,12 @@ function createJskitClientBootstrapPlugin() {
309
391
  resolve: {
310
392
  ...userResolve,
311
393
  dedupe
394
+ },
395
+ server: {
396
+ proxy: {
397
+ ...installedProxyEntries,
398
+ ...userProxyEntries
399
+ }
312
400
  }
313
401
  };
314
402
  },
@@ -368,5 +456,6 @@ export {
368
456
  resolveInstalledClientPackageIds,
369
457
  resolveLocalScopePackageIds,
370
458
  resolveInstalledClientModules,
459
+ resolveInstalledViteProxyEntries,
371
460
  createJskitClientBootstrapPlugin
372
461
  };
@@ -16,7 +16,8 @@ import {
16
16
  resolveClientOptimizeExcludeSpecifiers,
17
17
  resolveLocalScopePackageIds,
18
18
  resolveInstalledClientPackageIds,
19
- resolveInstalledClientModules
19
+ resolveInstalledClientModules,
20
+ resolveInstalledViteProxyEntries
20
21
  } from "./clientBootstrapPlugin.js";
21
22
 
22
23
  async function writeJson(filePath, value) {
@@ -440,6 +441,69 @@ test("resolveInstalledClientModules resolves a packageMetadata from a file depen
440
441
  assert.equal(modules[0].packageMetadataClientProviders[0].export, "LocalClientProvider");
441
442
  });
442
443
 
444
+ test("resolveInstalledViteProxyEntries derives proxy config directly from package metadata", () => {
445
+ const proxy = resolveInstalledViteProxyEntries(
446
+ [
447
+ {
448
+ packageId: "@example/realtime",
449
+ packageMetadata: {
450
+ vite: {
451
+ proxy: {
452
+ "/socket.io": {
453
+ changeOrigin: true,
454
+ ws: true
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+ ],
461
+ {
462
+ proxyTarget: "http://localhost:3000"
463
+ }
464
+ );
465
+
466
+ assert.deepEqual(proxy, {
467
+ "/socket.io": {
468
+ target: "http://localhost:3000",
469
+ changeOrigin: true,
470
+ ws: true
471
+ }
472
+ });
473
+ proxy["/socket.io"].prependPath = false;
474
+ assert.equal(proxy["/socket.io"].prependPath, false);
475
+ });
476
+
477
+ test("resolveInstalledViteProxyEntries rejects ambiguous package proxy paths", () => {
478
+ assert.throws(
479
+ () => resolveInstalledViteProxyEntries(
480
+ ["@example/alpha", "@example/beta"].map((packageId) => ({
481
+ packageId,
482
+ packageMetadata: {
483
+ vite: {
484
+ proxy: {
485
+ "/socket.io": { target: "http://localhost:3000" }
486
+ }
487
+ }
488
+ }
489
+ }))
490
+ ),
491
+ /declared by both @example\/alpha and @example\/beta/u
492
+ );
493
+ });
494
+
495
+ test("resolveInstalledViteProxyEntries rejects malformed package proxy metadata", () => {
496
+ assert.throws(
497
+ () => resolveInstalledViteProxyEntries([
498
+ {
499
+ packageId: "@example/realtime",
500
+ packageMetadata: { vite: { proxy: { "/socket.io": { ws: "yes" } } } }
501
+ }
502
+ ], { proxyTarget: "http://localhost:3000" }),
503
+ /requires ws to be boolean/u
504
+ );
505
+ });
506
+
443
507
  test("resolveLocalScopePackageIds reads @local packages from package.json", async () => {
444
508
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-local-scope-"));
445
509
  await writeJson(path.join(tempRoot, "package.json"), {
@@ -489,6 +553,57 @@ test("createJskitClientBootstrapPlugin resolves and loads virtual module", async
489
553
  }
490
554
  });
491
555
 
556
+ test("createJskitClientBootstrapPlugin contributes installed package proxies without generated state", async () => {
557
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-proxy-"));
558
+ const previousCwd = process.cwd();
559
+
560
+ try {
561
+ await declareInstalledPackages(tempRoot, { "@example/realtime": {} });
562
+ const packageRoot = path.join(tempRoot, "node_modules", "@example", "realtime");
563
+ await writePackageMetadata(packageRoot, {
564
+ packageId: "@example/realtime",
565
+ version: "1.0.0",
566
+ vite: {
567
+ proxy: {
568
+ "/socket.io": {
569
+ changeOrigin: true,
570
+ ws: true
571
+ }
572
+ }
573
+ }
574
+ });
575
+
576
+ process.chdir(tempRoot);
577
+ const plugin = createJskitClientBootstrapPlugin({
578
+ proxyTarget: "http://localhost:3000"
579
+ });
580
+ const config = await plugin.config({
581
+ server: {
582
+ proxy: {
583
+ "/api": {
584
+ target: "http://localhost:3000",
585
+ changeOrigin: true
586
+ }
587
+ }
588
+ }
589
+ });
590
+
591
+ assert.deepEqual(config.server.proxy, {
592
+ "/api": {
593
+ target: "http://localhost:3000",
594
+ changeOrigin: true
595
+ },
596
+ "/socket.io": {
597
+ target: "http://localhost:3000",
598
+ changeOrigin: true,
599
+ ws: true
600
+ }
601
+ });
602
+ } finally {
603
+ process.chdir(previousCwd);
604
+ }
605
+ });
606
+
492
607
  test("createJskitClientBootstrapPlugin resolves multiple local packages before Vite dependency resolution", async () => {
493
608
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-plugin-local-resolution-"));
494
609
  const previousCwd = process.cwd();
@@ -19,7 +19,8 @@ const JSKIT_PACKAGE_CONFIG_KEYS = Object.freeze([
19
19
  "mutations",
20
20
  "optionPolicies",
21
21
  "options",
22
- "runtime"
22
+ "runtime",
23
+ "vite"
23
24
  ]);
24
25
 
25
26
  async function readJsonFile(filePath, fallback = {}) {
@@ -39,7 +39,8 @@ test("createPackageMetadata accepts only the canonical package.json.jskit fields
39
39
  "mutations",
40
40
  "optionPolicies",
41
41
  "options",
42
- "runtime"
42
+ "runtime",
43
+ "vite"
43
44
  ]);
44
45
  assert.throws(
45
46
  () => createPackageMetadata({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/kernel",
3
- "version": "0.1.156",
3
+ "version": "0.1.158",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "json-rest-schema": "^1.0.17"
@@ -4,7 +4,7 @@ export {
4
4
  KernelError,
5
5
  ProviderNormalizationError,
6
6
  DuplicateProviderError,
7
- ProviderDependencyError,
7
+ ProviderStartOrderError,
8
8
  ProviderLifecycleError
9
9
  } from "../../shared/runtime/kernelErrors.js";
10
10
  export { KernelCoreServiceProvider } from "./KernelCoreServiceProvider.js";
@@ -1,7 +1,7 @@
1
1
  import { createContainer } from "./container.js";
2
2
  import {
3
3
  DuplicateProviderError,
4
- ProviderDependencyError,
4
+ ProviderStartOrderError,
5
5
  ProviderLifecycleError,
6
6
  ProviderNormalizationError
7
7
  } from "./kernelErrors.js";
@@ -133,7 +133,7 @@ class Application {
133
133
 
134
134
  return {
135
135
  id: providerId,
136
- dependsOn: normalizeStringArray(rawProvider.dependsOn || providerInstance.dependsOn),
136
+ startsAfter: normalizeStringArray(rawProvider.startsAfter ?? providerInstance.startsAfter),
137
137
  provider: providerInstance
138
138
  };
139
139
  }
@@ -147,7 +147,7 @@ class Application {
147
147
 
148
148
  return {
149
149
  id: providerId,
150
- dependsOn: normalizeStringArray(provider.dependsOn || provider.constructor?.dependsOn),
150
+ startsAfter: normalizeStringArray(provider.startsAfter ?? provider.constructor?.startsAfter),
151
151
  provider
152
152
  };
153
153
  }
@@ -155,7 +155,7 @@ class Application {
155
155
  throw new ProviderNormalizationError("Provider entry must be a class or object instance.");
156
156
  }
157
157
 
158
- sortProviderGraph(entries = []) {
158
+ sortProviderStartOrder(entries = []) {
159
159
  const byId = new Map(entries.map((entry) => [entry.id, entry]));
160
160
  const visited = new Set();
161
161
  const visiting = new Set();
@@ -166,17 +166,17 @@ class Application {
166
166
  return;
167
167
  }
168
168
  if (visiting.has(providerId)) {
169
- throw new ProviderDependencyError(`Provider dependency cycle detected: ${[...lineage, providerId].join(" -> ")}`);
169
+ throw new ProviderStartOrderError(`Provider start-order cycle detected: ${[...lineage, providerId].join(" -> ")}`);
170
170
  }
171
171
 
172
172
  const entry = byId.get(providerId);
173
173
  if (!entry) {
174
- throw new ProviderDependencyError(`Provider \"${lineage[lineage.length - 1] || "<unknown>"}\" depends on missing provider \"${providerId}\".`);
174
+ throw new ProviderStartOrderError(`Provider \"${lineage[lineage.length - 1] || "<unknown>"}\" starts after missing provider \"${providerId}\".`);
175
175
  }
176
176
 
177
177
  visiting.add(providerId);
178
- for (const dependencyId of entry.dependsOn) {
179
- visit(dependencyId, [...lineage, providerId]);
178
+ for (const earlierProviderId of entry.startsAfter) {
179
+ visit(earlierProviderId, [...lineage, providerId]);
180
180
  }
181
181
  visiting.delete(providerId);
182
182
  visited.add(providerId);
@@ -192,7 +192,7 @@ class Application {
192
192
 
193
193
  configureProviders(providers = []) {
194
194
  const normalized = this.normalizeProviderEntries(providers);
195
- const ordered = this.sortProviderGraph(normalized);
195
+ const ordered = this.sortProviderStartOrder(normalized);
196
196
 
197
197
  this.providerEntries = ordered;
198
198
  this.diagnostics.providerOrder = ordered.map((entry) => entry.id);
@@ -288,7 +288,7 @@ function createApplication(options = {}) {
288
288
  return new Application(options);
289
289
  }
290
290
 
291
- function createProviderClass({ id, dependsOn = [], register = null, boot = null, shutdown = null } = {}) {
291
+ function createProviderClass({ id, startsAfter = [], register = null, boot = null, shutdown = null } = {}) {
292
292
  const providerId = normalizeText(id);
293
293
  if (!providerId) {
294
294
  throw new ProviderNormalizationError("createProviderClass requires id.");
@@ -297,7 +297,7 @@ function createProviderClass({ id, dependsOn = [], register = null, boot = null,
297
297
  class DynamicProvider extends ServiceProvider {
298
298
  static id = providerId;
299
299
 
300
- static dependsOn = normalizeStringArray(dependsOn);
300
+ static startsAfter = normalizeStringArray(startsAfter);
301
301
 
302
302
  async register(app) {
303
303
  if (typeof register === "function") {
@@ -13,6 +13,6 @@ export {
13
13
  KernelError,
14
14
  ProviderNormalizationError,
15
15
  DuplicateProviderError,
16
- ProviderDependencyError,
16
+ ProviderStartOrderError,
17
17
  ProviderLifecycleError
18
18
  } from "./kernelErrors.js";
@@ -13,13 +13,13 @@ class KernelError extends Error {
13
13
 
14
14
  class ProviderNormalizationError extends KernelError {}
15
15
  class DuplicateProviderError extends KernelError {}
16
- class ProviderDependencyError extends KernelError {}
16
+ class ProviderStartOrderError extends KernelError {}
17
17
  class ProviderLifecycleError extends KernelError {}
18
18
 
19
19
  export {
20
20
  KernelError,
21
21
  ProviderNormalizationError,
22
22
  DuplicateProviderError,
23
- ProviderDependencyError,
23
+ ProviderStartOrderError,
24
24
  ProviderLifecycleError
25
25
  };
@@ -6,7 +6,8 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import test from "node:test";
8
8
  import { fileURLToPath } from "node:url";
9
- import { KernelError } from "./kernelErrors.js";
9
+ import { Application, createProviderClass } from "./application.js";
10
+ import { KernelError, ProviderStartOrderError } from "./kernelErrors.js";
10
11
 
11
12
  const APPLICATION_PATH = fileURLToPath(new URL("./application.js", import.meta.url));
12
13
 
@@ -22,6 +23,69 @@ test("KernelError preserves structured details while exposing the standard cause
22
23
  assert.equal(error.details.providerId, "example.provider");
23
24
  });
24
25
 
26
+ test("startsAfter orders provider registration, boot, and shutdown", async () => {
27
+ const calls = [];
28
+ const EarlierProvider = createProviderClass({
29
+ id: "example.earlier",
30
+ register() {
31
+ calls.push("register:earlier");
32
+ },
33
+ boot() {
34
+ calls.push("boot:earlier");
35
+ },
36
+ shutdown() {
37
+ calls.push("shutdown:earlier");
38
+ }
39
+ });
40
+ const LaterProvider = createProviderClass({
41
+ id: "example.later",
42
+ startsAfter: ["example.earlier"],
43
+ register() {
44
+ calls.push("register:later");
45
+ },
46
+ boot() {
47
+ calls.push("boot:later");
48
+ },
49
+ shutdown() {
50
+ calls.push("shutdown:later");
51
+ }
52
+ });
53
+
54
+ const app = new Application();
55
+ await app.start({ providers: [LaterProvider, EarlierProvider] });
56
+ await app.shutdown();
57
+
58
+ assert.deepEqual(calls, [
59
+ "register:earlier",
60
+ "register:later",
61
+ "boot:earlier",
62
+ "boot:later",
63
+ "shutdown:later",
64
+ "shutdown:earlier"
65
+ ]);
66
+ });
67
+
68
+ test("startsAfter rejects missing providers and start-order cycles", () => {
69
+ const missingApp = new Application();
70
+ assert.throws(
71
+ () => missingApp.configureProviders([{ id: "example.later", startsAfter: ["example.missing"] }]),
72
+ (error) =>
73
+ error instanceof ProviderStartOrderError &&
74
+ /starts after missing provider "example\.missing"/u.test(error.message)
75
+ );
76
+
77
+ const cycleApp = new Application();
78
+ assert.throws(
79
+ () => cycleApp.configureProviders([
80
+ { id: "example.alpha", startsAfter: ["example.beta"] },
81
+ { id: "example.beta", startsAfter: ["example.alpha"] }
82
+ ]),
83
+ (error) =>
84
+ error instanceof ProviderStartOrderError &&
85
+ /Provider start-order cycle detected/u.test(error.message)
86
+ );
87
+ });
88
+
25
89
  test("provider lifecycle failures expose their cause in Node test output", async (context) => {
26
90
  const fixtureRoot = await mkdtemp(path.join(tmpdir(), "jskit-kernel-error-"));
27
91
  context.after(() => rm(fixtureRoot, { recursive: true, force: true }));