@xframework/x 0.4.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/.turbo/turbo-build.log +46 -0
- package/CHANGELOG.md +7 -0
- package/dist/adapter.cjs +50 -0
- package/dist/adapter.cjs.map +1 -0
- package/dist/adapter.d.cts +2 -0
- package/dist/adapter.d.ts +2 -0
- package/dist/adapter.js +21 -0
- package/dist/adapter.js.map +1 -0
- package/dist/core/adapters.cjs +48 -0
- package/dist/core/adapters.cjs.map +1 -0
- package/dist/core/adapters.d.cts +19 -0
- package/dist/core/adapters.d.ts +19 -0
- package/dist/core/adapters.js +21 -0
- package/dist/core/adapters.js.map +1 -0
- package/dist/core/types.cjs +19 -0
- package/dist/core/types.cjs.map +1 -0
- package/dist/core/types.d.cts +25 -0
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/x.cjs +134 -0
- package/dist/core/x.cjs.map +1 -0
- package/dist/core/x.d.cts +17 -0
- package/dist/core/x.d.ts +17 -0
- package/dist/core/x.js +107 -0
- package/dist/core/x.js.map +1 -0
- package/dist/index.cjs +134 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +107 -0
- package/dist/index.js.map +1 -0
- package/package.json +33 -0
- package/src/__tests__/async-adapter.test.ts +87 -0
- package/src/__tests__/composition.test.ts +45 -0
- package/src/__tests__/core.test.ts +23 -0
- package/src/__tests__/error-handling.test.ts +71 -0
- package/src/__tests__/sync-adapter.test.ts +83 -0
- package/src/__tests__/type-system.test.ts +78 -0
- package/src/adapter.ts +1 -0
- package/src/core/adapters.ts +26 -0
- package/src/core/types.ts +47 -0
- package/src/core/x.ts +163 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +3 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
2
|
+
import { createX } from "..";
|
3
|
+
import { SyncAdapter } from "../adapter";
|
4
|
+
|
5
|
+
interface Config {
|
6
|
+
key: string;
|
7
|
+
value: number;
|
8
|
+
}
|
9
|
+
|
10
|
+
class ConfigAdapter extends SyncAdapter<Config> {
|
11
|
+
export(): Config {
|
12
|
+
return { key: "test", value: 42 };
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
16
|
+
class NumberAdapter extends SyncAdapter<number> {
|
17
|
+
constructor(private value: number) {
|
18
|
+
super();
|
19
|
+
}
|
20
|
+
export() {
|
21
|
+
return this.value;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
class StringAdapter extends SyncAdapter<string> {
|
26
|
+
constructor(private deps: { number: number }) {
|
27
|
+
super();
|
28
|
+
}
|
29
|
+
export() {
|
30
|
+
return `value: ${this.deps.number}`;
|
31
|
+
}
|
32
|
+
}
|
33
|
+
|
34
|
+
// type-system.test.ts
|
35
|
+
describe("Type System Tests", () => {
|
36
|
+
it("should properly type exports", () => {
|
37
|
+
const result = createX()
|
38
|
+
.syncAdapter("config", () => new ConfigAdapter())
|
39
|
+
.build();
|
40
|
+
|
41
|
+
// TypeScript should recognize these properties
|
42
|
+
expect(result.config.key).toBe("test");
|
43
|
+
expect(result.config.value).toBe(42);
|
44
|
+
});
|
45
|
+
|
46
|
+
it("should properly type dependencies", () => {
|
47
|
+
class DependentAdapter extends SyncAdapter<string> {
|
48
|
+
constructor(private deps: { config: { value: number } }) {
|
49
|
+
super();
|
50
|
+
}
|
51
|
+
export() {
|
52
|
+
return `value: ${this.deps.config.value}`;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
const result = createX()
|
57
|
+
.syncAdapter("config", () => new ConfigAdapter())
|
58
|
+
.syncAdapter("dependent", (deps) => new DependentAdapter(deps))
|
59
|
+
.build();
|
60
|
+
|
61
|
+
expect(result.dependent).toBe("value: 42");
|
62
|
+
});
|
63
|
+
|
64
|
+
it("should maintain type safety with use()", () => {
|
65
|
+
const first = createX().syncAdapter("num", () => new NumberAdapter(1));
|
66
|
+
const second = createX()
|
67
|
+
.use(first)
|
68
|
+
.syncAdapter("str", ({ num }) => new StringAdapter({ number: num }));
|
69
|
+
|
70
|
+
// TypeScript should recognize these types
|
71
|
+
type SecondType = typeof second;
|
72
|
+
type BuildType = Awaited<ReturnType<SecondType["build"]>>;
|
73
|
+
|
74
|
+
const result = second.build() as BuildType;
|
75
|
+
expect(result.num).toBeDefined();
|
76
|
+
expect(result.str).toBeDefined();
|
77
|
+
});
|
78
|
+
});
|
package/src/adapter.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export { AsyncAdapter, SyncAdapter, AdapterError } from "./core/adapters";
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import type { SyncAdapterType, AsyncAdapterType } from "./types";
|
2
|
+
|
3
|
+
export abstract class SyncAdapter<TExport> implements SyncAdapterType<TExport> {
|
4
|
+
readonly __type = "sync" as const;
|
5
|
+
init?(): void;
|
6
|
+
abstract export(): TExport;
|
7
|
+
}
|
8
|
+
|
9
|
+
export abstract class AsyncAdapter<TExport>
|
10
|
+
implements AsyncAdapterType<TExport>
|
11
|
+
{
|
12
|
+
readonly __type = "async" as const;
|
13
|
+
init?(): Promise<void>;
|
14
|
+
abstract export(): Promise<TExport>;
|
15
|
+
}
|
16
|
+
|
17
|
+
export class AdapterError extends Error {
|
18
|
+
constructor(
|
19
|
+
message: string,
|
20
|
+
public readonly adapterName: string,
|
21
|
+
public readonly cause?: unknown,
|
22
|
+
) {
|
23
|
+
super(message);
|
24
|
+
this.name = "AdapterError";
|
25
|
+
}
|
26
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
// Core adapter type definitions
|
2
|
+
export type SyncAdapterType<TExport = unknown> = {
|
3
|
+
__type: "sync";
|
4
|
+
init?(): void;
|
5
|
+
export(): TExport;
|
6
|
+
};
|
7
|
+
|
8
|
+
export type AsyncAdapterType<TExport = unknown> = {
|
9
|
+
__type: "async";
|
10
|
+
init?(): Promise<void>;
|
11
|
+
export(): Promise<TExport>;
|
12
|
+
};
|
13
|
+
|
14
|
+
export type AnyAdapter = SyncAdapterType | AsyncAdapterType;
|
15
|
+
export type AdapterRegistry = Record<string, AnyAdapter>;
|
16
|
+
|
17
|
+
// Helper type to extract the exported type from an adapter
|
18
|
+
export type ExportType<TAdapter> = TAdapter extends
|
19
|
+
| SyncAdapterType<infer TExport>
|
20
|
+
| AsyncAdapterType<infer TExport>
|
21
|
+
? TExport
|
22
|
+
: never;
|
23
|
+
|
24
|
+
// Result type with metadata container
|
25
|
+
export type AdapterResult<TAdapters extends AdapterRegistry> = {
|
26
|
+
[Key in keyof TAdapters]: ExportType<TAdapters[Key]>;
|
27
|
+
} & {
|
28
|
+
_: {
|
29
|
+
adapters: TAdapters;
|
30
|
+
};
|
31
|
+
};
|
32
|
+
|
33
|
+
export type BuildOutput<
|
34
|
+
TAdapters extends AdapterRegistry,
|
35
|
+
TIsAsync extends boolean,
|
36
|
+
> = TIsAsync extends true
|
37
|
+
? Promise<AdapterResult<TAdapters>>
|
38
|
+
: AdapterResult<TAdapters>;
|
39
|
+
|
40
|
+
// Factory types
|
41
|
+
export type SyncAdapterCreator<TExport, TDeps extends AdapterRegistry> = (
|
42
|
+
dependencies: AdapterResult<TDeps>,
|
43
|
+
) => SyncAdapterType<TExport>;
|
44
|
+
|
45
|
+
export type AsyncAdapterCreator<TExport, TDeps extends AdapterRegistry> = (
|
46
|
+
dependencies: AdapterResult<TDeps>,
|
47
|
+
) => Promise<AsyncAdapterType<TExport>> | AsyncAdapterType<TExport>;
|
package/src/core/x.ts
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
import { AdapterError } from "./adapters";
|
2
|
+
import type {
|
3
|
+
SyncAdapterCreator,
|
4
|
+
SyncAdapterType,
|
5
|
+
AsyncAdapterCreator,
|
6
|
+
AsyncAdapterType,
|
7
|
+
AdapterRegistry,
|
8
|
+
AdapterResult,
|
9
|
+
BuildOutput,
|
10
|
+
} from "./types";
|
11
|
+
|
12
|
+
type AdapterFactory<T extends AdapterRegistry> =
|
13
|
+
| [(deps: AdapterResult<T>) => SyncAdapterType<unknown>, false]
|
14
|
+
| [(deps: AdapterResult<T>) => Promise<AsyncAdapterType<unknown>>, true];
|
15
|
+
|
16
|
+
class X<
|
17
|
+
TAdapters extends AdapterRegistry = Record<string, never>,
|
18
|
+
TIsAsync extends boolean = false,
|
19
|
+
> {
|
20
|
+
constructor(
|
21
|
+
private readonly adapterFactories: Record<
|
22
|
+
string,
|
23
|
+
AdapterFactory<TAdapters>
|
24
|
+
> = {} as Record<string, AdapterFactory<TAdapters>>,
|
25
|
+
private readonly isAsync: TIsAsync = false as TIsAsync,
|
26
|
+
) {}
|
27
|
+
|
28
|
+
syncAdapter<TKey extends string, TExport>(
|
29
|
+
name: TKey,
|
30
|
+
factory: SyncAdapterCreator<TExport, TAdapters>,
|
31
|
+
): X<TAdapters & Record<TKey, SyncAdapterType<TExport>>, TIsAsync> {
|
32
|
+
type NewAdapters = TAdapters & Record<TKey, SyncAdapterType<TExport>>;
|
33
|
+
const newFactories = { ...this.adapterFactories };
|
34
|
+
// We know this is safe because factory is a SyncAdapterCreator
|
35
|
+
(newFactories as Record<string, unknown>)[name] = [
|
36
|
+
factory,
|
37
|
+
false,
|
38
|
+
] as AdapterFactory<NewAdapters>;
|
39
|
+
return new X(
|
40
|
+
newFactories as Record<string, AdapterFactory<NewAdapters>>,
|
41
|
+
this.isAsync,
|
42
|
+
);
|
43
|
+
}
|
44
|
+
|
45
|
+
asyncAdapter<TKey extends string, TExport>(
|
46
|
+
name: TKey,
|
47
|
+
factory: AsyncAdapterCreator<TExport, TAdapters>,
|
48
|
+
): X<TAdapters & Record<TKey, AsyncAdapterType<TExport>>, true> {
|
49
|
+
type NewAdapters = TAdapters & Record<TKey, AsyncAdapterType<TExport>>;
|
50
|
+
const wrappedFactory = async (deps: AdapterResult<TAdapters>) => {
|
51
|
+
const result = factory(deps);
|
52
|
+
return result instanceof Promise ? await result : result;
|
53
|
+
};
|
54
|
+
|
55
|
+
const newFactories = { ...this.adapterFactories };
|
56
|
+
// We know this is safe because wrappedFactory returns a Promise<AsyncAdapterType>
|
57
|
+
(newFactories as Record<string, unknown>)[name] = [
|
58
|
+
wrappedFactory,
|
59
|
+
true,
|
60
|
+
] as AdapterFactory<NewAdapters>;
|
61
|
+
return new X(
|
62
|
+
newFactories as Record<string, AdapterFactory<NewAdapters>>,
|
63
|
+
true,
|
64
|
+
);
|
65
|
+
}
|
66
|
+
|
67
|
+
use<TOtherAdapters extends AdapterRegistry, TOtherAsync extends boolean>(
|
68
|
+
other: X<TOtherAdapters, TOtherAsync>,
|
69
|
+
): X<TAdapters & TOtherAdapters, TIsAsync | TOtherAsync> {
|
70
|
+
type CombinedAdapters = TAdapters & TOtherAdapters;
|
71
|
+
const newFactories = {
|
72
|
+
...this.adapterFactories,
|
73
|
+
...other["adapterFactories"],
|
74
|
+
} as Record<string, AdapterFactory<CombinedAdapters>>;
|
75
|
+
return new X(newFactories, this.isAsync || other["isAsync"]);
|
76
|
+
}
|
77
|
+
|
78
|
+
build(): BuildOutput<TAdapters, TIsAsync> {
|
79
|
+
return this.isAsync
|
80
|
+
? (this.buildAsync() as BuildOutput<TAdapters, TIsAsync>)
|
81
|
+
: (this.buildSync() as BuildOutput<TAdapters, TIsAsync>);
|
82
|
+
}
|
83
|
+
|
84
|
+
private buildSync(): AdapterResult<TAdapters> {
|
85
|
+
const result = {
|
86
|
+
_: { adapters: {} as TAdapters },
|
87
|
+
} as AdapterResult<TAdapters>;
|
88
|
+
|
89
|
+
for (const [key, [factory, isAsync]] of Object.entries(
|
90
|
+
this.adapterFactories,
|
91
|
+
)) {
|
92
|
+
if (isAsync) {
|
93
|
+
throw new AdapterError(
|
94
|
+
`Cannot use async adapter "${key}" in sync build`,
|
95
|
+
key,
|
96
|
+
);
|
97
|
+
}
|
98
|
+
|
99
|
+
const adapter = factory(result) as SyncAdapterType<unknown>;
|
100
|
+
|
101
|
+
if (adapter.__type !== "sync") {
|
102
|
+
throw new AdapterError(
|
103
|
+
`Factory returned async adapter "${key}" in sync build`,
|
104
|
+
key,
|
105
|
+
);
|
106
|
+
}
|
107
|
+
|
108
|
+
(result._.adapters as Record<string, SyncAdapterType<unknown>>)[key] =
|
109
|
+
adapter;
|
110
|
+
adapter.init?.();
|
111
|
+
(result as Record<string, unknown>)[key] = adapter.export();
|
112
|
+
}
|
113
|
+
|
114
|
+
return result;
|
115
|
+
}
|
116
|
+
|
117
|
+
private async buildAsync(): Promise<AdapterResult<TAdapters>> {
|
118
|
+
const result = {
|
119
|
+
_: { adapters: {} as TAdapters },
|
120
|
+
} as AdapterResult<TAdapters>;
|
121
|
+
|
122
|
+
for (const [key, [factory, isAsync]] of Object.entries(
|
123
|
+
this.adapterFactories,
|
124
|
+
)) {
|
125
|
+
const currentResult = { ...result };
|
126
|
+
const adapter = isAsync
|
127
|
+
? await (
|
128
|
+
factory as (
|
129
|
+
deps: AdapterResult<TAdapters>,
|
130
|
+
) => Promise<AsyncAdapterType<unknown>>
|
131
|
+
)(currentResult)
|
132
|
+
: (
|
133
|
+
factory as (
|
134
|
+
deps: AdapterResult<TAdapters>,
|
135
|
+
) => SyncAdapterType<unknown>
|
136
|
+
)(currentResult);
|
137
|
+
|
138
|
+
(
|
139
|
+
result._.adapters as Record<
|
140
|
+
string,
|
141
|
+
SyncAdapterType<unknown> | AsyncAdapterType<unknown>
|
142
|
+
>
|
143
|
+
)[key] = adapter;
|
144
|
+
|
145
|
+
if (adapter.init) {
|
146
|
+
if (adapter.__type === "async") {
|
147
|
+
await adapter.init();
|
148
|
+
} else {
|
149
|
+
adapter.init();
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
(result as Record<string, unknown>)[key] =
|
154
|
+
adapter.__type === "async" ? await adapter.export() : adapter.export();
|
155
|
+
}
|
156
|
+
|
157
|
+
return result;
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
export function createX() {
|
162
|
+
return new X();
|
163
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export { createX } from "./core/x";
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED