@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.
Files changed (43) hide show
  1. package/bun.lock +20 -0
  2. package/coverage/lcov.info +1078 -0
  3. package/package.json +43 -0
  4. package/src/cache/cache.ts +3 -0
  5. package/src/cache/manager.ts +115 -0
  6. package/src/config/app.ts +5 -0
  7. package/src/config/clone.ts +9 -0
  8. package/src/config/env.global.d.ts +5 -0
  9. package/src/config/env.ts +79 -0
  10. package/src/config/repository.ts +204 -0
  11. package/src/filesystem/adapters/local.ts +104 -0
  12. package/src/filesystem/filesystem.ts +21 -0
  13. package/src/foundation/application.ts +207 -0
  14. package/src/index.ts +33 -0
  15. package/src/rendering/adapters/react.tsx +27 -0
  16. package/src/rendering/renderer.ts +13 -0
  17. package/src/runtimes/react.tsx +22 -0
  18. package/src/storage/adapters/indexed-db.ts +180 -0
  19. package/src/storage/adapters/local-storage.ts +46 -0
  20. package/src/storage/adapters/memory.ts +35 -0
  21. package/src/storage/manager.ts +78 -0
  22. package/src/storage/storage.ts +8 -0
  23. package/src/support/facades/cache.ts +46 -0
  24. package/src/support/facades/config.ts +67 -0
  25. package/src/support/facades/facade.ts +42 -0
  26. package/src/support/facades/storage.ts +46 -0
  27. package/src/support/providers/config-service-provider.ts +19 -0
  28. package/src/support/service-provider.ts +25 -0
  29. package/src/support/str.ts +126 -0
  30. package/tests/application.test.ts +236 -0
  31. package/tests/cache-facade.test.ts +45 -0
  32. package/tests/cache.test.ts +68 -0
  33. package/tests/config-clone.test.ts +31 -0
  34. package/tests/config-env.test.ts +88 -0
  35. package/tests/config-facade.test.ts +96 -0
  36. package/tests/config-repository.test.ts +124 -0
  37. package/tests/facade-base.test.ts +80 -0
  38. package/tests/filesystem.test.ts +81 -0
  39. package/tests/runtime-react.test.tsx +37 -0
  40. package/tests/service-provider.test.ts +23 -0
  41. package/tests/storage-facade.test.ts +46 -0
  42. package/tests/storage.test.ts +264 -0
  43. package/tests/str.test.ts +73 -0
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@raubjo/architect-core",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "scripts": {
10
+ "test": "bun test",
11
+ "test:coverage": "bun test --coverage"
12
+ },
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./application": "./src/foundation/application.ts",
16
+ "./config/app": "./src/config/app.ts",
17
+ "./config/env": "./src/config/env.ts",
18
+ "./config/repository": "./src/config/repository.ts",
19
+ "./cache/manager": "./src/cache/manager.ts",
20
+ "./cache/cache": "./src/cache/cache.ts",
21
+ "./facades/cache": "./src/support/facades/cache.ts",
22
+ "./storage/manager": "./src/storage/manager.ts",
23
+ "./storage/storage": "./src/storage/storage.ts",
24
+ "./storage/adapters/memory": "./src/storage/adapters/memory.ts",
25
+ "./storage/adapters/local-storage": "./src/storage/adapters/local-storage.ts",
26
+ "./storage/adapters/indexed-db": "./src/storage/adapters/indexed-db.ts",
27
+ "./facades/storage": "./src/support/facades/storage.ts",
28
+ "./filesystem/filesystem": "./src/filesystem/filesystem.ts",
29
+ "./filesystem/local-adapter": "./src/filesystem/adapters/local.ts",
30
+ "./rendering/renderer": "./src/rendering/renderer.ts",
31
+ "./rendering/react": "./src/rendering/adapters/react.tsx",
32
+ "./facades/config": "./src/support/facades/config.ts",
33
+ "./providers/config-service-provider": "./src/support/providers/config-service-provider.ts",
34
+ "./runtimes/react": "./src/runtimes/react.tsx",
35
+ "./str": "./src/support/str.ts",
36
+ "./service-provider": "./src/support/service-provider.ts",
37
+ "./facade": "./src/support/facades/facade.ts"
38
+ },
39
+ "devDependencies": {
40
+ "@types/react": "^19.2.14",
41
+ "@types/react-dom": "^19.2.3"
42
+ }
43
+ }
@@ -0,0 +1,3 @@
1
+ import type { StorageAdapter } from "../storage/storage";
2
+
3
+ export type CacheStore = StorageAdapter;
@@ -0,0 +1,115 @@
1
+ import type ConfigRepository from "../config/repository";
2
+ import IndexedDbAdapter from "../storage/adapters/indexed-db";
3
+ import LocalStorageAdapter from "../storage/adapters/local-storage";
4
+ import MemoryStorageAdapter from "../storage/adapters/memory";
5
+ import type { StorageAdapter } from "../storage/storage";
6
+ import type { CacheStore } from "./cache";
7
+
8
+ type CacheStoreConfig = {
9
+ driver?: string;
10
+ };
11
+
12
+ function isRecord(value: unknown): value is Record<string, unknown> {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ export default class CacheManager implements StorageAdapter {
17
+ protected stores: Record<string, CacheStore>;
18
+ protected active: string;
19
+
20
+ constructor(stores: Record<string, CacheStore>, active = "memory") {
21
+ this.stores = stores;
22
+ this.active = active in this.stores ? active : firstStoreName(this.stores);
23
+ }
24
+
25
+ static fromConfig(config: ConfigRepository): CacheManager {
26
+ const stores = CacheManager.storesFromConfig(config);
27
+ const active = config.string("cache.default", "memory");
28
+
29
+ return new CacheManager(stores, active);
30
+ }
31
+
32
+ protected static storesFromConfig(config: ConfigRepository): Record<string, CacheStore> {
33
+ const baseDrivers = CacheManager.defaultDrivers();
34
+ const configured = config.get<Record<string, unknown>>("cache.stores", {}) ?? {};
35
+ if (!isRecord(configured) || Object.keys(configured).length === 0) {
36
+ return baseDrivers;
37
+ }
38
+
39
+ const stores: Record<string, CacheStore> = {};
40
+ for (const [name, storeConfig] of Object.entries(configured)) {
41
+ const driver = resolveDriver(storeConfig, name);
42
+ stores[name] = baseDrivers[driver] ?? baseDrivers.memory;
43
+ }
44
+
45
+ return stores;
46
+ }
47
+
48
+ protected static defaultDrivers(): Record<string, CacheStore> {
49
+ const memory = new MemoryStorageAdapter();
50
+ const hasWindow = typeof window !== "undefined";
51
+ const hasLocal = hasWindow && typeof window.localStorage !== "undefined";
52
+ const hasIndexed = typeof globalThis.indexedDB !== "undefined";
53
+
54
+ return {
55
+ memory,
56
+ local: hasLocal ? new LocalStorageAdapter(window.localStorage) : memory,
57
+ indexed: hasIndexed ? new IndexedDbAdapter() : memory,
58
+ };
59
+ }
60
+
61
+ store(name?: string): CacheStore {
62
+ const target = typeof name === "string" ? name : this.active;
63
+ if (!(target in this.stores)) {
64
+ throw new Error(`Cache store [${target}] is not defined.`);
65
+ }
66
+
67
+ return this.stores[target];
68
+ }
69
+
70
+ use(name: string): this {
71
+ this.active = this.store(name) ? name : this.active;
72
+ return this;
73
+ }
74
+
75
+ get<T = unknown>(key: string): Promise<T | null> {
76
+ return this.store().get<T>(key);
77
+ }
78
+
79
+ set<T = unknown>(key: string, value: T): Promise<void> {
80
+ return this.store().set<T>(key, value);
81
+ }
82
+
83
+ has(key: string): Promise<boolean> {
84
+ return this.store().has(key);
85
+ }
86
+
87
+ delete(key: string): Promise<void> {
88
+ return this.store().delete(key);
89
+ }
90
+
91
+ clear(): Promise<void> {
92
+ return this.store().clear();
93
+ }
94
+
95
+ keys(): Promise<string[]> {
96
+ return this.store().keys();
97
+ }
98
+ }
99
+
100
+ function firstStoreName(stores: Record<string, CacheStore>): string {
101
+ if ("memory" in stores) {
102
+ return "memory";
103
+ }
104
+
105
+ return Object.keys(stores)[0];
106
+ }
107
+
108
+ function resolveDriver(value: unknown, fallback: string): string {
109
+ if (!isRecord(value)) {
110
+ return fallback;
111
+ }
112
+
113
+ const storeConfig = value as CacheStoreConfig;
114
+ return typeof storeConfig.driver === "string" ? storeConfig.driver : fallback;
115
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ name: "Application",
3
+ timezone: "UTC",
4
+ locale: "en",
5
+ };
@@ -0,0 +1,9 @@
1
+ import type { ConfigItems } from "./repository";
2
+
3
+ export function cloneConfigItems(items: ConfigItems): ConfigItems {
4
+ if (typeof structuredClone === "function") {
5
+ return structuredClone(items) as ConfigItems;
6
+ }
7
+
8
+ return JSON.parse(JSON.stringify(items)) as ConfigItems;
9
+ }
@@ -0,0 +1,5 @@
1
+ declare global {
2
+ function env<T = unknown>(key: string, defaultValue?: T | null): T | unknown | null;
3
+ }
4
+
5
+ export {};
@@ -0,0 +1,79 @@
1
+ type EnvMap = Record<string, unknown>;
2
+
3
+ function normalizeEnvValue(value: unknown): unknown {
4
+ if (typeof value !== "string") {
5
+ return value;
6
+ }
7
+
8
+ const normalized = value.trim().toLowerCase();
9
+ if (normalized === "true" || normalized === "(true)") {
10
+ return true;
11
+ }
12
+
13
+ if (normalized === "false" || normalized === "(false)") {
14
+ return false;
15
+ }
16
+
17
+ if (normalized === "null" || normalized === "(null)") {
18
+ return null;
19
+ }
20
+
21
+ if (normalized === "empty" || normalized === "(empty)") {
22
+ return "";
23
+ }
24
+
25
+ return value;
26
+ }
27
+
28
+ function resolveProcessEnv(): EnvMap {
29
+ const processValue = (
30
+ globalThis as { process?: { env?: EnvMap } }
31
+ ).process;
32
+
33
+ if (!processValue?.env) {
34
+ return {};
35
+ }
36
+
37
+ return processValue.env;
38
+ }
39
+
40
+ function resolveImportMetaEnv(): EnvMap {
41
+ const testEnv = (
42
+ globalThis as {
43
+ __iocImportMetaEnvForTests?: EnvMap;
44
+ }
45
+ ).__iocImportMetaEnvForTests;
46
+ if (testEnv) {
47
+ return testEnv;
48
+ }
49
+
50
+ const meta = import.meta as ImportMeta & { env?: EnvMap };
51
+ return meta.env ?? {};
52
+ }
53
+
54
+ export function env<T = unknown>(key: string, defaultValue: T | null = null): T | unknown | null {
55
+ const importMetaEnv = resolveImportMetaEnv();
56
+ if (key in importMetaEnv) {
57
+ return normalizeEnvValue(importMetaEnv[key]);
58
+ }
59
+
60
+ const processEnv = resolveProcessEnv();
61
+ if (key in processEnv) {
62
+ return normalizeEnvValue(processEnv[key]);
63
+ }
64
+
65
+ return defaultValue;
66
+ }
67
+
68
+ export function registerGlobalEnv(): void {
69
+ const globalScope = globalThis as { env?: typeof env };
70
+ if (typeof globalScope.env !== "function") {
71
+ globalScope.env = env;
72
+ }
73
+ }
74
+
75
+ export const __envTesting = {
76
+ normalizeEnvValue,
77
+ resolveImportMetaEnv,
78
+ resolveProcessEnv,
79
+ };
@@ -0,0 +1,204 @@
1
+ export type ConfigItems = Record<string, unknown>;
2
+ export type ConfigDefaults = Record<string | number, unknown>;
3
+
4
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+
8
+ function resolveDefault<T>(defaultValue: T | (() => T)): T {
9
+ return typeof defaultValue === "function" ? (defaultValue as () => T)() : defaultValue;
10
+ }
11
+
12
+ function dataGet(source: unknown, path: string, defaultValue: unknown = null): unknown {
13
+ if (!path) {
14
+ return source;
15
+ }
16
+
17
+ const segments = path.split(".");
18
+ let cursor: unknown = source;
19
+
20
+ for (const segment of segments) {
21
+ if (isPlainObject(cursor) && segment in cursor) {
22
+ cursor = cursor[segment];
23
+ continue;
24
+ }
25
+
26
+ return resolveDefault(defaultValue);
27
+ }
28
+
29
+ return cursor;
30
+ }
31
+
32
+ function dataSet(target: Record<string, unknown>, path: string, value: unknown): void {
33
+ const segments = path.split(".");
34
+ let cursor: Record<string, unknown> = target;
35
+
36
+ for (let i = 0; i < segments.length; i += 1) {
37
+ const segment = segments[i];
38
+ const isLast = i === segments.length - 1;
39
+
40
+ if (isLast) {
41
+ cursor[segment] = value;
42
+ return;
43
+ }
44
+
45
+ if (!isPlainObject(cursor[segment])) {
46
+ cursor[segment] = {};
47
+ }
48
+
49
+ cursor = cursor[segment] as Record<string, unknown>;
50
+ }
51
+ }
52
+
53
+ function dataForget(target: Record<string, unknown>, path: string): void {
54
+ const segments = path.split(".");
55
+ let cursor: Record<string, unknown> = target;
56
+
57
+ for (let i = 0; i < segments.length; i += 1) {
58
+ const segment = segments[i];
59
+ const isLast = i === segments.length - 1;
60
+
61
+ if (isLast) {
62
+ delete cursor[segment];
63
+ return;
64
+ }
65
+
66
+ if (!isPlainObject(cursor[segment])) {
67
+ return;
68
+ }
69
+
70
+ cursor = cursor[segment] as Record<string, unknown>;
71
+ }
72
+ }
73
+
74
+ export default class ConfigRepository {
75
+ protected items: ConfigItems;
76
+
77
+ constructor(items: ConfigItems = {}) {
78
+ this.items = items;
79
+ }
80
+
81
+ has(key: string | string[]): boolean {
82
+ const keys = Array.isArray(key) ? key : [key];
83
+ for (const configKey of keys) {
84
+ if (this.get(configKey) == null) {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ return true;
90
+ }
91
+
92
+ get<T = unknown>(
93
+ key: string | string[],
94
+ defaultValue: T | (() => T) | null = null,
95
+ ): T | Record<string, unknown> | null {
96
+ if (Array.isArray(key)) {
97
+ return this.getMany(key);
98
+ }
99
+
100
+ return dataGet(this.items, key, defaultValue) as T | null;
101
+ }
102
+
103
+ getMany(keys: string[] | ConfigDefaults): Record<string, unknown> {
104
+ const results: Record<string, unknown> = {};
105
+
106
+ if (Array.isArray(keys)) {
107
+ for (const key of keys) {
108
+ results[key] = this.get(key);
109
+ }
110
+
111
+ return results;
112
+ }
113
+
114
+ for (const [key, defaultValue] of Object.entries(keys)) {
115
+ results[key] = this.get(key, defaultValue);
116
+ }
117
+
118
+ return results;
119
+ }
120
+
121
+ string(key: string, defaultValue: string | (() => string) | null = null): string {
122
+ const value = this.get<string>(key, defaultValue);
123
+ if (typeof value !== "string") {
124
+ throw new TypeError(`Configuration value [${key}] is not a string.`);
125
+ }
126
+
127
+ return value;
128
+ }
129
+
130
+ integer(key: string, defaultValue: number | (() => number) | null = null): number {
131
+ const value = this.get<number>(key, defaultValue);
132
+ if (!Number.isInteger(value)) {
133
+ throw new TypeError(`Configuration value [${key}] is not an integer.`);
134
+ }
135
+
136
+ return value;
137
+ }
138
+
139
+ float(key: string, defaultValue: number | (() => number) | null = null): number {
140
+ const value = this.get<number>(key, defaultValue);
141
+ if (typeof value !== "number" || Number.isNaN(value)) {
142
+ throw new TypeError(`Configuration value [${key}] is not a float.`);
143
+ }
144
+
145
+ return value;
146
+ }
147
+
148
+ boolean(key: string, defaultValue: boolean | (() => boolean) | null = null): boolean {
149
+ const value = this.get<boolean>(key, defaultValue);
150
+ if (typeof value !== "boolean") {
151
+ throw new TypeError(`Configuration value [${key}] is not a boolean.`);
152
+ }
153
+
154
+ return value;
155
+ }
156
+
157
+ array<T = unknown>(key: string, defaultValue: T[] | (() => T[]) | null = null): T[] {
158
+ const value = this.get<T[]>(key, defaultValue);
159
+ if (!Array.isArray(value)) {
160
+ throw new TypeError(`Configuration value [${key}] is not an array.`);
161
+ }
162
+
163
+ return value;
164
+ }
165
+
166
+ set(key: string | ConfigItems, value: unknown = null): void {
167
+ const payload = isPlainObject(key) ? key : { [key]: value };
168
+
169
+ for (const [configKey, configValue] of Object.entries(payload)) {
170
+ dataSet(this.items, configKey, configValue);
171
+ }
172
+ }
173
+
174
+ prepend(key: string, value: unknown): void {
175
+ const values = this.array<unknown>(key, []);
176
+ this.set(key, [value, ...values]);
177
+ }
178
+
179
+ push(key: string, value: unknown): void {
180
+ const values = this.array<unknown>(key, []);
181
+ values.push(value);
182
+ this.set(key, values);
183
+ }
184
+
185
+ all(): ConfigItems {
186
+ return this.items;
187
+ }
188
+
189
+ offsetExists(key: string): boolean {
190
+ return this.has(key);
191
+ }
192
+
193
+ offsetGet<T = unknown>(key: string): T | null {
194
+ return this.get<T>(key) as T | null;
195
+ }
196
+
197
+ offsetSet(key: string, value: unknown): void {
198
+ this.set(key, value);
199
+ }
200
+
201
+ offsetUnset(key: string): void {
202
+ dataForget(this.items, key);
203
+ }
204
+ }
@@ -0,0 +1,104 @@
1
+ import type { ConfigItems } from "../../config/repository";
2
+ import type { FileSystemAdapter } from "../filesystem";
3
+
4
+ type ConfigModule = { default?: unknown };
5
+ type GlobLoader = (
6
+ pattern: string | string[],
7
+ options?: { eager?: boolean },
8
+ ) => Record<string, unknown>;
9
+
10
+ function fileNameWithoutExtension(path: string): string {
11
+ const file = path.split("/").pop() ?? path;
12
+ return file.replace(/\.[^/.]+$/, "");
13
+ }
14
+
15
+ function normalizeBasePath(basePath: string): string {
16
+ const trimmed = basePath.trim();
17
+ if (!trimmed || trimmed === "." || trimmed === "./" || trimmed === "/") {
18
+ return "";
19
+ }
20
+
21
+ let normalized = trimmed.replace(/\\/g, "/");
22
+ normalized = normalized.replace(/^\.\//, "");
23
+ normalized = normalized.replace(/^\/+/, "");
24
+ normalized = normalized.replace(/\/+$/, "");
25
+
26
+ return normalized;
27
+ }
28
+
29
+ function isPathInConfigDirectories(path: string, basePath: string): boolean {
30
+ const normalizedPath = path.replace(/\\/g, "/");
31
+ const trimmedPath = normalizedPath.replace(/^\/+/, "");
32
+ const normalizedBasePath = normalizeBasePath(basePath);
33
+
34
+ const targets = normalizedBasePath
35
+ ? [`${normalizedBasePath}/config/`, `${normalizedBasePath}/src/config/`]
36
+ : ["config/", "src/config/"];
37
+
38
+ for (const target of targets) {
39
+ if (
40
+ trimmedPath.startsWith(target) ||
41
+ normalizedPath.includes(`/${target}`) ||
42
+ normalizedPath.endsWith(`/${target.slice(0, -1)}`)
43
+ ) {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ return false;
49
+ }
50
+
51
+ export default class LocalAdapter implements FileSystemAdapter {
52
+ constructor() {}
53
+
54
+ loadConfigItems(basePath: string): ConfigItems {
55
+ const viteGlob = (
56
+ import.meta as ImportMeta & {
57
+ glob?: GlobLoader;
58
+ }
59
+ ).glob;
60
+ const testGlob = (
61
+ globalThis as {
62
+ __iocConfigGlobForTests?: GlobLoader;
63
+ }
64
+ ).__iocConfigGlobForTests;
65
+ const glob =
66
+ typeof viteGlob === "function"
67
+ ? viteGlob
68
+ : typeof testGlob === "function"
69
+ ? testGlob
70
+ : null;
71
+ if (!glob) {
72
+ return {};
73
+ }
74
+
75
+ const modules = glob(
76
+ [
77
+ "/config/*.{js,mjs,cjs,ts,mts,cts}",
78
+ "/src/config/*.{js,mjs,cjs,ts,mts,cts}",
79
+ ],
80
+ { eager: true },
81
+ ) as Record<string, unknown>;
82
+
83
+ const discovered: ConfigItems = {};
84
+ for (const [path, loaded] of Object.entries(modules)) {
85
+ if (!isPathInConfigDirectories(path, basePath)) {
86
+ continue;
87
+ }
88
+
89
+ const key = fileNameWithoutExtension(path);
90
+ const module = loaded as ConfigModule;
91
+ if (module && "default" in module && module.default !== undefined) {
92
+ discovered[key] = module.default;
93
+ }
94
+ }
95
+
96
+ return discovered;
97
+ }
98
+ }
99
+
100
+ export const __localAdapterTesting = {
101
+ fileNameWithoutExtension,
102
+ normalizeBasePath,
103
+ isPathInConfigDirectories,
104
+ };
@@ -0,0 +1,21 @@
1
+ import type { ConfigItems } from "../config/repository";
2
+
3
+ export interface FileSystemAdapter {
4
+ loadConfigItems(basePath: string): ConfigItems;
5
+ }
6
+
7
+ export class FileSystem {
8
+ protected adapter: FileSystemAdapter;
9
+
10
+ constructor(adapter: FileSystemAdapter) {
11
+ this.adapter = adapter;
12
+ }
13
+
14
+ setAdapter(adapter: FileSystemAdapter): void {
15
+ this.adapter = adapter;
16
+ }
17
+
18
+ loadConfigItems(basePath: string): ConfigItems {
19
+ return this.adapter.loadConfigItems(basePath);
20
+ }
21
+ }