@fastact/core 0.1.0-dev → 0.2.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 CHANGED
@@ -0,0 +1,99 @@
1
+ # ⚡ FastAct — Backend Without the Bullsh\*t.
2
+
3
+ **FastAct** is a lightweight, ultra-fast backend framework for Node.js built with **TypeScript** and **Fastify**.
4
+
5
+ It is designed for engineers who are tired of bloated enterprise monsters, endless decorators, heavy reflection magic, and overwhelming boilerplate. **FastAct** brings back the joy of writing clean, predictable, and rock-solid server-side code.
6
+
7
+ [Documentation (Coming Soon)] • [CLI Utility: fac] • [Community]
8
+
9
+ ---
10
+
11
+ ## 🔥 Key Features & Philosophy
12
+
13
+ ### 🚫 No Decorator Pollution
14
+
15
+ Forget about `@Injectable()`, `@Inject()`, or `@Module()`. Your code should be pure TypeScript. FastAct uses an explicit, declarative, and fluent API for building dependencies, which plays perfectly with native tools and unit tests.
16
+
17
+ ### 🛡 Symbol-Driven IoC Container
18
+
19
+ Our custom built-in Dependency Injection (DI) container operates entirely on unique `Symbol` tokens. No string name collisions, no accidental service overwrites. You get 100% type safety and perfect IDE autocomplete out of the box.
20
+
21
+ ### 🧬 Advanced Lifecycles (Singleton & Scoped)
22
+
23
+ FastAct natively supports both lazy `Singleton` instances and fully isolated `Scoped` contexts for every single HTTP request or CLI command. This makes it an ideal fit for managing MikroORM's `EntityManager` and isolating database transactions safely.
24
+
25
+ ### 🔀 Unified Web & CLI Architecture (Standalone Mode)
26
+
27
+ Initialize your entire application with a single `createApp()` function. Need a web server? Pass `runServer: true`. Need a lightweight cron job, queue worker, or a CLI command via our `fac` utility? Turn off the server, and the IoC container will assemble only the required services without opening ports or wasting memory.
28
+
29
+ ---
30
+
31
+ ## 🎹 Code Showcase
32
+
33
+ ### 1. Define Unique Tokens
34
+
35
+ ```typescript
36
+ // src/modules/auth/auth.tokens.ts
37
+ import type { InjectionToken } from '@fastactjs/core';
38
+ import type { AuthService } from './auth.service';
39
+
40
+ export const AUTH_DI = {
41
+ AuthService: Symbol('AuthService') as InjectionToken<AuthService>,
42
+ };
43
+ ```
44
+
45
+ ### 2. Register Dependencies Cleanly (Fluent API)
46
+
47
+ ```typescript
48
+ // src/modules/auth/auth.module.ts
49
+ import { AUTH_DI } from './auth.tokens';
50
+ import { AuthService } from './auth.service';
51
+ import type { ContainerBuilder } from '@fastactjs/core';
52
+
53
+ export async function createContainerModule(builder: ContainerBuilder) {
54
+ // Simple, elegant, and instantly readable for developers worldwide
55
+ builder.add(AUTH_DI.AuthService).asClass(AuthService).singleton();
56
+ }
57
+ ```
58
+
59
+ ### 3. Ignite the Engine
60
+
61
+ ```typescript
62
+ // src/main.ts
63
+ import { createApp } from '@fastactjs/core';
64
+ import path from 'node:path';
65
+
66
+ async function bootstrap() {
67
+ const app = await createApp({
68
+ modulesDir: path.join(__dirname, 'modules'),
69
+ runServer: true,
70
+ port: 3000,
71
+ });
72
+
73
+ await app.start();
74
+ }
75
+ bootstrap();
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 🛠 Our Powerful CLI: `fac`
81
+
82
+ Manage your FastAct applications with our sharp and blunt console utility — `fac`. Fast, efficient, straight to the point.
83
+
84
+ ```bash
85
+ # Generate a new module structure automatically (Zero Boilerplate)
86
+ \$ fac add module user
87
+
88
+ # Run the project in development mode with hot-reload
89
+ \$ fac start --dev
90
+
91
+ # Execute a standalone CLI command / cron script
92
+ \$ fac run cron:sync-users
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 🌏 Join the Movement
98
+
99
+ We are building a backend framework that speaks the same language of simplicity to engineers in Moscow, Seoul, Tokyo, and San Francisco. If you share our passion for clean JavaScript/TypeScript without "magic" crutches — drop a star ⭐️ and let's reshape backend development together.
@@ -0,0 +1,8 @@
1
+ import { App } from './app';
2
+ import type { NormalAppOptions, StandaloneAppOptions } from './types';
3
+ export declare class AppFactory {
4
+ static create(options?: NormalAppOptions): Promise<App>;
5
+ static createStandalone(options?: StandaloneAppOptions): Promise<App>;
6
+ }
7
+ export declare function createApp(options?: NormalAppOptions): Promise<App>;
8
+ export declare function createStandaloneApp(options?: StandaloneAppOptions): Promise<App>;
@@ -0,0 +1,24 @@
1
+ import { App } from './app';
2
+ import { ContainerBuilder } from './ioc';
3
+ export class AppFactory {
4
+ // prettier-ignore
5
+ static async create(options) {
6
+ const container = await new ContainerBuilder().build();
7
+ return new App(container, { runServer: true, ...options });
8
+ }
9
+ // prettier-ignore
10
+ static async createStandalone(options) {
11
+ const container = await new ContainerBuilder().build();
12
+ return new App(container, { runServer: false, ...options });
13
+ }
14
+ }
15
+ // prettier-ignore
16
+ export async function createApp(options) {
17
+ const container = await new ContainerBuilder().build();
18
+ return new App(container, { runServer: true, ...options });
19
+ }
20
+ // prettier-ignore
21
+ export async function createStandaloneApp(options) {
22
+ const container = await new ContainerBuilder().build();
23
+ return new App(container, { runServer: false, ...options });
24
+ }
package/dist/app.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { FastifyInstance } from 'fastify';
2
+ import { type ContainerInstance } from './ioc';
3
+ import type { NormalAppOptions, StandaloneAppOptions } from './types';
4
+ export type AppOptions = NormalAppOptions & StandaloneAppOptions & {
5
+ runServer?: boolean;
6
+ };
7
+ export declare class App {
8
+ private readonly container;
9
+ private readonly server;
10
+ private readonly options;
11
+ constructor(container: ContainerInstance, options?: AppOptions);
12
+ getContainer(): ContainerInstance;
13
+ getServer(): FastifyInstance;
14
+ start(): Promise<void>;
15
+ close(): Promise<void>;
16
+ private stopServer;
17
+ }
package/dist/app.js ADDED
@@ -0,0 +1,63 @@
1
+ import chalk from 'chalk';
2
+ import fastify from 'fastify';
3
+ import { fastactIocPlugin } from './ioc';
4
+ import { DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT, LOG_PREFIX, } from './constants';
5
+ export class App {
6
+ container;
7
+ server;
8
+ options;
9
+ constructor(container, options = {}) {
10
+ this.options = options;
11
+ this.container = container;
12
+ if (this.options.runServer !== false) {
13
+ this.server = fastify(this.options.server);
14
+ this.server.register(fastactIocPlugin, {
15
+ container: this.container,
16
+ });
17
+ }
18
+ }
19
+ getContainer() {
20
+ return this.container;
21
+ }
22
+ getServer() {
23
+ if (!this.server) {
24
+ throw new Error('Fastify server is not initialized in standalone mode');
25
+ }
26
+ return this.server;
27
+ }
28
+ async start() {
29
+ if (this.options.runServer === false) {
30
+ // prettier-ignore
31
+ console.log(`${LOG_PREFIX.WARN} Standalone context initialized (CLI mode)`);
32
+ return;
33
+ }
34
+ // Защита для TypeScript (хотя при runServer !== false сервер точно есть)
35
+ if (!this.server)
36
+ return;
37
+ const host = this.options.host ?? DEFAULT_SERVER_HOST;
38
+ const port = this.options.port ?? DEFAULT_SERVER_PORT;
39
+ try {
40
+ await this.server.listen({ port, host });
41
+ console.log(`${chalk.green.bold('[FastAct]')} Server is now listening on ${host}:${port}`);
42
+ }
43
+ catch (err) {
44
+ // console.log(`${LOG_PREFIX.ERROR} ${err}`);
45
+ throw err;
46
+ }
47
+ }
48
+ async close() {
49
+ //if (this.container?.dispose) await this.container.dispose();
50
+ await this.stopServer();
51
+ if (this.options.runServer !== false)
52
+ return;
53
+ console.log(`${LOG_PREFIX.INFO} Standalone application stopped`);
54
+ process.exit(0);
55
+ }
56
+ async stopServer() {
57
+ if (!this.server)
58
+ return;
59
+ console.log(`${LOG_PREFIX.WARN} Shutting down the server...`);
60
+ await this.server.close();
61
+ console.log(`${LOG_PREFIX.INFO} Server gracefully stopped`);
62
+ }
63
+ }
package/dist/constants.js CHANGED
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  export const DEFAULT_SERVER_HOST = 'localhost';
3
3
  export const DEFAULT_SERVER_PORT = 3000;
4
4
  export const LOG_PREFIX = {
5
- INFO: chalk.green.bold('[fastact]'),
6
- WARN: chalk.yellow.bold('[fastact]'),
7
- ERROR: chalk.red.bold('[fastact]'),
5
+ INFO: chalk.green.bold('[FastAct]'),
6
+ WARN: chalk.yellow.bold('[FastAct]'),
7
+ ERROR: chalk.red.bold('[FastAct]'),
8
8
  };
@@ -0,0 +1,16 @@
1
+ import type { AppConfig } from './types';
2
+ /**
3
+ * Helper function to define FastAct configuration with TypeScript inference
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * // fastact.config.ts
8
+ * import { defineConfig } from '@fastact/core'
9
+ *
10
+ * export default defineConfig({
11
+ * modules: ['./dist/**\/*.module.js'],
12
+ * modulesTs: ['./src/**\/*.module.ts'],
13
+ * })
14
+ * ```
15
+ */
16
+ export declare function defineConfig<T extends AppConfig = AppConfig>(config: T): T;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Helper function to define FastAct configuration with TypeScript inference
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * // fastact.config.ts
7
+ * import { defineConfig } from '@fastact/core'
8
+ *
9
+ * export default defineConfig({
10
+ * modules: ['./dist/**\/*.module.js'],
11
+ * modulesTs: ['./src/**\/*.module.ts'],
12
+ * })
13
+ * ```
14
+ */
15
+ export function defineConfig(config) {
16
+ return config;
17
+ }
@@ -0,0 +1,6 @@
1
+ export declare abstract class FastactError extends Error {
2
+ abstract readonly code: string;
3
+ abstract readonly statusCode: number;
4
+ constructor(message: string);
5
+ get fullCode(): string;
6
+ }
@@ -0,0 +1,10 @@
1
+ export class FastactError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = this.constructor.name;
5
+ Object.setPrototypeOf(this, new.target.prototype);
6
+ }
7
+ get fullCode() {
8
+ return `FA_${this.code.toUpperCase()}`;
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ export declare class DomainError extends Error {
2
+ constructor(message: string);
3
+ }
@@ -0,0 +1,6 @@
1
+ export class DomainError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = this.constructor.name;
5
+ }
6
+ }
@@ -0,0 +1 @@
1
+ export * from './domain.error';
@@ -0,0 +1 @@
1
+ export * from './domain.error';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
- export * from './ioc';
1
+ export * from './errors';
2
2
  export * from './types';
3
- export { createApp } from './application-factory';
3
+ export * from './ioc';
4
+ export { defineConfig } from './define-config';
5
+ export { createApp, createStandaloneApp, AppFactory as FastAct } from './app-factory';
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
- export * from './ioc';
1
+ export * from './errors';
2
2
  export * from './types';
3
- export { createApp } from './application-factory';
3
+ export * from './ioc';
4
+ export { defineConfig } from './define-config';
5
+ // prettier-ignore
6
+ export { createApp, createStandaloneApp, AppFactory as FastAct } from './app-factory';
@@ -1,18 +1,23 @@
1
- import type { IContainerBuilder, ContainerInstance, InjectionToken, RegistrationTarget } from './types';
1
+ import type { ContainerInstance, InjectionToken, RegistrationTarget } from './types';
2
+ /** Represents a module that can be loaded into the container builder. */
3
+ export type ContainerModule = (builder: ContainerBuilder) => Promise<void>;
2
4
  /**
3
5
  * Dependency container builder with a fluent registration API.
4
6
  *
5
- * Example: `builder.add(DI.UserService).asClass(createUserService).withDeps(DI.Database).scoped()`.
7
+ * Example: `builder.add(DI.UserService).asClass(UserService).withDeps(DI.Database).scoped()`.
6
8
  */
7
- export declare class ContainerBuilder implements IContainerBuilder {
9
+ export declare class ContainerBuilder {
8
10
  private container;
9
11
  private register;
10
12
  /**
11
- * Starts dependency registration using its unique token.
13
+ * Starts dependency registration using its token.
12
14
  *
13
15
  * Specify a class with `asClass()`, a factory with `asFactory()`, or a ready
14
16
  * value with `asValue()`.
15
17
  */
16
- add<T>(token: InjectionToken<T>): RegistrationTarget<T>;
18
+ add<T>(token: InjectionToken<T>): RegistrationTarget<T, this>;
17
19
  build(): Promise<ContainerInstance>;
20
+ private findProjectRoot;
21
+ /** Helper method for loading the config with TS/JS support. */
22
+ private loadConfig;
18
23
  }
@@ -1,11 +1,13 @@
1
1
  import path from 'node:path';
2
+ import { existsSync } from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
2
4
  import { glob } from 'node:fs/promises';
3
5
  import chalk from 'chalk';
4
6
  import { Container } from './container';
5
7
  /**
6
8
  * Dependency container builder with a fluent registration API.
7
9
  *
8
- * Example: `builder.add(DI.UserService).asClass(createUserService).withDeps(DI.Database).scoped()`.
10
+ * Example: `builder.add(DI.UserService).asClass(UserService).withDeps(DI.Database).scoped()`.
9
11
  */
10
12
  export class ContainerBuilder {
11
13
  container = new Container();
@@ -14,20 +16,20 @@ export class ContainerBuilder {
14
16
  return this;
15
17
  }
16
18
  /**
17
- * Starts dependency registration using its unique token.
19
+ * Starts dependency registration using its token.
18
20
  *
19
21
  * Specify a class with `asClass()`, a factory with `asFactory()`, or a ready
20
22
  * value with `asValue()`.
21
23
  */
22
24
  add(token) {
23
25
  const withFactory = (factory) => {
26
+ // prettier-ignore
24
27
  // prettier-ignore
25
28
  const withLifecycle = (deps = []) => ({
26
- /** One instance per scope, such as an HTTP request. */
27
29
  scoped: () => this.register(token, factory, deps, 'scoped'),
28
- /** One instance for the container's entire lifetime. */
30
+ /** Registers one instance for the container's entire lifetime. */
29
31
  singleton: () => this.register(token, factory, deps, 'singleton'),
30
- /** A new instance each time the dependency is resolved. */
32
+ /** Registers a new instance for each resolution. */
31
33
  transient: () => this.register(token, factory, deps, 'transient'),
32
34
  });
33
35
  return {
@@ -42,6 +44,25 @@ export class ContainerBuilder {
42
44
  return {
43
45
  /** Specifies a class that the container instantiates with `new`. */
44
46
  asClass: (Class) => withFactory((...deps) => new Class(...deps)),
47
+ /*
48
+ withFactory((...deps) => {
49
+ const instance = new Class(...deps);
50
+ const allProps = new Set([
51
+ ...Object.getOwnPropertyNames(instance),
52
+ ...Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
53
+ ]);
54
+
55
+ allProps.forEach((name) => {
56
+ if (name === 'constructor') return;
57
+ const value = (instance as any)[name];
58
+ if (typeof value === 'function' && value.prototype !== undefined) {
59
+ (instance as any)[name] = value.bind(instance);
60
+ }
61
+ });
62
+
63
+ return instance;
64
+ }),
65
+ */
45
66
  /** Specifies a factory that creates the dependency instance. */
46
67
  asFactory: (factory) => withFactory(factory),
47
68
  /** Registers a ready value as a singleton dependency. */
@@ -55,43 +76,85 @@ export class ContainerBuilder {
55
76
  let moduleErrorName;
56
77
  try {
57
78
  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) { ... }"`);
79
+ const root = this.findProjectRoot(runDir);
80
+ const config = await this.loadConfig(root);
81
+ const isTypeScriptRuntime = process.argv[1]?.endsWith('.ts') ||
82
+ typeof globalThis.Bun !==
83
+ 'undefined';
84
+ let masks;
85
+ if (isTypeScriptRuntime) {
86
+ masks =
87
+ config.modulesTs && config.modulesTs.length > 0
88
+ ? config.modulesTs
89
+ : ['src/**/*.module.ts'];
90
+ }
91
+ else {
92
+ masks =
93
+ config.modules && config.modules.length > 0
94
+ ? config.modules
95
+ : ['dist/**/*.module.js'];
96
+ }
97
+ for (const mask of masks) {
98
+ const cleanPathMask = mask.startsWith('./') ? mask.slice(2) : mask;
99
+ const modulesGlob = glob(cleanPathMask, {
100
+ cwd: root,
101
+ exclude: (p) => p.includes('node_modules') ||
102
+ p.includes('.git') ||
103
+ (isTypeScriptRuntime ? p.endsWith('.js') : p.endsWith('.ts')), // Smart exclusion: ignore dist in TS runtime, ignore src in JS runtime
104
+ });
105
+ for await (const relativePath of modulesGlob) {
106
+ const fullPath = path.resolve(root, relativePath);
107
+ moduleErrorName = fullPath;
108
+ // In production this will be a native, fast import() of a plain JS file
109
+ const fileUrl = pathToFileURL(fullPath).href;
110
+ const moduleExport = await import(fileUrl);
111
+ const initModule = moduleExport.createContainerModule;
112
+ if (typeof initModule === 'function') {
113
+ await initModule(this);
114
+ }
115
+ else {
116
+ throw new Error(`Missing required "export function createContainerModule(builder) { ... }"`);
117
+ }
75
118
  }
76
119
  }
77
120
  }
78
121
  catch (err) {
79
- console.log(`${chalk.red.bold('[fastact]')} IoC auto-load failed: ${err instanceof Error ? err.message : err} in the module: ${moduleErrorName}`);
122
+ console.log(`${chalk.red.bold('[FastAct]')} IoC auto-load failed: ${err instanceof Error ? err.message : err} in the module: ${moduleErrorName}`);
80
123
  }
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
124
  return this.container;
96
125
  }
126
+ findProjectRoot(startDir) {
127
+ let currentDir = startDir;
128
+ while (currentDir !== path.parse(currentDir).root) {
129
+ if (existsSync(path.join(currentDir, 'package.json'))) {
130
+ return currentDir;
131
+ }
132
+ currentDir = path.dirname(currentDir);
133
+ }
134
+ return startDir;
135
+ }
136
+ /** Helper method for loading the config with TS/JS support. */
137
+ async loadConfig(projectRootDir) {
138
+ const extensions = ['.ts', '.js', '.mts', '.mjs', '.cts', '.cjs'];
139
+ let configPath = '';
140
+ for (const ext of extensions) {
141
+ const file = path.join(projectRootDir, `fastact.config${ext}`);
142
+ if (existsSync(file)) {
143
+ configPath = file;
144
+ break;
145
+ }
146
+ }
147
+ if (!configPath)
148
+ return {};
149
+ try {
150
+ // Convert the absolute path to a file:// URL (required for ESM import)
151
+ const fileUrl = pathToFileURL(configPath).href;
152
+ const configModule = await import(fileUrl);
153
+ return configModule.default || configModule;
154
+ }
155
+ catch (err) {
156
+ console.log(`${chalk.yellow.bold('[FastAct]')} Failed to load config file: ${err instanceof Error ? err.message : err}`);
157
+ return {};
158
+ }
159
+ }
97
160
  }
@@ -1,24 +1,24 @@
1
1
  import type { Lifecycle, Factory, InjectionToken } from './types';
2
2
  export declare class Container {
3
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;
4
+ private scopeCaches;
5
+ /** Registers a dependency factory, its dependencies, and its lifecycle. */
6
+ register<T, D extends any[]>(token: InjectionToken<T>, factory: Factory<T, D>, deps?: InjectionToken<D[number]>[], lifecycle?: Lifecycle): this;
7
7
  /** Registers a ready value as a singleton dependency. */
8
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
- */
9
+ /** Resolves a dependency according to its lifecycle. */
13
10
  get<T>(token: InjectionToken<T>, scopeId?: string, resolvesSingleton?: boolean, resolutionPath?: InjectionToken<any>[]): T;
11
+ private resolveSingleton;
12
+ private resolveScoped;
13
+ private getScopeCache;
14
14
  private build;
15
- /** Creates an isolated scope for scoped dependencies. */
15
+ /** Creates a scoped resolver that reuses instances for the given scope ID. */
16
16
  createScope(id: string): ScopedContainer;
17
17
  /** Removes all cached scoped dependencies for the specified scope. */
18
18
  clearScope(id: string): void;
19
19
  private clearTokenFromScopes;
20
20
  }
21
- /** A container with a predefined scope identifier. */
21
+ /** Resolves dependencies within a predefined scope. */
22
22
  export declare class ScopedContainer {
23
23
  private parent;
24
24
  scopeId: string;
@@ -1,8 +1,8 @@
1
1
  export class Container {
2
2
  registry = new Map();
3
- scoped = new Map();
4
- /** Registers a dependency factory and its dependency list. */
5
- register(token, factory, deps, lifecycle = 'scoped') {
3
+ scopeCaches = new Map();
4
+ /** Registers a dependency factory, its dependencies, and its lifecycle. */
5
+ register(token, factory, deps = [], lifecycle = 'singleton') {
6
6
  this.registry.set(token, {
7
7
  factory,
8
8
  deps,
@@ -24,37 +24,47 @@ export class Container {
24
24
  this.clearTokenFromScopes(token);
25
25
  return this;
26
26
  }
27
- /**
28
- * Resolves a dependency. Scoped dependencies require a scope identifier,
29
- * such as an HTTP request identifier.
30
- */
27
+ /** Resolves a dependency according to its lifecycle. */
31
28
  get(token, scopeId, resolvesSingleton = false, resolutionPath = []) {
32
29
  const entry = this.registry.get(token);
33
- if (!entry)
30
+ if (!entry) {
34
31
  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
32
  }
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);
33
+ const resolvers = {
34
+ singleton: () => this.resolveSingleton(token, entry, scopeId, resolutionPath),
35
+ scoped: () =>
36
+ // prettier-ignore
37
+ this.resolveScoped(token, entry, scopeId, resolvesSingleton, resolutionPath),
38
+ transient: () => this.build(token, entry, scopeId, resolvesSingleton, resolutionPath),
39
+ };
40
+ return resolvers[entry.lifecycle]?.();
41
+ }
42
+ resolveSingleton(token, entry, scopeId, resolutionPath) {
43
+ if (!entry.isInitialized) {
44
+ entry.instance = this.build(token, entry, scopeId, true, resolutionPath);
45
+ entry.isInitialized = true;
46
+ }
47
+ return entry.instance;
48
+ }
49
+ resolveScoped(token, entry, scopeId, resolvesSingleton, resolutionPath) {
50
+ if (resolvesSingleton) {
51
+ throw new Error(`Singleton dependency chain cannot include scoped dependency ${String(token)}`);
52
+ }
53
+ if (!scopeId)
54
+ throw new Error(`Scope ID required for ${String(token)}`);
55
+ const scopeCache = this.getScopeCache(scopeId);
56
+ if (!scopeCache.has(token)) {
57
+ scopeCache.set(token, this.build(token, entry, scopeId, false, resolutionPath));
58
+ }
59
+ return scopeCache.get(token);
60
+ }
61
+ getScopeCache(scopeId) {
62
+ let scopeCache = this.scopeCaches.get(scopeId);
63
+ if (!scopeCache) {
64
+ scopeCache = new Map();
65
+ this.scopeCaches.set(scopeId, scopeCache);
56
66
  }
57
- return this.build(token, entry, scopeId, resolvesSingleton, resolutionPath);
67
+ return scopeCache;
58
68
  }
59
69
  build(token, entry, scopeId, resolvesSingleton, resolutionPath) {
60
70
  if (resolutionPath.includes(token)) {
@@ -65,21 +75,21 @@ export class Container {
65
75
  const args = entry.deps.map((dep) => this.get(dep, scopeId, resolvesSingleton, nextPath));
66
76
  return entry.factory(...args);
67
77
  }
68
- /** Creates an isolated scope for scoped dependencies. */
78
+ /** Creates a scoped resolver that reuses instances for the given scope ID. */
69
79
  createScope(id) {
70
80
  return new ScopedContainer(this, id);
71
81
  }
72
82
  /** Removes all cached scoped dependencies for the specified scope. */
73
83
  clearScope(id) {
74
- this.scoped.delete(id);
84
+ this.scopeCaches.delete(id);
75
85
  }
76
86
  clearTokenFromScopes(token) {
77
- for (const scopeCache of this.scoped.values()) {
87
+ for (const scopeCache of this.scopeCaches.values()) {
78
88
  scopeCache.delete(token);
79
89
  }
80
90
  }
81
91
  }
82
- /** A container with a predefined scope identifier. */
92
+ /** Resolves dependencies within a predefined scope. */
83
93
  export class ScopedContainer {
84
94
  parent;
85
95
  scopeId;
@@ -0,0 +1,18 @@
1
+ import type { FastifyPluginAsync } from 'fastify';
2
+ import type { ContainerInstance, ScopedContainerInstance } from './types';
3
+ interface InternalRootContainer extends ContainerInstance {
4
+ createScope(id: string): InternalScopedContainer;
5
+ }
6
+ interface InternalScopedContainer extends ScopedContainerInstance {
7
+ dispose(): void | Promise<void>;
8
+ }
9
+ declare module 'fastify' {
10
+ interface FastifyRequest {
11
+ container: ContainerInstance;
12
+ scopedContainer: ScopedContainerInstance;
13
+ }
14
+ }
15
+ export declare const fastactIocPlugin: FastifyPluginAsync<{
16
+ container: InternalRootContainer;
17
+ }>;
18
+ export {};
@@ -0,0 +1,21 @@
1
+ import fp from 'fastify-plugin';
2
+ // prettier-ignore
3
+ const plugin = async (fastify, options) => {
4
+ fastify.decorateRequest('container', null);
5
+ fastify.decorateRequest('scopedContainer', null);
6
+ fastify.addHook('onRequest', async (req) => {
7
+ const r = req;
8
+ r.container = options.container;
9
+ r.scopedContainer = options.container.createScope(req.id);
10
+ });
11
+ fastify.addHook('onResponse', async (req) => {
12
+ const r = req;
13
+ if (r.scopedContainer && typeof r.scopedContainer.dispose === 'function') {
14
+ await r.scopedContainer.dispose();
15
+ }
16
+ // Зануляем ссылки, очищая память для Garbage Collector
17
+ r.container = null;
18
+ r.scopedContainer = null;
19
+ });
20
+ };
21
+ export const fastactIocPlugin = fp(plugin, { name: '@fastact/ioc-plugin' });
@@ -1,2 +1,4 @@
1
1
  export * from './types';
2
2
  export * from './container-builder';
3
+ export * from './container';
4
+ export * from './fastify-plugin';
package/dist/ioc/index.js CHANGED
@@ -3,3 +3,5 @@ export * from './types';
3
3
  // __type?: T;
4
4
  //}
5
5
  export * from './container-builder';
6
+ export * from './container';
7
+ export * from './fastify-plugin';
@@ -1,14 +1,14 @@
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
1
  export interface ContainerInstance {
7
2
  get<T>(token: InjectionToken<T>): T;
8
3
  }
4
+ export interface ScopedContainerInstance {
5
+ get<T>(token: InjectionToken<T>): T;
6
+ }
7
+ /** A symbol token carrying the type of the dependency it identifies. */
9
8
  export interface SymbolToken<T> extends Symbol {
10
9
  readonly __type?: T;
11
10
  }
11
+ /** A string or typed symbol used to identify a dependency. */
12
12
  export type InjectionToken<T> = string | SymbolToken<T>;
13
13
  /** The lifetime of a registered dependency. */
14
14
  export type Lifecycle = 'singleton' | 'scoped' | 'transient';
@@ -20,16 +20,16 @@ export interface DepEntry<T> {
20
20
  instance?: T;
21
21
  isInitialized: boolean;
22
22
  }
23
- export interface RegistrationLifecycle {
24
- scoped(): IContainerBuilder;
25
- singleton(): IContainerBuilder;
26
- transient(): IContainerBuilder;
23
+ export interface RegistrationLifecycle<B = any> {
24
+ scoped(): B;
25
+ singleton(): B;
26
+ transient(): B;
27
27
  }
28
- export interface RegistrationWithDependencies extends RegistrationLifecycle {
29
- withDeps(...deps: Array<InjectionToken<any> | InjectionToken<any>[]>): RegistrationLifecycle;
28
+ export interface RegistrationWithDependencies<B> extends RegistrationLifecycle<B> {
29
+ withDeps(...deps: Array<InjectionToken<any> | InjectionToken<any>[]>): RegistrationLifecycle<B>;
30
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;
31
+ export interface RegistrationTarget<V, B> {
32
+ asClass(Class: new (...args: any[]) => V): RegistrationWithDependencies<B>;
33
+ asFactory(factory: (...args: any[]) => V): RegistrationWithDependencies<B>;
34
+ asValue(value: V): B;
35
35
  }
package/dist/types.d.ts CHANGED
@@ -1,16 +1,46 @@
1
- import type { FastifyInstance, FastifyBaseLogger, FastifyServerOptions, RawServerBase, RawServerDefault } from 'fastify';
2
- import { ContainerInstance } from './ioc';
1
+ import type { FastifyInstance, FastifyHttpOptions } from 'fastify';
2
+ import type { ContainerInstance } from './ioc';
3
3
  export interface Application {
4
4
  getContainer(): ContainerInstance;
5
5
  getServer(): FastifyInstance;
6
- /** Starts the application. */
7
- start(): Promise<void>;
8
- /** Stops the application. */
9
- stop(): Promise<void>;
6
+ /** Close the application. */
7
+ close(): Promise<void>;
10
8
  }
11
- export interface ApplicationOptions<RawServer extends RawServerBase = RawServerDefault, Logger extends FastifyBaseLogger = FastifyBaseLogger> {
9
+ /**
10
+ export interface ApplicationOptions<
11
+ RawServer extends RawServerBase = RawServerDefault,
12
+ Logger extends FastifyBaseLogger = FastifyBaseLogger,
13
+ > {
14
+ host?: string;
15
+ port?: number;
16
+ // runServer?: boolean;
17
+ server?: FastifyServerOptions<RawServer, Logger>;
18
+ }
19
+ **/
20
+ export interface NormalAppOptions {
12
21
  host?: string;
13
22
  port?: number;
14
- runServer?: boolean;
15
- server?: FastifyServerOptions<RawServer, Logger>;
23
+ server?: FastifyHttpOptions<any>;
24
+ }
25
+ export interface StandaloneAppOptions {
26
+ }
27
+ export interface AppConfig {
28
+ /**
29
+ * Glob patterns for compiled JS module files to auto-load.
30
+ *
31
+ * @example
32
+ * ```js
33
+ * modules: ['./dist/**\/*.module.js'],
34
+ * ```
35
+ */
36
+ modules?: string[];
37
+ /**
38
+ * Glob patterns for TypeScript module files to auto-load.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * modulesTs: ['./src/**\/*.module.ts'],
43
+ * ```
44
+ */
45
+ modulesTs?: string[];
16
46
  }
@@ -0,0 +1 @@
1
+ export * from './logger';
@@ -0,0 +1 @@
1
+ export * from './logger';
@@ -0,0 +1,5 @@
1
+ export declare const logger: {
2
+ warn(message: string): void;
3
+ error(message: string, error?: any): void;
4
+ info(message: string): void;
5
+ };
@@ -0,0 +1,17 @@
1
+ const RESET = '\x1b[0m';
2
+ const BOLD_YELLOW = '\x1b[1;33m';
3
+ const BOLD_RED = '\x1b[1;31m';
4
+ const BOLD_GREEN = '\x1b[1;32m';
5
+ export const logger = {
6
+ warn(message) {
7
+ console.warn(`${BOLD_YELLOW}[FastAct]${RESET} ${message}`);
8
+ },
9
+ error(message, error) {
10
+ const errMessage = error instanceof Error ? error.message : String(error || '');
11
+ const suffix = errMessage ? `: ${errMessage}` : '';
12
+ console.error(`${BOLD_RED}[FastAct]${RESET} ${message}${suffix}`);
13
+ },
14
+ info(message) {
15
+ console.log(`${BOLD_GREEN}[FastAct]${RESET} ${message}`);
16
+ },
17
+ };
@@ -0,0 +1,6 @@
1
+ export declare class ValidationError extends Error {
2
+ readonly code = "E_VALIDATION_ERROR";
3
+ readonly statusCode = 422;
4
+ readonly errors: any[];
5
+ constructor(rawErrors: any[]);
6
+ }
@@ -0,0 +1,11 @@
1
+ export class ValidationError extends Error {
2
+ code = 'E_VALIDATION_ERROR';
3
+ statusCode = 422;
4
+ errors;
5
+ constructor(rawErrors) {
6
+ super('Validation failed');
7
+ this.name = 'ValidationError';
8
+ this.errors = rawErrors;
9
+ Object.setPrototypeOf(this, ValidationError.prototype);
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ export declare const fastactValidationPlugin: (fastify: import("fastify").FastifyInstance<import("fastify").RawServerDefault, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>) => Promise<void>;
@@ -0,0 +1,23 @@
1
+ import fp from 'fastify-plugin';
2
+ import Ajv from 'ajv';
3
+ import { ValidationError } from './errors';
4
+ const ajv = new Ajv({ coerceTypes: true, useDefaults: true, removeAdditional: true });
5
+ const compilersCache = new Map();
6
+ export const fastactValidationPlugin = fp(async (fastify) => {
7
+ // Регистрируем метод в прототип запроса Fastify
8
+ fastify.decorateRequest('validateWith', async function (schema) {
9
+ const request = this;
10
+ let validateFn = compilersCache.get(schema);
11
+ if (!validateFn) {
12
+ validateFn = ajv.compile(schema);
13
+ compilersCache.set(schema, validateFn);
14
+ }
15
+ // Валидируем body (или можно расширить до query/params)
16
+ const isValid = validateFn(request.body);
17
+ if (!isValid) {
18
+ // Выкидываем кастомную ошибку пакета validation
19
+ throw new ValidationError(validateFn.errors);
20
+ }
21
+ return request.body;
22
+ });
23
+ });
package/package.json CHANGED
@@ -1,18 +1,21 @@
1
1
  {
2
2
  "name": "@fastact/core",
3
- "version": "0.1.0-dev",
3
+ "version": "0.2.0-dev",
4
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
5
  "license": "MIT",
6
6
  "author": "Igor Bezlepkin",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "github:fastactjs/core"
9
+ "url": "git+https://github.com/fastactjs/core.git"
10
10
  },
11
11
  "type": "module",
12
12
  "main": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts",
14
14
  "scripts": {
15
- "build": "tsc"
15
+ "build": "tsc",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:ui": "vitest --ui"
16
19
  },
17
20
  "keywords": [
18
21
  "fastact",
@@ -31,14 +34,19 @@
31
34
  "node": ">=22.0.0"
32
35
  },
33
36
  "dependencies": {
34
- "chalk": "^6.0.0",
35
- "fastify": "^5.12.1"
37
+ "chalk": "^6.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "fastify": "^5.12.1",
41
+ "fastify-plugin": "^6.0.0"
36
42
  },
37
43
  "devDependencies": {
38
44
  "@types/node": "^22.0.0",
39
- "typescript": "^5.5.0"
45
+ "fastify": "^5.12.1",
46
+ "typescript": "^5.5.0",
47
+ "vitest": "^5.0.0"
40
48
  },
41
49
  "files": [
42
50
  "dist"
43
51
  ]
44
- }
52
+ }
@@ -1,2 +0,0 @@
1
- import type { Application, ApplicationOptions } from './types';
2
- export declare function createApp(options?: ApplicationOptions): Promise<Application>;
@@ -1,7 +0,0 @@
1
- import { App } from './application';
2
- import { ContainerBuilder } from './ioc';
3
- // prettier-ignore
4
- export async function createApp(options) {
5
- const container = await new ContainerBuilder().build();
6
- return new App(container, options);
7
- }
@@ -1,13 +0,0 @@
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
- }
@@ -1,42 +0,0 @@
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
- }