@jskit-ai/kernel 0.1.155 → 0.1.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/vite/clientBootstrapPlugin.js +95 -6
- package/client/vite/clientBootstrapPlugin.test.js +114 -1
- package/internal/node/installedPackages.js +2 -1
- package/internal/node/installedPackages.test.js +2 -1
- package/package.json +1 -4
- package/server/kernel/index.js +1 -1
- package/shared/runtime/application.js +11 -11
- package/shared/runtime/index.js +1 -1
- package/shared/runtime/kernelErrors.js +2 -2
- package/shared/runtime/kernelErrors.test.js +65 -1
- package/shared/support/crudFieldContract.js +0 -322
- package/shared/support/crudFieldContract.test.js +0 -67
- package/shared/support/crudListFilters.js +0 -855
- package/shared/support/crudListFilters.test.js +0 -323
- package/shared/support/crudLookup.js +0 -190
- package/shared/support/crudLookup.test.js +0 -256
|
@@ -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
|
-
|
|
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] = Object.freeze({
|
|
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 Object.freeze(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
|
|
284
|
-
|
|
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,67 @@ 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
|
+
});
|
|
474
|
+
|
|
475
|
+
test("resolveInstalledViteProxyEntries rejects ambiguous package proxy paths", () => {
|
|
476
|
+
assert.throws(
|
|
477
|
+
() => resolveInstalledViteProxyEntries(
|
|
478
|
+
["@example/alpha", "@example/beta"].map((packageId) => ({
|
|
479
|
+
packageId,
|
|
480
|
+
packageMetadata: {
|
|
481
|
+
vite: {
|
|
482
|
+
proxy: {
|
|
483
|
+
"/socket.io": { target: "http://localhost:3000" }
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}))
|
|
488
|
+
),
|
|
489
|
+
/declared by both @example\/alpha and @example\/beta/u
|
|
490
|
+
);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("resolveInstalledViteProxyEntries rejects malformed package proxy metadata", () => {
|
|
494
|
+
assert.throws(
|
|
495
|
+
() => resolveInstalledViteProxyEntries([
|
|
496
|
+
{
|
|
497
|
+
packageId: "@example/realtime",
|
|
498
|
+
packageMetadata: { vite: { proxy: { "/socket.io": { ws: "yes" } } } }
|
|
499
|
+
}
|
|
500
|
+
], { proxyTarget: "http://localhost:3000" }),
|
|
501
|
+
/requires ws to be boolean/u
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
|
|
443
505
|
test("resolveLocalScopePackageIds reads @local packages from package.json", async () => {
|
|
444
506
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-local-scope-"));
|
|
445
507
|
await writeJson(path.join(tempRoot, "package.json"), {
|
|
@@ -489,6 +551,57 @@ test("createJskitClientBootstrapPlugin resolves and loads virtual module", async
|
|
|
489
551
|
}
|
|
490
552
|
});
|
|
491
553
|
|
|
554
|
+
test("createJskitClientBootstrapPlugin contributes installed package proxies without generated state", async () => {
|
|
555
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-proxy-"));
|
|
556
|
+
const previousCwd = process.cwd();
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
await declareInstalledPackages(tempRoot, { "@example/realtime": {} });
|
|
560
|
+
const packageRoot = path.join(tempRoot, "node_modules", "@example", "realtime");
|
|
561
|
+
await writePackageMetadata(packageRoot, {
|
|
562
|
+
packageId: "@example/realtime",
|
|
563
|
+
version: "1.0.0",
|
|
564
|
+
vite: {
|
|
565
|
+
proxy: {
|
|
566
|
+
"/socket.io": {
|
|
567
|
+
changeOrigin: true,
|
|
568
|
+
ws: true
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
process.chdir(tempRoot);
|
|
575
|
+
const plugin = createJskitClientBootstrapPlugin({
|
|
576
|
+
proxyTarget: "http://localhost:3000"
|
|
577
|
+
});
|
|
578
|
+
const config = await plugin.config({
|
|
579
|
+
server: {
|
|
580
|
+
proxy: {
|
|
581
|
+
"/api": {
|
|
582
|
+
target: "http://localhost:3000",
|
|
583
|
+
changeOrigin: true
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
assert.deepEqual(config.server.proxy, {
|
|
590
|
+
"/api": {
|
|
591
|
+
target: "http://localhost:3000",
|
|
592
|
+
changeOrigin: true
|
|
593
|
+
},
|
|
594
|
+
"/socket.io": {
|
|
595
|
+
target: "http://localhost:3000",
|
|
596
|
+
changeOrigin: true,
|
|
597
|
+
ws: true
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
} finally {
|
|
601
|
+
process.chdir(previousCwd);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
492
605
|
test("createJskitClientBootstrapPlugin resolves multiple local packages before Vite dependency resolution", async () => {
|
|
493
606
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "jskit-client-bootstrap-plugin-local-resolution-"));
|
|
494
607
|
const previousCwd = process.cwd();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/kernel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.157",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"json-rest-schema": "^1.0.17"
|
|
@@ -44,9 +44,6 @@
|
|
|
44
44
|
"./shared/support/tokens": "./shared/support/tokens.js",
|
|
45
45
|
"./shared/support/normalize": "./shared/support/normalize.js",
|
|
46
46
|
"./shared/support/permissions": "./shared/support/permissions.js",
|
|
47
|
-
"./shared/support/crudListFilters": "./shared/support/crudListFilters.js",
|
|
48
|
-
"./shared/support/crudFieldContract": "./shared/support/crudFieldContract.js",
|
|
49
|
-
"./shared/support/crudLookup": "./shared/support/crudLookup.js",
|
|
50
47
|
"./shared/support/jsonApiFieldsets": "./shared/support/jsonApiFieldsets.js",
|
|
51
48
|
"./shared/support/generatedUiContract": "./shared/support/generatedUiContract.js",
|
|
52
49
|
"./shared/support/deepFreeze": "./shared/support/deepFreeze.js",
|
package/server/kernel/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
KernelError,
|
|
5
5
|
ProviderNormalizationError,
|
|
6
6
|
DuplicateProviderError,
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
179
|
-
visit(
|
|
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.
|
|
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,
|
|
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
|
|
300
|
+
static startsAfter = normalizeStringArray(startsAfter);
|
|
301
301
|
|
|
302
302
|
async register(app) {
|
|
303
303
|
if (typeof register === "function") {
|
package/shared/runtime/index.js
CHANGED
|
@@ -13,13 +13,13 @@ class KernelError extends Error {
|
|
|
13
13
|
|
|
14
14
|
class ProviderNormalizationError extends KernelError {}
|
|
15
15
|
class DuplicateProviderError extends KernelError {}
|
|
16
|
-
class
|
|
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
|
-
|
|
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 {
|
|
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 }));
|