@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,46 @@
|
|
|
1
|
+
import type CacheManager from "../../cache/manager";
|
|
2
|
+
import type { CacheStore } from "../../cache/cache";
|
|
3
|
+
import Facade from "./facade";
|
|
4
|
+
|
|
5
|
+
export default class Cache extends Facade {
|
|
6
|
+
private constructor() {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
protected static getFacadeAccessor(): string {
|
|
11
|
+
return "cache";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static store(name?: string): CacheStore {
|
|
15
|
+
return this.resolveFacadeInstance<CacheManager>().store(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static use(name: string): Cache {
|
|
19
|
+
this.resolveFacadeInstance<CacheManager>().use(name);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static get<T = unknown>(key: string): Promise<T | null> {
|
|
24
|
+
return this.resolveFacadeInstance<CacheManager>().get<T>(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static set<T = unknown>(key: string, value: T): Promise<void> {
|
|
28
|
+
return this.resolveFacadeInstance<CacheManager>().set<T>(key, value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static has(key: string): Promise<boolean> {
|
|
32
|
+
return this.resolveFacadeInstance<CacheManager>().has(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static delete(key: string): Promise<void> {
|
|
36
|
+
return this.resolveFacadeInstance<CacheManager>().delete(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static clear(): Promise<void> {
|
|
40
|
+
return this.resolveFacadeInstance<CacheManager>().clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static keys(): Promise<string[]> {
|
|
44
|
+
return this.resolveFacadeInstance<CacheManager>().keys();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type ConfigRepository from "../../config/repository";
|
|
2
|
+
import type { ConfigDefaults, ConfigItems } from "../../config/repository";
|
|
3
|
+
import Facade from "./facade";
|
|
4
|
+
|
|
5
|
+
export default class Config extends Facade {
|
|
6
|
+
private constructor() {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
protected static getFacadeAccessor(): string {
|
|
11
|
+
return "config";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static has(key: string | string[]): boolean {
|
|
15
|
+
return this.resolveFacadeInstance<ConfigRepository>().has(key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static get<T = unknown>(
|
|
19
|
+
key: string | string[],
|
|
20
|
+
defaultValue: T | (() => T) | null = null,
|
|
21
|
+
): T | Record<string, unknown> | null {
|
|
22
|
+
return this.resolveFacadeInstance<ConfigRepository>().get<T>(key, defaultValue);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static getMany(keys: string[] | ConfigDefaults): Record<string, unknown> {
|
|
26
|
+
return this.resolveFacadeInstance<ConfigRepository>().getMany(keys);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static string(key: string, defaultValue: string | (() => string) | null = null): string {
|
|
30
|
+
return this.resolveFacadeInstance<ConfigRepository>().string(key, defaultValue);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static integer(key: string, defaultValue: number | (() => number) | null = null): number {
|
|
34
|
+
return this.resolveFacadeInstance<ConfigRepository>().integer(key, defaultValue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static float(key: string, defaultValue: number | (() => number) | null = null): number {
|
|
38
|
+
return this.resolveFacadeInstance<ConfigRepository>().float(key, defaultValue);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static boolean(
|
|
42
|
+
key: string,
|
|
43
|
+
defaultValue: boolean | (() => boolean) | null = null,
|
|
44
|
+
): boolean {
|
|
45
|
+
return this.resolveFacadeInstance<ConfigRepository>().boolean(key, defaultValue);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static array<T = unknown>(key: string, defaultValue: T[] | (() => T[]) | null = null): T[] {
|
|
49
|
+
return this.resolveFacadeInstance<ConfigRepository>().array<T>(key, defaultValue);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static set(key: string | ConfigItems, value: unknown = null): void {
|
|
53
|
+
this.resolveFacadeInstance<ConfigRepository>().set(key, value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static prepend(key: string, value: unknown): void {
|
|
57
|
+
this.resolveFacadeInstance<ConfigRepository>().prepend(key, value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static push(key: string, value: unknown): void {
|
|
61
|
+
this.resolveFacadeInstance<ConfigRepository>().push(key, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static all(): ConfigItems {
|
|
65
|
+
return this.resolveFacadeInstance<ConfigRepository>().all();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Application } from "../../foundation/application";
|
|
2
|
+
|
|
3
|
+
export default abstract class Facade {
|
|
4
|
+
protected static resolvedInstance = new Map<Parameters<typeof Application.make>[0], unknown>();
|
|
5
|
+
|
|
6
|
+
protected constructor() {}
|
|
7
|
+
|
|
8
|
+
protected static getFacadeAccessor(): Parameters<typeof Application.make>[0] {
|
|
9
|
+
throw new Error("Facade does not implement getFacadeAccessor().");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static clearResolvedInstance(name: Parameters<typeof Application.make>[0]): void {
|
|
13
|
+
this.resolvedInstance.delete(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static clearResolvedInstances(): void {
|
|
17
|
+
this.resolvedInstance.clear();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected static resolveFacadeInstance<T>(): T {
|
|
21
|
+
const accessor = this.getFacadeAccessor();
|
|
22
|
+
|
|
23
|
+
if (this.resolvedInstance.has(accessor)) {
|
|
24
|
+
return this.resolvedInstance.get(accessor) as T;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const instance = Application.make<T>(accessor);
|
|
28
|
+
this.resolvedInstance.set(accessor, instance);
|
|
29
|
+
|
|
30
|
+
return instance;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected static callFacadeMethod<T = unknown>(method: string, ...args: unknown[]): T {
|
|
34
|
+
const instance = this.resolveFacadeInstance<Record<string, (...a: unknown[]) => unknown>>();
|
|
35
|
+
const callable = instance[method];
|
|
36
|
+
if (typeof callable !== "function") {
|
|
37
|
+
throw new Error(`Method [${method}] does not exist on resolved facade instance.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return callable.apply(instance, args) as T;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type StorageManager from "../../storage/manager";
|
|
2
|
+
import type { StorageAdapter } from "../../storage/storage";
|
|
3
|
+
import Facade from "./facade";
|
|
4
|
+
|
|
5
|
+
export default class Storage extends Facade {
|
|
6
|
+
private constructor() {
|
|
7
|
+
super();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
protected static getFacadeAccessor(): string {
|
|
11
|
+
return "storage";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static drv(name?: string): StorageAdapter {
|
|
15
|
+
return this.resolveFacadeInstance<StorageManager>().drv(name);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static use(name: string): Storage {
|
|
19
|
+
this.resolveFacadeInstance<StorageManager>().use(name);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static get<T = unknown>(key: string): Promise<T | null> {
|
|
24
|
+
return this.resolveFacadeInstance<StorageManager>().get<T>(key);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static set<T = unknown>(key: string, value: T): Promise<void> {
|
|
28
|
+
return this.resolveFacadeInstance<StorageManager>().set<T>(key, value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static has(key: string): Promise<boolean> {
|
|
32
|
+
return this.resolveFacadeInstance<StorageManager>().has(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static delete(key: string): Promise<void> {
|
|
36
|
+
return this.resolveFacadeInstance<StorageManager>().delete(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static clear(): Promise<void> {
|
|
40
|
+
return this.resolveFacadeInstance<StorageManager>().clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static keys(): Promise<string[]> {
|
|
44
|
+
return this.resolveFacadeInstance<StorageManager>().keys();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import appConfig from "../../config/app";
|
|
2
|
+
import ConfigRepository, { type ConfigItems } from "../../config/repository";
|
|
3
|
+
import ServiceProvider, { type ServiceProviderContext } from "../service-provider";
|
|
4
|
+
|
|
5
|
+
export default class ConfigServiceProvider extends ServiceProvider {
|
|
6
|
+
protected items: ConfigItems;
|
|
7
|
+
|
|
8
|
+
constructor(items: ConfigItems = { app: appConfig }) {
|
|
9
|
+
super();
|
|
10
|
+
this.items = items;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
register({ container }: ServiceProviderContext): void {
|
|
14
|
+
const repository = new ConfigRepository(this.items);
|
|
15
|
+
|
|
16
|
+
container.bind("config").toConstantValue(repository);
|
|
17
|
+
container.bind(ConfigRepository).toConstantValue(repository);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Container } from "inversify";
|
|
2
|
+
|
|
3
|
+
export type Cleanup = () => void;
|
|
4
|
+
|
|
5
|
+
export type ServiceProviderContext = {
|
|
6
|
+
container: Container;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default class ServiceProvider {
|
|
10
|
+
constructor() {}
|
|
11
|
+
|
|
12
|
+
register(_context: ServiceProviderContext): void | Cleanup {}
|
|
13
|
+
|
|
14
|
+
boot(_context: ServiceProviderContext): void | Cleanup {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class DeferrableServiceProvider extends ServiceProvider {
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
provides(): Array<string> {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
function splitWords(value: string): string[] {
|
|
2
|
+
const normalized = value
|
|
3
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
4
|
+
.replace(/[_\-.]+/g, " ")
|
|
5
|
+
.trim();
|
|
6
|
+
|
|
7
|
+
if (!normalized) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return normalized.split(/\s+/);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeForSlug(value: string): string {
|
|
15
|
+
return value
|
|
16
|
+
.normalize("NFKD")
|
|
17
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
20
|
+
.replace(/^-+|-+$/g, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default class Str {
|
|
24
|
+
constructor() {}
|
|
25
|
+
|
|
26
|
+
static lower(value: string): string {
|
|
27
|
+
return value.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static upper(value: string): string {
|
|
31
|
+
return value.toUpperCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static length(value: string): number {
|
|
35
|
+
return value.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static contains(haystack: string, needle: string | string[], ignoreCase = false): boolean {
|
|
39
|
+
const source = ignoreCase ? haystack.toLowerCase() : haystack;
|
|
40
|
+
const needles = Array.isArray(needle) ? needle : [needle];
|
|
41
|
+
|
|
42
|
+
for (const part of needles) {
|
|
43
|
+
const target = ignoreCase ? part.toLowerCase() : part;
|
|
44
|
+
if (source.includes(target)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static startsWith(haystack: string, needle: string | string[]): boolean {
|
|
53
|
+
const needles = Array.isArray(needle) ? needle : [needle];
|
|
54
|
+
for (const part of needles) {
|
|
55
|
+
if (haystack.startsWith(part)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static endsWith(haystack: string, needle: string | string[]): boolean {
|
|
64
|
+
const needles = Array.isArray(needle) ? needle : [needle];
|
|
65
|
+
for (const part of needles) {
|
|
66
|
+
if (haystack.endsWith(part)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
static replace(search: string | RegExp, replace: string, subject: string): string {
|
|
75
|
+
return subject.replace(search, replace);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static snake(value: string, separator = "_"): string {
|
|
79
|
+
const words = splitWords(value).map((word) => word.toLowerCase());
|
|
80
|
+
return words.join(separator);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static kebab(value: string): string {
|
|
84
|
+
return Str.snake(value, "-");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static studly(value: string): string {
|
|
88
|
+
const words = splitWords(value);
|
|
89
|
+
let output = "";
|
|
90
|
+
for (const word of words) {
|
|
91
|
+
output += word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return output;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
static camel(value: string): string {
|
|
98
|
+
const studly = Str.studly(value);
|
|
99
|
+
if (!studly) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return studly.charAt(0).toLowerCase() + studly.slice(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static slug(value: string, separator = "-"): string {
|
|
107
|
+
const slugged = normalizeForSlug(value);
|
|
108
|
+
if (separator === "-") {
|
|
109
|
+
return slugged;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return slugged.replace(/-/g, separator);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function registerGlobalStr(): void {
|
|
117
|
+
const globalScope = globalThis as { Str?: typeof Str };
|
|
118
|
+
if (typeof globalScope.Str === "undefined") {
|
|
119
|
+
globalScope.Str = Str;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const __strTesting = {
|
|
124
|
+
normalizeForSlug,
|
|
125
|
+
splitWords,
|
|
126
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import ReactDOM from "react-dom/client";
|
|
3
|
+
import ConfigRepository from "../src/config/repository";
|
|
4
|
+
import { __applicationTesting, Application } from "../src/foundation/application";
|
|
5
|
+
import ServiceProvider from "../src/support/service-provider";
|
|
6
|
+
|
|
7
|
+
describe("Application", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
(globalThis as { window?: unknown; document?: unknown }).window = undefined;
|
|
10
|
+
(globalThis as { window?: unknown; document?: unknown }).document = undefined;
|
|
11
|
+
(
|
|
12
|
+
globalThis as { __iocConfigGlobForTests?: unknown }
|
|
13
|
+
).__iocConfigGlobForTests = undefined;
|
|
14
|
+
Application.clearConfigCache();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("make throws when container is not initialized", () => {
|
|
18
|
+
expect(() => Application.make("config")).toThrow(
|
|
19
|
+
"Application container is not available. Call run() first.",
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("run with custom renderer executes lifecycle and cleanup in reverse order", () => {
|
|
24
|
+
const calls: string[] = [];
|
|
25
|
+
let beforeUnload: (() => void) | undefined;
|
|
26
|
+
|
|
27
|
+
(globalThis as { window: { addEventListener: (event: string, cb: () => void) => void } })
|
|
28
|
+
.window = {
|
|
29
|
+
addEventListener: (_event, cb) => {
|
|
30
|
+
beforeUnload = cb;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
class DemoProvider extends ServiceProvider {
|
|
35
|
+
register({ container }: { container: { bind: (id: string) => { toConstantValue: (v: unknown) => void } } }) {
|
|
36
|
+
calls.push("provider.register");
|
|
37
|
+
container.bind("demo").toConstantValue("value");
|
|
38
|
+
return () => calls.push("provider.register.cleanup");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
boot() {
|
|
42
|
+
calls.push("provider.boot");
|
|
43
|
+
return () => calls.push("provider.boot.cleanup");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const app = Application.configure("./")
|
|
48
|
+
.withProviders([new DemoProvider()])
|
|
49
|
+
.withServices(({ container }) => {
|
|
50
|
+
calls.push("services");
|
|
51
|
+
container.bind("services.value").toConstantValue(1);
|
|
52
|
+
return () => calls.push("services.cleanup");
|
|
53
|
+
})
|
|
54
|
+
.withStartup(() => {
|
|
55
|
+
calls.push("startup");
|
|
56
|
+
return () => calls.push("startup.cleanup");
|
|
57
|
+
})
|
|
58
|
+
.withRoot(() => null)
|
|
59
|
+
.withRenderer({
|
|
60
|
+
render: () => {
|
|
61
|
+
calls.push("renderer");
|
|
62
|
+
return () => calls.push("renderer.cleanup");
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const running = app.run();
|
|
67
|
+
expect(Application.make("demo")).toBe("value");
|
|
68
|
+
expect(Application.make("storage")).toBeTruthy();
|
|
69
|
+
expect(Application.make("cache")).toBeTruthy();
|
|
70
|
+
expect(running.container.get("services.value")).toBe(1);
|
|
71
|
+
expect(calls).toEqual([
|
|
72
|
+
"provider.register",
|
|
73
|
+
"services",
|
|
74
|
+
"provider.boot",
|
|
75
|
+
"startup",
|
|
76
|
+
"renderer",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
expect(typeof beforeUnload).toBe("function");
|
|
80
|
+
beforeUnload?.();
|
|
81
|
+
|
|
82
|
+
expect(calls).toEqual([
|
|
83
|
+
"provider.register",
|
|
84
|
+
"services",
|
|
85
|
+
"provider.boot",
|
|
86
|
+
"startup",
|
|
87
|
+
"renderer",
|
|
88
|
+
"renderer.cleanup",
|
|
89
|
+
"startup.cleanup",
|
|
90
|
+
"provider.boot.cleanup",
|
|
91
|
+
"services.cleanup",
|
|
92
|
+
"provider.register.cleanup",
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
expect(() => Application.make("demo")).toThrow(
|
|
96
|
+
"Application container is not available. Call run() first.",
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("exposes helper behavior used for config discovery", () => {
|
|
101
|
+
expect(__applicationTesting.fileNameWithoutExtension("/src/config/app.ts")).toBe("app");
|
|
102
|
+
expect(__applicationTesting.normalizeBasePath("./")).toBe("");
|
|
103
|
+
expect(__applicationTesting.normalizeBasePath("./src")).toBe("src");
|
|
104
|
+
expect(__applicationTesting.isPathInConfigDirectories("/src/config/app.ts", "./")).toBe(
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
expect(
|
|
108
|
+
__applicationTesting.isPathInConfigDirectories(
|
|
109
|
+
"/workspace/src/config/app.ts",
|
|
110
|
+
"/workspace",
|
|
111
|
+
),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("loads config modules through glob loader and caches by basePath", () => {
|
|
116
|
+
(
|
|
117
|
+
globalThis as {
|
|
118
|
+
__iocConfigGlobForTests?: (
|
|
119
|
+
pattern: string | string[],
|
|
120
|
+
options?: { eager?: boolean },
|
|
121
|
+
) => Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
).__iocConfigGlobForTests = () => ({
|
|
124
|
+
"/src/config/app.ts": { default: { name: "From App Config" } },
|
|
125
|
+
"/src/config/cache.ts": { default: { store: "memory" } },
|
|
126
|
+
"/other/path/ignored.ts": { default: { nope: true } },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
(globalThis as { window: { addEventListener: (event: string, cb: () => void) => void } })
|
|
130
|
+
.window = {
|
|
131
|
+
addEventListener: () => {},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const first = Application.configure("./src").run();
|
|
135
|
+
const second = Application.configure("./src").run();
|
|
136
|
+
|
|
137
|
+
const firstConfig = first.container.get(ConfigRepository);
|
|
138
|
+
const secondConfig = second.container.get(ConfigRepository);
|
|
139
|
+
|
|
140
|
+
expect(firstConfig.get("app")).toEqual({ name: "From App Config" });
|
|
141
|
+
expect(firstConfig.get("cache")).toEqual({ store: "memory" });
|
|
142
|
+
expect(firstConfig.get("ignored")).toBeNull();
|
|
143
|
+
expect(firstConfig.all()).toEqual(secondConfig.all());
|
|
144
|
+
expect(firstConfig.all()).not.toBe(secondConfig.all());
|
|
145
|
+
|
|
146
|
+
// Cached source config should be cloned into each app instance.
|
|
147
|
+
firstConfig.set("cache.store", "updated");
|
|
148
|
+
expect(secondConfig.get("cache.store")).toBe("memory");
|
|
149
|
+
|
|
150
|
+
first.stop();
|
|
151
|
+
second.stop();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("renderer requires root component", () => {
|
|
155
|
+
(globalThis as { window: { addEventListener: (event: string, cb: () => void) => void } })
|
|
156
|
+
.window = {
|
|
157
|
+
addEventListener: () => {},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const app = Application.configure("./").withRenderer({ render: () => () => {} });
|
|
161
|
+
|
|
162
|
+
expect(() => app.run()).toThrow(
|
|
163
|
+
"Root component is required when using a custom renderer.",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("default react renderer throws if mount node is missing", () => {
|
|
168
|
+
(globalThis as { window: { addEventListener: (event: string, cb: () => void) => void } })
|
|
169
|
+
.window = {
|
|
170
|
+
addEventListener: () => {},
|
|
171
|
+
};
|
|
172
|
+
(globalThis as { document: { getElementById: (id: string) => null } }).document = {
|
|
173
|
+
getElementById: () => null,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const app = Application.configure("./").withRoot(() => null);
|
|
177
|
+
|
|
178
|
+
expect(() => app.run()).toThrow("Missing mount node #root.");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("default react renderer mounts and unmounts when stopped", () => {
|
|
182
|
+
let beforeUnload: (() => void) | undefined;
|
|
183
|
+
let rendered = 0;
|
|
184
|
+
let unmounted = 0;
|
|
185
|
+
|
|
186
|
+
(globalThis as { window: { addEventListener: (event: string, cb: () => void) => void } })
|
|
187
|
+
.window = {
|
|
188
|
+
addEventListener: (_event, cb) => {
|
|
189
|
+
beforeUnload = cb;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
(globalThis as { document: { getElementById: (id: string) => object | null } }).document = {
|
|
193
|
+
getElementById: () => ({}),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const originalCreateRoot = (ReactDOM as { createRoot: (node: unknown) => unknown }).createRoot;
|
|
197
|
+
(ReactDOM as { createRoot: (node: unknown) => { render: () => void; unmount: () => void } })
|
|
198
|
+
.createRoot = () => ({
|
|
199
|
+
render: () => {
|
|
200
|
+
rendered += 1;
|
|
201
|
+
},
|
|
202
|
+
unmount: () => {
|
|
203
|
+
unmounted += 1;
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const running = Application.configure("./").withRoot(() => null).run();
|
|
209
|
+
expect(rendered).toBe(1);
|
|
210
|
+
running.stop();
|
|
211
|
+
expect(unmounted).toBe(1);
|
|
212
|
+
// Stop twice should still be safe if beforeunload callback also runs.
|
|
213
|
+
beforeUnload?.();
|
|
214
|
+
expect(unmounted).toBe(2);
|
|
215
|
+
} finally {
|
|
216
|
+
(
|
|
217
|
+
ReactDOM as { createRoot: (node: unknown) => unknown }
|
|
218
|
+
).createRoot = originalCreateRoot;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("uses fallback no-op renderer cleanup when custom renderer returns nothing", () => {
|
|
223
|
+
(globalThis as { window: { addEventListener: (event: string, cb: () => void) => void } })
|
|
224
|
+
.window = {
|
|
225
|
+
addEventListener: (_event, cb) => {
|
|
226
|
+
cb();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const app = Application.configure("./")
|
|
231
|
+
.withRoot(() => null)
|
|
232
|
+
.withRenderer({ render: () => undefined });
|
|
233
|
+
|
|
234
|
+
expect(() => app.run()).not.toThrow();
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { Container } from "inversify";
|
|
3
|
+
import { Application } from "../src/foundation/application";
|
|
4
|
+
import CacheManager from "../src/cache/manager";
|
|
5
|
+
import Cache from "../src/support/facades/cache";
|
|
6
|
+
import Facade from "../src/support/facades/facade";
|
|
7
|
+
import MemoryStorageAdapter from "../src/storage/adapters/memory";
|
|
8
|
+
|
|
9
|
+
describe("Cache 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 manager = new CacheManager({ memory: new MemoryStorageAdapter() }, "memory");
|
|
17
|
+
const container = new Container();
|
|
18
|
+
container.bind("cache").toConstantValue(manager);
|
|
19
|
+
container.bind(CacheManager).toConstantValue(manager);
|
|
20
|
+
(Application as unknown as { container: Container | null }).container = container;
|
|
21
|
+
|
|
22
|
+
await Cache.set("name", "ioc");
|
|
23
|
+
expect(await Cache.get("name")).toBe("ioc");
|
|
24
|
+
expect(await Cache.has("name")).toBe(true);
|
|
25
|
+
expect(await Cache.keys()).toEqual(["name"]);
|
|
26
|
+
expect(Cache.store()).toBe(manager.store());
|
|
27
|
+
Cache.use("memory");
|
|
28
|
+
await Cache.delete("name");
|
|
29
|
+
expect(await Cache.get("name")).toBeNull();
|
|
30
|
+
await Cache.set("x", 1);
|
|
31
|
+
await Cache.clear();
|
|
32
|
+
expect(await Cache.keys()).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("uses expected facade accessor", () => {
|
|
36
|
+
expect((Cache as unknown as { getFacadeAccessor: () => string }).getFacadeAccessor()).toBe(
|
|
37
|
+
"cache",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("facade class constructor is defined", () => {
|
|
42
|
+
const instance = new (Cache as unknown as { new (): object })();
|
|
43
|
+
expect(instance).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
});
|