@raubjo/architect-core 0.1.0
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/bun.lock +20 -0
- package/coverage/lcov.info +1078 -0
- package/package.json +43 -0
- package/src/cache/cache.ts +3 -0
- package/src/cache/manager.ts +115 -0
- package/src/config/app.ts +5 -0
- package/src/config/clone.ts +9 -0
- package/src/config/env.global.d.ts +5 -0
- package/src/config/env.ts +79 -0
- package/src/config/repository.ts +204 -0
- package/src/filesystem/adapters/local.ts +104 -0
- package/src/filesystem/filesystem.ts +21 -0
- package/src/foundation/application.ts +207 -0
- package/src/index.ts +33 -0
- package/src/rendering/adapters/react.tsx +27 -0
- package/src/rendering/renderer.ts +13 -0
- package/src/runtimes/react.tsx +22 -0
- package/src/storage/adapters/indexed-db.ts +180 -0
- package/src/storage/adapters/local-storage.ts +46 -0
- package/src/storage/adapters/memory.ts +35 -0
- package/src/storage/manager.ts +78 -0
- package/src/storage/storage.ts +8 -0
- package/src/support/facades/cache.ts +46 -0
- package/src/support/facades/config.ts +67 -0
- package/src/support/facades/facade.ts +42 -0
- package/src/support/facades/storage.ts +46 -0
- package/src/support/providers/config-service-provider.ts +19 -0
- package/src/support/service-provider.ts +25 -0
- package/src/support/str.ts +126 -0
- package/tests/application.test.ts +236 -0
- package/tests/cache-facade.test.ts +45 -0
- package/tests/cache.test.ts +68 -0
- package/tests/config-clone.test.ts +31 -0
- package/tests/config-env.test.ts +88 -0
- package/tests/config-facade.test.ts +96 -0
- package/tests/config-repository.test.ts +124 -0
- package/tests/facade-base.test.ts +80 -0
- package/tests/filesystem.test.ts +81 -0
- package/tests/runtime-react.test.tsx +37 -0
- package/tests/service-provider.test.ts +23 -0
- package/tests/storage-facade.test.ts +46 -0
- package/tests/storage.test.ts +264 -0
- package/tests/str.test.ts +73 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ConfigRepository from "../src/config/repository";
|
|
3
|
+
import CacheManager from "../src/cache/manager";
|
|
4
|
+
|
|
5
|
+
describe("CacheManager", () => {
|
|
6
|
+
test("uses cache.default and configured stores", async () => {
|
|
7
|
+
const manager = CacheManager.fromConfig(
|
|
8
|
+
new ConfigRepository({
|
|
9
|
+
cache: {
|
|
10
|
+
default: "persistent",
|
|
11
|
+
stores: {
|
|
12
|
+
persistent: { driver: "memory" },
|
|
13
|
+
fast: { driver: "memory" },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
await manager.set("key", "value");
|
|
20
|
+
expect(await manager.get("key")).toBe("value");
|
|
21
|
+
expect(await manager.has("key")).toBe(true);
|
|
22
|
+
expect(await manager.keys()).toEqual(["key"]);
|
|
23
|
+
await manager.delete("key");
|
|
24
|
+
expect(await manager.get("key")).toBeNull();
|
|
25
|
+
await manager.set("other", 1);
|
|
26
|
+
await manager.clear();
|
|
27
|
+
expect(await manager.keys()).toEqual([]);
|
|
28
|
+
|
|
29
|
+
manager.use("fast");
|
|
30
|
+
await manager.set("fast-key", 123);
|
|
31
|
+
expect(await manager.get("fast-key")).toBe(123);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("falls back to default stores and memory default", async () => {
|
|
35
|
+
const manager = CacheManager.fromConfig(new ConfigRepository({}));
|
|
36
|
+
await manager.set("a", 1);
|
|
37
|
+
expect(await manager.get("a")).toBe(1);
|
|
38
|
+
expect(manager.store("memory")).toBeTruthy();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolves missing driver/store behavior", () => {
|
|
42
|
+
const manager = CacheManager.fromConfig(
|
|
43
|
+
new ConfigRepository({
|
|
44
|
+
cache: {
|
|
45
|
+
default: "missing",
|
|
46
|
+
stores: {
|
|
47
|
+
only: { driver: "unknown" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(manager.store()).toBeTruthy();
|
|
54
|
+
expect(() => manager.store("nope")).toThrow("Cache store [nope] is not defined.");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("handles non-object cache.stores by using defaults", () => {
|
|
58
|
+
const manager = CacheManager.fromConfig(
|
|
59
|
+
new ConfigRepository({
|
|
60
|
+
cache: {
|
|
61
|
+
stores: "bad-value",
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(manager.store("memory")).toBeTruthy();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { cloneConfigItems } from "../src/config/clone";
|
|
3
|
+
|
|
4
|
+
describe("cloneConfigItems", () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
(globalThis as { structuredClone?: typeof structuredClone }).structuredClone =
|
|
7
|
+
originalStructuredClone;
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const originalStructuredClone = globalThis.structuredClone;
|
|
11
|
+
|
|
12
|
+
test("uses structuredClone when available", () => {
|
|
13
|
+
const input = { app: { name: "IOC" } };
|
|
14
|
+
|
|
15
|
+
const cloned = cloneConfigItems(input);
|
|
16
|
+
expect(cloned).toEqual(input);
|
|
17
|
+
expect(cloned).not.toBe(input);
|
|
18
|
+
expect(cloned.app).not.toBe(input.app);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("falls back to JSON clone when structuredClone is unavailable", () => {
|
|
22
|
+
(globalThis as { structuredClone?: typeof structuredClone }).structuredClone = undefined;
|
|
23
|
+
|
|
24
|
+
const input = { app: { name: "IOC" } };
|
|
25
|
+
const cloned = cloneConfigItems(input);
|
|
26
|
+
|
|
27
|
+
expect(cloned).toEqual(input);
|
|
28
|
+
expect(cloned).not.toBe(input);
|
|
29
|
+
expect(cloned.app).not.toBe(input.app);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { __envTesting, env, registerGlobalEnv } from "../src/config/env";
|
|
3
|
+
|
|
4
|
+
describe("env helper", () => {
|
|
5
|
+
const originalProcess = (globalThis as { process?: unknown }).process;
|
|
6
|
+
const originalGlobalEnv = (globalThis as { env?: unknown }).env;
|
|
7
|
+
const originalImportMetaEnv = (
|
|
8
|
+
globalThis as { __iocImportMetaEnvForTests?: unknown }
|
|
9
|
+
).__iocImportMetaEnvForTests;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
(globalThis as { process?: unknown }).process = originalProcess;
|
|
13
|
+
(globalThis as { env?: unknown }).env = originalGlobalEnv;
|
|
14
|
+
(
|
|
15
|
+
globalThis as { __iocImportMetaEnvForTests?: unknown }
|
|
16
|
+
).__iocImportMetaEnvForTests = originalImportMetaEnv;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("returns default when key does not exist", () => {
|
|
20
|
+
(globalThis as { process?: { env: Record<string, unknown> } }).process = {
|
|
21
|
+
env: {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(env("MISSING", "fallback")).toBe("fallback");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("reads values from process env with laravel-like casts", () => {
|
|
28
|
+
(
|
|
29
|
+
globalThis as { __iocImportMetaEnvForTests?: unknown }
|
|
30
|
+
).__iocImportMetaEnvForTests = undefined;
|
|
31
|
+
(globalThis as { process?: { env: Record<string, unknown> } }).process = {
|
|
32
|
+
env: {
|
|
33
|
+
BOOL_TRUE: "true",
|
|
34
|
+
BOOL_FALSE: "(false)",
|
|
35
|
+
NULL_VAL: "null",
|
|
36
|
+
EMPTY_VAL: "(empty)",
|
|
37
|
+
RAW: "abc",
|
|
38
|
+
NUM: 123,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
expect(env("BOOL_TRUE")).toBe(true);
|
|
43
|
+
expect(env("BOOL_FALSE")).toBe(false);
|
|
44
|
+
expect(env("NULL_VAL", "x")).toBeNull();
|
|
45
|
+
expect(env("EMPTY_VAL", "x")).toBe("");
|
|
46
|
+
expect(env("RAW")).toBe("abc");
|
|
47
|
+
expect(env("NUM")).toBe(123);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("prefers import-meta env when key exists there", () => {
|
|
51
|
+
(
|
|
52
|
+
globalThis as { __iocImportMetaEnvForTests?: Record<string, unknown> }
|
|
53
|
+
).__iocImportMetaEnvForTests = {
|
|
54
|
+
CACHE_STORE: "(true)",
|
|
55
|
+
};
|
|
56
|
+
(globalThis as { process?: { env: Record<string, unknown> } }).process = {
|
|
57
|
+
env: {
|
|
58
|
+
CACHE_STORE: "false",
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
expect(env("CACHE_STORE")).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("registers global env function without overriding existing one", () => {
|
|
66
|
+
(globalThis as { env?: unknown }).env = undefined;
|
|
67
|
+
registerGlobalEnv();
|
|
68
|
+
expect(typeof (globalThis as { env?: unknown }).env).toBe("function");
|
|
69
|
+
|
|
70
|
+
const custom = () => "custom";
|
|
71
|
+
(globalThis as { env?: unknown }).env = custom;
|
|
72
|
+
registerGlobalEnv();
|
|
73
|
+
expect((globalThis as { env?: unknown }).env).toBe(custom);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("exposes env helper internals", () => {
|
|
77
|
+
expect(__envTesting.normalizeEnvValue("(true)")).toBe(true);
|
|
78
|
+
expect(typeof __envTesting.resolveImportMetaEnv()).toBe("object");
|
|
79
|
+
|
|
80
|
+
(globalThis as { process?: unknown }).process = undefined;
|
|
81
|
+
expect(__envTesting.resolveProcessEnv()).toEqual({});
|
|
82
|
+
|
|
83
|
+
(globalThis as { process?: { env: Record<string, unknown> } }).process = {
|
|
84
|
+
env: { APP_ENV: "local" },
|
|
85
|
+
};
|
|
86
|
+
expect(__envTesting.resolveProcessEnv()).toEqual({ APP_ENV: "local" });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Container } from "inversify";
|
|
3
|
+
import ConfigRepository from "../src/config/repository";
|
|
4
|
+
import { Application } from "../src/foundation/application";
|
|
5
|
+
import Config from "../src/support/facades/config";
|
|
6
|
+
import Facade from "../src/support/facades/facade";
|
|
7
|
+
import ConfigServiceProvider from "../src/support/providers/config-service-provider";
|
|
8
|
+
|
|
9
|
+
describe("Config facade", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
Facade.clearResolvedInstances();
|
|
12
|
+
(Application as unknown as { container: Container | null }).container = null;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("resolves repository from the container using the config accessor", () => {
|
|
16
|
+
const container = new Container();
|
|
17
|
+
new ConfigServiceProvider({
|
|
18
|
+
app: {
|
|
19
|
+
name: "From Provider",
|
|
20
|
+
},
|
|
21
|
+
}).register({ container });
|
|
22
|
+
|
|
23
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
24
|
+
|
|
25
|
+
expect(Config.get("app.name")).toBe("From Provider");
|
|
26
|
+
expect(container.get(ConfigRepository).get("app.name")).toBe("From Provider");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("uses expected facade accessor name", () => {
|
|
30
|
+
expect((Config as unknown as { getFacadeAccessor: () => string }).getFacadeAccessor()).toBe(
|
|
31
|
+
"config",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("facade class constructor is defined", () => {
|
|
36
|
+
const instance = new (Config as unknown as { new (): object })();
|
|
37
|
+
expect(instance).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("uses facade cache until resolved instances are cleared", () => {
|
|
41
|
+
const container = new Container();
|
|
42
|
+
container
|
|
43
|
+
.bind("config")
|
|
44
|
+
.toConstantValue(new ConfigRepository({ app: { name: "First" } }));
|
|
45
|
+
container
|
|
46
|
+
.bind(ConfigRepository)
|
|
47
|
+
.toConstantValue(new ConfigRepository({ app: { name: "First" } }));
|
|
48
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
49
|
+
|
|
50
|
+
expect(Config.get("app.name")).toBe("First");
|
|
51
|
+
|
|
52
|
+
// Rebind underlying container value; facade should still return cached first instance.
|
|
53
|
+
container.unbind("config");
|
|
54
|
+
container.bind("config").toConstantValue(new ConfigRepository({ app: { name: "Second" } }));
|
|
55
|
+
expect(Config.get("app.name")).toBe("First");
|
|
56
|
+
|
|
57
|
+
Facade.clearResolvedInstances();
|
|
58
|
+
expect(Config.get("app.name")).toBe("Second");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("delegates full method surface to repository", () => {
|
|
62
|
+
const container = new Container();
|
|
63
|
+
const repository = new ConfigRepository({
|
|
64
|
+
app: {
|
|
65
|
+
name: "IOC",
|
|
66
|
+
retries: 3,
|
|
67
|
+
ratio: 1.5,
|
|
68
|
+
enabled: true,
|
|
69
|
+
tags: ["base"],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
container.bind("config").toConstantValue(repository);
|
|
73
|
+
container.bind(ConfigRepository).toConstantValue(repository);
|
|
74
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
75
|
+
|
|
76
|
+
expect(Config.has("app.name")).toBe(true);
|
|
77
|
+
expect(Config.getMany(["app.name"])).toEqual({ "app.name": "IOC" });
|
|
78
|
+
expect(Config.string("app.name")).toBe("IOC");
|
|
79
|
+
expect(Config.integer("app.retries")).toBe(3);
|
|
80
|
+
expect(Config.float("app.ratio")).toBe(1.5);
|
|
81
|
+
expect(Config.boolean("app.enabled")).toBe(true);
|
|
82
|
+
expect(Config.array("app.tags")).toEqual(["base"]);
|
|
83
|
+
|
|
84
|
+
Config.set("app.name", "Changed");
|
|
85
|
+
Config.prepend("app.tags", "first");
|
|
86
|
+
Config.push("app.tags", "last");
|
|
87
|
+
|
|
88
|
+
expect(Config.get("app.name")).toBe("Changed");
|
|
89
|
+
expect(Config.array("app.tags")).toEqual(["first", "base", "last"]);
|
|
90
|
+
expect(Config.all()).toMatchObject({
|
|
91
|
+
app: {
|
|
92
|
+
name: "Changed",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ConfigRepository from "../src/config/repository";
|
|
3
|
+
|
|
4
|
+
describe("ConfigRepository", () => {
|
|
5
|
+
test("gets nested values by dot key", () => {
|
|
6
|
+
const repository = new ConfigRepository({
|
|
7
|
+
app: {
|
|
8
|
+
name: "IOC Application",
|
|
9
|
+
timezone: "UTC",
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(repository.get("app.name")).toBe("IOC Application");
|
|
14
|
+
expect(repository.get("app.timezone")).toBe("UTC");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("supports defaults and lazy default callbacks", () => {
|
|
18
|
+
const repository = new ConfigRepository({});
|
|
19
|
+
|
|
20
|
+
expect(repository.get("app.locale", "en")).toBe("en");
|
|
21
|
+
expect(repository.get("app.name", () => "fallback")).toBe("fallback");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("supports set, prepend, and push", () => {
|
|
25
|
+
const repository = new ConfigRepository({
|
|
26
|
+
app: {
|
|
27
|
+
middlewares: ["auth"],
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
repository.prepend("app.middlewares", "throttle");
|
|
32
|
+
repository.push("app.middlewares", "verified");
|
|
33
|
+
repository.set("app.name", "IOC");
|
|
34
|
+
|
|
35
|
+
expect(repository.get("app.name")).toBe("IOC");
|
|
36
|
+
expect(repository.array("app.middlewares")).toEqual([
|
|
37
|
+
"throttle",
|
|
38
|
+
"auth",
|
|
39
|
+
"verified",
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("supports getMany with list and defaults map", () => {
|
|
44
|
+
const repository = new ConfigRepository({
|
|
45
|
+
app: { name: "IOC Application" },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(repository.getMany(["app.name", "app.locale"])).toEqual({
|
|
49
|
+
"app.name": "IOC Application",
|
|
50
|
+
"app.locale": null,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(
|
|
54
|
+
repository.getMany({
|
|
55
|
+
"app.name": "fallback",
|
|
56
|
+
"app.locale": "en",
|
|
57
|
+
}),
|
|
58
|
+
).toEqual({
|
|
59
|
+
"app.name": "IOC Application",
|
|
60
|
+
"app.locale": "en",
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("typed accessors return expected values", () => {
|
|
65
|
+
const repository = new ConfigRepository({
|
|
66
|
+
app: {
|
|
67
|
+
name: "IOC",
|
|
68
|
+
retries: 3,
|
|
69
|
+
ratio: 0.75,
|
|
70
|
+
enabled: true,
|
|
71
|
+
tags: ["alpha"],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(repository.string("app.name")).toBe("IOC");
|
|
76
|
+
expect(repository.integer("app.retries")).toBe(3);
|
|
77
|
+
expect(repository.float("app.ratio")).toBe(0.75);
|
|
78
|
+
expect(repository.boolean("app.enabled")).toBe(true);
|
|
79
|
+
expect(repository.array("app.tags")).toEqual(["alpha"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("typed accessors throw when type does not match", () => {
|
|
83
|
+
const repository = new ConfigRepository({
|
|
84
|
+
app: {
|
|
85
|
+
name: 123,
|
|
86
|
+
retries: "3",
|
|
87
|
+
ratio: "0.75",
|
|
88
|
+
enabled: "yes",
|
|
89
|
+
tags: "nope",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(() => repository.string("app.name")).toThrow(
|
|
94
|
+
"Configuration value [app.name] is not a string.",
|
|
95
|
+
);
|
|
96
|
+
expect(() => repository.integer("app.retries")).toThrow(
|
|
97
|
+
"Configuration value [app.retries] is not an integer.",
|
|
98
|
+
);
|
|
99
|
+
expect(() => repository.float("app.ratio")).toThrow(
|
|
100
|
+
"Configuration value [app.ratio] is not a float.",
|
|
101
|
+
);
|
|
102
|
+
expect(() => repository.boolean("app.enabled")).toThrow(
|
|
103
|
+
"Configuration value [app.enabled] is not a boolean.",
|
|
104
|
+
);
|
|
105
|
+
expect(() => repository.array("app.tags")).toThrow(
|
|
106
|
+
"Configuration value [app.tags] is not an array.",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("supports offset-style helpers", () => {
|
|
111
|
+
const repository = new ConfigRepository({
|
|
112
|
+
app: { name: "IOC Application" },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(repository.offsetExists("app.name")).toBe(true);
|
|
116
|
+
expect(repository.offsetGet("app.name")).toBe("IOC Application");
|
|
117
|
+
|
|
118
|
+
repository.offsetSet("app.name", "Changed");
|
|
119
|
+
expect(repository.offsetGet("app.name")).toBe("Changed");
|
|
120
|
+
|
|
121
|
+
repository.offsetUnset("app.name");
|
|
122
|
+
expect(repository.offsetExists("app.name")).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Container } from "inversify";
|
|
3
|
+
import { Application } from "../src/foundation/application";
|
|
4
|
+
import Facade from "../src/support/facades/facade";
|
|
5
|
+
|
|
6
|
+
class BrokenFacade extends Facade {
|
|
7
|
+
static callAccessorForTest() {
|
|
8
|
+
return (this as unknown as { getFacadeAccessor: () => unknown }).getFacadeAccessor();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class MethodFacade extends Facade {
|
|
13
|
+
protected static getFacadeAccessor() {
|
|
14
|
+
return "method.target";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static callMethod<T>(method: string, ...args: unknown[]) {
|
|
18
|
+
return (
|
|
19
|
+
this as unknown as {
|
|
20
|
+
callFacadeMethod: <R>(methodName: string, ...values: unknown[]) => R;
|
|
21
|
+
}
|
|
22
|
+
).callFacadeMethod<T>(method, ...args);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class ConcreteFacade extends Facade {
|
|
27
|
+
protected static getFacadeAccessor() {
|
|
28
|
+
return "noop";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Facade base", () => {
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
Facade.clearResolvedInstances();
|
|
35
|
+
(Application as unknown as { container: Container | null }).container = null;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("throws when accessor is not implemented", () => {
|
|
39
|
+
expect(() => BrokenFacade.callAccessorForTest()).toThrow(
|
|
40
|
+
"Facade does not implement getFacadeAccessor().",
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("callFacadeMethod dispatches to target method", () => {
|
|
45
|
+
const container = new Container();
|
|
46
|
+
container.bind("method.target").toConstantValue({
|
|
47
|
+
sum: (a: number, b: number) => a + b,
|
|
48
|
+
});
|
|
49
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
50
|
+
|
|
51
|
+
expect(MethodFacade.callMethod<number>("sum", 2, 3)).toBe(5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("callFacadeMethod throws for missing target method", () => {
|
|
55
|
+
const container = new Container();
|
|
56
|
+
container.bind("method.target").toConstantValue({});
|
|
57
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
58
|
+
|
|
59
|
+
expect(() => MethodFacade.callMethod("missing")).toThrow(
|
|
60
|
+
"Method [missing] does not exist on resolved facade instance.",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("can clear a single resolved instance", () => {
|
|
65
|
+
const container = new Container();
|
|
66
|
+
container.bind("method.target").toConstantValue({
|
|
67
|
+
value: () => "cached",
|
|
68
|
+
});
|
|
69
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
70
|
+
|
|
71
|
+
expect(MethodFacade.callMethod<string>("value")).toBe("cached");
|
|
72
|
+
Facade.clearResolvedInstance("method.target");
|
|
73
|
+
expect(MethodFacade.callMethod<string>("value")).toBe("cached");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("base facade constructor is invokable through subclass", () => {
|
|
77
|
+
const instance = new ConcreteFacade();
|
|
78
|
+
expect(instance).toBeInstanceOf(ConcreteFacade);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { FileSystem } from "../src/filesystem/filesystem";
|
|
3
|
+
import LocalAdapter, { __localAdapterTesting } from "../src/filesystem/adapters/local";
|
|
4
|
+
|
|
5
|
+
describe("FileSystem domain", () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
(
|
|
8
|
+
globalThis as {
|
|
9
|
+
__iocConfigGlobForTests?: unknown;
|
|
10
|
+
}
|
|
11
|
+
).__iocConfigGlobForTests = undefined;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("delegates loading to adapter and supports replacing adapter", () => {
|
|
15
|
+
const calls: string[] = [];
|
|
16
|
+
const adapterA = {
|
|
17
|
+
loadConfigItems: (basePath: string) => {
|
|
18
|
+
calls.push(`a:${basePath}`);
|
|
19
|
+
return { app: { name: "A" } };
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
const adapterB = {
|
|
23
|
+
loadConfigItems: (basePath: string) => {
|
|
24
|
+
calls.push(`b:${basePath}`);
|
|
25
|
+
return { app: { name: "B" } };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const filesystem = new FileSystem(adapterA);
|
|
30
|
+
expect(filesystem.loadConfigItems("./")).toEqual({ app: { name: "A" } });
|
|
31
|
+
|
|
32
|
+
filesystem.setAdapter(adapterB);
|
|
33
|
+
expect(filesystem.loadConfigItems("./src")).toEqual({ app: { name: "B" } });
|
|
34
|
+
expect(calls).toEqual(["a:./", "b:./src"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("loads config defaults from matching directories", () => {
|
|
38
|
+
(
|
|
39
|
+
globalThis as {
|
|
40
|
+
__iocConfigGlobForTests?: (
|
|
41
|
+
pattern: string | string[],
|
|
42
|
+
options?: { eager?: boolean },
|
|
43
|
+
) => Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
).__iocConfigGlobForTests = () => ({
|
|
46
|
+
"/src/config/app.ts": { default: { name: "App" } },
|
|
47
|
+
"/src/config/cache.ts": { default: { driver: "memory" } },
|
|
48
|
+
"/src/config/no-default.ts": { named: true },
|
|
49
|
+
"/ignored/config/app.ts": { default: { name: "Ignored" } },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const adapter = new LocalAdapter();
|
|
53
|
+
|
|
54
|
+
expect(adapter.loadConfigItems("./src")).toEqual({
|
|
55
|
+
app: { name: "App" },
|
|
56
|
+
cache: { driver: "memory" },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("helper functions normalize and match config paths", () => {
|
|
61
|
+
expect(__localAdapterTesting.fileNameWithoutExtension("/src/config/app.ts")).toBe("app");
|
|
62
|
+
expect(__localAdapterTesting.normalizeBasePath("./")).toBe("");
|
|
63
|
+
expect(__localAdapterTesting.normalizeBasePath("./src")).toBe("src");
|
|
64
|
+
expect(__localAdapterTesting.isPathInConfigDirectories("/src/config/app.ts", "./")).toBe(
|
|
65
|
+
true,
|
|
66
|
+
);
|
|
67
|
+
expect(
|
|
68
|
+
__localAdapterTesting.isPathInConfigDirectories(
|
|
69
|
+
"/workspace/src/config/app.ts",
|
|
70
|
+
"/workspace",
|
|
71
|
+
),
|
|
72
|
+
).toBe(true);
|
|
73
|
+
expect(__localAdapterTesting.isPathInConfigDirectories("/tmp/elsewhere/conf/app.ts", "./"))
|
|
74
|
+
.toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns empty config when no glob implementation is available", () => {
|
|
78
|
+
const adapter = new LocalAdapter();
|
|
79
|
+
expect(adapter.loadConfigItems("./")).toEqual({});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { Container } from "inversify";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import { ApplicationProvider, useService } from "../src/runtimes/react";
|
|
6
|
+
|
|
7
|
+
describe("React runtime", () => {
|
|
8
|
+
test("ApplicationProvider + useService resolves from container", () => {
|
|
9
|
+
const container = new Container();
|
|
10
|
+
const token = Symbol("token");
|
|
11
|
+
container.bind(token).toConstantValue("resolved");
|
|
12
|
+
|
|
13
|
+
function Probe() {
|
|
14
|
+
const value = useService<string>(token);
|
|
15
|
+
return <div>{value}</div>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const html = renderToString(
|
|
19
|
+
<ApplicationProvider container={container}>
|
|
20
|
+
<Probe />
|
|
21
|
+
</ApplicationProvider>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(html).toContain("resolved");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("useService throws when provider is missing", () => {
|
|
28
|
+
function Probe() {
|
|
29
|
+
useService("missing");
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
expect(() => renderToString(<Probe />)).toThrow(
|
|
34
|
+
"Application container is not available in React context.",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ServiceProvider, {
|
|
3
|
+
DeferrableServiceProvider,
|
|
4
|
+
} from "../src/support/service-provider";
|
|
5
|
+
import { Container } from "inversify";
|
|
6
|
+
|
|
7
|
+
describe("ServiceProvider base classes", () => {
|
|
8
|
+
test("default register and boot are no-ops", () => {
|
|
9
|
+
const provider = new ServiceProvider();
|
|
10
|
+
const container = new Container();
|
|
11
|
+
|
|
12
|
+
expect(provider.register({ container })).toBeUndefined();
|
|
13
|
+
expect(provider.boot({ container })).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("deferrable provider default provides list is empty", () => {
|
|
17
|
+
const provider = new DeferrableServiceProvider();
|
|
18
|
+
const container = new Container();
|
|
19
|
+
expect(provider.register({ container })).toBeUndefined();
|
|
20
|
+
expect(provider.boot({ container })).toBeUndefined();
|
|
21
|
+
expect(provider.provides()).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Container } from "inversify";
|
|
3
|
+
import { Application } from "../src/foundation/application";
|
|
4
|
+
import StorageManager from "../src/storage/manager";
|
|
5
|
+
import MemoryStorageAdapter from "../src/storage/adapters/memory";
|
|
6
|
+
import Storage from "../src/support/facades/storage";
|
|
7
|
+
import Facade from "../src/support/facades/facade";
|
|
8
|
+
|
|
9
|
+
describe("Storage facade", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
Facade.clearResolvedInstances();
|
|
12
|
+
(Application as unknown as { container: Container | null }).container = null;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("resolves manager and delegates methods", async () => {
|
|
16
|
+
const container = new Container();
|
|
17
|
+
const manager = new StorageManager({ memory: new MemoryStorageAdapter() }, "memory");
|
|
18
|
+
container.bind("storage").toConstantValue(manager);
|
|
19
|
+
container.bind(StorageManager).toConstantValue(manager);
|
|
20
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
21
|
+
|
|
22
|
+
await Storage.set("name", "ioc");
|
|
23
|
+
expect(await Storage.get("name")).toBe("ioc");
|
|
24
|
+
expect(await Storage.has("name")).toBe(true);
|
|
25
|
+
expect(await Storage.keys()).toEqual(["name"]);
|
|
26
|
+
expect(Storage.drv()).toBe(manager.drv());
|
|
27
|
+
expect(Storage.drv("memory")).toBe(manager.drv("memory"));
|
|
28
|
+
Storage.use("memory");
|
|
29
|
+
await Storage.delete("name");
|
|
30
|
+
expect(await Storage.get("name")).toBeNull();
|
|
31
|
+
await Storage.set("x", 1);
|
|
32
|
+
await Storage.clear();
|
|
33
|
+
expect(await Storage.keys()).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("uses expected facade accessor", () => {
|
|
37
|
+
expect((Storage as unknown as { getFacadeAccessor: () => string }).getFacadeAccessor()).toBe(
|
|
38
|
+
"storage",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("facade class constructor is defined", () => {
|
|
43
|
+
const instance = new (Storage as unknown as { new (): object })();
|
|
44
|
+
expect(instance).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
});
|