@fastact/core 0.1.0-dev
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/README.md +0 -0
- package/dist/application-factory.d.ts +2 -0
- package/dist/application-factory.js +7 -0
- package/dist/application.d.ts +13 -0
- package/dist/application.js +42 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/ioc/container-builder.d.ts +18 -0
- package/dist/ioc/container-builder.js +97 -0
- package/dist/ioc/container.d.ts +29 -0
- package/dist/ioc/container.js +97 -0
- package/dist/ioc/index.d.ts +2 -0
- package/dist/ioc/index.js +5 -0
- package/dist/ioc/types.d.ts +35 -0
- package/dist/ioc/types.js +1 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { ContainerInstance } from './ioc';
|
|
3
|
+
import type { ApplicationOptions, Application } from './types';
|
|
4
|
+
export declare class App implements Application {
|
|
5
|
+
private readonly container;
|
|
6
|
+
private readonly server;
|
|
7
|
+
private readonly options;
|
|
8
|
+
constructor(container: ContainerInstance, options?: ApplicationOptions);
|
|
9
|
+
getContainer(): ContainerInstance;
|
|
10
|
+
getServer(): FastifyInstance;
|
|
11
|
+
start(): Promise<void>;
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fastify from 'fastify';
|
|
3
|
+
import { DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT } from './constants';
|
|
4
|
+
export class App {
|
|
5
|
+
container;
|
|
6
|
+
server;
|
|
7
|
+
options;
|
|
8
|
+
constructor(container, options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.container = container;
|
|
11
|
+
this.server = fastify(this.options.server);
|
|
12
|
+
}
|
|
13
|
+
getContainer() {
|
|
14
|
+
return this.container;
|
|
15
|
+
}
|
|
16
|
+
getServer() {
|
|
17
|
+
return this.server;
|
|
18
|
+
}
|
|
19
|
+
async start() {
|
|
20
|
+
/** Exit immediately when running as a CLI command or worker without a server. */
|
|
21
|
+
if (this.options.runServer === false) {
|
|
22
|
+
console.log(chalk.yellow('FastAct initialized in standalone (CLI) mode'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const host = this.options.host ?? DEFAULT_SERVER_HOST;
|
|
26
|
+
const port = this.options.port ?? DEFAULT_SERVER_PORT;
|
|
27
|
+
try {
|
|
28
|
+
await this.server.ready();
|
|
29
|
+
await this.server.listen({ port, host });
|
|
30
|
+
console.log(`${chalk.green.bold('[fastact]')} Server is now listening on ${host}:${port}`);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.log(`${chalk.red.bold('[fastact]')} ${err}`);
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async stop() {
|
|
38
|
+
console.log(`${chalk.yellow.bold('[fastact] Shutting down the server...')}`);
|
|
39
|
+
await this.server.close();
|
|
40
|
+
console.log(chalk.bgGreenBright.bold('[fastact] Server gracefully stopped'));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export const DEFAULT_SERVER_HOST = 'localhost';
|
|
3
|
+
export const DEFAULT_SERVER_PORT = 3000;
|
|
4
|
+
export const LOG_PREFIX = {
|
|
5
|
+
INFO: chalk.green.bold('[fastact]'),
|
|
6
|
+
WARN: chalk.yellow.bold('[fastact]'),
|
|
7
|
+
ERROR: chalk.red.bold('[fastact]'),
|
|
8
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IContainerBuilder, ContainerInstance, InjectionToken, RegistrationTarget } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Dependency container builder with a fluent registration API.
|
|
4
|
+
*
|
|
5
|
+
* Example: `builder.add(DI.UserService).asClass(createUserService).withDeps(DI.Database).scoped()`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ContainerBuilder implements IContainerBuilder {
|
|
8
|
+
private container;
|
|
9
|
+
private register;
|
|
10
|
+
/**
|
|
11
|
+
* Starts dependency registration using its unique token.
|
|
12
|
+
*
|
|
13
|
+
* Specify a class with `asClass()`, a factory with `asFactory()`, or a ready
|
|
14
|
+
* value with `asValue()`.
|
|
15
|
+
*/
|
|
16
|
+
add<T>(token: InjectionToken<T>): RegistrationTarget<T>;
|
|
17
|
+
build(): Promise<ContainerInstance>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { glob } from 'node:fs/promises';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { Container } from './container';
|
|
5
|
+
/**
|
|
6
|
+
* Dependency container builder with a fluent registration API.
|
|
7
|
+
*
|
|
8
|
+
* Example: `builder.add(DI.UserService).asClass(createUserService).withDeps(DI.Database).scoped()`.
|
|
9
|
+
*/
|
|
10
|
+
export class ContainerBuilder {
|
|
11
|
+
container = new Container();
|
|
12
|
+
register(token, factory, deps, lifecycle) {
|
|
13
|
+
this.container.register(token, factory, deps, lifecycle);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Starts dependency registration using its unique token.
|
|
18
|
+
*
|
|
19
|
+
* Specify a class with `asClass()`, a factory with `asFactory()`, or a ready
|
|
20
|
+
* value with `asValue()`.
|
|
21
|
+
*/
|
|
22
|
+
add(token) {
|
|
23
|
+
const withFactory = (factory) => {
|
|
24
|
+
// prettier-ignore
|
|
25
|
+
const withLifecycle = (deps = []) => ({
|
|
26
|
+
/** One instance per scope, such as an HTTP request. */
|
|
27
|
+
scoped: () => this.register(token, factory, deps, 'scoped'),
|
|
28
|
+
/** One instance for the container's entire lifetime. */
|
|
29
|
+
singleton: () => this.register(token, factory, deps, 'singleton'),
|
|
30
|
+
/** A new instance each time the dependency is resolved. */
|
|
31
|
+
transient: () => this.register(token, factory, deps, 'transient'),
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
...withLifecycle(),
|
|
35
|
+
/**
|
|
36
|
+
* Passes factory dependency tokens in the order of its arguments.
|
|
37
|
+
* Accepts individual tokens or a single array of tokens.
|
|
38
|
+
*/
|
|
39
|
+
withDeps: (...deps) => withLifecycle(deps.flat()),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
/** Specifies a class that the container instantiates with `new`. */
|
|
44
|
+
asClass: (Class) => withFactory((...deps) => new Class(...deps)),
|
|
45
|
+
/** Specifies a factory that creates the dependency instance. */
|
|
46
|
+
asFactory: (factory) => withFactory(factory),
|
|
47
|
+
/** Registers a ready value as a singleton dependency. */
|
|
48
|
+
asValue: (value) => {
|
|
49
|
+
this.container.registerValue(token, value);
|
|
50
|
+
return this;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async build() {
|
|
55
|
+
let moduleErrorName;
|
|
56
|
+
try {
|
|
57
|
+
const runDir = path.dirname(process.argv[1]);
|
|
58
|
+
const modulesGlob = glob('**/*.module.{ts,js}', {
|
|
59
|
+
cwd: runDir,
|
|
60
|
+
exclude: (p) => p.includes('node_modules'),
|
|
61
|
+
});
|
|
62
|
+
for await (const relativePath of modulesGlob) {
|
|
63
|
+
const fullPath = path.resolve(runDir, relativePath);
|
|
64
|
+
//console.log(
|
|
65
|
+
// `${chalk.green.bold('[fastact]')} Found module file: ${relativePath}`
|
|
66
|
+
//);
|
|
67
|
+
const moduleExport = require(fullPath);
|
|
68
|
+
moduleErrorName = fullPath;
|
|
69
|
+
const initModule = moduleExport.createContainerModule;
|
|
70
|
+
if (typeof initModule === 'function') {
|
|
71
|
+
await initModule(this);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.warn(`${chalk.yellow.bold('[fastact]')} File ${relativePath} skipped: missing "export function createContainerModule(builder) { ... }"`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.log(`${chalk.red.bold('[fastact]')} IoC auto-load failed: ${err instanceof Error ? err.message : err} in the module: ${moduleErrorName}`);
|
|
80
|
+
}
|
|
81
|
+
const container = this.container;
|
|
82
|
+
/* container.get = <T>(token: InjectionToken<T>): T => {
|
|
83
|
+
const key = token.description;
|
|
84
|
+
if (!key) {
|
|
85
|
+
throw new Error('Symbol token must have a description to be resolved!');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const dependency = container[key];
|
|
89
|
+
if (!dependency) {
|
|
90
|
+
throw new Error(`IoC dependency for Symbol(${key}) not found`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return dependency;
|
|
94
|
+
}; */
|
|
95
|
+
return this.container;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Lifecycle, Factory, InjectionToken } from './types';
|
|
2
|
+
export declare class Container {
|
|
3
|
+
private registry;
|
|
4
|
+
private scoped;
|
|
5
|
+
/** Registers a dependency factory and its dependency list. */
|
|
6
|
+
register<T, D extends any[]>(token: InjectionToken<T>, factory: Factory<T, D>, deps: InjectionToken<D[number]>[], lifecycle?: Lifecycle): this;
|
|
7
|
+
/** Registers a ready value as a singleton dependency. */
|
|
8
|
+
registerValue<T>(token: InjectionToken<T>, value: T): this;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves a dependency. Scoped dependencies require a scope identifier,
|
|
11
|
+
* such as an HTTP request identifier.
|
|
12
|
+
*/
|
|
13
|
+
get<T>(token: InjectionToken<T>, scopeId?: string, resolvesSingleton?: boolean, resolutionPath?: InjectionToken<any>[]): T;
|
|
14
|
+
private build;
|
|
15
|
+
/** Creates an isolated scope for scoped dependencies. */
|
|
16
|
+
createScope(id: string): ScopedContainer;
|
|
17
|
+
/** Removes all cached scoped dependencies for the specified scope. */
|
|
18
|
+
clearScope(id: string): void;
|
|
19
|
+
private clearTokenFromScopes;
|
|
20
|
+
}
|
|
21
|
+
/** A container with a predefined scope identifier. */
|
|
22
|
+
export declare class ScopedContainer {
|
|
23
|
+
private parent;
|
|
24
|
+
scopeId: string;
|
|
25
|
+
constructor(parent: Container, scopeId: string);
|
|
26
|
+
get<T>(token: InjectionToken<T>): T;
|
|
27
|
+
/** Clears the dependency cache for the current scope. */
|
|
28
|
+
clear(): void;
|
|
29
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class Container {
|
|
2
|
+
registry = new Map();
|
|
3
|
+
scoped = new Map();
|
|
4
|
+
/** Registers a dependency factory and its dependency list. */
|
|
5
|
+
register(token, factory, deps, lifecycle = 'scoped') {
|
|
6
|
+
this.registry.set(token, {
|
|
7
|
+
factory,
|
|
8
|
+
deps,
|
|
9
|
+
lifecycle,
|
|
10
|
+
isInitialized: false,
|
|
11
|
+
});
|
|
12
|
+
this.clearTokenFromScopes(token);
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
/** Registers a ready value as a singleton dependency. */
|
|
16
|
+
registerValue(token, value) {
|
|
17
|
+
this.registry.set(token, {
|
|
18
|
+
factory: () => value,
|
|
19
|
+
deps: [],
|
|
20
|
+
lifecycle: 'singleton',
|
|
21
|
+
instance: value,
|
|
22
|
+
isInitialized: true,
|
|
23
|
+
});
|
|
24
|
+
this.clearTokenFromScopes(token);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolves a dependency. Scoped dependencies require a scope identifier,
|
|
29
|
+
* such as an HTTP request identifier.
|
|
30
|
+
*/
|
|
31
|
+
get(token, scopeId, resolvesSingleton = false, resolutionPath = []) {
|
|
32
|
+
const entry = this.registry.get(token);
|
|
33
|
+
if (!entry)
|
|
34
|
+
throw new Error(`Dependency ${String(token)} not found`);
|
|
35
|
+
if (entry.lifecycle === 'singleton') {
|
|
36
|
+
if (!entry.isInitialized) {
|
|
37
|
+
entry.instance = this.build(token, entry, scopeId, true, resolutionPath);
|
|
38
|
+
entry.isInitialized = true;
|
|
39
|
+
}
|
|
40
|
+
return entry.instance;
|
|
41
|
+
}
|
|
42
|
+
if (entry.lifecycle === 'scoped') {
|
|
43
|
+
if (resolvesSingleton) {
|
|
44
|
+
throw new Error(`Singleton dependency chain cannot include scoped dependency ${String(token)}`);
|
|
45
|
+
}
|
|
46
|
+
if (!scopeId)
|
|
47
|
+
throw new Error(`Scope ID required for ${String(token)}`);
|
|
48
|
+
if (!this.scoped.has(scopeId)) {
|
|
49
|
+
this.scoped.set(scopeId, new Map());
|
|
50
|
+
}
|
|
51
|
+
const scopeCache = this.scoped.get(scopeId);
|
|
52
|
+
if (!scopeCache.has(token)) {
|
|
53
|
+
scopeCache.set(token, this.build(token, entry, scopeId, false, resolutionPath));
|
|
54
|
+
}
|
|
55
|
+
return scopeCache.get(token);
|
|
56
|
+
}
|
|
57
|
+
return this.build(token, entry, scopeId, resolvesSingleton, resolutionPath);
|
|
58
|
+
}
|
|
59
|
+
build(token, entry, scopeId, resolvesSingleton, resolutionPath) {
|
|
60
|
+
if (resolutionPath.includes(token)) {
|
|
61
|
+
const chain = [...resolutionPath, token].map(String).join(' -> ');
|
|
62
|
+
throw new Error(`Circular dependency detected: ${chain}`);
|
|
63
|
+
}
|
|
64
|
+
const nextPath = [...resolutionPath, token];
|
|
65
|
+
const args = entry.deps.map((dep) => this.get(dep, scopeId, resolvesSingleton, nextPath));
|
|
66
|
+
return entry.factory(...args);
|
|
67
|
+
}
|
|
68
|
+
/** Creates an isolated scope for scoped dependencies. */
|
|
69
|
+
createScope(id) {
|
|
70
|
+
return new ScopedContainer(this, id);
|
|
71
|
+
}
|
|
72
|
+
/** Removes all cached scoped dependencies for the specified scope. */
|
|
73
|
+
clearScope(id) {
|
|
74
|
+
this.scoped.delete(id);
|
|
75
|
+
}
|
|
76
|
+
clearTokenFromScopes(token) {
|
|
77
|
+
for (const scopeCache of this.scoped.values()) {
|
|
78
|
+
scopeCache.delete(token);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** A container with a predefined scope identifier. */
|
|
83
|
+
export class ScopedContainer {
|
|
84
|
+
parent;
|
|
85
|
+
scopeId;
|
|
86
|
+
constructor(parent, scopeId) {
|
|
87
|
+
this.parent = parent;
|
|
88
|
+
this.scopeId = scopeId;
|
|
89
|
+
}
|
|
90
|
+
get(token) {
|
|
91
|
+
return this.parent.get(token, this.scopeId);
|
|
92
|
+
}
|
|
93
|
+
/** Clears the dependency cache for the current scope. */
|
|
94
|
+
clear() {
|
|
95
|
+
this.parent.clearScope(this.scopeId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Public contract for configuring and building a dependency container. */
|
|
2
|
+
export interface IContainerBuilder {
|
|
3
|
+
add<T>(token: InjectionToken<T>): RegistrationTarget<T>;
|
|
4
|
+
build(): Promise<ContainerInstance>;
|
|
5
|
+
}
|
|
6
|
+
export interface ContainerInstance {
|
|
7
|
+
get<T>(token: InjectionToken<T>): T;
|
|
8
|
+
}
|
|
9
|
+
export interface SymbolToken<T> extends Symbol {
|
|
10
|
+
readonly __type?: T;
|
|
11
|
+
}
|
|
12
|
+
export type InjectionToken<T> = string | SymbolToken<T>;
|
|
13
|
+
/** The lifetime of a registered dependency. */
|
|
14
|
+
export type Lifecycle = 'singleton' | 'scoped' | 'transient';
|
|
15
|
+
export type Factory<T, Deps extends any[] = any[]> = (...deps: Deps) => T;
|
|
16
|
+
export interface DepEntry<T> {
|
|
17
|
+
factory: Factory<T>;
|
|
18
|
+
deps: InjectionToken<any>[];
|
|
19
|
+
lifecycle: Lifecycle;
|
|
20
|
+
instance?: T;
|
|
21
|
+
isInitialized: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface RegistrationLifecycle {
|
|
24
|
+
scoped(): IContainerBuilder;
|
|
25
|
+
singleton(): IContainerBuilder;
|
|
26
|
+
transient(): IContainerBuilder;
|
|
27
|
+
}
|
|
28
|
+
export interface RegistrationWithDependencies extends RegistrationLifecycle {
|
|
29
|
+
withDeps(...deps: Array<InjectionToken<any> | InjectionToken<any>[]>): RegistrationLifecycle;
|
|
30
|
+
}
|
|
31
|
+
export interface RegistrationTarget<T> {
|
|
32
|
+
asClass(Class: new (...deps: any[]) => T): RegistrationWithDependencies;
|
|
33
|
+
asFactory(factory: Factory<T>): RegistrationWithDependencies;
|
|
34
|
+
asValue(value: T): IContainerBuilder;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyBaseLogger, FastifyServerOptions, RawServerBase, RawServerDefault } from 'fastify';
|
|
2
|
+
import { ContainerInstance } from './ioc';
|
|
3
|
+
export interface Application {
|
|
4
|
+
getContainer(): ContainerInstance;
|
|
5
|
+
getServer(): FastifyInstance;
|
|
6
|
+
/** Starts the application. */
|
|
7
|
+
start(): Promise<void>;
|
|
8
|
+
/** Stops the application. */
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface ApplicationOptions<RawServer extends RawServerBase = RawServerDefault, Logger extends FastifyBaseLogger = FastifyBaseLogger> {
|
|
12
|
+
host?: string;
|
|
13
|
+
port?: number;
|
|
14
|
+
runServer?: boolean;
|
|
15
|
+
server?: FastifyServerOptions<RawServer, Logger>;
|
|
16
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fastact/core",
|
|
3
|
+
"version": "0.1.0-dev",
|
|
4
|
+
"description": "A fantastic, vanilla-flavored web framework core powered by Fastify. Built for JS purists: strictly 0 decorators, 0 reflect-metadata magic, and 100% predictable asynchronous flow.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Igor Bezlepkin",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "github:fastactjs/core"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"fastact",
|
|
19
|
+
"fastify",
|
|
20
|
+
"web",
|
|
21
|
+
"app",
|
|
22
|
+
"http",
|
|
23
|
+
"application",
|
|
24
|
+
"framework",
|
|
25
|
+
"router",
|
|
26
|
+
"nodejs",
|
|
27
|
+
"deno",
|
|
28
|
+
"bun"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22.0.0"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"chalk": "^6.0.0",
|
|
35
|
+
"fastify": "^5.12.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.0.0",
|
|
39
|
+
"typescript": "^5.5.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
]
|
|
44
|
+
}
|